Compare commits
No commits in common. "ea5052f04aa2f127ca0c9b906da1fb348ad7102d" and "be2e215b398a2a9cdde63fec2bd0049b94775897" have entirely different histories.
ea5052f04a
...
be2e215b39
32
.devcontainer/devcontainer.json
Normal file
32
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/alpine
|
||||
{
|
||||
"name": "Hugo",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "klakegg/hugo:ext-alpine",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [1313],
|
||||
|
||||
"onCreateCommand": {
|
||||
"lfs": "apk add git-lfs",
|
||||
"pagefind": "export VER=v1.0.3 && wget \"https://github.com/CloudCannon/pagefind/releases/download/${VER}/pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\" && tar -xf \"pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\" && mv pagefind /usr/bin/ && rm \"pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\""
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["streetsidesoftware.code-spell-checker"]
|
||||
}
|
||||
}
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "uname -a",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
7
.gitattributes
vendored
7
.gitattributes
vendored
|
|
@ -1,7 +1,6 @@
|
|||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.avif filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.svg filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
|||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,10 +1,2 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
public
|
||||
.hugo_build.lock
|
||||
|
|
|
|||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "themes/catafalque"]
|
||||
path = themes/catafalque
|
||||
url = https://github.com/SeriousBug/hugo-theme-catafalque.git
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cSpell.words": ["Actix", "Gandi"],
|
||||
"cSpell.enabled": true,
|
||||
"cSpell.enableFiletypes": ["markdown"]
|
||||
}
|
||||
31
.vscode/tasks.json
vendored
Normal file
31
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "dev",
|
||||
"type": "shell",
|
||||
"command": "hugo server -D",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"command": "hugo",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "pagefind",
|
||||
"type": "shell",
|
||||
"command": "pagefind --site public",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "view",
|
||||
"type": "shell",
|
||||
"command": "python3 -m http.server 1313 --directory public",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
recursive: true
|
||||
|
||||
steps:
|
||||
build:
|
||||
image: node:22
|
||||
image: klakegg/hugo:ext-alpine
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run build
|
||||
- hugo
|
||||
search:
|
||||
image: seriousbug/pagefind
|
||||
commands:
|
||||
- pagefind --site public
|
||||
deploy:
|
||||
image: minio/mc
|
||||
secrets: [MINIO_URL, ACCESS_KEY, SECRET_KEY]
|
||||
commands:
|
||||
- mc alias set minio $MINIO_URL $ACCESS_KEY $SECRET_KEY
|
||||
- mc mirror --overwrite build/ minio/bgenc.net/
|
||||
- mc mirror --overwrite public/ minio/bgenc.net/
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -1,38 +0,0 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
5
archetypes/default.md
Normal file
5
archetypes/default.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
217
config.toml
Normal file
217
config.toml
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
baseURL = "/"
|
||||
title = "Kaan Barmore-Genç"
|
||||
languageCode = "en-us"
|
||||
theme = "catafalque"
|
||||
|
||||
PygmentsCodeFences = true
|
||||
PygmentsStyle = "monokai"
|
||||
|
||||
paginate = 20
|
||||
rssLimit = 60 # Maximum number of items in the RSS feed.
|
||||
copyright = "Contents are licensed under <a rel='license' href='http://creativecommons.org/licenses/by/4.0/'>CC 4.0</a> unless specified otherwise.<br />Source code for this website is available at <a href='https://gitea.bgenc.net/kaan/bgenc.net'>gitea.bgenc.net</a>."
|
||||
|
||||
# googleAnalytics = ""
|
||||
# disqusShortname = ""
|
||||
|
||||
archetypeDir = "archetypes"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
layoutDir = "layouts"
|
||||
publishDir = "public"
|
||||
|
||||
buildDrafts = false
|
||||
buildFuture = false
|
||||
buildExpired = false
|
||||
canonifyURLs = true
|
||||
|
||||
enableRobotsTXT = true
|
||||
enableGitInfo = false
|
||||
enableEmoji = true
|
||||
enableMissingTranslationPlaceholders = false
|
||||
disableRSS = false
|
||||
disableSitemap = false
|
||||
disable404 = false
|
||||
disableHugoGeneratorInject = false
|
||||
|
||||
[permalinks]
|
||||
posts = "/:filename/"
|
||||
|
||||
[author]
|
||||
name = "Kaan Barmore-Genç"
|
||||
|
||||
[blackfriday]
|
||||
hrefTargetBlank = true
|
||||
|
||||
#[taxonomies]
|
||||
# tag = "tags"
|
||||
# category = "categories"
|
||||
# series = "series"
|
||||
|
||||
[params]
|
||||
dateform = "Jan 2, 2006"
|
||||
dateformShort = "Jan 2"
|
||||
dateformNum = "2006-01-02"
|
||||
dateformNumTime = "2006-01-02 15:04"
|
||||
|
||||
# Metadata mostly used in document's head
|
||||
#
|
||||
description = "Website of Kaan Barmore-Genç, and his personal blog"
|
||||
keywords = ""
|
||||
images = [""]
|
||||
|
||||
# Home subtitle of the index page.
|
||||
#
|
||||
homeSubtitle = [
|
||||
"Hi! I'm a Software Engineer, an avid Linux user, an enthusiast of many programming languages, a <a href='/recipes'>home cook</a>, and an amateur gardener.",
|
||||
"My interests include building web and mobile applications, both front and back end. Over the years I learned and used many programming languages and technologies, including JavaScript, TypeScript, React, React Native, Python, Java, C, C++, Clojure, Rust, and Haskell. Pretty much everything I've worked on is open source and available on <a href='https://github.com/SeriousBug/'>my Github page</a>.",
|
||||
]
|
||||
|
||||
enablePagefind = true
|
||||
|
||||
# Set a background for the homepage
|
||||
# backgroundImage = "assets/images/background.jpg"
|
||||
|
||||
# Prefix of link to the git commit detail page. GitInfo must be enabled.
|
||||
#
|
||||
# gitUrl = ""
|
||||
|
||||
# Set disableReadOtherPosts to true in order to hide the links to other posts.
|
||||
#
|
||||
disableReadOtherPosts = false
|
||||
|
||||
# Enable theme toggle
|
||||
#
|
||||
# This options enables the theme toggle for the theme.
|
||||
# Per default, this option is off.
|
||||
# The theme is respecting the prefers-color-scheme of the operating systeme.
|
||||
# With this option on, the page user is able to set the scheme he wants.
|
||||
enableThemeToggle = true
|
||||
|
||||
# Sharing buttons
|
||||
#
|
||||
# There are a lot of buttons preconfigured. If you want to change them,
|
||||
# generate the buttons here: https://sharingbuttons.io
|
||||
# and add them into your own `layouts/partials/sharing-buttons.html`
|
||||
#
|
||||
enableSharingButtons = false
|
||||
|
||||
# Global language menu
|
||||
#
|
||||
# Enables the global language menu.
|
||||
#
|
||||
enableGlobalLanguageMenu = false
|
||||
|
||||
# Integrate Javascript files or stylesheets by adding the url to the external assets or by
|
||||
# linking local files with their path relative to the static folder, e.g. "css/styles.css"
|
||||
#
|
||||
customCSS = []
|
||||
customJS = []
|
||||
|
||||
# Toggle this option need to rebuild SCSS, requires extended version of Hugo
|
||||
#
|
||||
justifyContent = false # Set "text-align: justify" to .post-content.
|
||||
|
||||
# Custom footer
|
||||
# If you want, you can easily override the default footer with your own content.
|
||||
#
|
||||
[params.footer]
|
||||
trademark = false
|
||||
rss = true
|
||||
copyright = true
|
||||
author = false
|
||||
bottomText = [
|
||||
"This website is available at <a href='https://bgenc.net'>bgenc.net</a>, or as an onion service at <a href='http://bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion/'>bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion</a> which you can view through the <a href='https://www.torproject.org/download/' target='_blank'>Tor browser</a>.",
|
||||
]
|
||||
|
||||
topText = []
|
||||
# bottomText = [
|
||||
# "Powered by <a href=\"http://gohugo.io\">Hugo</a>",
|
||||
# "Made with ❤ by <a href=\"https://github.com/rhazdon\">Djordje Atlialp</a>",
|
||||
# ]
|
||||
|
||||
# Colors for favicons
|
||||
#
|
||||
[params.favicon.color]
|
||||
mask = "#1b1c1d"
|
||||
msapplication = "#1b1c1d"
|
||||
theme = "#1b1c1d"
|
||||
|
||||
[params.logo]
|
||||
logoMark = ">"
|
||||
logoText = "Kaan Barmore-Genç"
|
||||
logoHomeLink = "/"
|
||||
# Set true to remove the logo cursor entirely.
|
||||
# logoCursorDisabled = false
|
||||
# Set to a valid CSS color to change the cursor in the logo.
|
||||
# logoCursorColor = "#67a2c9"
|
||||
# Set to a valid CSS time value to change the animation duration, "0s" to disable.
|
||||
# logoCursorAnimate = "2s"
|
||||
|
||||
# Commento is more than just a comments widget you can embed —
|
||||
# it’s a return to the roots of the internet.
|
||||
# An internet without the tracking and invasions of privacy.
|
||||
# An internet that is simple and lightweight.
|
||||
# An internet that is focused on interesting discussions, not ads.
|
||||
# A better internet.
|
||||
# Uncomment this to enable Commento.
|
||||
#
|
||||
# [params.commento]
|
||||
# url = ""
|
||||
|
||||
[params.portrait]
|
||||
path = "/img/profile.2022.12.jpeg"
|
||||
pathWebp = "/img/profile.2022.12.webp"
|
||||
pathAvif = "/img/profile.2022.12.avif"
|
||||
alt = "A picture of Kaan, wearing a beanie, in front of some shrubbery."
|
||||
maxWidth = "20rem"
|
||||
|
||||
# Social icons
|
||||
[[params.social]]
|
||||
name = "email"
|
||||
url = "mailto:kaan@bgenc.net"
|
||||
me = true
|
||||
|
||||
[[params.social]]
|
||||
name = "github"
|
||||
url = "https://github.com/SeriousBug/"
|
||||
me = true
|
||||
|
||||
[[params.social]]
|
||||
name = "mastodon"
|
||||
url = "https://fosstodon.org/@kaan"
|
||||
me = true
|
||||
|
||||
[[params.social]]
|
||||
name = "linkedin"
|
||||
url = "https://www.linkedin.com/in/kaan-barmore-genc"
|
||||
|
||||
[[params.social]]
|
||||
name = "cv"
|
||||
title = "CV"
|
||||
url = "/extra/cv.pdf"
|
||||
|
||||
# [languages]
|
||||
# [languages.en]
|
||||
# subtitle = "Hello Friend NG Theme"
|
||||
# weight = 1
|
||||
# copyright = '<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a>'
|
||||
|
||||
# [languages.fr]
|
||||
# subtitle = "Hello Friend NG Theme"
|
||||
# weight = 2
|
||||
# copyright = '<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a>'
|
||||
|
||||
[menu]
|
||||
#[[menu.main]]
|
||||
#identifier = "about"
|
||||
#name = "About"
|
||||
#url = "about/"
|
||||
[[menu.main]]
|
||||
identifier = "posts"
|
||||
name = "Blog"
|
||||
url = "posts/"
|
||||
|
||||
[[menu.main]]
|
||||
identifier = "portfolio"
|
||||
name = "Portfolio"
|
||||
url = "portfolio/"
|
||||
BIN
content/apple-touch-icon.png
(Stored with Git LFS)
Normal file
BIN
content/apple-touch-icon.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
content/extra/Crafty Poster.pdf
Normal file
BIN
content/extra/Crafty Poster.pdf
Normal file
Binary file not shown.
BIN
content/extra/DepAware Poster.pdf
Normal file
BIN
content/extra/DepAware Poster.pdf
Normal file
Binary file not shown.
BIN
content/extra/cv.pdf
Executable file
BIN
content/extra/cv.pdf
Executable file
Binary file not shown.
70
content/extra/emacs.css
Normal file
70
content/extra/emacs.css
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* pygments.org "emacs" style */
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #008800; font-style: italic } /* Comment */
|
||||
.highlight .err { border: 1px solid #FF0000 } /* Error */
|
||||
.highlight .k { color: #AA22FF; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #666666 } /* Operator */
|
||||
.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #008800 } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */
|
||||
.highlight .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #888888 } /* Generic.Output */
|
||||
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #0044DD } /* Generic.Traceback */
|
||||
.highlight .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #AA22FF; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #AA22FF } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #00BB00; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .m { color: #666666 } /* Literal.Number */
|
||||
.highlight .s { color: #BB4444 } /* Literal.String */
|
||||
.highlight .na { color: #BB4444 } /* Name.Attribute */
|
||||
.highlight .nb { color: #AA22FF } /* Name.Builtin */
|
||||
.highlight .nc { color: #0000FF } /* Name.Class */
|
||||
.highlight .no { color: #880000 } /* Name.Constant */
|
||||
.highlight .nd { color: #AA22FF } /* Name.Decorator */
|
||||
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #00A000 } /* Name.Function */
|
||||
.highlight .nl { color: #A0A000 } /* Name.Label */
|
||||
.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #B8860B } /* Name.Variable */
|
||||
.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mb { color: #666666 } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #666666 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #BB4444 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #BB4444 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #BB4444 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #BB4444 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #BB4444 } /* Literal.String.Double */
|
||||
.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #BB4444 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #008000 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #BB4444 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #B8860B } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #00A000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #B8860B } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #B8860B } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #B8860B } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #B8860B } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
52
content/extra/kaangenc.gpg
Normal file
52
content/extra/kaangenc.gpg
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
2Vw6bCeYRslIgNo=
|
||||
=tme2
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
BIN
content/favicon-16x16.png
(Stored with Git LFS)
Normal file
BIN
content/favicon-16x16.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
content/favicon-32x32.png
(Stored with Git LFS)
Normal file
BIN
content/favicon-32x32.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
content/favicon.ico
Normal file
BIN
content/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
content/img/profile.2022.12.avif
Normal file
BIN
content/img/profile.2022.12.avif
Normal file
Binary file not shown.
BIN
content/img/profile.2022.12.jpeg
Normal file
BIN
content/img/profile.2022.12.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
BIN
content/img/profile.2022.12.webp
Normal file
BIN
content/img/profile.2022.12.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
content/img/tor-censorship-snowflake-chart.webp
Normal file
BIN
content/img/tor-censorship-snowflake-chart.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
57
content/portfolio.md
Normal file
57
content/portfolio.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
This page lists the projects that I'm most proud of, and that I'm actively developing and supporting.
|
||||
|
||||
### [Gandi Live Dns Rust  ](https://github.com/SeriousBug/gandi-live-dns-rust)
|
||||
|
||||
A dynamic DNS system that works with Gandi's live DNS feature. Allows you to
|
||||
host servers without a static IP address by updating DNS records whenever your
|
||||
IP changes. Flexible deployments through Docker or system packages with systemd
|
||||
timers.
|
||||
|
||||
### [Cuttlestore  ](https://github.com/SeriousBug/cuttlestore)
|
||||
|
||||
A generic key-value storage library for Rust. Cuttlestore allows you to write
|
||||
your code once and run it on many key-value stores. Right now it comes with
|
||||
support for Redis and Sqlite, with planned support for CouchDB and DynamoDB.
|
||||
|
||||
### [Rust Embed for Web  ](https://github.com/SeriousBug/rust-embed-for-web)
|
||||
|
||||
Embed files into your Rust executable. You can embed HTML, CSS, JavaScript
|
||||
files, all your assets into your server to bundle them together. This simplifies
|
||||
updates as your assets are always guaranteed to update together with your server.
|
||||
|
||||
This started as a fork of an existing project, but became a significant rewrite
|
||||
of it. It includes many features useful for web servers like precomputed header
|
||||
values and precompressed file contents.
|
||||
|
||||
### [Rust Embed Responder for Actix Web  ](https://github.com/SeriousBug/actix-web-rust-embed-responder)
|
||||
|
||||
A sibling project to Rust Embed for Web, this is a responder for Actix Web that
|
||||
efficiently serves your embedded files. It handles cache validation and content
|
||||
type negotiation, and is built for high performance.
|
||||
|
||||
### [Bulgur Cloud  ](https://github.com/bulgur-cloud/bulgur-cloud)
|
||||
|
||||

|
||||
|
||||
An easy to self host cloud file storage and sharing system. It's similar to Google Drive or NextCloud, but effortless to set up and maintain. Built in Rust and TypeScript, using Actix-Web and React Native.
|
||||
|
||||
### [live limit  ](https://github.com/SeriousBug/live-limit)
|
||||
|
||||
A TypeScript library that can limit the number of concurrent async operations
|
||||
running at a time. This is useful for making concurrent requests to a server
|
||||
without overloading your connection. Works with promises, has no dependencies,
|
||||
and comes under 1kb minzipped.
|
||||
|
||||
### [Query Method Middleware for Actix Web  ](https://github.com/SeriousBug/actix-web-query-method-middleware)
|
||||
|
||||
Actix Web middleware that allows you to submit HTML forms using methods other
|
||||
than `POST`. Forms normally can only be submitted through `GET` or `POST`
|
||||
methods, but this middleware reroutes requests using a query parameter to other
|
||||
methods.
|
||||
|
||||
### [Http Drogue](https://github.com/SeriousBug/http-drogue) 
|
||||
|
||||

|
||||
|
||||
A tiny self-hosted service to download files over http, with support for resuming and restarting failed downloads.
|
||||
Built with Rust, basic HTML templates using Askama, TailwindCSS, and DaisyUI. Uses no javascript!
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Adding an HTML-only interface for Bulgur Cloud'
|
||||
title: "Adding an HTML-only interface for Bulgur Cloud"
|
||||
date: 2022-04-17T04:49:20-04:00
|
||||
draft: true
|
||||
toc: false
|
||||
|
|
@ -36,7 +36,7 @@ and Actix will finish rendering and serving the page for me. Finally,
|
|||
great because you can ship a single binary which includes all the files you
|
||||
need!
|
||||
|
||||

|
||||

|
||||
|
||||
It's all hand-written HTML and CSS. It was very quick to get all of this
|
||||
working. Once the web app is done, I'll come back to this interface to add full
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Solving React Redux Triggering Too Many Re-Renders'
|
||||
title: "Solving React Redux Triggering Too Many Re-Renders"
|
||||
date: 2022-09-18T18:13:31-04:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -12,7 +12,7 @@ tags:
|
|||
---
|
||||
|
||||
This might be obvious for some, but I was struggling with a performance issue in
|
||||
[Bulgur Cloud](/portfolio), my React (well, React Native) based
|
||||
[Bulgur Cloud](/portfolio/#bulgur-cloud), my React (well, React Native) based
|
||||
web application. Bulgur Cloud is an app like Google Drive or NextCloud, and one
|
||||
of the features is that you can upload files. But I noticed that the page would
|
||||
slow down to a crawl and my computers fans would spin up during uploads. It
|
||||
|
|
@ -24,10 +24,12 @@ browser and enabled the "Highlight updates when components render" option. Then
|
|||
I used the network tab to add a throttle so I could see the upload happen more
|
||||
slowly, and started another upload.
|
||||
|
||||
{{<raw>}}
|
||||
<video controls width="100%">
|
||||
<source src="/vid/react-redux-causes-re-renders.mp4" type="video/mp4">
|
||||
<p>A video showing a progress bar slowly increasing. As the progress bar goes up, the entire screen flashes with blue borders.</p>
|
||||
</video>
|
||||
{{</raw>}}
|
||||
|
||||
Pretty much the entire screen flashes every time the progress bar goes up.
|
||||
Something **is** causing unnecessary re-renders! The next step in my diagnosis
|
||||
|
|
@ -86,22 +88,19 @@ comparison. I used a `shallowEquals` function to do a single-depth comparison of
|
|||
the objects (the object is flat so I don't need recursion).
|
||||
|
||||
```ts
|
||||
export function shallowEquals<
|
||||
Left extends Record<string, unknown>,
|
||||
Right extends Record<string, unknown>
|
||||
>(left: Left, right: Right) {
|
||||
if (Object.keys(left).length !== Object.keys(right).length) return false;
|
||||
for (const key of Object.keys(left)) {
|
||||
if (left[key] !== right[key]) return false;
|
||||
}
|
||||
return true;
|
||||
export function shallowEquals<Left extends Record<string, unknown>, Right extends Record<string, unknown>>(left: Left, right: Right) {
|
||||
if (Object.keys(left).length !== Object.keys(right).length) return false;
|
||||
for (const key of Object.keys(left)) {
|
||||
if (left[key] !== right[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ...
|
||||
const { access_token, site } = useAppSelector(
|
||||
(selector) => pick(selector.auth, 'access_token', 'site'),
|
||||
shallowEquals
|
||||
);
|
||||
const { access_token, site } = useAppSelector((selector) =>
|
||||
pick(selector.auth, "access_token", "site"),
|
||||
shallowEquals
|
||||
);
|
||||
```
|
||||
|
||||
Let's look at the profile now:
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: "Browser Caching: Assets not revalidated when server sends a 304 'Not Modified' for html page"
|
||||
date: 2022-10-15T20:56:36-04:00
|
||||
toc: false
|
||||
images:
|
||||
tags:
|
||||
- dev
|
||||
- web
|
||||
---
|
||||
|
||||
I've been working on some web server middleware, and hit a weird issue that I
|
||||
couldn't find documented anywhere. First, let's look at an overview of how
|
||||
browser caching works:
|
||||
|
||||
If your web server sends an
|
||||
[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) header in
|
||||
a HTTP response, the web browser may choose to cache the response. Next time the
|
||||
same object is requested, the browser may add an
|
||||
[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
|
||||
header to let the server know that the browser might have the object cached. At this point, the server should respond with the
|
||||
[304 Not Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)
|
||||
code and skip sending the response. This can also happen with the
|
||||
[Last Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
|
||||
and
|
||||
[If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
|
||||
headers if `ETag` is not supported as well.
|
||||
|
||||
After implementing this in my middleware, I made a quick test website to try it
|
||||
out. That's when I ran into a weird behavior: the browser would revalidate the
|
||||
HTML page itself with the `If-None-Match` header, but when the server responded
|
||||
with `304` it would not attempt to revalidate the linked stylesheets, scripts,
|
||||
and images. The browser would not request them at all and immediately use the
|
||||
cached version. It looks like if the server responds with `304` on the HTML
|
||||
page, the browser assumes that all the linked assets are not modified as well.
|
||||
That means that if the asset does change (you weren't using something like
|
||||
fingerprinting or versioning on your assets), then the browser will use outdated
|
||||
assets. Oops!
|
||||
|
||||
Luckily it looks like there's an easy solution: add `Cache-Control: no-cache`
|
||||
header to your responses. `no-cache` doesn't actually mean "don't cache at all",
|
||||
but rather means that the browser needs to revalidate objects before using
|
||||
the cached version.
|
||||
|
||||
Without the `Cache-Control` header:
|
||||

|
||||
With the `Cache-Control` header:
|
||||

|
||||
|
|
@ -5,13 +5,13 @@ toc: false
|
|||
images:
|
||||
tags:
|
||||
- dev
|
||||
- web
|
||||
- web
|
||||
---
|
||||
|
||||
I've been seeing this error a lot while working on my project [Bulgur Cloud](/bulgur-cloud-intro/).
|
||||
It seems to show up only on Firefox:
|
||||
|
||||

|
||||

|
||||
|
||||
What is curious was that I was not actually loading the file mentioned in the
|
||||
error message. I tried looking up what the error might mean, but all that came
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Why I use Dev containers for most of my projects'
|
||||
title: "Why I use Dev containers for most of my projects"
|
||||
date: 2023-02-09T23:14:05-05:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -44,7 +44,7 @@ stick with docker compose alone then!).
|
|||
VSCode comes with commands to automatically generate a dev container
|
||||
configuration for you by answering a few questions.
|
||||
|
||||

|
||||

|
||||
|
||||
At the core of dev containers, what sets it apart from just using Docker is the
|
||||
"features". These are pre-made recipes that install some tool or set up some
|
||||
|
|
@ -55,13 +55,13 @@ install or set up anything else that features didn't cover.
|
|||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||
},
|
||||
"updateContentCommand": "pnpm install"
|
||||
// ...
|
||||
// ...
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||
},
|
||||
"updateContentCommand": "pnpm install",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -119,38 +119,42 @@ Then, you just tell your dev container config to use the docker compose file.
|
|||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "devcontainer",
|
||||
"workspaceFolder": "/workspaces/my-project",
|
||||
"name": "my-project",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "devcontainer",
|
||||
"workspaceFolder": "/workspaces/my-project",
|
||||
|
||||
// Adding the Rust compiler, plus the AWS cli so I can access the S3 API of minio from the CLI.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {},
|
||||
"ghcr.io/devcontainers/features/aws-cli:1": {}
|
||||
},
|
||||
// Adding the Rust compiler, plus the AWS cli so I can access the S3 API of minio from the CLI.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {},
|
||||
"ghcr.io/devcontainers/features/aws-cli:1": {}
|
||||
},
|
||||
|
||||
// The project I'm working on exposes the port 8080.
|
||||
// I forward that out so I can look at it on my browser.
|
||||
"forwardPorts": [8080],
|
||||
// The project I'm working on exposes the port 8080.
|
||||
// I forward that out so I can look at it on my browser.
|
||||
"forwardPorts": [8080],
|
||||
|
||||
// Set up the development AWS config and credentials with test values,
|
||||
"onCreateCommand": "mkdir -p ~/.aws/ && /bin/echo -e '[default]\nregion = local' > ~/.aws/config && /bin/echo -e '[default]\naws_access_key_id = minioadmin\naws_secret_access_key = minioadmin' > ~/.aws/credentials",
|
||||
// Set up the development AWS config and credentials with test values,
|
||||
"onCreateCommand": "mkdir -p ~/.aws/ && /bin/echo -e '[default]\nregion = local' > ~/.aws/config && /bin/echo -e '[default]\naws_access_key_id = minioadmin\naws_secret_access_key = minioadmin' > ~/.aws/credentials",
|
||||
|
||||
// Create the S3 bucket
|
||||
"postCreateCommand": "aws s3 --endpoint-url $S3_ENDPOINT mb s3://my-bucket",
|
||||
// Create the S3 bucket
|
||||
"postCreateCommand": "aws s3 --endpoint-url $S3_ENDPOINT mb s3://my-bucket",
|
||||
|
||||
// I found that I have to add this, but it's not the default. Not sure why.
|
||||
"remoteUser": "root",
|
||||
// I found that I have to add this, but it's not the default. Not sure why.
|
||||
"remoteUser": "root",
|
||||
|
||||
"customizations": {
|
||||
// You can even add in VSCode extensions that everyone working on the project
|
||||
// would need, without them having to install it on their own setup manually.
|
||||
"vscode": {
|
||||
"extensions": ["rust-lang.rust-analyzer", "streetsidesoftware.code-spell-checker"]
|
||||
}
|
||||
}
|
||||
"customizations": {
|
||||
// You can even add in VSCode extensions that everyone working on the project
|
||||
// would need, without them having to install it on their own setup manually.
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
That's it! Run the "Dev Containers: Reopen in Container" command in VSCode, give
|
||||
|
|
@ -70,17 +70,18 @@ side menu.
|
|||
Scroll to the bottom, where you'll find "Required workflows". Click
|
||||
to add a workflow.
|
||||
|
||||

|
||||

|
||||
|
||||
Then select the repository where you added your action, and
|
||||
write the path to the workflow file within that repository.
|
||||
|
||||

|
||||
|
||||
|
||||
You're now done! All PRs in all repositories will run the do not merge label
|
||||
check, and will prevent you from merging any PR with the label.
|
||||
|
||||

|
||||

|
||||
|
||||
One caveat is
|
||||
that there seems to be a bug on Github's end of things where for any PR that was
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Setting up my blog as an Onion service (Tor hidden service)'
|
||||
title: "Setting up my blog as an Onion service (Tor hidden service)"
|
||||
date: 2023-03-05T15:54:13-05:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -12,7 +12,7 @@ As [online services are happy to turn over our data to the authorities](https://
|
|||
it is crucial for Tor to exist so journalists, activists, whistle-blowers, and
|
||||
anyone living under oppressive regimes can access information and communicate freely.
|
||||
|
||||

|
||||

|
||||
|
||||
But there is really no reason for Tor to be used solely by people trying to
|
||||
avoid censorship or stay private. In fact, I think it is good for people to use
|
||||
|
|
@ -82,12 +82,12 @@ I finally added the tor container to a `docker-compose.yml` to make it easier to
|
|||
rebuild if needed. That looks like this:
|
||||
|
||||
```yml
|
||||
tor-hidden-service:
|
||||
image: seriousbug/tor
|
||||
restart: always
|
||||
volumes:
|
||||
- ./tor:/etc/tor
|
||||
- /var/run/nginx:/var/run/tor
|
||||
tor-hidden-service:
|
||||
image: seriousbug/tor
|
||||
restart: always
|
||||
volumes:
|
||||
- ./tor:/etc/tor
|
||||
- /var/run/nginx:/var/run/tor
|
||||
```
|
||||
|
||||
I also needed to make the tor directory with the configuration file and services
|
||||
|
|
@ -95,3 +95,4 @@ owned by root, and use 700 as the file permission. Otherwise Tor refuses to star
|
|||
|
||||
Once all of this is set up, I restarted nginx and my Tor container. And that was about it!
|
||||
The website is now accessible through Tor! You can find it at [bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion](http://bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion/).
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Self Hosted Backups with Minio, Kopia, and Tailscale'
|
||||
title: "Self Hosted Backups with Minio, Kopia, and Tailscale"
|
||||
date: 2023-04-25T00:11:31-04:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -19,7 +19,7 @@ limit them into specific folders too. But I kept hitting issues while setting th
|
|||
refusing connections whenever the limit is active. While I'm sure there's an answer to why I was failing to set this up,
|
||||
I came up with an easier solution: Minio.
|
||||
|
||||
Minio is an S3 compatible, self hosted block storage service.
|
||||
Minio is an S3 compatible, self hosted block storage service.
|
||||
It is generally meant to be used in clusters, but there's nothing to stop you from putting it on a single device!
|
||||
You do lose a few features like file locking, but most features still work.
|
||||
Kopia has S3 support, so it should work with Minio.
|
||||
|
|
@ -34,8 +34,8 @@ minio:
|
|||
volumes:
|
||||
- minio-data:/data
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
env_file:
|
||||
- .minio.env
|
||||
```
|
||||
|
|
@ -43,7 +43,7 @@ minio:
|
|||
Then in `.minio.env`, I enter the root username and password:
|
||||
|
||||
```
|
||||
MINIO_ROOT_USER=...
|
||||
MINIO_ROOT_USER=...
|
||||
MINIO_ROOT_PASSWORD=...
|
||||
```
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ to devices on your Tailscale network. And another feature allows you to generate
|
|||
certificates for your MagicDNS domains. This is really cool because the generated certificates are "real",
|
||||
they are not self-signed certificates and you don't need anything special for browsers and other tools to accept them.
|
||||
|
||||

|
||||

|
||||
|
||||
So putting these together, I enabled MagicDNS and HTTPS certificates for my network. Then,
|
||||
I generated my certificates with `sudo tailscale cert --cert-file public.crt --key-file private.key hostname.network.ts.net`,
|
||||
|
|
@ -83,8 +83,8 @@ minio:
|
|||
- minio-data:/data
|
||||
- ./certs:/certs
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
env_file:
|
||||
- .minio.env
|
||||
```
|
||||
|
|
@ -104,19 +104,28 @@ backups bucket with this policy:
|
|||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:*"],
|
||||
"Resource": ["arn:aws:s3:::backup/*", "arn:aws:s3:::backup"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:ListAllMyBuckets"],
|
||||
"Resource": ["arn:aws:s3:::*"]
|
||||
}
|
||||
]
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::backup/*",
|
||||
"arn:aws:s3:::backup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListAllMyBuckets"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'CSS only placeholder for contenteditable elements'
|
||||
title: "CSS only placeholder for contenteditable elements"
|
||||
date: 2023-07-02T13:06:15-05:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -31,23 +31,24 @@ way to add a placeholder to a `contenteditable` span without javascript. Here it
|
|||
```html
|
||||
<span contenteditable="true" data-placeholder="click on me and type"></span>
|
||||
<style>
|
||||
/* Add the placeholder text */
|
||||
[data-placeholder]::before {
|
||||
content: attr(data-placeholder);
|
||||
/* Or whatever */
|
||||
color: gray;
|
||||
}
|
||||
/* Hide the placeholder when selected, or when there is text inside */
|
||||
[data-placeholder]:focus::before,
|
||||
[data-placeholder]:not(:empty)::before {
|
||||
content: none;
|
||||
}
|
||||
/* Add the placeholder text */
|
||||
[data-placeholder]::before {
|
||||
content: attr(data-placeholder);
|
||||
/* Or whatever */
|
||||
color: gray;
|
||||
}
|
||||
/* Hide the placeholder when selected, or when there is text inside */
|
||||
[data-placeholder]:focus::before, [data-placeholder]:not(:empty)::before {
|
||||
content: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
And here what it looks like:
|
||||
|
||||
{{<raw>}}
|
||||
<iframe width="100%" height="300" src="//jsfiddle.net/SeriousBug/t9hmgyq5/13/embedded/result,html,css/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
{{</raw>}}
|
||||
|
||||
This also works with a `div`, but there is one caveat: If the user types
|
||||
something into the `div` then deletes it, the browser will leave a `<br/>` in
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Getting theme colors in JavaScript using React with DaisyUI and TailwindCSS'
|
||||
title: "Getting theme colors in JavaScript using React with DaisyUI and TailwindCSS"
|
||||
date: 2023-08-10T00:18:27-05:00
|
||||
toc: false
|
||||
images:
|
||||
|
|
@ -23,7 +23,7 @@ this code:
|
|||
|
||||
```ts
|
||||
function getPropertyValue(name: string) {
|
||||
return getComputedStyle(document.body).getPropertyValue(name);
|
||||
return getComputedStyle(document.body).getPropertyValue(name);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -36,10 +36,10 @@ primary and secondary colors, and the colors for the text that goes on top of
|
|||
them.
|
||||
|
||||
```ts
|
||||
const primary = getPropertyValue('--p');
|
||||
const secondary = getPropertyValue('--s');
|
||||
const primaryText = getPropertyValue('--pc');
|
||||
const secondaryText = getPropertyValue('--sc');
|
||||
const primary = getPropertyValue("--p");
|
||||
const secondary = getPropertyValue("--s");
|
||||
const primaryText = getPropertyValue("--pc");
|
||||
const secondaryText = getPropertyValue("--sc");
|
||||
```
|
||||
|
||||
This is all great! But I had one more concern: I needed a way to change these
|
||||
|
|
@ -54,18 +54,18 @@ Here's how that code looks like:
|
|||
|
||||
```ts
|
||||
export function useThemeColor() {
|
||||
const themeFetcher = useCallback(() => {
|
||||
const primary = getPropertyValue('--p');
|
||||
const primaryText = getPropertyValue('--pc');
|
||||
const secondary = getPropertyValue('--s');
|
||||
const secondaryText = getPropertyValue('--sc');
|
||||
return { primary, primaryText, secondary, secondaryText };
|
||||
}, []);
|
||||
const themeFetcher = useCallback(() => {
|
||||
const primary = getPropertyValue("--p");
|
||||
const primaryText = getPropertyValue("--pc");
|
||||
const secondary = getPropertyValue("--s");
|
||||
const secondaryText = getPropertyValue("--sc");
|
||||
return { primary, primaryText, secondary, secondaryText };
|
||||
}, []);
|
||||
|
||||
// The key "data:theme" could be anything, as long as it's unique in the app
|
||||
const { data: color, mutate } = useSWR('data:theme', themeFetcher);
|
||||
// The key "data:theme" could be anything, as long as it's unique in the app
|
||||
const { data: color, mutate } = useSWR("data:theme", themeFetcher);
|
||||
|
||||
return { ...color, mutate };
|
||||
return { ...color, mutate };
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ export default function Dashboard() {
|
|||
|
||||
Here's what that looks like:
|
||||
|
||||

|
||||

|
||||
|
||||
To keep the colors changing whenever the user toggles the theme, you then just
|
||||
have to call the `mutate` function inside your toggle button.
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
title: "Building a Well Organized Rust CLI Tool"
|
||||
date: 2023-12-09T00:05:58-06:00
|
||||
toc: false
|
||||
images:
|
||||
tags:
|
||||
- dev
|
||||
- rust
|
||||
---
|
||||
|
||||
Here's a pattern came up with for building CLI tools in Rust using
|
||||
[Clap](https://crates.io/crates/clap) with derive and using a custom trait.
|
||||
First, define a "run command" trait that all commands will use.
|
||||
|
||||
```rs
|
||||
#[async_trait::async_trait]
|
||||
pub trait RunCommand {
|
||||
async fn run(&self) -> anyhow::Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
I'm using [async-trait](https://crates.io/crates/async-trait) so my commands can
|
||||
be async, and [anyhow](https://crates.io/crates/anyhow) for error handling
|
||||
because I don't particularly care about the types of the errors my commands
|
||||
might return. Neither of these are required though, skip them if you prefer.
|
||||
|
||||
The "trick" is to then implement your `RunCommand` trait for all the commands
|
||||
and subcommands for your Clap parser. This is just how traits work, but what's
|
||||
useful is that it makes it really easy to organize your commands and subcommands
|
||||
into different folders while keeping the definition of the command next to the
|
||||
implementation of that command.
|
||||
|
||||
For example, imagine a program with commands like:
|
||||
|
||||
```
|
||||
myprogram query --some-params
|
||||
myprogram account create --other-params
|
||||
myprogram account delete --even-more-params
|
||||
```
|
||||
|
||||
I'd recommend organizing the code into a folder structure like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── account
|
||||
│ ├── create.rs
|
||||
│ ├── delete.rs
|
||||
│ └── mod.rs
|
||||
├── mod.rs
|
||||
├── query.rs
|
||||
└── run_command.rs <-- the RunCommand trait is defined here
|
||||
```
|
||||
|
||||
These files will then look like this:
|
||||
|
||||
##### `account/create.rs`:
|
||||
|
||||
```rs
|
||||
#[derive(Debug, Args)]
|
||||
pub struct AccountCreate { /* params */ }
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RunCommand for AccountCreate {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
// Your command implementation here!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### `account/delete.rs`
|
||||
|
||||
Omitted for brevity, same as create
|
||||
|
||||
##### `account/mod.rs`:
|
||||
|
||||
```rs
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountCommands {
|
||||
Create(AccountCreate),
|
||||
Delete(AccountDelete),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RunCommand for AccountCommands {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Self::Create(cmd) => cmd.run().await,
|
||||
Self::Delete(cmd) => cmd.run().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Account {
|
||||
#[command(subcommand)]
|
||||
command: AccountCommands,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RunCommand for Account {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
self.command.run().await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### `query.rs`:
|
||||
|
||||
```rs
|
||||
#[derive(Debug, Args)]
|
||||
pub struct QueryCommand { /* params */ }
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RunCommand for QueryCommand {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
// Your command implementation here!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### `mod.rs`
|
||||
|
||||
Finally tying it all together:
|
||||
|
||||
```rs
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
// Any global options that apply to all commands go below!
|
||||
#[arg(short, long)]
|
||||
pub verbosity: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RunCommand for Cli {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
if let Some(command) = &self.command {
|
||||
command.run().await?;
|
||||
exit(0);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
Account(Account),
|
||||
Query(QueryCommand),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RunCommand for Commands {
|
||||
async fn run(&self) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Commands::Account(account) => account.run().await,
|
||||
Commands::Query(query) => query.run().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, in `main` all you have to do is call your trait:
|
||||
|
||||
```rs
|
||||
let cli = Cli::parse();
|
||||
cli.run().await.unwrap();
|
||||
```
|
||||
|
||||
I love the filesystem-routing like feel of this. Whenever you see a command in
|
||||
the CLI, you immediately know what file to go to if you need to change or fix
|
||||
something. Please don't discount the value of this! As your application grows,
|
||||
it can be a massive headache to come across a bug and then having to dig around
|
||||
the entire codebase to find where that code is coming from. If all commands have
|
||||
a particular file, you can always start your search from that file and use "go
|
||||
to definition" to dig into the implementation.
|
||||
This also helps if you are adding a new command, it's immediately obvious where
|
||||
the code needs to go. Simple but elegant, and a significant productivity boost.
|
||||
208
content/posts/2024.01.01.mobile-dev-what-i-enjoy-using.md
Normal file
208
content/posts/2024.01.01.mobile-dev-what-i-enjoy-using.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
---
|
||||
title: "What I'm enjoying using for mobile app development"
|
||||
date: 2024-01-02T04:33:55Z
|
||||
draft: false
|
||||
toc: false
|
||||
images:
|
||||
tags:
|
||||
- dev
|
||||
- react
|
||||
- mobile
|
||||
- typescript
|
||||
---
|
||||
|
||||
I like baking bread. Well, most of all I love bread, and baking it myself is
|
||||
more convenient than tracking down a good bakery nearby. I used to bake a lot
|
||||
--and as a hipster I'll tell you that it was before everyone else started doing
|
||||
it at the start of the pandemic-- but I got bored of it some time ago and quit.
|
||||
But I've been eating more bread recently, and decided I could bake something
|
||||
better, and cheaper, than the bread from the grocery store. I'm pretty happy
|
||||
with what I've been baking too, they are all turning out with a good crust and
|
||||
soft insides. What's the inside of the bread called? Anyway, my bread is good
|
||||
but a little bland, so I decided to also get back into sourdough bread to get
|
||||
some nice flavor.
|
||||
|
||||
Sourdough bread is made with sourdough starter, a live culture of yeast and
|
||||
bacteria that needs to be maintained with regular feedings of flour. So before
|
||||
you can bake sourdough bread, you must first prepare your starter and grow it
|
||||
for a few weeks to get its strength up, then keep it alive for as long as you
|
||||
want to keep baking. And before you can prepare a sourdough starter, you must
|
||||
first develop a mobile app.
|
||||
|
||||
Well, of course I could have just set up reminder in my calendar and skip having
|
||||
to develop an actual app. Or just use on of the proprietary ones. But why do the
|
||||
easy thing when you can waste tens --if not hundreds-- of hours developing an
|
||||
app to maybe save a few seconds of your time or to make a point about using open
|
||||
source software? At least it will teach me some stuff along the way.
|
||||
|
||||
## What tech to use?
|
||||
|
||||
But before I could start developing an app, I had to decide what to build it
|
||||
with. My first two requirements: it had to be cross platform, and it had to be
|
||||
able to show scheduled notifications on my phone. My first thought was to use a
|
||||
PWA. PWAs are web apps that can be "installed" on your phone like a regular app,
|
||||
but run entirely on your browser. It's sort of an Electron-lite. That would be
|
||||
great since I already know a lot about web development, alas PWAs can't send
|
||||
scheduled notifications while the app is in the background. That's a pretty
|
||||
vital part of this project so that rules out PWAs. Also I think Safari has some
|
||||
problems with PWAs? So that also would have ruled it out if I somehow worked
|
||||
around the notifications.
|
||||
|
||||
Similar to PWAs, I could have used something like Electron to develop an app
|
||||
using web technologies. In this case it would be an actual app, so I could
|
||||
actually run code in the background and send scheduled notifications. Hurray!
|
||||
Except that I wasn't sure I wanted to do this. My experience with apps developed
|
||||
this way is terrible, and it's often very obvious someone basically put a web
|
||||
browser in kiosk mode. These apps are often slow, the UIs are completely devoid
|
||||
of the native look and feel, and they are often full of weird browserisms like
|
||||
being able to hold and select the text on the page. Maybe this perception is a form of [Survivorship bias](https://en.wikipedia.org/wiki/Survivorship_bias#In_the_military)
|
||||
and there are lots of apps I used developed with these technologies that work great,
|
||||
but I decided to see what else was available first.
|
||||
|
||||
After more digging, I finally narrowed my choices down to 2 options: React
|
||||
Native and Flutter. React Native takes the usual React, and instead of rendering the virtual DOM into a real DOM it renders it
|
||||
to native UI components. React Native is something I used before, with my [Bulgur Cloud](https://bulgur-cloud.github.io) project.
|
||||
While I liked React Native, I actually ended up rewriting Bulgur Cloud --in Next.js-- so I was a little hesitant.
|
||||
Flutter on the other hand uses Dart, and is developed by Google with a focus on cross platform apps.
|
||||
Flutter works by rendering everything to a canvas... which sort of surprised me.
|
||||
My biggest concern with this was performance and accessibility, using native components
|
||||
gives you both out of the box. If you render everything yourself, then it's
|
||||
your responsibility to handle accessibility and optimize rendering.
|
||||
Articles and discussions I came across online reassure me that Flutter is "doing better" at all of these,
|
||||
so hopefully it's not a problem.
|
||||
|
||||
At this point I was still paralyzed by this decision, so I ended up creating a project with both to check out
|
||||
the out of the box example. Which really did not help, they both work and look fine.
|
||||
The code is pretty easy to understand with both. I have concerns with the future of both projects,
|
||||
because React Native feels too under control of Expo and Flutter feels too under control of Google.
|
||||
|
||||
I finally broke my paralysis by just going with React Native. I already know
|
||||
React, I know a ton of stuff about the React ecosystem, I've already used React
|
||||
Native before. Flutter and the Dart language look fine and I'm sure I could
|
||||
learn them, but it's just easier for me to get this project off the ground if I
|
||||
use what I already know. I also remembered that the reason why I rewrote Bulgur
|
||||
Cloud was because React Native for Web is a bad experience, but in this case I
|
||||
don't care about the web so it's not a problem.
|
||||
|
||||
## To Expo, or not to Expo?
|
||||
|
||||
When you start using React Native, the first thing you see is Expo. Look at the
|
||||
official getting started guide for React Native and it immediately recommends
|
||||
you use Expo. Expo then keeps coming up again and again in the docs. It's easy
|
||||
to see why:
|
||||
|
||||
- Expo solves a major getting started hurdle: installing Android Studio or XCode
|
||||
and connecting it to your phone so you can run it. Instead, you install the
|
||||
Expo Go app from your phones app store and scan a QR code to get started
|
||||
immediately.
|
||||
- Expo comes with a lot of built-in packages. If you go with Expo, you can use
|
||||
all these packages *and* you can still use all the regular React Native
|
||||
packages.
|
||||
- Expo doesn't technically lock you into anything, you can "eject" from Expo at
|
||||
any time.
|
||||
|
||||
I decided to use Expo. While I have some concerns that Expo is an
|
||||
investor-backed company and not a community effort, the Expo packages are all
|
||||
open source. Expo Go is open source. And you can still install Android Studio
|
||||
and XCode and build everything yourself.
|
||||
|
||||
## What else?
|
||||
|
||||
Expo comes with a lot out of the box, but there were some packages I wanted to
|
||||
play with and some packages I knew I wanted on top of that. Some of these include:
|
||||
|
||||
### Tamagui
|
||||
|
||||
[Tamagui](https://tamagui.dev) is a UI kit for React Native. I first saw it
|
||||
advertised as a "headless" library meaning that it implements all the
|
||||
functionality but comes with no styles, so you style everything yourself. I
|
||||
later found out that this was not entirely correct though because Tamagui does
|
||||
come with default styles, but lets you opt out of them or customize them.
|
||||
|
||||
I really like Tamagui because it comes as a complete package. It's not just UI
|
||||
components but also animations, CSS shorthands and a token based design system.
|
||||
It also comes with a cool "sub-theme" system that allows you to have variants of
|
||||
your themes, so you don't just have a "dark" theme but you can also have
|
||||
"dark-forest" and "dark-blue" and "dark-whatever" to create different themes for
|
||||
different sections of you app without having to hand-code everything.
|
||||
|
||||
### Expo-Sqlite
|
||||
|
||||
[Expo-sqlite](https://docs.expo.dev/versions/latest/sdk/sqlite/) is an expo package, but it's an optional one.
|
||||
It gives you Sqlite. That's about all. Sqlite is awesome, so why not.
|
||||
There are some other options for storage, some even support additional features like encryption
|
||||
if that's something you need. But I don't think it's crucial if I encrypt
|
||||
my sourdough starter data, so I'd rather take the Sqlite features.
|
||||
|
||||
### nearform/sql
|
||||
|
||||
[@nearform/sql](https://github.com/nearform/sql) is a library for SQL injection
|
||||
prevention. What's cool about it is that you get to write SQL queries with the
|
||||
javascript string interpolation without risking SQL injection. So you can do:
|
||||
|
||||
```js
|
||||
DB.query(SQL`INSERT INTO students(name) VALUES (${name})`);
|
||||
```
|
||||
|
||||
without little [Bobby Tables](https://xkcd.com/327/) ruining your day.
|
||||
|
||||
nearform/sql is not compatible with Expo-sqlite out of the box, but a basic
|
||||
wrapper function can easily get you there. Here it is, I'd
|
||||
publish it on npm but it's so short you might as well just copy and paste it.
|
||||
|
||||
```ts
|
||||
export function sql(strings: any, ...values: any[]): Query {
|
||||
const statement = SQL(strings, ...values);
|
||||
return {
|
||||
sql: statement.text,
|
||||
args: statement.values,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### SWR
|
||||
|
||||
[SWR](https://swr.vercel.app) describes itself as "React Hooks for Data
|
||||
Fetching". My little app is local only and doesn't need to fetch data from
|
||||
anywhere, so why SWR? Well, SWR is great whenever you need to get data from
|
||||
anywhere outside of your apps own state. In my case, I need to get data out of
|
||||
SQLite and into my app, so I use SWR to fetch the data from the database.
|
||||
|
||||
The main use case for me is the caching and deduplication. SWR caches results
|
||||
and won't fetch again if the same hook is called multiple times, so you can use
|
||||
your hooks everywhere without having to worry about the same data being fetched
|
||||
multiple times for no reason. You can also invalidate the cache whenever you
|
||||
want, which will invalidate all users of the hook and fetch the data just once
|
||||
to update everything.
|
||||
|
||||
You need some workarounds to get SWR working in React Native, but [official docs have you covered](https://swr.vercel.app/docs/advanced/react-native.en-US).
|
||||
|
||||
### Formik
|
||||
|
||||
I was hesitant and I dragged my feet the very first time I encountered
|
||||
[Formik](https://formik.org), but after trying it once I actually fell in love
|
||||
with it. It really does everything you need a form to do. The best part to me is
|
||||
that you get to avoid having a million `useState`s in your form, and thanks to
|
||||
the Formik context you can organize your code without drilling values and
|
||||
setters down into components.
|
||||
|
||||
You have to do a bit of work for Formik to work in React Native, but the [official guide tells you what to do](https://formik.org/docs/guides/react-native)
|
||||
so it's an easy change. Wrap that in a custom form input component and you can forget
|
||||
the incompatibility exists at all.
|
||||
|
||||
## Some More Packages
|
||||
|
||||
This is getting really long (for my usual posts), so a few more quick mentions:
|
||||
|
||||
- `date-fns` is the definitive date & time library. It's incredible.
|
||||
- `radash` is like lodash. I'm honestly not fully sure if it's better or worse, but I do keep coming back to it.
|
||||
- `rrule` makes calculating recurring dates easy. You can serialize and deserialize your rrule's which I also appreciate.
|
||||
- `ulidx` is a random ID generator. It's like nanoid, but at the cost of just a few more characters they are ordered by creation date.
|
||||
- `zod` is my favorite schema tool. Never leave home without it.
|
||||
|
||||
## Fin
|
||||
|
||||
I'm about done with the basic functionality I wanted for this app, so I'm hoping
|
||||
to have a version of this up on the Google Play store by the end of the week.
|
||||
|
||||
Oh and I still haven't made that sourdough starter,
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue