the home site for me: also iteration 3 or 4 of my site

Compare changes

Choose any two refs to compare.

Changed files
+9677 -1817
.github
content
highlight_themes
hooks
sass
scripts
static
badges
blog
adding-a-copy-button
airbuds
analyzing-implications-of-online-safety-legislation
atuin
degraded-zpool-proxmox
exporting-from-plausible
garmin-vivoactive-homeassistant
hilton-tomfoolery
install-truenas-core-proxmox
mega
monaspace-vs-code-install
my-animations
my-life-story-with-tech
remove-exif-git-hook
spherical-ray-diagrams
spotify-to-apple-music
ssd-removal-mbp-2017
tangled-sync
test-post
favicon
js
now
pfp
pfps
tags
accessibility
apple
archival
atproto
biography
cool-stuff
essays
fancy
graphql
hilton
homelab
meta
mildrant
music
nix
physics
project
reverse-engineering
shell
teardown
tool
tutorial
yap-fest
verify
syntaxes
templates
tools
.github/images/og.png

This is a binary file and will not be displayed.

.github/images/preview.webp

This is a binary file and will not be displayed.

.github/images/ss.png

This is a binary file and will not be displayed.

+4 -1
.gitignore
···
public
+
.zola-build
node_modules
-
.env
+
.env
+
.crush
+
.DS_Store
+5
.imgbotconfig
···
+
{
+
"schedule": "daily",
+
"aggressiveCompression": "true"
+
}
+
+45 -12
README.md
···
-
# site@zera
+
<h3 align="center">
+
<img src="https://cloud-4mfbnf9u2-hack-club-bot.vercel.app/0img_3132.png" width="350" alt="site@zera"/>
+
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/transparent.png" height="30" width="0px"/>
+
</h3>
+
+
<p align="center">
+
<i>My site v4 (i think)</i>
+
</p>
+
+
<p align="center">
+
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break-thin.svg" />
+
</p>
+
+
![screenshot of the website](https://raw.githubusercontent.com/taciturnaxolotl/zera/refs/heads/main/.github/images/preview.webp)
+
-
<figure align="center">
-
<img src="https://github.com/kcoderhtml/zera/blob/master/.github/images/ss.png?raw=true" alt="screenshot of the website"/>
-
<figcaption><i>My site v4 (i think)</i></figcaption>
-
</figure>
+
<p align="center">
+
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break-thin.svg" />
+
</p>
## Special Features
- The whole website can be statically rendered in `~93ms`
- Deployed via cloudflare pages with a total push to deploy time of `~20s`
-
- self hosted analytics with plausible, publicaly accessible at this [dashboard](https://nexus.kieranklukas.com/dunkirk.sh/)
+
- blazing fast privacy preserving view counter with [abacus](https://jasoncameron.dev/abacus/)
+
```html
+
<script>
+
function cb(res) {
+
const fmt = new Intl.NumberFormat('en', { notation: 'compact' });
+
const elements = document.querySelectorAll("[id='visits']");
+
elements.forEach(el => {
+
el.innerText = fmt.format(res.value);
+
el.title = res.value + " visits";
+
});
+
}
+
</script>
+
<script async src="https://abacus.jasoncameron.dev/hit/namespace/counter?callback=cb"></script>
+
```
- Automatic OG image via a custom script using puppeteer.
-
![og image example](https://github.com/kcoderhtml/zera/blob/master/.github/images/og.png?raw=true)
+
![og image example](https://raw.githubusercontent.com/taciturnaxolotl/zera/refs/heads/main/static/blog/hilton-tomfoolery/og.png)
## Awesome projects that made this possible
Huge thanks to [Speyll/anemone](https://github.com/Speyll/anemone) for the template that helped me understand [Zola](https://www.getzola.org/)
-
This site's theme is based off of the awesome project [Speyll/suCSS/](https://github.com/) with my own flavoring on top and the code theme is based off of [uncomfyhalomacro/catppuccin-zola](https://github.com/uncomfyhalomacro/catppuccin-zola) modified to work with `data-theme`.
+
This site's theme is based off of the awesome project [Speyll/suCSS/](https://github.com/) with my own flavoring on top and the code theme is based off of [uncomfyhalomacro/catppuccin-zola](https://github.com/uncomfyhalomacro/catppuccin-zola) modified to work with `data-theme` (and then removed again lol).
-
---
+
<p align="center">
+
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+
</p>
-
_ยฉ 2024 Kieran Klukas_
-
_Content Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)_
-
_Code Licensed under [AGPL 3.0](LICENSE.md)_
+
<p align="center">
+
<code>&copy 2024-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a></code>
+
</p>
+
+
<p align="center">
+
<a href="https://github.com/taciturnaxolotl/zera/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=Code License&message=AGPL 3.0&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a>
+
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=Content License&message=CC BY-NC-SA 4.0&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a>
+
</p>
+25 -29
biome.json
···
{
-
"$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
-
"vcs": {
-
"enabled": false,
-
"clientKind": "git",
-
"useIgnoreFile": false
-
},
-
"files": {
-
"ignoreUnknown": false,
-
"ignore": []
-
},
-
"formatter": {
-
"formatWithErrors": true,
-
"enabled": true,
-
"indentStyle": "tab"
-
},
-
"organizeImports": {
-
"enabled": true
-
},
-
"linter": {
-
"enabled": true,
-
"rules": {
-
"recommended": true
-
}
-
},
-
"javascript": {
-
"formatter": {
-
"quoteStyle": "double"
-
}
-
}
+
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
+
"vcs": {
+
"enabled": false,
+
"clientKind": "git",
+
"useIgnoreFile": false
+
},
+
"files": {
+
"ignoreUnknown": false
+
},
+
"formatter": {
+
"formatWithErrors": true,
+
"enabled": true,
+
"indentStyle": "tab"
+
},
+
"linter": {
+
"enabled": true,
+
"rules": {
+
"recommended": true
+
}
+
},
+
"javascript": {
+
"formatter": {
+
"quoteStyle": "double"
+
}
+
}
}
+241
bun.lock
···
+
{
+
"lockfileVersion": 1,
+
"configVersion": 1,
+
"workspaces": {
+
"": {
+
"dependencies": {
+
"dotenv": "^16.4.7",
+
"glob": "^13.0.0",
+
},
+
"devDependencies": {
+
"@types/bun": "latest",
+
"puppeteer": "^23.6.0",
+
},
+
},
+
},
+
"packages": {
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
+
+
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
+
+
"@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
+
+
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
+
+
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
+
+
"@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],
+
+
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
+
+
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
+
+
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
+
+
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
+
+
"bare-fs": ["bare-fs@4.5.2", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw=="],
+
+
"bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="],
+
+
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
+
+
"bare-stream": ["bare-stream@2.7.0", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A=="],
+
+
"bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="],
+
+
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+
"basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
+
+
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+
+
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
+
+
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
+
+
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+
"chromium-bidi": ["chromium-bidi@0.11.0", "", { "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA=="],
+
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
+
+
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
+
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
+
+
"devtools-protocol": ["devtools-protocol@0.0.1367902", "", {}, "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="],
+
+
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
+
+
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+
+
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
+
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
+
+
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
+
+
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
+
+
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
+
+
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
+
+
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
+
+
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
+
+
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
+
+
"glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
+
+
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
+
+
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
+
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+
+
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
+
+
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
+
+
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
+
+
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
+
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
+
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
+
+
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
+
+
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
+
+
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
+
+
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
+
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+
+
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
+
+
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
+
+
"puppeteer": ["puppeteer@23.11.1", "", { "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1367902", "puppeteer-core": "23.11.1", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw=="],
+
+
"puppeteer-core": ["puppeteer-core@23.11.1", "", { "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "debug": "^4.4.0", "devtools-protocol": "0.0.1367902", "typed-query-selector": "^2.12.0", "ws": "^8.18.0" } }, "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg=="],
+
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+
+
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
+
+
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
+
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
+
+
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+
"tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
+
+
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
+
+
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
+
+
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
+
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+
"typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="],
+
+
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
+
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
+
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
+
+
"zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
+
+
"proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
+
}
+
}
bun.lockb

This is a binary file and will not be displayed.

+7 -9
config.toml
···
compile_sass = true
minify_html = true
generate_feeds = true
+
feed_filenames = ["rss.xml", "atom.xml"]
default_language = "en"
-
taxonomies = [
-
{name = "tags", feed = true},
-
]
+
taxonomies = [{ name = "tags", feed = true }]
[markdown]
render_emoji = true
···
highlight_code = true
highlight_theme = "css"
+
extra_syntaxes_and_themes = ["syntaxes"]
[slugify]
paths = "on"
···
author = "Kieran Klukas"
display_author = true
-
favicon = "favicon.ico"
-
image = ""
-
default_theme = "light"
list_pages = false
twitter_card = true
header_nav = [
-
{ url = "/", name = "/home/" },
-
{ url = "/pfp", name = "/pfp/" },
-
{ url = "/blog", name = "/blog/" }
+
{ url = "/", name = "/root" },
+
{ url = "/verify", name = "/verify" },
+
{ url = "/now", name = "/now" },
+
{ url = "/blog", name = "/blog" },
]
+29 -22
content/_index.md
···
+++
+++
-
<div style="display: flex; justify-content: center;">
-
<img src="/pfps/current.webp" alt="an image of kieran holding a white kitten" width="512" height="512"/>
+
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 2rem;">
+
<img src="/pfps/fall.jpg" alt="kieran wearing a robotics sweatshirt and standing in front of a tree with fall leaves" width="512" height="512" class="u-photo"/>
+
{{ is() }}
</div>
-
## About me
+
# About me
+
+
Erlo! My name is Kieran Klukas i'm {{ age(length=0) }} years old and love cyber, anything with micro-controllers, obscure languages, nix :nix:, and yummy food :)
-
Erlo! My name is Kieran Klukas and i'm a homeschooled coder who is {{ age(length=0) }} years old and loves film making, fpv, and typescript :)
+
> flake.nix
-
> init.ts
-
```ts
-
const kieran = {
-
name: "kieran klukas"
-
age: {{ age(length=2) }}
-
education: ["Homeschooled", "Dual Enrollment"]
-
favFoods: ["lo mein", "bacon fried rice", "pretty much any meat"]
+
```nix
+
{
+
description = "a short bit about me";
+
+
outputs = { self, ... }:
+
let
+
kieran = rec {
+
name = "Kieran Klukas";
+
pronouns = "he/him";
+
aliases = [ "taciturnaxolotl" "krn" ];
+
location = "Westerville, Ohio, USA";
+
hobbies = [ "frc" "ctfs" "random side projects"];
+
};
+
in
+
{
+
inherit kieran;
+
};
}
```
-
A few special features this site has
-
- The whole website can be statically rendered in `~93ms`
-
- Deployed via cloudflare pages with a total push to deploy time of `~20s`
-
- Automatic OG image via a custom script using puppeteer.
-
- self hosted analytics with plausible, publicaly accessible at this [dashboard](https://nexus.kieranklukas.com/dunkirk.sh/)
+
this site has page hits (<code id="visits">0</code> and counting) via [abacus](https://jasoncameron.dev/abacus/) but they are completely anonymous and just http requests so no sketchy analytics here!
-
## Want to talk to me?
+
# Want to talk to me?
-
Do you want to hire me? (I will answer immediately :) If you just have a question or want to talk I'll still answer (admittedly more sadly).
+
I'm open to projects or just random questions! Feel free to reach out with any of the following or anything on [/verify](/verify)
-
- Email: [me@dunkirk.sh](mailto:me@dunkirk.sh)
-
- Matrix: [@kieran:dumpsterfire.icu](https://matrix.to/#/@kieran.matrix.dumpsterfire.icu)
+
- Email: [kieran@dunkirk.sh](mailto:kieran@dunkirk.sh)
- Hackclub Slack: [@krn](https://hackclub.slack.com/team/U062UG485EE) (only if you are a highschooler or younger; [join here](https://hackclub.com/slack/))
-
-
_I wouldn't count on reaching me via Matrix. I tend to check it once in a blue moon; email is probably your best bet._
+
- If you just want to know when I make a new post then you can subscribe to the [:rss:](rss.xml) feed
+22 -21
content/blog/2023-07-10_install-truenas-core-proxmox.md
···
[taxonomies]
tags = ["tutorial", "archival"]
-
-
[extra]
-
has_toc = true
+++
-
{{ img(id="https://cloud-f19fn8u8j-hack-club-bot.vercel.app/0image.png" alt="screenshot of the vault vm in proxmox" caption="my active vault storing 1.8TB of old projects") }}
+
I have been using Proxmox for a while now but I've also wanted to make use of some large HDDs that have been lying around. I really didn't want to get another machine just for TrueNAS so I decided to install it on Proxmox. This is how I did it.
+
+
<!-- more -->
+
+
![screenshot of the vault vm in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/81f100961a1c4033c0c50c9192b48521e968a8e8_0image.png){caption="my active vault storing 1.8TB of old projects"}
## Introduction
···
Sign-in to Proxmox and upload your ISO to the local storage or, download the file directly from the link using the built-in ISO fetcher.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Ww212rUDQ_Ms9P2WhJMwz.png" alt="download iso tool in proxmox") }}
+
![download iso tool in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/45ce25b0bd1accf6a31004e0d71dc92783151266_Ww212rUDQ_Ms9P2WhJMwz.png)
Next to create the VM, the only thing that needs to be changed from the defaults is the memory, which I set to `8192 MB` (8 GB).
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/bDT9VdIMMG1LWvv1RwNKl.png" alt="create a vm modal in proxmox") }}
+
![create a vm modal in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/1e5d72e4a50907ad23fc1f24a7163cb4898f9bd2_bDT9VdIMMG1LWvv1RwNKl.png)
Now finish creating the VM and click on the VM after it is created. Go to options and enable start at boot.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/ImxOHWJNuRL3yiF12cQfe.png" alt="start at boot checkbox") }}
+
![start at boot checkbox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/81a9d9897274eaac53e0d2cc3ff7075cb4ba8cdd_ImxOHWJNuRL3yiF12cQfe.png)
Next, we need to pass through the physical drives to the VM. Open a terminal on the Proxmox server (use the built-in terminal or ssh in) and run the following command. Only run the part after the #.
···
Now find your VM_ID, mine is 102.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/gwjgFbI5IrnJSTLTB0PeX.png" alt="vm list in proxmox") }}
+
![vm list in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/ac8724875dd07e84903cfe0004a435041a21c35c_gwjgFbI5IrnJSTLTB0PeX.png)
Run the following command, replacing the VM_ID and DISK_ID with yours.
···
Here is how it appears in Proxmox:
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/QBpmqflEHmPiUHbd8JVk2.png" alt="hardware page of the vm in proxmox") }}
+
![hardware page of the vm in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c313b32c8c099ea8644663e3d796c12a861ac8e8_QBpmqflEHmPiUHbd8JVk2.png)
If everything went well, then you can start your VM now. After it finishes booting up, you will get the screen below. Make sure Install/Upgrade is selected and hit enter.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/UFqhrRdD3GkP1_No5lWaj.png" alt="truenas startup screen") }}
+
![truenas startup screen](https://hc-cdn.hel1.your-objectstorage.com/s/v3/0eb213f7699f79c389328cdc9f591bcde475e4fe_UFqhrRdD3GkP1_No5lWaj.png)
You will then get this screen, use space to select the first drive and hit enter.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/xD5QxmFtHxw10p624FgwM.png" alt="destination media screen") }}
+
![destination media screen](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c53f8a58f746d0ae045882c9563d83219ab9f29f_xD5QxmFtHxw10p624FgwM.png)
Hit enter one last time and enter your password.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/MZy3mN1cXBaicgVolVYs5.png" alt="confirm erase page") }}
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/KWq2P7Iok9LThOF5Xoj6l.png" alt="repeat password page") }}
+
![confirm erase page](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a0f2b37568b394eaff15e1b487ab68bbc3736cd1_MZy3mN1cXBaicgVolVYs5.png)
+
![repeat password page](https://hc-cdn.hel1.your-objectstorage.com/s/v3/0bccd9e726a027cf96ab98a8b313ea8c710b46f5_KWq2P7Iok9LThOF5Xoj6l.png)
Select BIOS, as this is the default mode for Proxmox VMs.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/RfXwEGx6oug1vVF3UZuCj.png" alt="boot via bios or via uefi screen") }}
+
![boot via bios or via uefi screen](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7f52c721a7fcb2542552d997224f7d02c3ebba9b_RfXwEGx6oug1vVF3UZuCj.png)
After about five to ten minutes, the installation process will finish and the VM will ask you to remove installation media and reboot.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/mFEH-FHY10H7NUAvYi0aE.png" alt="installation succeded message") }}
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/JPXkEQJgBmeATEE40HHpr.png" alt="hardware screen in proxmox") }}
+
![installation succeded message](https://hc-cdn.hel1.your-objectstorage.com/s/v3/f28005427ff5ea328550a079dcb7a65d07f09c95_mFEH-FHY10H7NUAvYi0aE.png)
+
![hardware screen in proxmox](https://hc-cdn.hel1.your-objectstorage.com/s/v3/acd7be1e3ac4903269c9af7e808287a94eb96398_JPXkEQJgBmeATEE40HHpr.png)
Select the installation media and remove it with the top button, go back to the console and hit enter, which will take you back to the main menu. On the main menu, select reboot with the arrow keys and hit enter.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/IfvdMuF6AVU_f0-_rngqq.png" alt="power options screen in truenas") }}
+
![power options screen in truenas](https://hc-cdn.hel1.your-objectstorage.com/s/v3/52eb8ec8c018c185d3a0361de58be8351b61fa79_IfvdMuF6AVU_f0-_rngqq.png)
Once the machine restarts, it will display an IP address in the console.
-
{{ img(id="https://cloud-pur64l07h-hack-club-bot.vercel.app/0image.png" alt="ip address displayed in proxmox console") }}
+
![ip address displayed in proxmox console](https://hc-cdn.hel1.your-objectstorage.com/s/v3/80f56ad99e3e9fa8a997798096214dd5592c40ac_0image.png)
Upon connecting to the IP address, you will get this screen. Use the root username and the password, previously configured, to login.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/ghvCsvwAJMudUCUGvcnCu.png" alt="truenas web ui signin page") }}
+
![truenas web ui signin page](https://hc-cdn.hel1.your-objectstorage.com/s/v3/5211ac631426d7ffb4add091c4308f460b5ef746_ghvCsvwAJMudUCUGvcnCu.png)
Once logged in, I updated the system using the button on the home screen.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/nrBop3a9ilvuc7h-0WPEG.png" alt="check for updates button in the truenas web ui") }}
+
![check for updates button in the truenas web ui](https://hc-cdn.hel1.your-objectstorage.com/s/v3/392e592dd232096380a29f4e769b80eb290fee31_nrBop3a9ilvuc7h-0WPEG.png)
I chose not to save the configuration file when prompted, proceeded to install the updates, and rebooted.
I hope you enjoyed the tutorial! My inspiration to make this came from watching [โ€œHow to run TrueNAS on Proxmox?โ€](https://www.youtube.com/watch?v=M3pKprTdNqQ) by [Christian Lempa](https://www.youtube.com/@christianlempa). I encourage you to watch his video if you want a video guide to installing TrueNAS on Proxmox.
-
* Written on `2023-07-10` and republished to this blog (with minor edits) on `2024-10-31`
+
* Written on `2023-07-10` and republished to this blog (with minor edits) on `2024-10-31`
+13 -14
content/blog/2023-08-04_garmin-vivoactive-homeassistant.md
···
[taxonomies]
tags = ["essays", "archival"]
-
-
[extra]
-
has_toc = true
+++
-
{{ img(id="https://cloud-au4cbwyfl-hack-club-bot.vercel.app/0img_3051.jpg" alt="a garmin watch with the apicall app open to a spotify page" caption="I can control spotify from my watch via api hooks how bout you?") }}
+
This morning I saw a [Reddit post](https://libreddit.kieranklukas.com/r/flipperzero/comments/ybjsvt/flipper_control_via_smartwatch/) where someone connected their flipper zero to a Fossil HR through [Gadgetbridge](https://gadgetbridge.org/). I immediately started [ducking,](https://libreddit.kieranklukas.com/r/duckduckgo/wiki/index#wiki_what_is_searching_on_duckduckgo_called.3F) trying to find out if I could do the same with my Garmin Vivoactive 4 but ended up realizing that there was no apparent way to connect the two. I did however find a widget compatible with my watch named [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) on the Connect IQ store.
-
This morning I saw a [Reddit post](https://libreddit.kieranklukas.com/r/flipperzero/comments/ybjsvt/flipper_control_via_smartwatch/) where someone connected their flipper zero to a Fossil HR through [Gadgetbridge](https://gadgetbridge.org/). I immediately started [ducking,](https://libreddit.kieranklukas.com/r/duckduckgo/wiki/index#wiki_what_is_searching_on_duckduckgo_called.3F) trying to find out if I could do the same with my Garmin Vivoactive 4 but ended up realizing that there was no apparent way to connect the two. I did however find a widget compatible with my watch named [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) on the Connect IQ store.
+
<!-- more -->
+
+
![a garmin watch with the apicall app open to a spotify page](https://hc-cdn.hel1.your-objectstorage.com/s/v3/3893acb0d518216e8051bb59f602748ffde69a68_0img_3051.jpg){caption="I can control spotify from my watch via api hooks how bout you?"}
This widget interested me because it allowed me to call any webhook I wanted utilizing the onboard Wi-Fi as well as through the Connect IQ app. This was a very important feature for me because I canโ€™t get the app to run on LineageOS as it keeps asking for the location permission even though it was already granted.
···
Now for the Google Assistant SDK / APICall / Home Assistant tutorial. The first thing you want to do is follow this guide, [Google Assistant SDK - Home Assistant](https://www.home-assistant.io/integrations/google_assistant_sdk#configuration), to install the Assistant SDK. Once you have completed that, go to Settings / Automations & Services.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Yha1bUhOH_iuWK30QR0F1.png" alt="arrow pointing to settings in home assistant") }}
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/RR0VzZqsU7uTxiNlqVGum.png" alt="arrow pointing to Automations & Services in home assistant") }}
+
![arrow pointing to settings in home assistant](https://hc-cdn.hel1.your-objectstorage.com/s/v3/24972b625b42a5348d22eebcd2116df317fa7a99_Yha1bUhOH_iuWK30QR0F1.png)
+
![arrow pointing to Automations & Services in home assistant](https://hc-cdn.hel1.your-objectstorage.com/s/v3/91729592f4a9cee25691c1e013244b5f3fb0d22b_RR0VzZqsU7uTxiNlqVGum.png)
This is where you can create the action that you want to trigger with your smartwatch. The first thing you need to do is to create a new automation. Save and name the automation you just created. Now add a trigger, scroll to the bottom of the list and select webhook. If done successfully, it will look like the image below.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/VqiM4d3wncM9BuoDR_FW7.png" alt="creating a new webhook in home assistant") }}
+
![creating a new webhook in home assistant](https://hc-cdn.hel1.your-objectstorage.com/s/v3/fe82d82d970e7560e285e4606b5f17d842ab4c6b_VqiM4d3wncM9BuoDR_FW7.png)
Now add an action. I decided to use the media player to play a song on Spotify. Also go back to the webhook section and click the settings icon next to the webhook ID. Change the settings to reflect below screenshot.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Xh3BtyMxA1MhI0rHuo3WG.png" alt="editing the webook in home assistant to allow GET queries") }}
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/rAbDGMrBS5fcGo7AzPT-O.png" alt="adding a play media block to the webhook") }}
+
![editing the webook in home assistant to allow GET queries](https://hc-cdn.hel1.your-objectstorage.com/s/v3/53c6ef6999ab4a636983cfcc879b75c4d7ba0375_Xh3BtyMxA1MhI0rHuo3WG.png)
+
![adding a play media block to the webhook](https://hc-cdn.hel1.your-objectstorage.com/s/v3/382d74c38d3323197636f0af125d3a5faff13e7f_rAbDGMrBS5fcGo7AzPT-O.png)
Now for the fun part. Download [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) onto your Garmin smartwatch and go to the configuration section for the app.
> Note: Iโ€™ll be using Garmin Express on my MacBook, but you can also use the Garmin Connect app on a phone.
-
{{ img(id="https://cloud-hsopd7dwj-hack-club-bot.vercel.app/0image.png" alt="garmin express app homepage on desktop") }}
+
![garmin express app homepage on desktop](https://hc-cdn.hel1.your-objectstorage.com/s/v3/37f7a72276cc3c1bc2575833cb4f663ce23bbbd6_0image.png)
If you are using Garmin Express, then you can access the app settings by selecting the 3 dots next to the app. You will have 36 possible API calls that you can enter.
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/-lSqNObL3TGNk0VQc8xOq.png" alt="ApiCall settings page" caption="Yes that formating is atrocious but it works at least!") }}
+
![ApiCall settings page](https://hc-cdn.hel1.your-objectstorage.com/s/v3/b98ab669cd8549519876b95d99af1d8edebb0f28_-lSqNObL3TGNk0VQc8xOq.png){caption="Yes that formatting is atrocious but it works at least!"}
> webhooks
```ts
···
These are the actions that I configured for my watch so far. To customize for your API calls you need to change the `deviceName`, `actionName`, and `url` fields. The `method` and `headers` need to stay the same across all actions. If you want to add an icon to that action, then you can configure that with the `actionIcon` field. A table with the possible icons is included below, sourced from APICallโ€™s [documentation](https://apicall.dumesnil.net/documentation_en.html).
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/119m02PEgn6_wcNGtCnjM.png" alt="ApiCall icons") }}
+
![ApiCall icons](https://hc-cdn.hel1.your-objectstorage.com/s/v3/e957a6b2b01133d35ccfe189e0466049a58da3bb_119m02PEgn6_wcNGtCnjM.png)
In conclusion, you can use APICall to trigger actions in home assistant from your Garmin smartwatch. I hope this tutorial proved to be useful, and have a great rest of your day (or night).
-
* Written on `2023-08-04` and republished to this blog on `2024-10-31`
+
* Written on `2023-08-04` and republished to this blog on `2024-10-31`
+3 -5
content/blog/2023-11-01_censorship-or-protection.md
···
[taxonomies]
tags = ["essays", "archival"]
-
-
[extra]
-
has_toc = true
+++
-
{{ img(id="https://cloud-quuwed8n2-hack-club-bot.vercel.app/0image.png" alt="child looking out window" caption="Law makers keeping producing new โ€œonline safety billsโ€ but do they really help?") }}
+
<!-- more -->
+
![child looking out window](https://hc-cdn.hel1.your-objectstorage.com/s/v3/5523fdca5f558bc9ff49b4053e02cd1ded0c6e43_0image.png){caption="Law makers keeping producing new โ€œonline safety billsโ€ but do they really help?"}
In the last few years, we have seen a wave of โ€œonline safety billsโ€ created by lawmakers that will ostensibly help protect children online. The US has the Protecting Kids on Social Media Act (PKSMA, S.1291) and the Kids Online Safety Act (KOSA, S.1409) while in the UK they have the Online Safety Bill (OSB). The main feature that all of these bills have in common is the censorship of online content for minors. The Electronic Frontier Foundation (EFF) has raised concerns over KOSA, saying, โ€œThe bill requires all websites, apps, and online platforms to filter and block legal speechโ€ (Mullin). These bills raise an important questionโ€“should the government regulate the online activities of children, or should that responsibility lie solely with parents?
···
Wisniewski, Pamela J., et al. โ€œPrivacy in Adolescence.โ€ _Modern Socio-Technical Perspectives on Privacy_, edited by Bart P. Knijnenburg et al., Springer International Publishing, 2022, pp. 315โ€“36. _Springer Link_, [](https://doi.org/10.1007/978-3-030-82786-1_14)[https://doi.org/10.1007/978-3-030-82786-1_14](https://www.brennancenter.org/our-work/research-reports/citizens-without-proof).
-
* Written on `2023-11-01` and republished to this blog on `2024-10-31`
+
* Written on `2023-11-01` and republished to this blog on `2024-10-31`
+102
content/blog/2023-11-10_monaspace-vs-code-install.md
···
+
+++
+
title = "Monaspace VS-Code install"
+
date = 2023-11-10
+
slug = "monaspace-vs-code-install"
+
description = "How to install the Github Next team's Monaspace font in VSCode"
+
+
[taxonomies]
+
tags = ["tutorial", "archival"]
+
+++
+
+
To install the Monaspace font on macOS (or windows or linux) with VS Code and enable multifont syntax highlighting with the [CSS JS Loader extension](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css), you can follow these steps:
+
+
<!-- more -->
+
+
![monaspace font in action](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7abefc1fcefa49d2ab5aac9afbafa1da41259382_KuOAwCEm9ypWEemv60Qs7.png){caption="This font is so pretty and has so many features its amazing. It's main downside is to work it takes to set it up."}
+
+
+
## 1. Download and install the Monaspace font:
+
+
First visit [https://github.com/githubnext/monaspace/releases/latest](https://github.com/githubnext/monaspace/releases/latest) and download the zip.
+
Next to install the Monaspace font:
+
- On macOS, drag the font files into font book.
+
- For windows, drag into the font window in settings.
+
- For Linux, clone the repo and run: `cd util; ./install_linux.sh`
+
+
## 2. Configure VS Code
+
+
Install the [Custom CSS and JS Loader](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css) plugin.
+
Set the font to one of the following options: `Monaspace Neon Var`, `Monaspace Argon Var`, `Monaspace Xeon Var`, `Monaspace Radon Var`, or `Monaspace Krypton Var`.
+
+
- You will find this option under _Editor: Font Family_ in the user preferences
+
+
![the available varients of the font](https://hc-cdn.hel1.your-objectstorage.com/s/v3/107040ab22c6ccfaa53c82bd5a328168f5d2d119_v0cMm5jcwHEgrvtBv4Syx.png)
+
+
+
Next enable font ligatures in the settings.json with following snippet:
+
+
> settings.json
+
```json
+
"editor.fontLigatures": "'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', calt', 'dlig'",
+
```
+
Now enable the custom CSS file within the `settings.json`, modifying the file path for Windows / MacOS / Linux if needed:
+
+
> still settings.json
+
```json
+
"vscode_custom_css.imports": [
+
"file:///Users/{{user}}/.vscode/style.css", // for mac (remove if not mac)
+
"file://C://Users/{{user}}/vscode/style.css" // for windows (remove if not windows)
+
"file:///home/{{user}}/.vscode/style.css" // for linux (remove if not windows)
+
],
+
```
+
+
## 3. Create custom CSS file at the path you specified above.
+
+
Depending on your VS Code version, the class names might be different, so you may need to use the developer tools to find the correct one.
+
The styles that worked for me on `VS Code version: 1.84.2 (Universal) commit: 1a5daa3a0231a0fbba4f14db7ec463cf99d7768e` are here:
+
+
> style.css
+
```css
+
/* Comment Class */
+
.mtk3 {
+
font-family: "Monaspace Radon Var";
+
font-weight: 500;
+
}
+
+
/* Copilot Classes */
+
.ghost-text-decoration {
+
font-family: "Monaspace Krypton Var";
+
font-weight: 200;
+
}
+
+
.ghost-text-decoration-preview {
+
font-family: "Monaspace Krypton Var";
+
font-weight: 200;
+
}
+
```
+
+
*Thanks to **[@fspoettel](https://github.com/fspoettel)** on GitHub for this trick to get the copilot classes when in dev mode*
+
+
> "You can inspect transient DOM elements by halting the app with a `debugger` after a delay with a debugger call inside a `setTimeout`."
+
>
+
> <cite>[@fspoettel](https://github.com/fspoettel)</cite>
+
+
You can copy the following snippet to do just that!
+
+
> console
+
```ts
+
setTimeout(() => {
+
debugger;
+
}, 10000);
+
```
+
+
Before you are finished make sure you have run the `Enable Custom CSS and JS` command from the command bar.
+
+
## Closing Remarks
+
+
That should be it! Hopefully you will have a beautiful custom font VS Code install.
+
+
If you are looking for a good theme, I can highly recommend the [Catppuccin](https://marketplace.visualstudio.com/items?itemName=Catppuccin.catppuccin-vsc) theme, as that is what I use myself. Be sure to check out [Monaspaceโ€™s website](https://monaspace.githubnext.com/) as it is a work of art. Happy Coding! ๐Ÿ‘ฉโ€๐Ÿ’ป
+
+
* *Updated 2024-08-22: changed mtk4 to mtk3 on the feedback of [mutammim](https://github.com/mutammim)*
+
* *Updated 2024-10-31: changed around the formatting of the post and moved to [dunkirk.sh](https://dunkirk.sh)*
+12 -13
content/blog/2024-08-03_ssd-removal-mbp-2017.md
···
[taxonomies]
tags = ["tutorial", "teardown", "archival"]
+
+++
-
[extra]
-
has_toc = true
-
+++
+
Hi! I've had a MacBook Pro 2017 for about a year now, and I got it used; it's been great so far until one day after updating it just refused to turn on I'm not entirely sure why this happened, but I replaced the battery and that didn't solve the issue so yeah ^_^
-
{{ img(id="https://cloud-owp7vmln1-hack-club-bot.vercel.app/0img_1846_1_.jpg" alt="MacBook proprietary blade SSD" caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage") }}
+
<!-- more -->
-
Hi! I've had a MacBook Pro 2017 for about a year now, and I got it used; it's been great so far until one day after updating it just refused to turn on I'm not entirely sure why this happened, but I replaced the battery and that didn't solve the issue so yeah ^_^
+
![MacBook proprietary blade SSD](https://hc-cdn.hel1.your-objectstorage.com/s/v3/d294f113bf415a0d1c544fbf3c2d0f4286d892a6_0img_1846_1_.jpg){caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage"}
I eventually decided to just try and remove the SSD from the MacBook and see if there was a way to recover any files from it (spoiler: there kinda is, but it's annoying) but I couldn't find any guide online and iFixit had nothing. So I decided to just try and yolo it and see if I could figure it out on my own, and surprisingly I actually managed to do it! Turns out, the process isn't that hard! I'll take you through the steps I took so that if you want to do this, it's much less of a hassle.
## Guide
1. the first thing you need to do is to remove the screws from the back of your MacBook. This will use a P5 Pentalobe driver, which I believe you can buy from iFixit as well as several other companies on Amazon.
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/1img_1838.jpg", alt="Removing the screws") }}
+
![Removing the screws](https://hc-cdn.hel1.your-objectstorage.com/s/v3/ed2c207dcd77b9dede8496aa40959042fca082ba_1img_1838.jpg)
1. next you need to crack open the shell of the MacBook by prying under the front (on the side where the MacBook opens). It's pretty helpful to have a suction cup or something to lift it up a bit so you can get your prying tool underneath (I used a flat plastic prying tool I got from the battery repair kit for this MacBook, but a guitar pick or credit card would probably also work)
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/2img_1839.jpg", alt="using a suction cup to lift the back shell") }}
+
![using a suction cup to lift the back shell](https://hc-cdn.hel1.your-objectstorage.com/s/v3/faf7ece0437ca9ae232361dc1305c7012c676d88_2img_1839.jpg)
1. now once you've got the back slightly opened up just run around the edge of the shell prying up on it until the front and two sides are free then just pull forward at a slight (15ish degree?) angle, and it should slide right out.
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/3img_1840.jpg", alt="the opened MacBook") }}
+
![the opened MacBook](https://hc-cdn.hel1.your-objectstorage.com/s/v3/0b55d2aed8ecda90aa39091bc5dacd72c2c5aab0_3img_1840.jpg)
1. once it's open, locate the silver metal block looking thing; this is your SSD
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/4img_1841.jpg", alt="the SSD") }}
+
![the SSD](https://hc-cdn.hel1.your-objectstorage.com/s/v3/acc1ae05460bf0792959c684e5104d225d309b5e_4img_1841.jpg)
1. now using a T5 Torx driver (why couldn't you just use one type of screws apple ๐Ÿ˜ญ; be more like framework) you need to unscrew the two screws on either side of the front of the SSD
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/7img_1844.jpg", alt="the screws") }}
+
![the screws](https://hc-cdn.hel1.your-objectstorage.com/s/v3/f04ad8221ae080dba828e8e0cad9492ae2691118_7img_1844.jpg)
1. now comes the slightly scary part (for me at least) you need to lift the black tape that's covering the top of the SSD (don't worry the SSD will be fine)
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/8img_1845.jpg", alt="the removed tape on the SSD") }}
+
![the removed tape on the SSD](https://hc-cdn.hel1.your-objectstorage.com/s/v3/d63a97738c3378a1813a7c59263c37b2e4120470_8img_1845.jpg)
1. now just slightly pull on the SSD (again at a slight angle) and it should pop right out!
-
{{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/9img_1846.jpg", alt="the SSD out of the MacBook") }}
+
![the SSD out of the MacBook](https://hc-cdn.hel1.your-objectstorage.com/s/v3/85632b1b9443498a770aae1687b6ff594aad79ff_9img_1846.jpg)
## Postlog and notes
I hope this helped if you are trying to do this your self! Now for recovering the data the two options I've found are a) buy a secondary MacBook of the exact same generation and model and swap your SSD in or b) pay some data recovery company a lot of money to probably do the same thing for you; neither option is super appealing to me, so I'll keep searching for alternatives and I will be sure to update this article if I do find any. As of today though (August 3rd 2024) I haven't been able to get a hold of another MacBook or adaptor to connect this to my computer but if you do find one definitely leave a comment on the hacker news post linked below!
* Posted on HackerNews on `2024-08-03` [hn://item/41147359](https://news.ycombinator.com/item?id=41147359)
-
* Republished to this blog on `2024-10-31` with minor edits
+
* Republished to this blog on `2024-10-31` with minor edits
+71 -84
content/blog/2024-10-11_example_post.md
···
+++
-
title = "Test Post"
+
title = "The *Mega* test case"
date = 2024-10-11
-
slug = "test-post"
-
description = "Testing out styling and features."
+
slug = "mega"
+
description = "How I write / leme check if that broke anything page"
[taxonomies]
tags = ["meta"]
···
make sure that if I change the CSS or anything I don't break any of it! This is also a
sort of light style guide for blog posts in general.
+
<!-- more -->
+
## Section Headers
Sections headers (prefixed with `##` in markdown) are the main content separators for posts, and
can be [linked to](#section-headers) directly. To link to them, the header's text needs to be
*kebab-cased*, so the above would be `#section-headers`.
-
### But what about sub-headers?
-
-
I usually use `###` sub-headers to ask the question I think the reader is (or should be) asking at
-
this point in the article. For example, if I just posted some code with an obvious error, I might
-
follow that up with `### Wait, won't that crash?` or something similar. Using this approach lets
-
me write posts in a conversational way, and helps me continually frame myself in the mind of the
-
reader.
+
Not quite a section header, the `<!-- more -->` tag is used to indicate where a post should be split for rss purposes. This should generally be right after the first paragraph.
### Table of Contents
···
has_toc = true
```
-
I don't like content that is nested more than 2 layers deep, so only `##` and `###` should be used
-
to divide things up.
+
The table of contents will only ever be generated for `##` and `###` headers. I don't particularly love the look of it and tend to write shorter posts so I hardly use it.
## Embedding Code
-
This is prominently a coding blog, so code will show up a lot. First off, a monospaced text block is
-
denoted by wrapping the text in triple back-tick characters <code>&#x0060;&#x0060;&#x0060;</code>.
-
-
```
-
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
-
โ”‚ This text is monospaced. โ”‚
-
โ”‚ This โ”‚
-
โ”‚ text โ”‚
-
โ”‚ is โ”‚
-
โ”‚ monospaced. โ”‚
-
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
-
```
+
I tend to do this alot so this is an important bit of the blog. All code blocks with a code type are progressively enhanced with a copy button.
### Syntax Highlighting
If you want syntax coloring, you put the name of the programming language immediately after the ticks.
So writing this:
-
~~~
+
~~~md
```rust
fn main() {
println!("Hello, world!");
···
Sometimes it can help to give a header to a code block to signal what it represents. To do this, you put
a single-line block quote immediately before the code block. So by prepending the following code with
-
`> src/main.rs`, I can produce this:
-
-
> src/main.rs
-
```rust
-
fn main() {
-
println!("This code is in main.rs!");
-
}
-
```
-
-
This can be useful to explicitly state the programming language or format being used:
-
-
> TOML
-
```toml
-
title = "Test Post"
-
slug = "test-post"
-
description = "Testing out styling and features."
+
`> src/index.ts`, I can produce this:
-
[taxonomies]
-
tags = ["meta"]
+
> src/index.ts
+
```ts
+
Bun.serve({
+
port: 3000,
+
fetch(req) {
+
return new Response("Hello, world!");
+
}
+
});
```
### Inline Code
As seen above, sometimes code items are mentioned in regular paragraphs, but you want to
-
draw attention to them. To do this, you can wrap it in &#x0060; back-tick quotes. For
+
draw attention to them. To do this, you can wrap it in back-tick (\`) quotes. For
example, if I wanted to mention Rust's `Vec<T>` type.
```md
···
> "This text will appear italicized in a quote box!"
-
### Reader Questions
-
-
When displaying reader questions, I start the block quote with a bolded name, like so:
-
-
> **SonicFan420x69 asks:**
-
>
-
> &ldquo;What is your opinion of the inimitable video game character, Sonic the Hedgehog? Please
-
> answer soon as it is a matter of life or death.&rdquo;
-
### Cited Quotations
For when I want to have a citation, I can use the html `<cite>` tag after the quote text and it
···
> as well as you deserve."
>
> <cite>Bilbo Baggins</cite>
-
-
## Icons &amp; Images
-
-
They were shown in the previous section, but icons (provided by [Remix Icon](https://remixicon.com/)),
-
can be used anywhere by inserting an `<i>` tag with the icon's class. These are useful for adding
-
some detail and decorating to the pages, and is another way to break up text.
## Embedding Media
···
You can also add captions to images:
-
<figure>
-
<img src="https://img.itch.zone/aW1hZ2UvNTU2NDU0LzI5MTYzNzkucG5n/original/8LIdCb.png" alt="NOISE1 screenshot">
-
<figcaption>
-
NOISE1 is a dark sci-fi hacker-typing stealth game.
-
</figcaption>
-
</figure>
+
```terra
+
{{/* img(id="https://url.com/image.png" alt="alt text" caption="this can be ommited if you want or added! It's optional :)") */}}
+
```
-
But there is no way to do this in markdown so you have to use the `<figure>` tag like so:
+
![MacBook proprietary blade SSD](https://hc-cdn.hel1.your-objectstorage.com/s/v3/d294f113bf415a0d1c544fbf3c2d0f4286d892a6_0img_1846_1_.jpg){caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage"}
-
```html
-
<figure>
-
<img src="/path/to/image.png" alt="Alt text goes here.">
-
<figcaption>Caption text goes here.</figcaption>
-
</figure>
+
You can also display multiple images side-by-side using the `imgs` shortcode with comma-separated URLs:
+
+
```terra
+
{{/* imgs(id="https://url.com/image1.png, https://url.com/image2.png" alt="alt text 1, alt text 2" caption="optional caption for both images") */}}
```
+
!![the copyright section](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c509aeaac769c3e5b99d5a7d320cc4a759db4ff5_img_8880.jpeg)[the ssh section](https://hc-cdn.hel1.your-objectstorage.com/s/v3/ed400c26ddfa37ab4a9ef4fd5a506f2dcc1bcfbb_img_8879.jpeg){caption="side by side images from the remarkable tutorial"}
+
### Videos
-
To embed a video, you use the `youtube` shortcode e.g.
+
To embed a video, you use the `youtube(id="", autoplay?=bool)` shortcode e.g.
-
> post.md
-
```md
-
{{/* youtube(id="kiWvNwuBbEE") */}}
-
```
+
{{ youtube(id="NodwjZF7uZw") }}
-
You can also add the `autoplay=true` flag to make the video autoplay.
+
### Bluesky posts
-
{{ youtube(id="NodwjZF7uZw") }}
+
This is handled by a shortcode `bluesky(post="")` and takes the post url as a parameter. These will automatically attach images and videos.
-
The shortcode is processed into an iframe which looks like this:
-
-
> post.html
-
```html
-
{{ youtube(id="kiWvNwuBbEE") }}
-
```
+
{{ bluesky(post="https://bsky.app/profile/svenninifl.bsky.social/post/3lnkivz3ans2k") }}
## Miscellaneous
···
---
But these should be used sparingly, if at all.
+
+
You can also use emojis inline from the hackclub slack like this :yay:! This is just done by writing `:emoji:` and it gets progressively enhanced with a bit of js as long as the emoji is in cachet!
+
+
## Callouts
+
+
Callouts are a great way to draw attention to important information. They come in several types:
+
+
### Info Callout
+
+
> [!INFO]
+
> This is an info callout! Use this for general information that readers should be aware of.
+
+
### Warning Callout
+
+
> [!WARNING]
+
> This is a warning callout! Use this to alert readers about potential issues or things to watch out for.
+
+
### Danger Callout
+
+
> [!DANGER]
+
> This is a danger callout! Use this for critical information that could cause problems if ignored.
+
+
### Tip Callout
+
+
> [!TIP]
+
> This is a tip callout! Use this to share helpful hints and best practices.
+
+
### Note Callout
+
+
> [!NOTE]
+
> This is a note callout! Use this for additional context or side information.
+
+
### Custom Title
+
+
{% callout(type="info", title="Custom Title Here") %}
+
You can also customize the title of any callout by adding a `title` parameter!
+
{% end %}
+7 -8
content/blog/2024-10-13_hilton_tomfoolery.md
···
[taxonomies]
tags = ["reverse engineering", "hilton"]
-
-
[extra]
-
has_toc = true
+++
I'm at a Hilton at the time of writing this, and I'm decently bored. Currently, I'm downloading the latest version of RogueMaster (0.420.0) to my flipper, as it is currently crashing every time I open the NFC app. My dad tried out the app unlock feature in the Hilton app for the first time today, which, as most new tech things, made me quite curious how it worked and whether I could break it. Based on playing with it, there seems to be a proximity reading (over Bluetooth? Perhaps a BLE beacon?) to detect if you are by your door but for a period of time (~20 sec) after getting that signal it allows you to unlock the door from across the room which I'm guessing means that it controls the locks via a central server. The current plan is to install the root cert (of mitmproxy) on my iPhone and then try and intercept those API calls and see if we can manipulate them in any interesting ways. I'm also planning on live blogging this, which I've never tried before. (I also wrote this whole article in vim ^_^)
+
<!-- more -->
+
## Connecting to Mitmproxy
I'm connecting over WireGuard, so I fired up mitmproxy with `mitmweb --mode wireguard` on my laptop. Connecting via WireGuard theoretically is pretty simple; all I need to do is to scan a qr code and connect. Unfortunately, the hotel Wi-Fi seems to be oddly segmented, and I can't access the WireGuard server or ping my laptop from my phone. I'm going to try firing up a hot spot on my dad's phone and see if that allows me to talk to my phone.
-
{{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/2install_profile.png" alt="screenshot of the root certificate install process" caption="You have to dig through several menus to trust it") }}
+
![screenshot of the root certificate install process](https://hc-cdn.hel1.your-objectstorage.com/s/v3/29c856921e88c31bfc2e2d73d09d287ce4aa149a_2install_profile.png){caption="You have to dig through several menus to trust it"}
I messed with getting my laptop to connect to my dad's phone, but it kept refusing for some reason. My next idea is to ngrok the WireGuard tunnel, which ended up failing because ngrok doesn't support UDP. Finally, after an embarrassingly long time, I realized that I could simply use `ngrok tcp 8080` and the HTTP proxy server built into mitmproxy instead. After installing the root certificate and trusting it in the iPhone settings, we were good to go!
···
## Locks
-
{{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/0hotel-key.png" alt="screenshot of the hotel digital key" caption="What it looks like in the app") }}
+
![screenshot of the hotel digital key](https://hc-cdn.hel1.your-objectstorage.com/s/v3/06dcbd4c5166d7f17e31027b687f32539d68aa44_0hotel-key.png){caption="What it looks like in the app"}
When using the unlock button, it made a request to this URL: `https://smetric.hilton.com/b/ss/hiltonglobalprod/10/IOSN030200030900/s65425920` with a payload of a URL encoded form.
···
## Wrap up
-
{{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/1bluetooth-scan.png" alt="screenshot of bluetooth scan" caption="The bluetooth scan of (what i belive is) the lift") }}
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/4e9bfb28c266eb29cea1568cedd3573be2ba1f97_1bluetooth-scan.png" alt="screenshot of bluetooth scan" caption="The bluetooth scan of (what i believe is) the lift") }}
I tried running a Bluetooth scan to see if I could find the locks, but nothing popped out as being a likely culprit. I did however find an interesting set of 3 Bluetooth devices named "clearsky smart fleet" which upon research seems to be scissor lifts / construction equipment made by a company called [JLG](https://smartfleet.jlg.com/) which is quite interesting. That would make sense, however, as I saw several scissor lifts outside the hotel on my way in.
-
{{ img(id="https://cloud-1asinv8kn-hack-club-bot.vercel.app/0img_2781.jpg" alt="image of JLG lift" caption="The same (probably) JLG lift in the wild!") }}
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/993ad810e42289ad3aaefa4093ede271a4ee1d12_0img_2781.jpg" alt="image of JLG lift" caption="The same (probably) JLG lift in the wild!") }}
By the time I'm writing this it's 6:41, and I need to eat breakfast, so I'll probably finish this post in the car this afternoon. Overall this was a fascinating experiment and while I sadly did fail at unlocking doors from my laptop I do feel more confident with reverse engineering app requests now! The next step would probably be to grab the app bundle and try to decompile it looking for the URLs we saw, but I don't have a mac on me, and I've never done that before. Next post?
Taking inspiration from the [LOWโ†TECH MAGAZINE](https://solar.lowtechmagazine.com/) I will be taking any questions / comments about this article via email and then posting them here to my site! If you have a question or comment, feel free to email me at [me@dunkirk.sh](mailto://me@dunkirk.sh). Now to go eat breakfast :)
-
{{ img(id="https://cloud-1asinv8kn-hack-club-bot.vercel.app/3img_2777.jpg" alt="image of my hotel breakfast" caption="A delicious waffle, mildy warm bacon, and under seasoned potatoes.") }}
+
![image of my hotel breakfast](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c974178c62fc836657a1c5e61cac90596c13a3bd_3img_2777.jpg){caption="A delicious waffle, mildy warm bacon, and under seasoned potatoes."}
+5 -6
content/blog/2024-10-23_hilton_decompilation.md
···
[taxonomies]
tags = ["reverse engineering", "hilton"]
-
-
[extra]
-
has_toc = true
+++
Ello! I'm back again! I'll be staying at a Hotel again in two days so I decided to try to decompile the app ahead of time so I can test stuff while I'm there. I decided to target the android app first because it seemed easier to decompile (i've partly decompiled an apk before about 3 and half years ago to embed a payload in it and I don't remember it being horrible) and I knew getting the apk itself would be far easier than from the Apple App Store.
-
{{ img(id="https://cloud-glc3mgu9t-hack-club-bot.vercel.app/0image.png" alt="screenshot of the nix packages entry" caption="prepackaged for nix; always a good sign") }}
+
<!-- more -->
+
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/4e667b8066044667ea63d5ec44222aef97dc764c_0image.png" alt="screenshot of the nix packages entry" caption="prepackaged for nix; always a good sign") }}
I was able to download the apk from the [apkcombo.com](https://apkcombo.com/downloader/#package=com.hilton.android.hhonors) website by simply inputing the play store URL so we were off to a good start. Apktool was already in [nix packages](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=apktool) so we didn't have to do anything fancy there. One `pkgs.unstable.apktool` and a `sudo nixos-rebuild switch` latter and we were ready to go. Then I waited another 2 days lol. Finally in the hotel room (again crunched on time; why do I never seem to learn?) I was able to decompile the apk and start looking around.
-
{{ img(id="https://cloud-qh7hbvivt-hack-club-bot.vercel.app/0image.png" alt="screenshot of the successful decompilation process" caption="all nicely decompiled") }}
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/55f3ffe6a3f8130fc7f389d5d151660364e99d93_0image.png" alt="screenshot of the successful decompilation process" caption="all nicely decompiled") }}
-
I started uploading the decompiled app to github ([kcoderhtml/hilton-honors](https://github.com/kcoderhtml/hilton-honors)) which was incredibly slow and then started poking around the app. The first thing I noticed was quite a few files with firebase in the name as well as several play store properties files. All of them seemed to follow the same pattern of having a `version`, `client`, and then file specific client key.
+
I started uploading the decompiled app to github ([taciturnaxolotl/hilton-honors](https://github.com/taciturnaxolotl/hilton-honors)) which was incredibly slow and then started poking around the app. The first thing I noticed was quite a few files with firebase in the name as well as several play store properties files. All of them seemed to follow the same pattern of having a `version`, `client`, and then file specific client key.
```text
$ ls unknown/firebase*
+18
content/blog/2024-12-16_airbuds.md
···
+
+++
+
title = "Airbuds"
+
date = 2024-12-16
+
slug = "airbuds"
+
description = "Trying to break their api."
+
draft = true
+
+
[taxonomies]
+
tags = ["reverse engineering", "graphql"]
+
+++
+
+
Recently my cousin introduced me to the [Airbuds](https://airbuds.fm) app. Naturally I used it for a little bit. Slept a bit. And then booted up Proxypin to see if I could extract phone numbers from the app. With the base requests it appeared that I couldn't (:sadge:). I could get my phone number for my own profile however so I knew that it was likely stored in a user record somewhere (editor kieran: *umm yeah duh*). The more interesting part of this though was that it was a graphql api.
+
+
<!-- more -->
+
+
## Phase 2
+
+
Now knowing that it had a graphql api I wanted to see if there was a way to reverse engineer it. I have had suprisingly little experience with them but doing some quick ducking revealed that they can potentially have introspection enabled allowing us to get a full schema of what we can get. That sounds awesome but hopefully from a security standpoint unlikely to be enabled.
+28
content/blog/2025-01-01_spotify-to-apple-music.md
···
+
+++
+
title = "Exodus of Spotify Songs to the land of Apple Music"
+
date = 2025-01-01
+
slug = "spotify-to-apple-music"
+
description = "Homegrown solution rather than paying for it ^-^"
+
draft = true
+
+
[taxonomies]
+
tags = ["apple", "music"]
+
+++
+
+
Today my family decided to get an Apple One subscription and use Apple Music instead of spotify. It makes sense from a cost standpoint (spotify is $20 a month vs $37.95 and `2TB` of storage plus all apple subscriptions) but I have about 3 years of history on spotify (1267 at time of writing) so manually transferring the songs isn't an option. I did some research but all I found was over priced apps and annoying python scripts.
+
+
<!-- more -->
+
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/f17f56ea1780c37519a4f2cc5d866124acfe476e_0img_3821.jpg" alt="screenshot of the apple music app saying welcome to apple music" caption="the proper horror this should/does instill ๐Ÿ’€") }}
+
+
## Shortcut Time
+
+
I haven't played around with apple shortcuts near enough but I know that they can be quite powerful (case in point [eieio.games](https://eieio.games/blog/doom-in-the-ios-photos-app/)). I looked to see whether spotify had a shortcut to get songs out first but didn't find anything (come on spotify!) but then when I checked Apple Music it expectedly had quite a few options. One of the options is add to playlist which when I tested it initially with the share sheet as input could take a spotify url. That got me thinking; why can't I just import a file of urls on new lines? Turns out that's exactly what you can do. If you start with a file as the input and then bring it to a split text block then you can route that directly to the add songs block! Whats even better is that you don't even need some fancy looping system, you can simply dump thousands of songs into it and it takes care of it super easily.
+
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/b05061099d67aa2297e991a074dd6e95bd33096d_0img_3824.png" alt="screenshot of the shortcut" caption="if you want to try it yourself you could build the shortcut from scratch or you can use the link below") }}
+
+
Now the second part of the puzzle was exporting the liked playlist. I really didn't want to mess with the slack api and registering an oauth app but then I remembered that you can simple just hit control + a to select songs in the desktop app ๐Ÿคฆ and turns out if you copy it then it literally just chucks it all into your clipboard as spotify links on newlines. A quick `vi test.txt` and sending the file to myself over slack latter I could simply select the song file and use the share sheet to import it. It took a solid 35 seconds to import but gave a nice progress bar up top!
+
+
### Adendum
+
+
- [the apple shortcut] for your copy pasta pleasure
+37
content/blog/2025-01-31_my-life-story-with-tech.md
···
+
+++
+
title = "My life story in tech so far ig ๐Ÿคท"
+
date = 2025-01-31
+
slug = "my-life-story-with-tech"
+
description = "I was applying for a college cybersecurity camp and wrote this absolute monster that amounts to an overview of my life in tech so far (till 16)"
+
+
[taxonomies]
+
tags = ["yap fest", "biography"]
+
+++
+
+
I was applying for a Cybersecurity college camp for this summer and realized this is honestly a pretty good summary of my life in tech so far (till i'm 16) and that I should probably make it a blog post soooo here it is!
+
+
<!-- more -->
+
+
## The yap
+
+
Hi! My name is Kieran, and I've been interested in / involved with cybersecurity and programming since I first started using a laptop at 10! I started out with a raspberry pi 3b+ which taught me how to use debian as well as the basics of creating and maintaining databases and web services. I moved on to an ubuntu laptop about a year latter and started using my raspberry pi as a home server to run small websites on our local lan. Soon I wanted to share them with others and expose them to the internet, so I learned how to use dns and port forwarding and then how to secure the server to prevent attacks with tools like fail2ban!
+
+
![2 boxes of electronics sitting on a closet shelf](https://hc-cdn.hel1.your-objectstorage.com/s/v3/b237edcfbdefc74f6296d157203ba539187a1e24_0image_from_ios.jpg){caption="I still have that same rpi today! It's joined with all the random tech bits in two enormously heavy bins in my closet"}
+
+
Over the next 2 years, I systematically read every single book in the tech section of my local library and became interested in white-hat hacking. I taught myself how to use kali linux and metasploit with the help of many web searches and had quite a bit of fun rooting and then sideloading custom payloads onto our families set of kindle fires (I was eventually restricted to just playing with just one but I did make a home security system with all of them once). I figured out wireshark and started playing with wifi protocals but eventually reached the limit of what I could figure out on my own and took a quick detour of two years to learn blender and build my first computer.
+
+
I became interested in home labs and self hosting services around 14 and bought an old workstation off ebay which combined with my set of 3 rasberry pis and several old laptops (and one old pentium tower that I found on the side of the road) made quite a nice playground for deploying my own services. Half a year later I had to pick it all up and move up north which was quite the adventure; my services got completely messed up during the move, and it took my a week or so tinkering with everything to get it back to a stable state.
+
+
![gif of my github contributions graph 2021-2025](https://hc-cdn.hel1.your-objectstorage.com/s/v3/bf06e9d57d41dd75e328b8898cfe04ef2f30a3f3_0contributions-graph.gif){caption="2021-2022 is mainly just unity and hugo sites lol; I really started seriously using it and doing contributions to other projects 2023-2025. You can also see where I broke my wrist in January of 2025"}
+
+
After the move, I became quite interested in front end development and started making quite a few websites and various random coding projects. If you look on my GitHub contributions graph ([github.com/taciturnaxolotl](https://github.com/taciturnaxolotl), you can see it go from a lightly speckled grid in 2021 and 2022 to a much more solid commit streak in 2023. I only had one week when I didn't code anything and that was the second week of the year :) Toward the end of that year I started learning about hardware design and made my first PCB! I also joined a wonderful community called hackclub where I met a ton of amazing teenagers who were also interested in tech just like me! I joined an FRC robotics team in January of the next year and had a blast designing, building, and programming a custom meter square, 150 lb, industrial robot to compete in that year's game!
+
+
![purple bubble logo](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a9e816e5c00a7bdd8e8f0dc8ad2180dfe9792aa5_logo.png_raw_true){caption="I loved working on purple bubble ๐Ÿ’– i worked with some pretty incredible people and learned a ton. ik know yall are probably reading this when rss drops it so ๐Ÿซถ"}
+
+
During that same time I also started a 501(c)3 named Purple Bubble with friends that I had met through Hackclub focused on making a secure, cost-effective, and privacy preserving messaging protocol. We drafted a specification and poured many, many hours into planning and developing the protocol over the next year but eventual realized that the messaging protocol space is *incredibly* hard and that there were innate flaws in our protocol that would compromise the security of the app (We couldn't find a good way to anonymize connections to a network of server's while also providing zero metadata transfer of messages between servers; we had originally planned for the protocol to be zero trust federated, but this proved to be a challenge that, no matter how hard we kept thinking and talking about it, we couldn't find a solution too). I learned a huge amount about organizing a group of people and running an organization through that experience and made some wonderful friends, so it wasn't entirely in vain.
+
+
My latest project and biggest learning experience in both security and development has been building a time tracking server for coding called Hackatime. It is fully compatible with the popular wakatime.org, which allows it to leverage the hundreds of existing extensions for tracking time spent coding in almost every popular IDE and editor. I made this as a part of an event Hackclub ran called High Seas where they encouraged high school students to make cool projects by giving out awesome prizes for time spent coding (you had to "ship" your project where it would get voted on by the other four thousand teens participating and then via a custom ELO system convert your hours into "doubloons" that could be redeemed for prizes like framework laptops, soldering irons, McMaster Car credits, and many others. If you want to learn more about it, the website is [highseas.hackclub.com](https://highseas.hackclub.com)). In order to track the time of the thousands of teenagers participating, I created this server which was handling thousands of users an hour and hundreds of requests a second. I learned how to scale the server and database and learned an incredible amount that only comes at scale. At one point I got an email that the database bill had increased so much over the previous month that we were going to hit both the `$1k` hard limit and then a `$4k` limit that I had placed on the monthly bill, expecting never to hit it. The team hosting the database (Cockroach DB) graciously offered to reduce our bill down to only `$500` which was incredible. There were many more instances where things broke, or where I discovered security issues that made me grow an insane amount in my knowledge of how to fix things and really pushed me out of my comfort zone. (If you want to take a look at the github repo it is at [github.com/hackclub/hackatime](https://github.com/hackclub/hackatime) and the hosted version is at [waka.hackclub.com](https;//waka.hackclub.com) with a live hours counted tracker)
+
+
![the cockroach charges in hcb](https://hc-cdn.hel1.your-objectstorage.com/s/v3/23f88382d4fabf9d9f3481c176f5eb5722a188b4_0image.png){caption="The price really sky rocketed as we started using it in prod ๐Ÿ˜‚"}
+
+
I'm still trying to figure out what exactly I want to major in, and I'm pretty solidly split between Comp Sci with a cybersecurity focus and Computer/Electrical Engineering. I'm hoping that this camp can help make that decision a bit more clear and give me a better understanding of what getting a major in Cyber Security would be like!
+120
content/blog/2025-02-02_degraded-zpool-proxmox.md
···
+
+++
+
title = "Fixing a degraded zpool on proxmox"
+
date = 2025-02-03T10:00:00
+
slug = "degraded-zpool-proxmox"
+
description = "replacing a failed drive in a proxmox zpool"
+
+
[taxonomies]
+
tags = ["homelab", "tutorial"]
+
+++
+
+
I decided to finally fix the network issues with my proxmox server (old static ip and used vlans which I hadn't setup with the new switch and router) as I had some time today but after fixing that fairly easily I discovered that my main `2.23 TB` zpool had a drive failure. Thankfully I had managed to stuff 3 disks into the case before so loosing one meant no data loss (thankfully ๐Ÿ˜ฌ; all my projects from the last 5 years as well as my entire video archive is on this pool). I still have 3 more disks of the same type so I can swap in a new one 2 more times after this.
+
+
<!-- more -->
+
+
![the zpool reporting a downed disk](https://hc-cdn.hel1.your-objectstorage.com/s/v3/e54fd32f9a72ef35d310cb3cdc299b297c87baea_2image.png){caption="That really scared the pants off me when I first saw it ๐Ÿ˜‚"}
+
+
## Actually fixing it
+
+
First I had to find the affected disk physically in my case. Because I was stupid I didn't bother to label them but thankfully the serial numbers of the drives are stuck to them with a sticker so that wasn't terrible.
+
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/a6512def9bbeedbc1315a8ee58c92fbfb9e4d169_0image_from_ios.jpg" alt="chick-fil-a macaroni and cheese with 2 nuggets and some ketchup" caption="(By this point I had spent 30 minutes moaning so I went to lunch)") }}
+
+
Now we can run `lsblk -o +MODEL,SERIAL` to find the serial number of our new drive.
+
+
> root@thespia:~# lsblk -o +MODEL,SERIAL
+
```bash
+
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS MODEL SERIAL
+
sda 8:0 0 698.6G 0 disk ST3750640NS 3QD0BG0J
+
โ”œโ”€sda1 8:1 0 698.6G 0 part
+
โ””โ”€sda9 8:9 0 8M 0 part
+
sdb 8:16 0 698.6G 0 disk ST3750640NS 3QD0BN6V
+
sdc 8:32 0 698.6G 0 disk ST3750640NS 3QD0BQ5G
+
โ”œโ”€sdc1 8:33 0 698.6G 0 part
+
โ””โ”€sdc9 8:41 0 8M 0 part
+
sdd 8:48 1 111.8G 0 disk Hitachi HTS543212L9SA02 090130FBEB00LGGJ35RF
+
โ”œโ”€sdd1 8:49 1 1007K 0 part
+
โ”œโ”€sdd2 8:50 1 512M 0 part /boot/efi
+
โ””โ”€sdd3 8:51 1 111.3G 0 part
+
โ”œโ”€pve-swap 253:0 0 8G 0 lvm [SWAP]
+
โ”œโ”€pve-root 253:1 0 37.8G 0 lvm /
+
โ”œโ”€pve-data_tmeta 253:2 0 1G 0 lvm
+
โ”‚ โ””โ”€pve-data-tpool 253:4 0 49.6G 0 lvm
+
โ”‚ โ”œโ”€pve-data 253:5 0 49.6G 1 lvm
+
โ”‚ โ”œโ”€pve-vm--100--cloudinit
+
โ”‚ โ”‚ 253:6 0 4M 0 lvm
+
โ”‚ โ”œโ”€pve-vm--101--cloudinit
+
โ”‚ โ”‚ 253:7 0 4M 0 lvm
+
โ”‚ โ”œโ”€pve-vm--103--disk--0
+
โ”‚ โ”‚ 253:8 0 4M 0 lvm
+
โ”‚ โ””โ”€pve-vm--103--disk--1
+
โ”‚ 253:9 0 32G 0 lvm
+
โ””โ”€pve-data_tdata 253:3 0 49.6G 0 lvm
+
โ””โ”€pve-data-tpool 253:4 0 49.6G 0 lvm
+
โ”œโ”€pve-data 253:5 0 49.6G 1 lvm
+
โ”œโ”€pve-vm--100--cloudinit
+
โ”‚ 253:6 0 4M 0 lvm
+
โ”œโ”€pve-vm--101--cloudinit
+
โ”‚ 253:7 0 4M 0 lvm
+
โ”œโ”€pve-vm--103--disk--0
+
โ”‚ 253:8 0 4M 0 lvm
+
โ””โ”€pve-vm--103--disk--1
+
253:9 0 32G 0 lvm
+
sde 8:64 0 465.8G 0 disk WDC WD5000AAKS-65YGA0 WD-WCAS83511331
+
โ”œโ”€sde1 8:65 0 465.8G 0 part
+
โ””โ”€sde9 8:73 0 8M 0 part
+
sdf 8:80 1 0B 0 disk Multi-Card 20120926571200000
+
zd0 230:0 0 32G 0 disk
+
โ”œโ”€zd0p1 230:1 0 100M 0 part
+
โ”œโ”€zd0p2 230:2 0 16M 0 part
+
โ”œโ”€zd0p3 230:3 0 31.4G 0 part
+
โ””โ”€zd0p4 230:4 0 522M 0 part
+
zd16 230:16 0 80G 0 disk
+
โ”œโ”€zd16p1 230:17 0 1M 0 part
+
โ””โ”€zd16p2 230:18 0 80G 0 part
+
zd32 230:32 0 4M 0 disk
+
zd48 230:48 0 80G 0 disk
+
โ”œโ”€zd48p1 230:49 0 1M 0 part
+
โ””โ”€zd48p2 230:50 0 80G 0 part
+
zd64 230:64 0 32G 0 disk
+
โ”œโ”€zd64p1 230:65 0 512K 0 part
+
โ””โ”€zd64p2 230:66 0 32G 0 part
+
zd80 230:80 0 1M 0 disk
+
```
+
+
Our two current drives are `3QD0BG0J` and `3QD0BQ5G` as we can see in proxmox but we can also see that they have partitions and `sdb/3QD0BN6V` does not so thats our target drive. Now we can find the disk by id with `ls /dev/disk/by-id | grep 3QD0BN6V` which gives us:
+
+
> ls /dev/disk/by-id | grep 3QD0BN6V
+
```bash
+
ata-ST3750640NS_3QD0BN6V
+
```
+
+
![chick-fil-a macaroni and cheese with 2 nuggets and some ketchup](https://hc-cdn.hel1.your-objectstorage.com/s/v3/f539cc5cb4e40b768f4b7bc6dc719467e438c6ed_0image_from_ios.jpg){caption="My case situation is a bit of a mess and I'm using old 7200rpm server drives for pretty much everything; the dream is a 3 drive 2 TB each m.2 nvme ssd setup, maybe someday ๐Ÿคท"}
+
+
We are going to go with the first id so no we move on to the zfs part. Running `zpool status vault-of-the-eldunari` we can get the status of the pool:
+
+
> zpool status vault-of-the-eldunari
+
```bash
+
pool: vault-of-the-eldunari
+
state: DEGRADED
+
status: One or more devices could not be used because the label is missing or
+
invalid. Sufficient replicas exist for the pool to continue
+
functioning in a degraded state.
+
action: Replace the device using 'zpool replace'.
+
see: https://openzfs.github.io/openzfs-docs/msg/ZFS-8000-4J
+
scan: resilvered 8.33G in 00:48:26 with 0 errors on Thu Nov 14 18:38:03 2024
+
config:
+
+
NAME STATE READ WRITE CKSUM
+
vault-of-the-eldunari DEGRADED 0 0 0
+
raidz1-0 DEGRADED 0 0 0
+
9201394420428878514 UNAVAIL 0 0 0 was /dev/disk/by-id/ata-ST3750640NS_3QD0BM29-part1
+
ata-ST3750640NS_3QD0BQ5G ONLINE 0 0 0
+
ata-ST3750640NS_3QD0BG0J ONLINE 0 0 0
+
+
errors: No known data errors
+
```
+
+
We can add our new disk with `zpool replace vault-of-the-eldunari 9201394420428878514 ata-ST3750640NS_3QD0BN6V` but first we wipe the disk from proxmox under the disks tab on our proxmox node to make sure its all clean before we restore the pool after we do that we also initalize a new gpt table. Now we are ready to replace the disk. Running this command can take quite a while and it doesn't output anything so sit tight. After waiting a few minutes proxmox reported that resilvering would take 1:49 minutes and it was 5% done already! I hope this helped at least one other person but I'm mainly writing this to remind myself how to do this when it inevitably happens again :)
+
+
![the zpool reporting a downed disk](https://hc-cdn.hel1.your-objectstorage.com/s/v3/8cc1c0d1717abacbc29d634004b14ec7475de0f2_0image.png){caption="It's slow but faster then I expected for HDDs"}
+50
content/blog/2025-02-15_remove-exif-git-hook.md
···
+
+++
+
title = "Cleaning exif data with git pre-commit"
+
date = 2025-02-15T19:57:01
+
slug = "remove-exif-git-hook"
+
description = "took longer then it probably should have ๐Ÿ˜Š"
+
+
[taxonomies]
+
tags = ["mildrant", "tutorial"]
+
+++
+
+
I saw this [post](https://jade.fyi/blog/pre-commit-exif-safety/) from [jade.fyi](https://jade.fyi) on using a git hook to clear exif data from your images before you commit them and realized I should probably implement that too lol. Interestingly jade also uses zola for her site but she used pre-commit hooks whereas I wanted to do something that used native git hooks.
+
+
<!-- more -->
+
+
I started with the naive method of just having a `.git/hooks/pre-commit` file that would run `exiftool` on the input but after realizing that hooks placed there wouldn't be synced to the repo decided that wasn't the best way. I moved to using a script that would symlink files from the `hooks` directory to `.git/hooks`. It worked moderately well but due to the fact that I used (yes I feel the shame admitting this [:uw_embarrassed:](https://cachet.dunkirk.sh/emojis/uw_embarrassed/r)) `#!/bin/bash` instead of `#!/usr/bin/env bash`. Not realizing my mistake and believing it to be related to the symlink I found [this stack overflow](https://stackoverflow.com/questions/4592838/symbolic-link-to-a-hook-in-git/#:~:text=While%20you%20can%20use%20symbolic%20links) answer which taught me that you can use `git config core.hooksPath hooks` to move the hooks directory to `./hooks` in the root of your repo! After doing that and it still not working (i feel very dense writing this lol) I finally realized that the shebang was wrong and then it worked!
+
+
![the commit hook finally working!](https://hc-cdn.hel1.your-objectstorage.com/s/v3/f2ba3f2dbad8c67eccc42ddbb51bc7128f85d049_9049d20038cc3058acee1bbe58c5ac3f.png){caption="phew"}
+
+
Is there anything at all to learn from this? Well yes actually! You can use the script below and the `git config core.hooksPath hooks` setting to scrub your own images!
+
+
> hooks/pre-commit
+
```bash
+
#!/usr/bin/env bash
+
+
# Check if exiftool is installed
+
if ! command -v exiftool &> /dev/null; then
+
echo "Error: exiftool is not installed. Please install it." >&2
+
exit 1
+
fi
+
+
while read -r file; do
+
case "$file" in
+
*.jpg|*.jpeg|*.png|*.gif|*.tiff|*.bmp)
+
echo "Removing EXIF data from: $file" >&2
+
exiftool -all= --icc_profile:all -tagsfromfile @ -orientation -overwrite_original "$file"
+
if [ $? -ne 0 ]; then
+
echo "Error: exiftool failed to process $file" >&2
+
exit 1
+
fi
+
git add "$file"
+
;;
+
*)
+
;;
+
esac
+
done < <(git diff --cached --name-only --diff-filter=ACMR)
+
+
exit -0
+
```
+
+
> if you want to add something or comment on the post then I posted about it on bluesky: [https://bsky.app/profile/dunkirk.sh/post/3liaybkkas226](https://bsky.app/profile/dunkirk.sh/post/3liaybkkas226)
+146
content/blog/2025-02-26_spherical-ray-diagrams.md
···
+
+++
+
title = "Determining the properties of a spherical mirror with ray diagrams"
+
date = 2025-02-26
+
slug = "spherical-ray-diagrams"
+
description = "yes i made a tool to help with it :)"
+
+
[taxonomies]
+
tags = ["tool", "fancy", "physics"]
+
+++
+
+
I was recently working through the Geometric Optics section of my physics textbook and was having trouble drawing all the ray diagrams (my wrist is still in a cast though that should come off in a few weeks) so I decided to try and make a tool to make them for me instead! I rather expected this to be a fairly simple process but instead it ended up being one of the most math intensive, most difficult โ€” and also most rewarding โ€” projects I've made recently!
+
+
<!-- more -->
+
+
## the tool (๐Ÿฅ roll please)
+
+
> this tool does support keyboard navigation btw ^-^
+
> `arrow keys` to move and `+` and `-` to zoom
+
+
{{ lensDiagram() }}
+
+
## the math
+
+
I was able to make it a bit simpler by restricting the domain of this tool to spherical mirrors (the only type used in this Module of my physics textbook) but I did tackle both concave and convex mirrors. It generates 3 rays: a horizontal ray, a ray through the focal point, and a ray through the radius of curvature. The first and last are quite easy to generate but the third was a bit more difficult. I ended up using a formula that I don't quite understand to get the point on the mirror where the ray intersects but it does work so ๐Ÿคท.
+
+
The horizontal ray was dead simple. Draw a line from the top of the arrow to the edge of the mirror and then draw another line from focal point through the intersection point in the mirror. The part of that ray that is behind the mirror is simply the extension of the ray for virtual images but the part in front of the mirror is the actual path of the ray.
+
+
```javascript
+
// Draw the horizontal ray
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
let intersectionX =
+
Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX;
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
intersectionX,
+
objY - h,
+
centerX - F * scale,
+
centerY,
+
);
+
ctx.stroke();
+
```
+
+
The ray through the radius of curvature was also fairly simple but alot more fun to figure out the math for. Since we know that there is a right angle triange between the arrow, center line, and the radius we can use the pythagorean theorem to find the missing side of the intersection height and then we can use the ratio of the radius to the arrow base to find the proper x offset.
+
+
```javascript
+
// Draw the ray through the radius of curvature
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(circleCenterX, centerY);
+
const extendedRay3 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
circleCenterX,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
centerX - R * scale,
+
centerY,
+
);
+
ctx.stroke();
+
```
+
+
The last ray, the one through the focal point, was the most difficult to figure out. I had to do quite a bit of geometry to find where this ray intersects the mirror. To find this intersection point I used a method that finds where a line intersects with a circle by solving a quadratic equation. This was necessary because the mirror is actually just part of a circle, and by finding where the ray intersects with that circle I can then determine if that intersection point is actually on the mirror's surface.
+
+
```javascript
+
// Draw the ray through the focal point
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(centerX - F * scale, centerY);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(0, extendedRay2[0].y);
+
ctx.stroke();
+
```
+
+
The method works by taking the equation of the line between our arrow tip and focal point (y = mx + b) and the equation of our mirror's circle ((x-h)ยฒ + (y-k)ยฒ = rยฒ) and substituting one into the other. This gives us a quadratic equation that we can solve to find the x coordinates of the intersection points. Once we have these x values, we can plug them back into our line equation to get the y coordinates.
+
+
Then we just need to check which of these intersection points is actually on the mirror's surface (since a line can intersect a circle in up to two points) and use that for our ray. From there, we can draw the reflected ray just like with the other two methods.
+
+
I will freely admit that I made heavy use of gpt-4o to figure out the inital equations as thats a bit beyond the current scope of my knowledge. The rest of the ray logic was too complex for gemini or claude to figure out so that bit was all me ๐Ÿ˜Ž
+
+
```javascript
+
// fancy complex scary math ๐Ÿ‘ป
+
function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) {
+
// Check if the input values are valid
+
if (radius <= 0) {
+
throw new Error("Invalid input values.");
+
}
+
+
// Calculate the slope of the line from (x1, h) to (x3, y3)
+
const m = (y3 - (centerY - h)) / (x3 - x1);
+
+
// Define the line equation: y = h + m * (x - x1)
+
// Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2
+
// y = h + m * (x - x1)
+
// (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2
+
+
// Coefficients for the quadratic equation
+
const a = 1 + m * m;
+
const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1);
+
const c =
+
centerX * centerX +
+
(centerY - h - centerY - m * x1) *
+
(centerY - h - centerY - m * x1) -
+
radius * radius;
+
+
// Calculate the discriminant
+
const discriminant = b * b - 4 * a * c;
+
+
if (discriminant < 0) {
+
throw new Error("No intersection found.");
+
}
+
+
// Calculate the two possible x values
+
const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a);
+
const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a);
+
+
// Calculate the corresponding y values
+
const yIntersect1 = centerY - h + m * (xIntersect1 - x1);
+
const yIntersect2 = centerY - h + m * (xIntersect2 - x1);
+
+
// Return the intersection points
+
return [
+
{ x: xIntersect1, y: yIntersect1 },
+
{ x: xIntersect2, y: yIntersect2 },
+
];
+
}
+
```
+99
content/blog/2025-03-14_my-animations.md
···
+
+++
+
title = "All my animation projects"
+
date = 2025-03-14
+
slug = "my-animations"
+
description = "finally collected in one place ๐ŸŽ‰"
+
+
[taxonomies]
+
tags = ["tool", "fancy", "physics"]
+
+++
+
+
The other day I realized that I never made a page to collect all of my animation projects in one place. I've made quite a few of them over the years and untill now they have just been sitting in a giant folder on my nas. Now they are all in a nice clean collection here ๐ŸŽ‰
+
+
<!-- more -->
+
+
## 2021
+
+
{{ youtube(id="O7SYcdUM8mI", caption="2021.01.27 first jelly jar") }}
+
+
![tesla in a showroom with fire jets](https://hc-cdn.hel1.your-objectstorage.com/s/v3/24338fe6379f23bebeda87ab6b0868c4a2890460_2f6bd410317b341df20d8349771bb716.jpg){caption="2021.02.10 tesla showroom"}
+
+
{{ youtube(id="7Ozt7WcVwt0", caption="2021.09.27 Chalet a la Tagia minecraft animation") }}
+
+
![cube diorama](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7ba5e3e478447e92bb4dc5091c99081c7294745c_90335a1f835749fe219a677a24aedd02.png){caption="2021.12.15 cube diorama"}
+
+
{{ youtube(id="O5iHoFwKQuE", caption="2021.12.17 creature walk cycle test") }}
+
+
{{ youtube(id="Mh4BL8O6-i8", caption="2021.12.21 minecraft water vfx test") }}
+
+
{{ youtube(id="xBn43UU_jak", caption="2021.12.23 fireball smoke / fire sim") }}
+
+
## 2022
+
+
{{ youtube(id="R9SwANdkMf0", caption="2022.01.05 wave ball motion effects") }}
+
+
{{ youtube(id="ru5QfeVqlUY", caption="2022.01.12 sunroom plant vfx") }}
+
+
{{ youtube(id="bHqE4aHSMLU", caption="2022.01.20 star chase") }}
+
+
{{ youtube(id="3JwTkVJ2WxU", caption="2022.01.21 star chase v2 in tunnel") }}
+
+
{{ youtube(id="mNeEJ-VE0o8", caption="2022.01.21 particle path guides test") }}
+
+
{{ youtube(id="Gy0K-Gi95Jg", caption="2022.01.22 lost music visualization") }}
+
+
![ice sphere](https://hc-cdn.hel1.your-objectstorage.com/s/v3/ed654ffc15801a163f4996e011371f433d66d8f6_c1759999d78c2a99312b5b34562c6f72.png){caption="2022.01.24 ice icosphere"}
+
+
![glass jar with marbles](https://hc-cdn.hel1.your-objectstorage.com/s/v3/766558e98ab81bd262f26e0650507669f88bbdb6_c9587628d15c605e99a3a72769bad60a.png){caption="2022.01.27 marble jar"}
+
+
{{ youtube(id="ue-hy7w1-JE", caption="2022.02.08 firefly particle sim") }}
+
+
{{ youtube(id="JrzjPBDBPF0", caption="2022.02.09 color changing blocks vfx") }}
+
+
{{ youtube(id="fh_cNR9QhdU", caption="2022.02.25 attempt at an epic chase scene") }}
+
+
{{ youtube(id="BGJbmXqCD5M", caption="2022.03.16 molecular plexus") }}
+
+
![twisted torus with flattened sphere in the center](https://hc-cdn.hel1.your-objectstorage.com/s/v3/67268702bc70adbfef7c9a2aaca996901e766737_57287dfc2f5eb7d6effe43f7ebbde030.png){caption="2022.03.16 twisted torus"}
+
+
{{ youtube(id="yT37oZmd4hc", caption="2022.03.17 hex tunnel") }}
+
+
{{ youtube(id="3SQN0L0wbhU", caption="2022.03.23 wavy strips motion effects") }}
+
+
![airship far bottom](https://hc-cdn.hel1.your-objectstorage.com/s/v3/f74e7ccb0591b5f3d1b2cff6c773fa88653aa6a8_b47952a40fd7695b9667357fb0c8386b.png){caption="2022.03.31 airship"}
+
+
![airship far side](https://hc-cdn.hel1.your-objectstorage.com/s/v3/1dddf1a9c6d8fcf5f907fd534e7a1cd1a679d1b9_52c9ccd4378fdac717a5a9f1b924000d.png)
+
+
![airship front top](https://hc-cdn.hel1.your-objectstorage.com/s/v3/d25c0834922bb1283bf8e994e2e550127f93aa31_4138d497e2deb1f9c7369762e08e5a7c.png)
+
+
![airship front cab](https://hc-cdn.hel1.your-objectstorage.com/s/v3/552636643eee62271818fed80b6e365de91aca05_bac28fe85728b6d009c4e520ccff3140.png)
+
+
![airship front side](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a62a9da89cff18aeeb95f1cc84027f86fc43cde9_963198d64f001f55d703cdc65a257e98.png)
+
+
![minecraft village front door with villager](https://hc-cdn.hel1.your-objectstorage.com/s/v3/966ccc3514057cbee79f1fa0b91750e909cbbce0_d3c7021428757b3bd607ffdbcc9daa7f.png){caption="2022.04.06 viking village"}
+
+
![minecraft village from across a small lake with a skeleton and witch](https://hc-cdn.hel1.your-objectstorage.com/s/v3/58c5f094932910f8f011a36c710ca6c7ac51396f_55aff1a73336f1262e625a667ca4d7f8.png){caption="2022.04.06 mountain lake village"}
+
+
{{ youtube(id="gPRrt_0NMKE", caption="2022.04.07 rolling balls motion effects") }}
+
+
{{ youtube(id="hHIv2yO9DvU", caption="2022.04.29 cloth sim") }}
+
+
{{ youtube(id="zqyv7GBTLGA", caption="2022.05.30 fire handwriting") }}
+
+
![a cylinder with a bunch of bumps on it](https://hc-cdn.hel1.your-objectstorage.com/s/v3/36eaad616bad2d70d5f066d6039b191ff26cfea2_42e90de7ab09d863c735a6ca74069fdc.png){caption="2022.06.24 the cylinder"}
+
+
{{ youtube(id="XVyMUROofZ8", caption="2022.07.21 the iconic donut") }}
+
+
{{ youtube(id="ZGrNNnujR3o", caption="2022.07.25 Appletree SMP minecraft animation") }}
+
+
{{ youtube(id="zRlgWbW1Qcw", caption="2022.08.01 mirror physics") }}
+
+
![the earth from space](https://hc-cdn.hel1.your-objectstorage.com/s/v3/d30ec95b2224c93cf244bb65f6bc9fda1c458149_b5876d1491b397c1a80fb8d7411fd627.png){caption="2022.08.30 the earth"}
+
+
![11 glowing pendulums swinging in a flowing curve](https://hc-cdn.hel1.your-objectstorage.com/s/v3/e30d3004c23540bf048cd5bbb3f5c98336db456f_bb000a699c4aa41413a282a31cfb0f59.png){caption="2022.08.31 glowing pendulums"}
+
+
![an orange flower in a flower pot with skyline dirt](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a5c2d4885ccaaa95628344bedf0fd7fb433034db_f1cdd1a422c0a3280541f3bbab4f662e.png){caption="2022.10.22 orange flower"}
+
+
## 2023
+
+
{{ youtube(id="qnbGPErmmoI", caption="2023.11.07 FIRST Digital Animation Award submission") }}
+225
content/blog/2025-03-18_adding-a-copy-button.md
···
+
+++
+
title = "Adding a copy code button"
+
date = 2025-03-14
+
slug = "adding-a-copy-button"
+
description = "continuing the chain :)"
+
+
[taxonomies]
+
tags = ["accessibility"]
+
+++
+
+
It took me a little over a month but I finally continued the chain of adding copy code buttons to your code blocks. It started with Salma Alam-Naylorโ€™s [post](https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/) which I saw on Hacker News but then [David Bushell](https://dbushell.com/2025/02/14/copy-code-button/) also posted on it and [Ragman](https://www.ragman.net/musings/copy_code/) made a bluesky post (sky? bloop? atproto bloop? honestly not sure what a more interesting name would be) and it's been saved in my mind since then that I should add it.
+
+
<!-- more -->
+
+
What finally pushed me over the edge was seeing the [Duckquill](https://duckquill.daudix.one) theme and its fancy code blocks. I cloned the theme (`git clone https://codeberg.org/daudix/duckquill.git`) and figured out that the actual copy code was some reasonably simple js in `static/copy-button.js`. I copied that file and messed with it a bit as well as the css (`sass/_pre-container.scss` and some icon stuff in `sass/_icon.scss`) to make it work with my theme and style.
+
+
A quick hash for cache busting and import later it all worked!
+
+
> templates/head.html
+
```html
+
{% set jsHash = get_hash(path="js/copy-button.js", sha_type=256,
+
base64=true) %}
+
<script
+
src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}"
+
defer
+
></script>
+
```
+
+
The one thing I expanded on was the ability to specify a file name / comment for the code block. When js is disabled a markdown `>` blockquote on the line before the code block will create a header tab for the code block. I snipped the header tab idea from [chevyray.dev](https://chevyray.dev) and I grew to quite like it so I didn't want to abandon it over a copy button.
+
+
Here is my code should you want to use it:
+
+
> static/js/copy-button.js
+
```js
+
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
+
document.addEventListener("DOMContentLoaded", () => {
+
const blocks = document.querySelectorAll("pre[class^='language-']");
+
+
for (const block of blocks) {
+
if (navigator.clipboard) {
+
// Code block header title
+
const title = document.createElement("span");
+
const lang = block.getAttribute("data-lang");
+
const comment =
+
block.previousElementSibling &&
+
(block.previousElementSibling.tagName === "blockquote" ||
+
block.previousElementSibling.nodeName === "BLOCKQUOTE")
+
? block.previousElementSibling
+
: null;
+
if (comment) block.previousElementSibling.remove();
+
title.innerHTML =
+
lang + (comment ? ` (${comment.textContent.trim()})` : "");
+
+
// Copy button icon
+
const icon = document.createElement("i");
+
icon.classList.add("icon");
+
+
// Copy button
+
const button = document.createElement("button");
+
const copyCodeText = "Copy code"; // Use hardcoded text instead of getElementById
+
button.setAttribute("title", copyCodeText);
+
button.appendChild(icon);
+
+
// Code block header
+
const header = document.createElement("div");
+
header.classList.add("header");
+
header.appendChild(title);
+
header.appendChild(button);
+
+
// Container that holds header and the code block itself
+
const container = document.createElement("div");
+
container.classList.add("pre-container");
+
container.appendChild(header);
+
+
// Move code block into the container
+
block.parentNode.insertBefore(container, block);
+
container.appendChild(block);
+
+
button.addEventListener("click", async () => {
+
await copyCode(block, header, button); // Pass the button here
+
});
+
}
+
}
+
+
async function copyCode(block, header, button) {
+
const code = block.querySelector("code");
+
const text = code.innerText;
+
+
await navigator.clipboard.writeText(text);
+
+
header.classList.add("active");
+
button.setAttribute("disabled", true);
+
+
header.addEventListener(
+
"animationend",
+
() => {
+
header.classList.remove("active");
+
button.removeAttribute("disabled");
+
},
+
{ once: true },
+
);
+
}
+
});
+
```
+
+
and the css:
+
+
> sass/css/_copy-button.scss
+
```scss
+
i.icon {
+
display: inline-block;
+
mask-size: cover;
+
background-color: currentColor;
+
width: 1rem;
+
height: 1rem;
+
font-style: normal;
+
font-variant: normal;
+
line-height: 0;
+
text-rendering: auto;
+
}
+
+
.pre-container {
+
--icon-copy: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' height='16' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 3c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3 0 .55-.45 1-1 1s-1-.45-1-1c0-.57-.43-1-1-1H3c-.57 0-1 .43-1 1v5c0 .57.43 1 1 1 .55 0 1 .45 1 1s-.45 1-1 1c-1.645 0-3-1.355-3-3zm5 5c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3v5c0 1.645-1.355 3-3 3H8c-1.645 0-3-1.355-3-3zm2 0v5c0 .57.43 1 1 1h5c.57 0 1-.43 1-1V8c0-.57-.43-1-1-1H8c-.57 0-1 .43-1 1m0 0'/%3E%3C/svg%3E");
+
--icon-done: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M7.883 0q-.486.008-.965.074a7.98 7.98 0 0 0-4.602 2.293 8.01 8.01 0 0 0-1.23 9.664 8.015 8.015 0 0 0 9.02 3.684 8 8 0 0 0 5.89-7.75 1 1 0 1 0-2 .008 5.986 5.986 0 0 1-4.418 5.816 5.996 5.996 0 0 1-6.762-2.766 5.99 5.99 0 0 1 .922-7.25 5.99 5.99 0 0 1 7.239-.984 1 1 0 0 0 1.363-.371c.273-.48.11-1.09-.371-1.367A8 8 0 0 0 9.492.14 8 8 0 0 0 7.882 0m7.15 1.998-.1.002a1 1 0 0 0-.687.34L7.95 9.535 5.707 7.29A1 1 0 0 0 4 8a1 1 0 0 0 .293.707l3 3c.195.195.465.3.742.293.277-.012.535-.133.719-.344l7-8A1 1 0 0 0 16 2.934a1 1 0 0 0-.34-.688 1 1 0 0 0-.627-.248'/%3E%3C/svg%3E");
+
+
margin: 1rem 0 1rem;
+
border-radius: 0.75rem;
+
+
.header {
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
border-radius: 0.2em 0.2em 0 0;
+
background-color: var(--accent);
+
background-size: 200%;
+
padding: 0.25rem;
+
height: 2.5rem;
+
+
span {
+
margin-inline-start: 0.75rem;
+
color: var(--purple-gray);
+
font-weight: bold;
+
line-height: 1;
+
}
+
+
button {
+
appearance: none;
+
transition: 200ms;
+
cursor: pointer;
+
border: none;
+
border-radius: 0.4rem;
+
background-color: transparent;
+
padding: 0.5rem;
+
color: var(--purple-gray);
+
line-height: 0;
+
+
&:hover {
+
background-color: color-mix(
+
in oklab,
+
var(--accent) 80%,
+
var(--purple-gray)
+
);
+
}
+
+
&:focus {
+
background-color: color-mix(
+
in oklab,
+
var(--accent) 80%,
+
var(--purple-gray)
+
);
+
}
+
+
&:active {
+
transform: scale(0.9);
+
}
+
+
&:disabled {
+
cursor: not-allowed;
+
+
&:active {
+
transform: none;
+
}
+
}
+
+
.icon {
+
-webkit-mask-image: var(--icon-copy);
+
mask-image: var(--icon-copy);
+
transition: 200ms;
+
+
:root[dir*="rtl"] & {
+
transform: scaleX(-1);
+
}
+
}
+
}
+
+
&.active {
+
button {
+
animation: active-copy 0.3s;
+
+
color: var(--purple-gray);
+
+
.icon {
+
-webkit-mask-image: var(--icon-done);
+
mask-image: var(--icon-done);
+
}
+
}
+
+
@keyframes active-copy {
+
50% {
+
transform: scale(0.9);
+
}
+
100% {
+
transform: none;
+
}
+
}
+
}
+
}
+
+
pre {
+
margin: 0;
+
box-shadow: none;
+
border-radius: 0 0 0.2em 0.2em;
+
}
+
}
+
```
+99
content/blog/2025-04-24_atuin.md
···
+
+++
+
title = "Musings about Atuin"
+
date = 2025-04-24
+
slug = "atuin"
+
description = "its a bit tricky on nix, but it's sooo worth it"
+
+
[taxonomies]
+
tags = ["shell", "nix", "cool stuff"]
+
+++
+
+
I've been on the fence about using [Atuin](https://atuin.sh) for about a month now. I heard about it from [Ellie](https://ellie.wtf) on bluesky and initially didn't bother setting it up since I didn't really care about whether my shell history was synced across devices as I'm only using one main device (framework 13 ๐Ÿ”ฅ) rn. I saw a repost of Ellie's about Atuin Desktop today and that finally pushed me over the edge to take the time to figure out how to get it setup with nix.
+
+
<!-- more -->
+
+
{{ bluesky(post="https://bsky.app/profile/ellie.wtf/post/3lng5ig2o722z") }}
+
+
And it wasn't that hard! Atuin is published on nixpkgs or can be installed via a flake and there is a home manager module for it too! Once you get past actual installation and into using agenix to declaritively manage your secrets then it gets annoying (proly mainly because i'm still pretty stupid when it comes to nix lol).
+
+
The first bit is that to access age secrets in home manager you have to actually export them in home manager (๐Ÿคฏ) and you can't just use the version from your `configuration.nix`. The second bit is that you **also** need to export the age file in your `configuration.nix` (that took me a solid half hour to figure out :uw_embarrassed:). The third and final thing however is that you can't just save the secret and key files with agenix like normal but you have to strip the line endings from them ๐Ÿ˜ญ.
+
+
Here's the basic scaffolding you'll need for your Nix configuration:
+
+
> configuration.nix
+
+
```nix
+
{ config, pkgs, ... }:
+
+
{
+
# ... configuration options
+
+
age.secrets = {
+
atuin-session = {
+
file = ../secrets/atuin-session.age;
+
mode = "0444";
+
};
+
atuin-key = {
+
file = ../secrets/atuin-key.age;
+
mode = "0444";
+
};
+
};
+
+
# ... more configuration options
+
}
+
```
+
+
and then the home-manager bit
+
+
> shell.nix in home manager
+
+
```nix
+
{ config, pkgs, ... }:
+
+
{
+
# ... some home-manager modules and configs
+
+
programs.atuin = {
+
enable = true;
+
settings = {
+
auto_sync = true;
+
sync_frequency = "5m";
+
sync_address = "https://api.atuin.sh";
+
search_mode = "fuzzy";
+
session_path = config.age.secrets."atuin-session".path;
+
key_path = config.age.secrets."atuin-key".path;
+
};
+
};
+
+
age.secrets = {
+
atuin-session = {
+
file = ../../secrets/atuin-session.age;
+
};
+
atuin-key = {
+
file = ../../secrets/atuin-key.age;
+
};
+
};
+
+
# ... even more home-manager configurations ๐Ÿ˜…
+
}
+
```
+
+
Now saving the secrets with agenix is not particularly tricky you just have to know that this is an option. Run `agenix -e atuin-session.age` and then paste in the session from `~/.local/share/atuin/session` and then instead of just saving like normal you need to run `:set binary` and then `:set noeol` and then you can save the file like normal.
+
+
Anyways now i'm enjoying my stats and it's on to the next project (proly [serif.blue](https://tangled.org/@dunkirk.sh/serif) ๐Ÿ‘€)
+
+
```
+
> atuin stats
+
[โ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎ] 1209 gc
+
[โ–ฎโ–ฎโ–ฎโ–ฎ ] 495 curl
+
[โ–ฎโ–ฎ ] 348 bun
+
[โ–ฎโ–ฎ ] 329 cat
+
[โ–ฎ ] 222 z
+
[โ–ฎ ] 200 g
+
[โ–ฎ ] 162 nix-shell
+
[โ–ฎ ] 145 cd
+
[โ–ฎ ] 138 vi
+
[โ–ฎ ] 138 ls
+
Total commands: 7062
+
Unique commands: 7060
+
```
+19
content/blog/2025-10-24_github-phishing.md
···
+
+++
+
title = "Novel phishing tactic using github notifications"
+
date = 2025-10-24
+
slug = "github-phishing"
+
description = "the creators certainly didn't execute this very well"
+
+
[taxonomies]
+
tags = ["phishing"]
+
+++
+
+
I received an email yesterday at `19:45 EST` titled `[yccombinator/-notification] Y-Combinator W2026 | $15M Y-Combinator & GitHub (Issue #126)`. From a quick glance it was easy to tell that it was a phising email funneling people to `https://y-comblnator.com/apply`. They did at least try to disguise the link but then there is a ton of whitespace and you can see that they tagged 32 github users including mine.
+
+
<!-- more -->
+
+
{{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/47a842d35a86d6ac16d717b40ee69f2f801ff852_screenshot_2025-09-23_at_21.23.19.png" alt="a screenshot of the email" caption="I've never seen something simultaniously this stupid and (as far as i can tell) novel") }}
+
+
Like most phishing emails I doubt most people would fall for this but if you were moving quickly and not thinking straight maybe you could fall for this?
+
+
Cloudflare has blocked the site due to phishing by now (13:17 Sept 24th) which is a shame since I would have loved to dig into the site a bit.
-102
content/blog/monaspace-vs-code-install.md
···
-
+++
-
title = "Monaspace VS-Code install"
-
date = 2023-11-10
-
slug = "monaspace-vs-code-install"
-
description = "How to install the Github Next team's Monaspace font in VSCode"
-
-
[taxonomies]
-
tags = ["tutorial", "archival"]
-
-
[extra]
-
has_toc = true
-
+++
-
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/KuOAwCEm9ypWEemv60Qs7.png" alt="monaspace font in action" caption="This font is so pretty and has so many features its amazing. It's main downside is to work it takes to set it up.") }}
-
-
To install the Monaspace font on macOS (or windows or linux) with VS Code and enable multifont syntax highlighting with the [CSS JS Loader extension](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css), you can follow these steps:
-
-
## 1. Download and install the Monaspace font:
-
-
First visit [https://github.com/githubnext/monaspace/releases/latest](https://github.com/githubnext/monaspace/releases/latest) and download the zip.
-
Next to install the Monaspace font:
-
- On macOS, drag the font files into font book.
-
- For windows, drag into the font window in settings.
-
- For Linux, clone the repo and run: `cd util; ./install_linux.sh`
-
-
## 2. Configure VS Code
-
-
Install the [Custom CSS and JS Loader](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css) plugin.
-
Set the font to one of the following options: `Monaspace Neon Var`, `Monaspace Argon Var`, `Monaspace Xeon Var`, `Monaspace Radon Var`, or `Monaspace Krypton Var`.
-
-
- You will find this option under _Editor: Font Family_ in the user preferences
-
-
{{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/v0cMm5jcwHEgrvtBv4Syx.png" alt="the available varients of the font") }}
-
-
-
Next enable font ligatures in the settings.json with following snippet:
-
-
> settings.json
-
```json
-
"editor.fontLigatures": "'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', calt', 'dlig'",
-
```
-
Now enable the custom CSS file within the `settings.json`, modifying the file path for Windows / MacOS / Linux if needed:
-
-
> still settings.json
-
```json
-
"vscode_custom_css.imports": [
-
"file:///Users/{{user}}/.vscode/style.css", // for mac (remove if not mac)
-
"file://C://Users/{{user}}/vscode/style.css" // for windows (remove if not windows)
-
"file:///home/{{user}}/.vscode/style.css" // for linux (remove if not windows)
-
],
-
```
-
-
## 3. Create custom CSS file at the path you specified above.
-
-
Depending on your VS Code version, the class names might be different, so you may need to use the developer tools to find the correct one.
-
The styles that worked for me on `VS Code version: 1.84.2 (Universal) commit: 1a5daa3a0231a0fbba4f14db7ec463cf99d7768e` are here:
-
-
> style.css
-
```css
-
/* Comment Class */
-
.mtk3 {
-
font-family: "Monaspace Radon Var";
-
font-weight: 500;
-
}
-
-
/* Copilot Classes */
-
.ghost-text-decoration {
-
font-family: "Monaspace Krypton Var";
-
font-weight: 200;
-
}
-
-
.ghost-text-decoration-preview {
-
font-family: "Monaspace Krypton Var";
-
font-weight: 200;
-
}
-
```
-
-
*Thanks to **[@fspoettel](https://github.com/fspoettel)** on GitHub for this trick to get the copilot classes when in dev mode*
-
-
> "You can inspect transient DOM elements by halting the app with a `debugger` after a delay with a debugger call inside a `setTimeout`."
-
>
-
> <cite>[@fspoettel](https://github.com/fspoettel)</cite>
-
-
You can copy the following snippet to do just that!
-
-
> console
-
```ts
-
setTimeout(() => {
-
debugger;
-
}, 10000);
-
```
-
-
Before you are finished make sure you have run the `Enable Custom CSS and JS` command from the command bar.
-
-
## Closing Remarks
-
-
That should be it! Hopefully you will have a beautiful custom font VS Code install.
-
-
If you are looking for a good theme, I can highly recommend the [Catppuccin](https://marketplace.visualstudio.com/items?itemName=Catppuccin.catppuccin-vsc) theme, as that is what I use myself. Be sure to check out [Monaspaceโ€™s webstite](https://monaspace.githubnext.com/) as it is a work of art. Happy Coding! ๐Ÿ‘ฉโ€๐Ÿ’ป
-
-
* *Updated 2024-08-22: changed mtk4 to mtk3 on the feedback of [mutammim](https://github.com/mutammim)*
-
* *Updated 2024-10-31: changed around the formating of the post and moved to [dunkirk.sh](https://dunkirk.sh)*
+9
content/now.md
···
+
+++
+
title = "Now"
+
description = "My status updates"
+
template = "page.html"
+
+++
+
+
## Status Updates
+
+
{{ now_status() }}
+30 -2
content/pfp.md
···
All my profile pictures over the years
+
# 2025
+
+
November 11th till present
+
+
![kieran with a robotics sweatshirt and fall leaves behind](/pfps/fall.webp)
+
+
September 13th till November 11th
+
+
![kieran with his hands framing his face](/pfps/hands.jpg)
+
+
June to September 13th
+
+
dynamically updating varient of the starry background one with the cat; the background would change with the time.
+
+
{{ bluesky(post="https://bsky.app/profile/serif.blue/post/3lqncouklcc2e") }}
+
+
February 27th to June
+
+
![kieran with an orange cast in a polaroid over a pinkish background](/pfps/instsqc-rat-pfp.webp)
+
+
February 24th to 27th
+
+
![kieran as a rat hand drawn in black over a purple gradient background](/pfps/kieranrat.webp)
+
# 2024
-
October to present
+
End of November to 2025 February
-
![kieran with a white and gray spotted kitten](/pfps/current.webp)
+
![kieran with a white and gray spotted kitten with a grainy background and star dust](/pfps/starry.webp)
+
+
October to very on of November
+
+
![kieran with a white and gray spotted kitten](/pfps/kitty.webp)
late September to early October
+30
content/verify.md
···
+
+++
+
title = "/Verify"
+
+++
+
+
Inspired by [@Molly White](https://www.mollywhite.net/verify/) and [@Rob Knight](https://rknight.me/verify) this page serves as verification of my various accounts.
+
+
# domains / email
+
+
I personally own and control this domain ([dunkirk.sh](https://dunkirk.sh)) as well as [kieranklukas.com](https://kieranklukas.com) and maintain email addresses on both domains. I also just got [serif.blue](https://serif.blue) which I'm super excited about! I have a redirect from [kieran.klukas.net](https://kieran.klukas.net) to dunkirk.sh that is also maintained.
+
+
If you want to contact me, via email to `kieran@dunkirk.sh`. I also send email from `kieran@hackclub.com` and ~`kieran@purplebubble.org`~.
+
+
# accounts
+
+
- Keyoxide: [aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A](https://keyoxide.org/aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A)
+
- Github: [@taciturnaxolotl](https://github.com/taciturnaxolotl) (formerly @kcoderhtml)
+
- [Hackclub Slack](https://hackclub.com/slack/): [@krn](https://hackclub.slack.com/team/U062UG485EE) (display name changes quite often though) with userID `U062UG485EE`
+
- Bluesky: [@dunkirk.sh](https://bsky.app/profile/dunkirk.sh)
+
- Mastodon: [@taciturnaxolotl@social.dino.icu](https://social.dino.icu/@taciturnaxolotl)
+
- Youtube: [@kieran.rambles](https://www.youtube.com/@kieran.rambles)
+
- Matrix: ~[@kieran:dumpsterfire.icu](https://matrix.to/#/@kieran.matrix.dumpsterfire.icu)~ or [@sclacker:matrix.org](https://matrix.to/#/@sclacker:matrix.org) (i'm active on here once in a blue moon so this isn't a great way to contact me urgently)
+
- Signal: `verox.89`
+
+
# keys
+
+
> SSH
+
+
```pub
+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzEEjvbL/ttqmYoDjxYQmDIq36BabROJoXgQKeh9liBxApwp+2PmgxROzTg42UrRc9pyrkq5kVfxG5hvkqCinhL1fMiowCSEs2L2/Cwi40g5ZU+QwdcwI8a4969kkI46PyB19RHkxg54OUORiIiso/WHGmqQsP+5wbV0+4riSnxwn/JXN4pmnE//stnyAyoiEZkPvBtwJjKb3Ni9n3eNLNs6gnaXrCtaygEZdebikr9kS2g9mM696HvIFgM6cdR/wZ7DcLbG3IdTXuHN7PC3xxL+Y4ek5iMreQIPmuvs4qslbthPGYoYbYLUQiRa9XO5s/ksIj5Z14f7anHE6cuTQVpvNWdGDOigyIVS5qU+4ZF7j+rifzOXVL48gmcAvw/uV68m5Wl/p0qsC/d8vI3GYwEsWG/EzpAlc07l8BU2LxWgN+d7uwBFaJV9VtmUDs5dcslsh8IbzmtC9gq3OLGjklxTfIl6qPiL8U33oc/UwqzvZUrI2BlbagvIZYy6rP+q0= me@dunkirk.sh
+
```
+1744
highlight_themes/Wildlife.tmTheme
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+
<!-- Generated by: TmTheme-Editor -->
+
<!-- ============================================ -->
+
<!-- app: http://tmtheme-editor.herokuapp.com -->
+
<!-- code: https://github.com/aziz/tmTheme-Editor -->
+
<plist version="1.0">
+
<dict>
+
<key>author</key>
+
<string>Taiwo Kareem</string>
+
<key>name</key>
+
<string>Wildlife</string>
+
<key>settings</key>
+
<array>
+
<dict>
+
<key>settings</key>
+
<dict>
+
<key>activeGuide</key>
+
<string></string>
+
<key>background</key>
+
<string>#FFFFFF</string>
+
<key>caret</key>
+
<string>#FF00FF</string>
+
<key>foreground</key>
+
<string>#000000</string>
+
<key>findHighlight</key>
+
<string>#00BFFF</string>
+
<key>findHighlightForeground</key>
+
<string>#f0f0f0</string>
+
<key>gutter</key>
+
<string></string>
+
<key>inactiveSelection</key>
+
<string>#ffcc9c</string>
+
<key>invisibles</key>
+
<string>#BFBFBF</string>
+
<key>lineHighlight</key>
+
<string>#00000012</string>
+
<key>selectionBorder</key>
+
<string>#E996F7</string>
+
<key>selection</key>
+
<string>#BAD6FD</string>
+
<key>stackGuide</key>
+
<string>#abcdef</string>
+
<key>shadow</key>
+
<string>#ff000011</string>
+
<key>bracketContentsForeground</key>
+
<string>#6600FF</string>
+
<key>bracketContentsOptions</key>
+
<string>stippled_underline</string>
+
<key>bracketsForeground</key>
+
<string>#800080</string>
+
<key>tagsForeground</key>
+
<string>#6600FF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Comment</string>
+
<key>scope</key>
+
<string>comment</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#F5FFFF</string>
+
<key>foreground</key>
+
<string>#919191</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>String</string>
+
<key>scope</key>
+
<string>string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00A33F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Number</string>
+
<key>scope</key>
+
<string>constant.numeric</string>
+
<key>settings</key>
+
<dict>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Built-In Constant</string>
+
<key>scope</key>
+
<string>constant.language</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C0D</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>User-Defined Constant</string>
+
<key>scope</key>
+
<string>constant.character, constant.other</string>
+
<key>settings</key>
+
<dict>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Variable</string>
+
<key>scope</key>
+
<string>variable.language, variable.other</string>
+
<key>settings</key>
+
<dict>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Keyword</string>
+
<key>scope</key>
+
<string>keyword</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Storage</string>
+
<key>scope</key>
+
<string>storage</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Type Name</string>
+
<key>scope</key>
+
<string>entity.name.type</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#21439C</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Inherited Class</string>
+
<key>scope</key>
+
<string>entity.other.inherited-class</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#21439C</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Function Name</string>
+
<key>scope</key>
+
<string>entity.name.function</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#21439C</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Function Argument</string>
+
<key>scope</key>
+
<string>variable.parameter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#993CF3</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Tag Name</string>
+
<key>scope</key>
+
<string>entity.name.tag, punctuation.definition.tag</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0000FF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Tag Attribute</string>
+
<key>scope</key>
+
<string>entity.other.attribute-name</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Library Function</string>
+
<key>scope</key>
+
<string>support.function</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C0D</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Library Constant</string>
+
<key>scope</key>
+
<string>support.constant</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C0D</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Library Class&#x2f;Type</string>
+
<key>scope</key>
+
<string>support.type, support.class</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C0D</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Library Variable</string>
+
<key>scope</key>
+
<string>support.variable</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C0D</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Invalid</string>
+
<key>scope</key>
+
<string>invalid</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#990000</string>
+
<key>foreground</key>
+
<string>#FFFFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>String Interpolation</string>
+
<key>scope</key>
+
<string>constant.other.placeholder</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#990000</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Text: Embedded Embedded Source</string>
+
<key>scope</key>
+
<string>text source text source</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#EFEFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Header</string>
+
<key>scope</key>
+
<string>meta.header, markup.heading</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Git Gutter: Deleted</string>
+
<key>scope</key>
+
<string>markup.deleted.git_gutter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F92672</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Git Gutter: Inserted</string>
+
<key>scope</key>
+
<string>markup.inserted.git_gutter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#22AA22</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Git Gutter: Changed</string>
+
<key>scope</key>
+
<string>markup.changed.git_gutter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#660066</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Git Gutter: Ignored</string>
+
<key>scope</key>
+
<string>markup.ignored.git_gutter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#3333CC</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Git Gutter: Untracked</string>
+
<key>scope</key>
+
<string>markup.untracked.git_gutter</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00CC99</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Annotations</string>
+
<key>scope</key>
+
<string>sublimelinter.annotations</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFAA</string>
+
<key>foreground</key>
+
<string>#FFFFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Error Outline</string>
+
<key>scope</key>
+
<string>sublimelinter.outline.illegal</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FF4A52</string>
+
<key>foreground</key>
+
<string>#FFFFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Error Underline</string>
+
<key>scope</key>
+
<string>sublimelinter.underline.illegal</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FF0000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Warning Outline</string>
+
<key>scope</key>
+
<string>sublimelinter.outline.warning</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#DF9400</string>
+
<key>foreground</key>
+
<string>#FFFFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Warning Underline</string>
+
<key>scope</key>
+
<string>sublimelinter.underline.warning</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FF0000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Violation Outline</string>
+
<key>scope</key>
+
<string>sublimelinter.outline.violation</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFFF33</string>
+
<key>foreground</key>
+
<string>#FFFFFF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Violation Underline</string>
+
<key>scope</key>
+
<string>sublimelinter.underline.violation</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FF0000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Gutter Mark</string>
+
<key>scope</key>
+
<string>sublimelinter.gutter-mark</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF0000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Error</string>
+
<key>scope</key>
+
<string>sublimelinter.mark.error</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#D02000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Sublimelinter: Warning</string>
+
<key>scope</key>
+
<string>sublimelinter.mark.warning</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#DDB700</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Character Escape</string>
+
<key>scope</key>
+
<string>constant.character.escape</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#EFEAF4</string>
+
<key>foreground</key>
+
<string>#4C4D00</string>
+
<key>fontStyle</key>
+
<string>italic bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Language Constants</string>
+
<key>scope</key>
+
<string>constant.language, constant.character, constant.other</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Numbers</string>
+
<key>scope</key>
+
<string>constant.numeric</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0000FF</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Preprocessor</string>
+
<key>scope</key>
+
<string>meta.preprocessor, meta.preprocessor</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#008000</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Punctuations: Comments</string>
+
<key>scope</key>
+
<string>punctuation.definition.comment</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Punctuations: Strings</string>
+
<key>scope</key>
+
<string>punctuation.definition.string</string>
+
<key>settings</key>
+
<dict>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Punctuations: Parameters&#x2f;Preprocessors</string>
+
<key>scope</key>
+
<string>punctuation.definition.parameters, punctuation.definition.preprocessor, meta.preprocessor</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string></string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Storage&#x2f;Antlr: Extends</string>
+
<key>scope</key>
+
<string>storage.modifier, meta.definition.class.extends.antlr </string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Support: Class</string>
+
<key>scope</key>
+
<string>support.class</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F12958</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Punctuations: Arguments</string>
+
<key>scope</key>
+
<string>punctuation.definition.arguments</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#26004C</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Antlr: Class&#x2f;Options</string>
+
<key>scope</key>
+
<string>meta.definition.class.antlr, meta.options-block.antlr, meta.options.antlr, meta.rule.antlr</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#A535AE</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Antlr: Rule</string>
+
<key>scope</key>
+
<string>meta.rule.antlr</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#080</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Antlr: Punctuations-Section</string>
+
<key>scope</key>
+
<string>punctuation.section.group.antlr, punctuation.section.options.antlr</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00004C</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>C++: Destructor</string>
+
<key>scope</key>
+
<string>meta.function.destructor</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>C++: Arguments</string>
+
<key>scope</key>
+
<string>meta.parens</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#993CF3</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>C++: Constructor Initializer Lists</string>
+
<key>scope</key>
+
<string>meta.function.constructor.initializer-list</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFE0</string>
+
<key>foreground</key>
+
<string>#BA064E</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>C: Prototypes</string>
+
<key>scope</key>
+
<string>meta.function.prototype</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>C: Constructor</string>
+
<key>scope</key>
+
<string>meta.function.constructor</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Cold Fusion: Sql Embedded</string>
+
<key>scope</key>
+
<string>source.sql.embedded</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFF6F6</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Property Lists</string>
+
<key>scope</key>
+
<string>meta.property-list.css</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFF4FF</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Property Name</string>
+
<key>scope</key>
+
<string>meta.property-name.css, support.type.property-name.css</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5555</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Property Value</string>
+
<key>scope</key>
+
<string>meta.property-value.css, support.constant.property-value.css</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Colors</string>
+
<key>scope</key>
+
<string>constant.other.color, support.constant.color</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#8000FF</string>
+
<key>fontStyle</key>
+
<string></string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Attribute Names</string>
+
<key>scope</key>
+
<string>entity.other.attribute-name</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0080FF</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Css: Variable Parameters</string>
+
<key>scope</key>
+
<string>variable.parameter.misc.css</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00AA00</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Csv: Row Header</string>
+
<key>scope</key>
+
<string>meta.tabular.row.header.csv</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: File Header</string>
+
<key>scope</key>
+
<string>meta.diff.header.from-file</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Punctuation Separator</string>
+
<key>scope</key>
+
<string>punctuation.definition.separator.diff</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#BF6514</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Changed Markup</string>
+
<key>scope</key>
+
<string>markup.changed.diff</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FEFEAE</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Punctuation Inserted</string>
+
<key>scope</key>
+
<string>punctuation.definition.inserted.diff, markup.inserted.diff</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#22AA22</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Deleted Markup</string>
+
<key>scope</key>
+
<string>markup.deleted.diff</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#A00000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Toc Line Numbers</string>
+
<key>scope</key>
+
<string>meta.toc-list.line-number.diff</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#2222FF</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Context Range</string>
+
<key>scope</key>
+
<string>meta.diff.range.context, meta.diff.range.unified, meta.diff.range.normal</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0000FF</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Diff: Index</string>
+
<key>scope</key>
+
<string>meta.diff.index</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F00</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Bold</string>
+
<key>scope</key>
+
<string>markup.bold</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0000FF</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Italic</string>
+
<key>scope</key>
+
<string>markup.italic</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0000FF</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: List</string>
+
<key>scope</key>
+
<string>markup.list, punctuation.definition.list_item</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FDF5EF</string>
+
<key>foreground</key>
+
<string>#800000</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Table</string>
+
<key>scope</key>
+
<string>markup.other.table, punctuation.definition.table, meta.separator, markup.other, punctuation.definition.field</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FB89F0</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Raw</string>
+
<key>scope</key>
+
<string>markup.raw,meta.raw.block, punctuation.definition.raw</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#8FDDF630</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Links</string>
+
<key>scope</key>
+
<string>markup.underline.link, punctuation.definition.link, meta.link</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C00</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Imagelinks</string>
+
<key>scope</key>
+
<string>meta.image.inline,meta.image.reference, punctuation.definition.image</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F0F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Quotes</string>
+
<key>scope</key>
+
<string>markup.quote</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FDF5E6</string>
+
<key>foreground</key>
+
<string>#228B22</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markup: Quotes Punctuation</string>
+
<key>scope</key>
+
<string>punctuation.definition.blockquote</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F0F</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Dylan: Class&#x2f;Options</string>
+
<key>scope</key>
+
<string>meta.definition.class, meta.rescue, meta.features</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Erlang: Directives&#x2f;Expression</string>
+
<key>scope</key>
+
<string>meta.directive, meta.expression</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Groovy: Method Call</string>
+
<key>scope</key>
+
<string>meta.method-call</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Graphics: Graph Attribute</string>
+
<key>scope</key>
+
<string>support.constant.attribute.graph.dot</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Graphics: Node Attribute</string>
+
<key>scope</key>
+
<string>support.constant.attribute.node.dot</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Graphics: Edge Attribute</string>
+
<key>scope</key>
+
<string>support.constant.attribute.edge.dot</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#97531F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Haml: Objects</string>
+
<key>scope</key>
+
<string>meta.section.object.haml</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Haml: Ruby Embedded Html</string>
+
<key>scope</key>
+
<string>source.ruby.embedded.html</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFEFD5</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Asp Html: Punctuation Section</string>
+
<key>scope</key>
+
<string>punctuation.section.embedded</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#55AAFF</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Css Tag</string>
+
<key>scope</key>
+
<string>source.css punctuation.definition.tag</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F00</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Embedded Ruby tag</string>
+
<key>scope</key>
+
<string>punctuation.section.embedded.ruby</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Javascript Tag</string>
+
<key>scope</key>
+
<string>source.js punctuation.definition.tag</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0A0</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Other Tag</string>
+
<key>scope</key>
+
<string>meta.tag.other punctuation.definition.tag</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F0F</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Entity Character</string>
+
<key>scope</key>
+
<string>constant.character.entity.html</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Html: Doctype Tag</string>
+
<key>scope</key>
+
<string>meta.tag.sgml</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Java: Constructors</string>
+
<key>scope</key>
+
<string>meta.definition.constructor.java, entity.name.function.constructor.java</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Javadoc: Comments</string>
+
<key>scope</key>
+
<string>comment.block.documentation.javadoc</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFCC</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Javadoc: Punctuations</string>
+
<key>scope</key>
+
<string>punctuation.definition.comment.javadoc, punctuation.definition.comment.begin.javadoc</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00F</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Javadoc: Directive Punctuations</string>
+
<key>scope</key>
+
<string>punctuation.definition.directive.begin.javadoc, punctuation.definition.directive.end.javadoc</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#963</string>
+
<key>fontStyle</key>
+
<string>italic bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group2</string>
+
<key>scope</key>
+
<string>source.json meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#900</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group3</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#606</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group4</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C00</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group5</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0C0</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group6</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#663</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group7</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F06</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group8</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#030</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group9</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group10</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#69F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group11</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F55</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group12</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#C90</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group13</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0C9</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group14</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F00</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Json:Group15</string>
+
<key>scope</key>
+
<string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F93</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Mail: Encoding</string>
+
<key>scope</key>
+
<string>meta.encoded-text.mail</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#9C0</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Makefile: Other Variables</string>
+
<key>scope</key>
+
<string>variable.other.makefile</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800080</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Makefile: Separator Punctuation</string>
+
<key>scope</key>
+
<string>punctuation.separator.continuation.makefile</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F90</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Makefile: Function</string>
+
<key>scope</key>
+
<string>meta.function.makefile</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markdown: Block Level</string>
+
<key>scope</key>
+
<string>meta.block-level.markdown</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#F9F9F6</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Markdown: References</string>
+
<key>scope</key>
+
<string>meta.link.reference</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFEBF9</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Matlab: Graphics Support</string>
+
<key>scope</key>
+
<string>support.graphics.matlab</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFDF0</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Matlab: Control Support</string>
+
<key>scope</key>
+
<string>support.function.control</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFDB</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Mediawiki: Punctuation Fix Reminder</string>
+
<key>scope</key>
+
<string>punctuation.fix_this_later</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#B20047</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Ocaml: Module</string>
+
<key>scope</key>
+
<string>meta.module</string>
+
<key>settings</key>
+
<dict>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Ocaml: Module Reference</string>
+
<key>scope</key>
+
<string>meta.module-reference.ocaml</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Ocaml: Action</string>
+
<key>scope</key>
+
<string>meta.action.ocaml</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Php: Embedded Block</string>
+
<key>scope</key>
+
<string>source.php.embedded.block.html</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFFA</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Php: Function Arguments</string>
+
<key>scope</key>
+
<string>meta.function.arguments</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#26004C</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Php: Object Function Call</string>
+
<key>scope</key>
+
<string>meta.function-call.object</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Plain Text: </string>
+
<key>scope</key>
+
<string>meta.bullet-point.light, meta.bullet-point.star, meta.bullet-point.strong, punctuation.definition.item.text, meta.paragraph.text</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Python: Language Variable </string>
+
<key>scope</key>
+
<string>variable.language.python</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#61210B</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Rd Documentation: </string>
+
<key>scope</key>
+
<string>punctuation.section.group.tex</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F00</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Regexp: Range</string>
+
<key>scope</key>
+
<string>constant.other.range.regexp</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#00F</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Regexp: Anchor</string>
+
<key>scope</key>
+
<string>keyword.control.anchors.regexp</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#690</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Regexp: Set</string>
+
<key>scope</key>
+
<string>keyword.control.set.regexp</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#909</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Regexp: Character Class</string>
+
<key>scope</key>
+
<string>keyword.other.backref-and-recursion.regexp</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#630</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Regexp: Quantifier</string>
+
<key>scope</key>
+
<string>keyword.operator.quantifier.regexp</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#099</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Restructured Text: Directive</string>
+
<key>scope</key>
+
<string>meta.other.directive.restructuredtext, punctuation.definition.raw.restructuredtext</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#F00</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Restructured Text: Commands</string>
+
<key>scope</key>
+
<string>markup.other.command.restructuredtext</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFFFFC</string>
+
<key>foreground</key>
+
<string>#933</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Restructured Text: Footnote</string>
+
<key>scope</key>
+
<string>meta.link.footnote</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#FFE0FF</string>
+
<key>foreground</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Restructured Text: Field</string>
+
<key>scope</key>
+
<string>punctuation.definition.field.restructuredtext</string>
+
<key>settings</key>
+
<dict>
+
<key>background</key>
+
<string>#933</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Restructured Text: Link</string>
+
<key>scope</key>
+
<string>punctuation.definition.link.restructuredtext</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#0C9</string>
+
<key>fontStyle</key>
+
<string>bold</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Ruby: Readwrite</string>
+
<key>scope</key>
+
<string>variable.other.readwrite, punctuation.separator.method.ruby</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#930</string>
+
<key>fontStyle</key>
+
<string>italic</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Ruby On Rails: Punctuation Separators</string>
+
<key>scope</key>
+
<string>punctuation.separator.key-value, punctuation.separator.variable</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#FF5600</string>
+
</dict>
+
</dict>
+
<dict>
+
<key>name</key>
+
<string>Shell Unix Generic: Heredoc Unquoted</string>
+
<key>scope</key>
+
<string>string.unquoted.heredoc</string>
+
<key>settings</key>
+
<dict>
+
<key>foreground</key>
+
<string>#800000</string>
+
</dict>
+
</dict>
+
</array>
+
<key>uuid</key>
+
<string>3ca46127-0e70-4501-9a4d-52accf12a12d</string>
+
<key>colorSpaceName</key>
+
<string>sRGB</string>
+
<key>semanticClass</key>
+
<string>theme.light.wildlife</string>
+
</dict>
+
</plist>
+57
hooks/pre-commit
···
+
#!/usr/bin/env bash
+
+
# Check if exiftool is installed
+
if ! command -v exiftool &> /dev/null; then
+
echo "Error: exiftool is not installed. Please install it." >&2
+
exit 1
+
fi
+
+
# Flag to track if we found any draft files
+
found_draft=0
+
+
# First pass: check for draft files
+
while read -r file; do
+
case "$file" in
+
*.md)
+
# Check if file contains draft = true within +++ header section
+
awk '
+
/^\+\+\+$/ { inblock = !inblock }
+
inblock && /draft = true/ { found = 1 }
+
END { exit(found ? 0 : 1) }
+
' "$file"
+
if [ $? -eq 0 ]; then
+
echo "Error: Draft file detected: $file" >&2
+
echo "Please remove draft status or unstage this file before committing." >&2
+
found_draft=1
+
fi
+
;;
+
*)
+
;;
+
esac
+
done < <(git diff --cached --name-only --diff-filter=ACMR)
+
+
# Exit if we found any draft files
+
if [ $found_draft -eq 1 ]; then
+
exit 1
+
fi
+
+
# Second pass: process images
+
while read -r file; do
+
case "$file" in
+
*.jpg|*.jpeg|*.png|*.gif|*.tiff|*.bmp)
+
# Store output of exiftool command
+
cleared_data=$(exiftool -all= --icc_profile:all -tagsfromfile @ -orientation -overwrite_original "$file")
+
if [ $? -ne 0 ]; then
+
echo "Error: exiftool failed to process $file" >&2
+
exit 1
+
fi
+
echo "Cleared EXIF data for $file:" >&2
+
echo "$cleared_data" >&2
+
git add "$file"
+
;;
+
*)
+
;;
+
esac
+
done < <(git diff --cached --name-only --diff-filter=ACMR)
+
+
exit 0
+19
package.json
···
+
{
+
"name": "zera",
+
"type": "module",
+
"scripts": {
+
"dev": "bun run scripts/dev.ts",
+
"serve": "bun run scripts/dev.ts",
+
"build": "bun run scripts/build.ts",
+
"preprocess": "bun run scripts/preprocess.ts",
+
"gen-og": "bun run scripts/genOG.ts"
+
},
+
"dependencies": {
+
"dotenv": "^16.4.7",
+
"glob": "^13.0.0"
+
},
+
"devDependencies": {
+
"@types/bun": "latest",
+
"puppeteer": "^23.6.0"
+
}
+
}
+159
sass/css/_copy-button.scss
···
+
i.icon {
+
display: inline-block;
+
mask-size: cover;
+
background-color: var(--accent-text);
+
width: 1rem;
+
height: 1rem;
+
font-style: normal;
+
font-variant: normal;
+
line-height: 0;
+
text-rendering: auto;
+
}
+
+
.pre-container {
+
--icon-copy: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' height='16' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 3c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3 0 .55-.45 1-1 1s-1-.45-1-1c0-.57-.43-1-1-1H3c-.57 0-1 .43-1 1v5c0 .57.43 1 1 1 .55 0 1 .45 1 1s-.45 1-1 1c-1.645 0-3-1.355-3-3zm5 5c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3v5c0 1.645-1.355 3-3 3H8c-1.645 0-3-1.355-3-3zm2 0v5c0 .57.43 1 1 1h5c.57 0 1-.43 1-1V8c0-.57-.43-1-1-1H8c-.57 0-1 .43-1 1m0 0'/%3E%3C/svg%3E");
+
--icon-done: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M7.883 0q-.486.008-.965.074a7.98 7.98 0 0 0-4.602 2.293 8.01 8.01 0 0 0-1.23 9.664 8.015 8.015 0 0 0 9.02 3.684 8 8 0 0 0 5.89-7.75 1 1 0 1 0-2 .008 5.986 5.986 0 0 1-4.418 5.816 5.996 5.996 0 0 1-6.762-2.766 5.99 5.99 0 0 1 .922-7.25 5.99 5.99 0 0 1 7.239-.984 1 1 0 0 0 1.363-.371c.273-.48.11-1.09-.371-1.367A8 8 0 0 0 9.492.14 8 8 0 0 0 7.882 0m7.15 1.998-.1.002a1 1 0 0 0-.687.34L7.95 9.535 5.707 7.29A1 1 0 0 0 4 8a1 1 0 0 0 .293.707l3 3c.195.195.465.3.742.293.277-.012.535-.133.719-.344l7-8A1 1 0 0 0 16 2.934a1 1 0 0 0-.34-.688 1 1 0 0 0-.627-.248'/%3E%3C/svg%3E");
+
+
background-color: var(--accent);
+
padding: 0.4em;
+
border-bottom: 5px solid var(--bg-light);
+
border-radius: 7px 7px 10px 10px;
+
+
.header {
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
border-radius: 0.2em 0.2em 0 0;
+
/* background-color: var(--accent); */
+
background-size: 200%;
+
padding: 0.25rem;
+
height: 2.5rem;
+
+
span {
+
margin-inline-start: 0.75rem;
+
color: var(--purple-gray);
+
font-weight: bold;
+
line-height: 1;
+
}
+
+
button {
+
appearance: none;
+
transition: 200ms;
+
cursor: pointer;
+
border: none;
+
border-radius: 0.4rem;
+
background-color: transparent;
+
padding: 0.5rem;
+
color: var(--purple-gray);
+
line-height: 0;
+
+
&:hover {
+
background-color: color-mix(
+
in oklab,
+
var(--accent) 80%,
+
var(--purple-gray)
+
);
+
}
+
+
&:focus {
+
background-color: color-mix(
+
in oklab,
+
var(--accent) 80%,
+
var(--purple-gray)
+
);
+
}
+
+
&:active {
+
transform: scale(0.9);
+
}
+
+
&:disabled {
+
cursor: not-allowed;
+
+
&:active {
+
transform: none;
+
}
+
}
+
+
.icon {
+
-webkit-mask-image: var(--icon-copy);
+
mask-image: var(--icon-copy);
+
transition: 200ms;
+
+
:root[dir*="rtl"] & {
+
transform: scaleX(-1);
+
}
+
}
+
}
+
+
&.active {
+
button {
+
animation: active-copy 0.3s;
+
+
color: var(--purple-gray);
+
+
.icon {
+
-webkit-mask-image: var(--icon-done);
+
mask-image: var(--icon-done);
+
}
+
}
+
+
@keyframes active-copy {
+
50% {
+
transform: scale(0.9);
+
}
+
100% {
+
transform: none;
+
}
+
}
+
}
+
}
+
+
pre {
+
margin: 0;
+
box-shadow: none;
+
border-radius: 0.3rem;
+
border: none;
+
}
+
}
+
+
blockquote:not(.pre-container blockquote):has(+ pre) {
+
margin-bottom: 0;
+
background-color: var(--accent);
+
border-radius: 7px 7px 0 0;
+
padding: 0.25rem;
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
+
p {
+
margin: 0;
+
padding: 0.25rem 0.75rem;
+
font-weight: bold;
+
line-height: 1;
+
}
+
}
+
+
pre.z-code:not(.pre-container pre) {
+
position: relative;
+
border: none;
+
background-color: var(--accent);
+
padding: 0.4em;
+
border-bottom: 5px solid var(--bg-light);
+
border-radius: 7px 7px 10px 10px;
+
+
code {
+
border-radius: 0.3rem;
+
display: block;
+
overflow-x: auto;
+
padding: 1em;
+
background-color: var(--nightshade-violet);
+
margin: 0;
+
border: none;
+
box-shadow: none;
+
}
+
}
+
+
blockquote:not(.pre-container blockquote) + pre.z-code {
+
border-radius: 0 0 7px 7px;
+
}
+32
sass/css/_emoji-inline.scss
···
+
.emoji-inline--wrapper {
+
vertical-align: baseline;
+
height: auto;
+
position: relative;
+
overflow: visible;
+
vertical-align: top;
+
object-fit: contain;
+
align-items: center;
+
display: inline-flex;
+
overflow: hidden;
+
width: 1.375rem;
+
height: 1.375rem;
+
}
+
+
.emoji-inline--wrapper img {
+
object-fit: contain;
+
position: absolute;
+
top: 50%;
+
overflow: hidden;
+
width: 1.375rem !important;
+
height: 1.375rem !important;
+
margin-top: -0.6875rem;
+
margin-left: 0 !important;
+
margin-right: 0 !important;
+
margin-bottom: 0 !important;
+
border: none !important;
+
border-radius: 0 !important;
+
padding: 0 !important;
+
opacity: 1 !important;
+
max-width: 1.375rem !important;
+
display: inline !important;
+
}
+109
sass/css/_lightbox.scss
···
+
#lightbox {
+
display: none;
+
position: fixed;
+
top: 0;
+
left: 0;
+
width: 100%;
+
height: 100%;
+
background-color: rgba(0, 0, 0, 0.8);
+
z-index: 9999;
+
justify-content: center;
+
align-items: center;
+
}
+
+
.lightbox-content {
+
position: relative;
+
max-width: 90%;
+
max-height: 90%;
+
display: flex;
+
flex-direction: column;
+
align-items: center;
+
justify-content: center;
+
}
+
+
#lightbox-img {
+
max-width: 100%;
+
max-height: 80vh;
+
object-fit: contain;
+
border: none;
+
padding: 0;
+
margin: 0;
+
background: transparent;
+
border-radius: 0;
+
}
+
+
.lightbox-controls {
+
display: flex;
+
gap: 2rem;
+
margin-top: 1rem;
+
align-items: center;
+
}
+
+
.lightbox-close {
+
position: fixed;
+
top: 20px;
+
right: 20px;
+
font-size: 40px;
+
color: var(--text);
+
background: transparent !important;
+
border: none;
+
cursor: pointer;
+
padding: 0;
+
line-height: 1;
+
-webkit-tap-highlight-color: transparent;
+
transition: color 120ms ease, transform 300ms ease;
+
}
+
+
.lightbox-close:hover {
+
background: transparent !important;
+
color: var(--accent);
+
background-color: transparent !important;
+
transform: rotate(90deg);
+
}
+
+
.lightbox-close:focus {
+
background: transparent !important;
+
background-color: transparent !important;
+
}
+
+
.lightbox-prev,
+
.lightbox-next {
+
font-size: 30px;
+
color: var(--text);
+
background: transparent !important;
+
border: none;
+
cursor: pointer;
+
padding: 0.5rem 1rem;
+
user-select: none;
+
-webkit-tap-highlight-color: transparent;
+
transition: color 120ms ease;
+
}
+
+
.lightbox-prev:hover,
+
.lightbox-next:hover {
+
background: transparent !important;
+
color: var(--accent);
+
background-color: transparent !important;
+
}
+
+
.lightbox-prev:focus,
+
.lightbox-next:focus {
+
background: transparent !important;
+
background-color: transparent !important;
+
}
+
+
@media only screen and (max-width: 720px) {
+
.lightbox-close {
+
top: 10px;
+
right: 10px;
+
}
+
}
+
+
.img-container {
+
cursor: pointer;
+
transition: opacity 120ms ease;
+
}
+
+
.img-container:hover {
+
opacity: 0.9;
+
}
+17
sass/css/_theme-toggle.scss
···
+
#theme-toggle-label i.icon {
+
--icon-dark: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z'/%3E%3C/svg%3E");
+
+
--icon-light: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z'/%3E%3C/svg%3E");
+
+
--icon-toggle: var(--icon-light);
+
+
-webkit-mask-image: var(--icon-toggle);
+
mask-image: var(--icon-toggle);
+
background-color: var(--accent);
+
color: var(--accent);
+
+
width: 1.2rem;
+
height: 1.2rem;
+
transform: translateY(0.25rem);
+
overflow: visible;
+
}
+3
sass/css/_view-transitions.scss
···
+
@view-transition {
+
navigation: auto;
+
}
-4
sass/css/main.sass
···
-
@import "reset"
-
@import "suCSS"
-
@import "syntax-theme"
-
@import "mods"
+10
sass/css/main.scss
···
+
@use "reset";
+
+
@use "suCSS";
+
@use "syntax-theme";
+
@use "mods";
+
+
@use "copy-button";
+
@use "view-transitions";
+
@use "emoji-inline";
+
@use "lightbox";
+302 -83
sass/css/mods.css
···
#nav-bar {
-
padding: 0.625rem 0 0 0;
-
display: flex;
-
flex-direction: row;
-
gap: 0.25rem;
-
flex-wrap: wrap;
-
justify-content: flex-end;
-
align-items: center;
-
align-content: flex-end;
+
padding: 0.625rem 0 0 0;
+
display: flex;
+
flex-direction: row;
+
flex-wrap: wrap;
+
justify-content: flex-end;
+
align-items: center;
+
align-content: flex-end;
+
}
+
+
#nav-bar a {
+
text-decoration: none;
+
color: var(--link);
+
padding: 0 0.25rem;
+
border-radius: 0.25rem;
+
transition: all 120ms ease;
+
position: relative;
+
font-weight: 600;
+
}
+
+
#nav-bar a:hover {
+
color: var(--accent);
+
background-color: color-mix(in oklab, var(--accent) 15%, transparent);
+
}
+
+
#nav-bar a.active {
+
color: var(--link-visited);
+
background-color: color-mix(in oklab, var(--accent) 20%, transparent);
+
margin: 0 0.15rem;
}
#footer-container {
-
display: flex;
-
flex-wrap: wrap;
-
justify-content: center;
-
align-items: center;
-
text-align: center;
-
padding-bottom: 0.5rem;
+
display: flex;
+
flex-direction: column;
+
justify-content: center;
+
align-items: center;
+
text-align: center;
+
padding-bottom: 0.5rem;
}
-
.accent-data {
-
color: var(--accent-light);
+
#footer-container p {
+
margin: 0;
}
-
.theme-transition {
-
transition: background-color 0.3s ease;
+
.accent-data {
+
color: var(--accent-dark);
}
.tags-data {
-
display: flex;
-
flex-direction: row;
-
flex-wrap: wrap;
-
justify-content: flex-end;
-
align-items: flex-start;
-
align-content: flex-end;
-
gap: 0.25rem;
+
display: flex;
+
flex-direction: row;
+
flex-wrap: wrap;
+
justify-content: flex-end;
+
align-items: flex-start;
+
align-content: flex-end;
+
gap: 0.25rem;
}
.title-list li {
-
margin-bottom: 0.375rem;
+
margin-bottom: 0.375rem;
}
/* icons settings */
.icons {
-
width: 1.3rem;
-
height: 1.3rem;
-
aspect-ratio: 1 / 1;
-
display: inline-block;
-
vertical-align: middle;
-
color: var(--text);
-
fill: var(--text);
-
background-color: transparent;
-
cursor: pointer;
+
width: 1.3rem;
+
height: 1.3rem;
+
aspect-ratio: 1 / 1;
+
display: inline-block;
+
vertical-align: middle;
+
color: var(--text);
+
fill: var(--text);
+
background-color: transparent;
+
cursor: pointer;
}
.icons:hover {
-
background-color: transparent;
-
color: var(--accent);
+
background-color: transparent;
+
color: var(--accent);
}
/* footnotes */
.footnote-definition {
-
margin: 0 0 0 0.125rem;
+
margin: 0 0 0 0.125rem;
}
.footnote-definition-label {
-
color: var(--accent);
+
color: var(--accent);
}
.footnote-definition p {
-
display: inline;
-
margin: 0.625rem 0 0 0.625rem;
+
display: inline;
+
margin: 0.625rem 0 0 0.625rem;
}
/* general classes */
.no-style {
-
padding: 0;
-
margin: 0;
-
border: none;
-
border-radius: 0;
+
padding: 0;
+
margin: 0;
+
border: none;
+
border-radius: 0;
}
.no-style:hover {
-
background-color: transparent;
-
color: var(--accent);
+
background-color: transparent;
+
color: var(--accent);
}
.center {
-
text-align: center;
+
text-align: center;
}
-
.center img {
-
display: block;
-
margin: 1rem auto;
+
.center .img-container {
+
margin: 1rem auto;
}
.center figcaption {
-
text-align: center;
+
text-align: center;
}
.float-right {
-
float: right;
+
float: right;
}
.float-left {
-
float: left;
+
float: left;
}
-
div code,
+
div>code,
li code,
p code {
-
color: var(--text);
-
background-color: var(--bg-light);
+
padding: 0.1em 0.3em 0.1em 0.3em;
+
color: var(--text-dark);
+
background-color: var(--bg-light);
}
pre {
-
border-top-left-radius: 0;
+
border-top-left-radius: 0;
}
blockquote {
-
padding-top: 0.2rem;
-
padding-bottom: 0.2rem;
+
padding-top: 0.2rem;
+
padding-bottom: 0.2rem;
}
blockquote:has(+ pre) {
-
display: inline-block;
-
border: none;
-
font-family: "code" !important;
-
font-size: 0.8rem;
-
font-weight: 600;
-
margin: 0;
-
margin-bottom: 0.2rem;
-
margin-block: 0 0;
-
border-top-left-radius: 5px;
-
border-top-right-radius: 5px;
-
padding-left: 0.75rem;
-
padding-right: 0.75rem;
-
padding-top: 0.25rem;
-
padding-bottom: 0.25rem;
-
position: relative;
-
background-color: var(--accent);
+
display: inline-block;
+
border: none;
+
font-family: "code", monospace !important;
+
font-size: 0.8rem;
+
font-weight: 600;
+
margin: 0;
+
margin-bottom: 0.2rem;
+
margin-block: 0 0;
+
border-top-left-radius: 0.2em;
+
border-top-right-radius: 0.2em;
+
padding-left: 0.75rem;
+
padding-right: 0.75rem;
+
padding-top: 0.25rem;
+
padding-bottom: 0.25rem;
+
position: relative;
+
background-color: var(--accent);
}
blockquote:has(+ pre) p {
-
margin: 0;
-
color: var(--accent-text);
+
margin: 0;
+
color: var(--accent-text);
+
}
+
+
blockquote:has(+ pre) p::selection {
+
background: var(--pink-puree);
}
.yt-embed {
-
width: 100%;
-
display: flex;
-
justify-content: center;
+
width: 100%;
+
display: flex;
+
flex-direction: column;
+
justify-content: center;
+
align-content: center;
+
text-align: center;
}
.yt-embed iframe {
-
width: 100%;
-
aspect-ratio: 16 / 9;
+
width: 100%;
+
aspect-ratio: 16 / 9;
+
align-self: center;
+
}
+
+
.yt-embed figcaption {
+
margin-top: 1rem;
+
text-align: center;
+
}
+
+
#elr table {
+
border-style: none;
+
}
+
+
#elr td {
+
border-style: dashed;
+
}
+
+
img.avatar {
+
width: 24px;
+
height: 24px;
+
aspect-ratio: 1 / 1;
+
border-radius: 50%;
+
vertical-align: middle;
+
display: inline-block;
+
border: none;
+
padding: 0;
+
margin: 0;
+
}
+
+
cite {
+
display: inline-flex;
+
align-items: center;
+
vertical-align: middle;
+
}
+
+
cite a {
+
display: inline-flex;
+
align-items: center;
+
}
+
+
cite a img.avatar {
+
margin-right: 5px;
+
}
+
+
.image-gallery {
+
display: flex;
+
flex-direction: column;
+
gap: 0.5rem;
+
padding-top: 0.5rem;
}
+
+
.image-gallery img {
+
max-width: 100%;
+
border-radius: 0.25rem;
+
padding: 0;
+
margin: 0;
+
}
+
+
.side-by-side {
+
display: flex;
+
flex-direction: row;
+
}
+
+
.side-by-side img {
+
max-width: calc(50% - 0.25rem);
+
}
+
+
.gallery-grid {
+
display: grid;
+
grid-template-columns: repeat(2, 1fr);
+
gap: 0.1rem;
+
}
+
+
.gallery-grid img {
+
width: 100%;
+
height: auto;
+
object-fit: cover;
+
}
+
+
.inlined-bubbles {
+
width: auto;
+
pointer-events: none;
+
display: block;
+
margin-top: 10px;
+
text-align: center;
+
}
+
+
.bubble {
+
padding: 0.7em 1.2em 0.7em 1.2em;
+
background: var(--accent);
+
border-bottom: 5px solid var(--bg-light);
+
border-radius: 7px 7px 10px 10px;
+
font-size: 1rem;
+
font-weight: bold;
+
color: var(--accent-text);
+
display: inline-block;
+
text-align: center;
+
transition:
+
transform 0.3s ease,
+
opacity 0.3s ease;
+
}
+
+
@keyframes bubbleIn {
+
0% {
+
opacity: 0;
+
transform: translateY(10px) scale(0.95);
+
}
+
+
100% {
+
opacity: 1;
+
transform: translateY(0) scale(1);
+
}
+
}
+
+
@media (prefers-reduced-motion: no-preference) {
+
.bubble.animate-in {
+
animation: bubbleIn 0.3s ease-out forwards;
+
}
+
}
+
+
.bubble a {
+
color: var(--accent-text);
+
text-decoration: none;
+
}
+
+
#time-ago {
+
font-weight: normal;
+
font-size: 0.8rem;
+
}
+
+
.badge-row {
+
display: flex;
+
flex-wrap: wrap;
+
justify-content: center;
+
gap: 8px;
+
padding-bottom: 0.5rem;
+
}
+
+
.badge-row img {
+
display: inline-block;
+
border: none;
+
border-radius: 0;
+
box-shadow: none;
+
max-width: 100%;
+
height: auto;
+
margin: 0;
+
padding: 0;
+
}
+
+
.badge-row a {
+
display: inline-flex;
+
align-items: center;
+
}
+
+
.img-group {
+
display: flex;
+
flex-direction: row;
+
gap: 1rem;
+
max-width: 100%;
+
justify-content: center;
+
align-items: flex-start;
+
}
+
+
.img-group .img-container {
+
background-color: var(--accent);
+
border-bottom: 4px solid var(--bg-light);
+
border-radius: 7px 7px 10px 10px;
+
padding: 0.35rem;
+
margin: 1rem 0;
+
line-height: 0;
+
}
+
+
.img-group img {
+
max-width: 100%;
+
height: auto;
+
border-radius: 0.35rem;
+
}
+
+
:root {
+
--nightshade-violet: oklch(0.27 0.0242 287.67);
+
--purple-night: oklch(27.58% 0.0203 289.13);
+
--red-crushed-grape: oklch(0.6829 0.1189 296.74);
+
--dark-crushed-grape: oklch(0.6261 0.1289 284.99);
+
--light-crushed-grape: oklch(0.7727 0.094 296.74);
+
--reseda-green: oklch(62.33% 0.0475 126.94);
+
--earth-yellow: oklch(86.49% 0.018 73.05);
+
--sunset: oklch(0.86 0.0213 73.05);
+
--ultra-violet: oklch(42.21% 0.0676 297.45);
+
--rose-quartz: oklch(65.32% 0.0585 311.96);
+
--pink-puree: oklch(75.65% 0.0555 290.76);
+
--lavendar-breeze: oklch(91.06% 0.0223 290.76);
+
--purple-gray: oklch(25.63% 0.0002 290.76);
+
--alice-blue: oklch(95.38% 0.0118 239.91);
+
}
+24 -24
sass/css/reset.css
···
*,
*::before,
*::after {
-
box-sizing: border-box;
-
-webkit-box-sizing: border-box;
+
box-sizing: border-box;
+
-webkit-box-sizing: border-box;
}
* {
-
margin: 0;
+
margin: 0;
}
/* Prevent font size inflation */
html {
-
-moz-text-size-adjust: none;
-
-webkit-text-size-adjust: none;
-
text-size-adjust: none;
+
-moz-text-size-adjust: none;
+
-webkit-text-size-adjust: none;
+
text-size-adjust: none;
}
/* Remove default margin in favour of better control in authored CSS */
···
blockquote,
dl,
dd {
-
margin-block-end: 0;
+
margin-block-end: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
···
ol,
ul[role="list"],
ol[role="list"] {
-
list-style: none;
+
list-style: none;
}
/* Set core body defaults */
body {
-
min-height: 100vh;
-
line-height: 1.5;
-
-webkit-font-smoothing: antialiased;
+
min-height: 100vh;
+
line-height: 1.5;
+
-webkit-font-smoothing: antialiased;
}
/* Set shorter line heights on headings and interactive elements */
···
button,
input,
label {
-
line-height: 1.1;
+
line-height: 1.1;
}
/* Balance text wrapping on headings */
···
h2,
h3,
h4 {
-
text-wrap: balance;
+
text-wrap: balance;
}
p,
···
h4,
h5,
h6 {
-
overflow-wrap: break-word;
+
overflow-wrap: break-word;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
-
text-decoration-skip-ink: auto;
+
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
···
video,
canvas,
svg {
-
max-width: 100%;
-
display: block;
+
max-width: 100%;
+
display: block;
}
/* Inherit fonts for inputs and buttons */
···
textarea,
select,
progress {
-
appearance: none;
-
-webkit-appearance: none;
-
-moz-appearance: none;
-
font: inherit;
+
appearance: none;
+
-webkit-appearance: none;
+
-moz-appearance: none;
+
font: inherit;
}
/* Make sure textareas without a rows attribute are not tiny */
textarea:not([rows]) {
-
min-height: 10em;
+
min-height: 10em;
}
/* Anything that has been anchored to should have extra scroll margin */
:target {
-
scroll-margin-block: 5ex;
+
scroll-margin-block: 5ex;
}
#root,
#__next {
-
isolation: isolate;
+
isolation: isolate;
}
+416 -359
sass/css/suCSS.css
···
:root,
::backdrop {
-
/* set sans-serif & mono fonts */
-
--sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir,
-
"Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica,
-
"Helvetica Neue", sans-serif;
-
--mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
-
--standard-border-radius: 5px;
+
/* set sans-serif & mono fonts */
+
--sans-font:
+
-apple-system, BlinkMacSystemFont, "Avenir Next", Avenir,
+
"Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica,
+
"Helvetica Neue", sans-serif;
+
--serif-font:
+
Superclarendon, "Bookman Old Style", "URW Bookman", "URW Bookman L",
+
"Georgia Pro", Georgia, serif;
+
--mono-font:
+
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
+
"DejaVu Sans Mono", monospace;
+
--standard-border-radius: 5px;
-
/* default colors */
-
--bg: #eeeeee;
-
--bg-light: #cbcdcd;
-
--text: #41474e;
-
--text-light: #686764;
-
--accent: #58310ac5;
-
--accent-light: #e08f67;
-
--accent-text: var(--bg);
-
--border: #646868;
-
--link: #573819c5;
-
}
-
-
/* theme media queries */
-
@media (prefers-color-scheme: dark) {
-
:root,
-
::backdrop {
-
color-scheme: dark;
-
--bg: #222529;
-
--bg-light: #464949;
-
--text: #d6d6d6;
-
--text-light: #c5c0b7;
-
--accent: #78b6ad;
-
--accent-light: #87c9e5;
-
--accent-text: var(--bg);
-
--border: #dbd5bc;
-
--link: #e2c8a2;
-
}
-
img,
-
video {
-
opacity: 0.8;
-
}
-
}
-
-
@media (prefers-color-scheme: light) {
-
:root,
-
::backdrop {
-
color-scheme: light;
-
--bg: #eeeeee;
-
--bg-light: #cbcdcd;
-
--text: #41474e;
-
--text-light: #686764;
-
--accent: #58310ac5;
-
--accent-light: #e08f67;
-
--accent-text: var(--bg);
-
--border: #646868;
-
--link: #573819c5;
-
}
-
}
-
-
[data-theme="light"] {
-
/* default (light) theme */
-
color-scheme: light;
-
--bg: #eeeeee;
-
--bg-light: #cbcdcd;
-
--text: #41474e;
-
--text-light: #686764;
-
--accent: #58310ac5;
-
--accent-light: #56412bc5;
-
--accent-text: var(--bg);
-
--border: #646868;
-
--link: #573819c5;
-
}
-
-
[data-theme="dark"] {
-
color-scheme: dark;
-
--bg: #222529;
-
--bg-light: #464949;
-
--text: #d6d6d6;
-
--text-light: #c5c0b7;
-
--accent: #78b4b6e3;
-
--accent-light: #c5edefe6;
-
--accent-text: var(--bg);
-
--border: #dbd5bc;
-
--link: #e2c8a2;
+
/* default colors */
+
color-scheme: dark;
+
--bg: var(--purple-night);
+
--noise-strength: 0.15;
+
--bg-light: var(--ultra-violet);
+
--text: var(--lavendar-breeze);
+
--text-light: var(--pink-puree);
+
--text-dark: oklch(80.28% 0.0111 204.11);
+
--accent: var(--rose-quartz);
+
--accent-dark: var(--dark-crushed-grape);
+
--accent-text: var(--purple-gray);
+
--link: var(--light-crushed-grape);
+
--link-visited: var(--red-crushed-grape);
+
--border: var(--pink-puree);
+
--selection: color-mix(in oklab, var(--accent), var(--purple-night) 50%);
}
::selection,
::-moz-selection {
-
color: var(--bg);
-
background: var(--accent);
+
color: var(--bg);
+
background: var(--selection);
}
/* chromium scrollbars */
::-webkit-scrollbar {
-
width: 8px;
-
height: 8px;
-
overflow: visible;
+
width: 8px;
+
height: 8px;
+
overflow: visible;
}
+
::-webkit-scrollbar-thumb {
-
background: var(--accent);
-
width: 12px;
+
background: var(--accent);
+
width: 12px;
}
+
::-webkit-scrollbar-track {
-
background: var(--bg-light);
+
background: var(--bg-light);
}
/* firefox scrollbars */
* {
-
scrollbar-color: var(--accent) var(--bg-light);
-
scrollbar-width: thin;
-
scrollbar-height: thin;
+
scrollbar-color: var(--accent) var(--bg-light);
+
scrollbar-width: auto;
}
html {
-
color-scheme: light dark;
-
font-family: var(--mono-font);
-
scroll-behavior: smooth;
+
color-scheme: light dark;
+
font-family: var(--mono-font);
+
scroll-behavior: smooth;
}
body {
-
min-height: 100svh;
-
color: var(--text);
-
background-color: var(--bg);
-
font-size: 1rem;
-
display: grid;
-
grid-template-columns: 1fr min(45rem, 90%) 1fr;
-
grid-template-rows: auto 1fr auto;
-
grid-row-gap: 0.625rem;
+
min-height: 100svh;
+
color: var(--text);
+
background: var(--bg);
+
position: relative;
+
font-size: 1rem;
+
display: grid;
+
grid-template-columns: 1fr min(45rem, 90%) 1fr;
+
grid-template-rows: auto 1fr auto;
+
grid-row-gap: 0.625rem;
}
-
body > * {
-
grid-column: 2;
+
+
body>* {
+
grid-column: 2;
}
-
body > footer {
-
color: var(--text-light);
-
font-size: 0.875;
+
body>footer {
+
color: var(--text-light);
+
font-size: 0.875;
}
/* Format headers */
h1 {
-
font-size: 2rem;
+
font-size: 2rem;
}
+
h2 {
-
font-size: 1.75rem;
+
font-size: 1.75rem;
}
+
h3 {
-
font-size: 1.5rem;
+
font-size: 1.5rem;
}
+
h4 {
-
font-size: 1.25rem;
+
font-size: 1.25rem;
}
+
h5 {
-
font-size: 1rem;
+
font-size: 1rem;
}
+
h6 {
-
font-size: 0.75rem;
+
font-size: 0.75rem;
}
h1,
···
h4,
h5,
h6 {
-
margin: 0.5em 0 0.5em 0;
+
margin: 0.5em 0 0.5em 0;
+
padding: 0.22em 0.4em 0.22em 0.4em;
+
background-color: var(--accent);
+
border-bottom: 5px solid var(--bg-light);
+
border-radius: 0.2em 0.2em 0.27em 0.27em;
+
color: var(--accent-text);
+
width: fit-content;
}
/* Fix line height when title wraps */
h1,
h2,
h3 {
-
line-height: 1.1;
-
}
-
-
h1::before,
-
h2::before,
-
h3::before,
-
h4::before,
-
h5::before,
-
h6::before {
-
color: var(--accent);
-
content: "# ";
+
line-height: 1.1;
}
@media only screen and (max-width: 720px) {
-
h1 {
-
font-size: 1.5rem;
-
}
-
h2 {
-
font-size: 1.25rem;
-
}
-
h3 {
-
font-size: 1rem;
-
}
-
h4 {
-
font-size: 0.75rem;
-
}
-
h5 {
-
font-size: 0.5rem;
-
}
-
h6 {
-
font-size: 0.25rem;
-
}
+
h1 {
+
font-size: 1.5rem;
+
}
+
+
h2 {
+
font-size: 1.25rem;
+
}
+
+
h3 {
+
font-size: 1rem;
+
}
+
+
h4 {
+
font-size: 0.75rem;
+
}
+
+
h5 {
+
font-size: 0.5rem;
+
}
+
+
h6 {
+
font-size: 0.25rem;
+
}
}
p {
-
margin: 1rem 0;
+
margin: 1rem 0;
}
/* format links */
-
a,
+
a {
+
color: var(--link);
+
text-decoration: none;
+
font-weight: 600;
+
transition: color 120ms ease;
+
}
+
a:visited {
-
text-decoration: none;
-
font-weight: bold;
-
font-style: italic;
-
border-radius: 0.125rem;
-
color: var(--link);
+
color: var(--link-visited);
}
-
a:hover {
-
background-color: var(--link);
-
color: var(--bg);
+
a:hover,
+
a:focus-visible {
+
color: var(--accent);
+
outline: none;
+
}
+
+
a:visited:hover,
+
a:visited:focus-visible {
+
color: var(--accent);
}
/* format lists */
ul {
-
list-style: none;
-
margin-top: 0.25rem;
-
margin-bottom: 0.25rem;
+
list-style: none;
+
margin-top: 0.25rem;
+
margin-bottom: 0.25rem;
}
ol {
-
list-style-type: decimal;
-
margin-top: 0.25rem;
-
margin-bottom: 0.25rem;
+
list-style-type: decimal;
+
margin-top: 0.25rem;
+
margin-bottom: 0.25rem;
}
li {
-
margin-bottom: 0.125rem;
+
margin-bottom: 0.125rem;
}
ul li::marker {
-
content: "ยป ";
-
color: var(--accent);
-
}
-
-
ul li:hover::marker {
-
content: "# ";
-
font-weight: 700;
-
color: var(--link);
+
content: "* ";
+
color: var(--accent);
+
font-size: 1.1rem;
}
ol li::marker {
-
color: var(--accent);
+
color: var(--accent);
}
ol li:hover::marker {
-
font-weight: 700;
-
color: var(--link);
+
font-weight: 700;
+
color: var(--link);
}
/* Use flexbox to allow items to wrap, as needed */
-
header > nav ul,
-
header > nav ol {
-
display: flex;
-
flex-direction: row;
-
flex-wrap: wrap;
-
align-content: space-around;
-
justify-content: right;
-
list-style-type: none;
-
margin: 0.5rem 0 0 0;
-
padding: 0;
-
gap: 1rem;
+
header>nav ul,
+
header>nav ol {
+
display: flex;
+
flex-direction: row;
+
flex-wrap: wrap;
+
align-content: space-around;
+
justify-content: right;
+
list-style-type: none;
+
margin: 0.5rem 0 0 0;
+
padding: 0;
+
gap: 1rem;
}
/* List items are inline elements, make them behave more like blocks */
-
header > nav ul li,
-
header > nav ol li {
-
display: inline-block;
+
header>nav ul li,
+
header>nav ol li {
+
display: inline-block;
}
/* Consolidate box styling */
aside,
details,
progress {
-
background-color: var(--bg-light);
-
border-radius: var(--standard-border-radius);
+
background-color: var(--bg-light);
+
border-radius: var(--standard-border-radius);
}
aside {
-
font-size: 1rem;
-
width: 35%;
-
padding: 0 10px;
-
margin-inline-start: 10px;
-
float: right;
+
font-size: 1rem;
+
width: 35%;
+
padding: 0 10px;
+
margin-inline-start: 10px;
+
float: right;
}
+
*[dir="rtl"] aside {
-
float: left;
+
float: left;
}
/* make aside full-width on mobile */
@media only screen and (max-width: 720px) {
-
aside {
-
width: 100%;
-
float: none;
-
margin-inline-start: 0;
-
}
+
aside {
+
width: 100%;
+
float: none;
+
margin-inline-start: 0;
+
}
}
details {
-
padding: 0.5rem;
+
padding: 0.5rem;
}
summary {
-
cursor: pointer;
-
font-weight: bold;
-
word-break: break-all;
+
cursor: pointer;
+
font-weight: bold;
+
word-break: break-all;
}
-
details[open] > summary + * {
-
margin-top: 0;
+
details[open]>summary+* {
+
margin-top: 0;
}
-
details[open] > summary {
-
margin-bottom: 0.5rem;
+
details[open]>summary {
+
margin-bottom: 0.5rem;
}
-
details[open] > :last-child {
-
margin-bottom: 0;
+
details[open]> :last-child {
+
margin-bottom: 0;
}
/* Format tables */
table {
-
border-collapse: collapse;
-
margin: 1.5rem 0;
-
display: block;
-
overflow-x: auto;
-
white-space: nowrap;
+
border-collapse: collapse;
+
margin: 1.5rem 0;
+
display: block;
+
overflow-x: auto;
+
white-space: nowrap;
}
td,
th {
-
border: 1px solid var(--border);
-
text-align: start;
-
padding: 0.5rem;
+
border: 1px solid var(--border);
+
text-align: start;
+
padding: 0.5rem;
}
th {
-
background-color: var(--bg-light);
-
font-weight: bold;
+
background-color: var(--bg-light);
+
font-weight: bold;
}
tr:nth-child(even) {
-
background-color: var(--bg-light);
+
background-color: var(--bg-light);
}
table caption {
-
text-align: left;
-
font-weight: bold;
-
margin: 0 0 0.4rem 1rem;
+
text-align: left;
+
font-weight: bold;
+
margin: 0 0 0.4rem 1rem;
}
/* format forms */
fieldset {
-
border: 1px dashed var(--accent);
-
border-radius: var(--standard-border-radius);
+
border: 1px dashed var(--accent);
+
border-radius: var(--standard-border-radius);
}
-
fieldset > legend {
-
color: var(--accent);
+
fieldset>legend {
+
color: var(--accent);
}
textarea,
···
input,
button,
.button {
-
font-size: inherit;
-
font-family: inherit;
-
padding: 0.25rem;
-
border-radius: var(--standard-border-radius);
-
box-shadow: none;
-
max-width: 100%;
-
display: inline-block;
+
font-size: inherit;
+
font-family: inherit;
+
padding: 0.25rem;
+
border-radius: var(--standard-border-radius);
+
box-shadow: none;
+
max-width: 100%;
+
display: inline-block;
}
textarea,
select,
input {
-
color: var(--text);
-
background-color: var(--bg);
-
border: 1px dashed var(--border);
+
color: var(--text);
+
background-color: var(--bg);
+
border: 1px dashed var(--border);
}
label {
-
display: block;
+
display: block;
}
fieldset label {
-
margin: 0 0 0.3rem 0;
+
margin: 0 0 0.3rem 0;
}
textarea {
-
max-width: 43.5rem;
-
resize: both;
+
max-width: 43.5rem;
+
resize: both;
}
textarea:not([cols]) {
-
width: 100%;
+
width: 100%;
}
@media only screen and (max-width: 720px) {
-
textarea,
-
select,
-
input {
-
width: 100%;
-
}
+
+
textarea,
+
select,
+
input {
+
width: 100%;
+
}
}
/* format buttons */
···
input[type="reset"],
input[type="button"],
label[type="button"] {
-
border: 1px solid var(--accent);
-
background-color: var(--accent);
-
color: var(--accent-text);
-
padding: 0.5rem 0.9rem;
-
text-decoration: none;
-
line-height: normal;
+
border: 1px solid var(--accent);
+
background-color: var(--accent);
+
color: var(--accent-text);
+
padding: 0.5rem 0.9rem;
+
text-decoration: none;
+
line-height: normal;
}
.button[aria-disabled="true"],
···
textarea:disabled,
select:disabled,
button[disabled] {
-
cursor: not-allowed;
-
background-color: var(--bg-light);
-
border-color: var(--bg-light);
-
color: var(--text-light);
+
cursor: not-allowed;
+
background-color: var(--bg-light);
+
border-color: var(--bg-light);
+
color: var(--text-light);
}
input[type="range"] {
-
padding: 0;
-
color: var(--accent);
+
padding: 0;
+
color: var(--accent);
}
abbr[title] {
-
cursor: help;
-
text-decoration-line: underline;
-
text-decoration-style: dotted;
+
cursor: help;
+
text-decoration-line: underline;
+
text-decoration-style: dotted;
}
button:enabled:hover,
···
input[type="reset"]:enabled:hover,
input[type="button"]:enabled:hover,
label[type="button"]:hover {
-
background-color: var(--accent-light);
-
border-color: var(--accent-light);
-
cursor: pointer;
+
background-color: var(--accent-dark);
+
border-color: var(--accent-dark);
+
cursor: pointer;
}
.button:focus-visible,
button:focus-visible:where(:enabled),
-
input:enabled:focus-visible:where(
-
[type="submit"],
-
[type="reset"],
-
[type="button"]
-
) {
-
outline: 2px solid var(--accent);
-
outline-offset: 1px;
+
input:enabled:focus-visible:where([type="submit"],
+
[type="reset"],
+
[type="button"]) {
+
outline: 2px solid var(--accent);
+
outline-offset: 1px;
}
/* checkbox and radio button style */
input[type="checkbox"],
input[type="radio"] {
-
vertical-align: middle;
-
position: relative;
-
width: min-content;
-
width: 14px;
-
height: 14px;
+
vertical-align: middle;
+
position: relative;
+
width: 14px;
+
height: 14px;
}
-
input[type="checkbox"] + label,
-
input[type="radio"] + label {
-
display: inline-block;
+
input[type="checkbox"]+label,
+
input[type="radio"]+label {
+
display: inline-block;
}
input[type="radio"] {
-
border-radius: 100%;
+
border-radius: 100%;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
-
background-color: var(--accent);
+
background-color: var(--accent);
}
@media only screen and (max-width: 720px) {
-
textarea,
-
select,
-
input {
-
width: 100%;
-
}
+
+
textarea,
+
select,
+
input {
+
width: 100%;
+
}
}
input[type="color"] {
-
height: 2.5rem;
-
padding: 0.2rem;
+
height: 2.5rem;
+
padding: 0.2rem;
}
input[type="file"] {
-
border: 0;
+
border: 0;
}
/* misc body elements */
hr {
-
border: 1px dashed var(--accent);
-
margin: 0.5rem 0 0.5rem 0;
+
border: 1px dashed var(--accent);
+
margin: 0.5rem 0 0.5rem 0;
}
mark {
-
padding: 0 0.25em 0 0.25em;
-
border-radius: var(--standard-border-radius);
-
background-color: var(--accent);
-
color: var(--bg);
+
padding: 0 0.25em 0 0.25em;
+
border-radius: var(--standard-border-radius);
+
background-color: var(--accent);
+
color: var(--bg);
}
mark a {
-
color: var(--link);
+
color: var(--link);
}
img,
video,
iframe[src^="https://www.youtube-nocookie.com"],
iframe[src^="https://www.youtube.com"] {
-
max-width: 90%;
-
height: auto;
-
padding: 0.125rem;
-
border: dashed 2px var(--accent);
-
border-radius: 15px;
+
max-width: 100%;
+
height: auto;
+
border-radius: 0.35rem;
+
}
+
+
.img-container {
+
background-color: var(--accent);
+
border-bottom: 4px solid var(--bg-light);
+
border-radius: 7px 7px 10px 10px;
+
padding: 0.35rem;
+
margin: 1rem;
+
display: inline-block;
+
max-width: 90%;
}
figure {
-
margin: 0;
-
display: block;
-
overflow-x: auto;
+
margin: 0;
+
display: block;
+
overflow-x: auto;
}
figcaption {
-
text-align: left;
-
font-size: 0.875rem;
-
color: var(--text-light);
-
margin: 0 0 1rem 1rem;
+
text-align: left;
+
font-size: 0.875rem;
+
color: var(--text-light);
+
margin: 0 0 1rem 1rem;
}
blockquote {
-
margin: 0 0 0 1.25rem;
-
padding: 0.5rem 0 0 0.5rem;
-
border-inline-start: 0.375rem solid var(--accent);
-
color: var(--text-light);
-
font-style: italic;
+
margin: 0 0 0 1.25rem;
+
padding: 0.5rem 0 0 0.5rem;
+
border-inline-start: 0.375rem solid var(--accent);
+
color: var(--text-light);
+
font-style: italic;
+
}
+
+
/* Callout styles */
+
.callout {
+
margin: 1.5rem 0;
+
padding: 1rem;
+
border-left: 0.25rem solid;
+
border-radius: 0.25rem;
+
background-color: var(--bg-light);
+
}
+
+
.callout-title {
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
+
margin-bottom: 0.5rem;
+
font-size: 1rem;
+
}
+
+
.callout-icon {
+
display: inline-flex;
+
width: 1.25rem;
+
height: 1.25rem;
+
flex-shrink: 0;
+
}
+
+
.callout-icon svg {
+
width: 100%;
+
height: 100%;
+
}
+
+
.callout-content {
+
font-style: normal;
+
color: var(--text);
+
}
+
+
.callout-content p:first-child {
+
margin-top: 0;
+
}
+
+
.callout-content p:last-child {
+
margin-bottom: 0;
+
}
+
+
.callout-blue {
+
border-color: #8aadf4;
+
}
+
+
.callout-blue .callout-icon {
+
color: #8aadf4;
+
}
+
+
.callout-yellow {
+
border-color: #eed49f;
+
}
+
+
.callout-yellow .callout-icon {
+
color: #eed49f;
+
}
+
+
.callout-red {
+
border-color: #ed8796;
}
+
.callout-red .callout-icon {
+
color: #ed8796;
+
}
+
+
.callout-green {
+
border-color: #a6da95;
+
}
+
+
.callout-green .callout-icon {
+
color: #a6da95;
+
}
+
+
.callout-gray {
+
border-color: #6e738d;
+
}
+
+
.callout-gray .callout-icon {
+
color: #6e738d;
+
}
+
+
p:has(cite) {
-
text-align: right;
-
font-size: 0.875rem;
-
color: var(--text-light);
-
font-weight: 600;
+
text-align: right;
+
font-size: 0.875rem;
+
color: var(--text-light);
+
font-weight: 600;
}
cite::before {
-
content: "โ€” ";
+
content: "โ€” ";
}
dt {
-
color: var(--text-light);
+
color: var(--text-light);
}
code,
···
pre span,
kbd,
samp {
-
font-family: var(--mono-font);
+
font-family: var(--mono-font);
}
pre {
-
border: 1px solid var(--accent);
-
max-height: 30rem;
-
padding: 0.625rem;
-
overflow-x: auto;
-
font-style: monospace;
+
border: 1px solid var(--accent);
+
max-height: 30rem;
+
padding: 0.625rem;
+
overflow-x: auto;
+
font-style: monospace;
+
}
+
+
/* Allow wrapping for specific code blocks (e.g., SSH keys) */
+
pre[data-lang="pub"],
+
pre.wrap {
+
white-space: pre-wrap;
+
word-break: break-all;
+
overflow-x: visible;
}
p code,
li code,
div code {
-
padding: 0 0.125rem 0 0.125rem;
-
border-radius: 3px;
-
color: var(--bg);
-
background-color: var(--text);
-
transition: background-color 0.3s ease;
+
padding: 0 0.125rem 0 0.125rem;
+
border-radius: 3px;
+
color: var(--bg);
+
background-color: var(--text);
}
pre code {
-
padding: 0;
-
border-radius: 0;
-
color: inherit;
-
background-color: inherit;
+
padding: 0;
+
border-radius: 0;
+
color: inherit;
+
background-color: inherit;
}
iframe {
-
max-width: 90%;
+
max-width: 90%;
}
/* progress bars */
progress {
-
width: 100%;
+
width: 100%;
}
progress:indeterminate {
-
background-color: var(--bg-light);
+
background-color: var(--bg-light);
}
progress::-webkit-progress-bar {
-
border-radius: var(--standard-border-radius);
-
background-color: var(--bg-light);
+
border-radius: var(--standard-border-radius);
+
background-color: var(--bg-light);
}
progress::-webkit-progress-value {
-
border-radius: var(--standard-border-radius);
-
background-color: var(--accent);
+
border-radius: var(--standard-border-radius);
+
background-color: var(--accent);
}
progress::-moz-progress-bar {
-
border-radius: var(--standard-border-radius);
-
background-color: var(--accent);
-
transition-property: width;
-
transition-duration: 0.3s;
+
border-radius: var(--standard-border-radius);
+
background-color: var(--accent);
+
transition-property: width;
+
transition-duration: 0.3s;
}
progress:indeterminate::-moz-progress-bar {
-
background-color: var(--bg-light);
+
background-color: var(--bg-light);
}
dialog {
-
max-width: 40rem;
-
margin: auto;
+
max-width: 40rem;
+
margin: auto;
}
dialog::backdrop {
-
background-color: var(--bg);
-
opacity: 0.8;
+
background-color: var(--bg);
+
opacity: 0.8;
}
@media only screen and (max-width: 720px) {
-
dialog {
-
max-width: 100%;
-
margin: auto 1em;
-
}
+
dialog {
+
max-width: 100%;
+
margin: auto 1em;
+
}
}
/* superscript & subscript */
/* prevent scripts from affecting line-height. */
sup,
sub {
-
vertical-align: baseline;
-
position: relative;
+
vertical-align: baseline;
+
position: relative;
}
sup {
-
top: -0.4em;
+
top: -0.4em;
}
sub {
-
top: 0.3em;
-
}
+
top: 0.3em;
+
}
+207 -492
sass/css/syntax-theme.css
···
* theme "Catppuccin" generated by syntect
*/
-
@supports not (-moz-appearance: none) {
-
.z-code {
-
transition: background-color 0.3s ease;
-
}
-
}
+
/* dark */
-
html[data-theme="light"] .z-code {
-
color: #4c4f69;
-
background-color: #f2efea;
+
.z-code {
+
color: #cad3f5;
+
background-color: var(--nightshade-violet);
}
-
html[data-theme="light"] .z-comment {
-
color: #9ca0b0;
-
font-style: italic;
+
.z-comment {
+
color: #6e738d;
+
font-style: italic;
}
-
html[data-theme="light"] .z-string {
-
color: #40a02b;
+
.z-string {
+
color: #a6da95;
}
-
html[data-theme="light"] .z-string.z-regexp {
-
color: #fe640b;
+
.z-string.z-regexp {
+
color: #f5a97f;
}
-
html[data-theme="light"] .z-constant.z-numeric {
-
color: #fe640b;
+
.z-constant.z-numeric {
+
color: #f5a97f;
}
-
html[data-theme="light"] .z-constant.z-language.z-boolean {
-
color: #fe640b;
-
font-weight: bold;
-
font-style: italic;
+
.z-constant.z-language.z-boolean {
+
color: #f5a97f;
+
font-weight: bold;
+
font-style: italic;
}
-
html[data-theme="light"] .z-constant.z-language {
-
color: #7287fd;
-
font-style: italic;
+
.z-constant.z-language {
+
color: #b7bdf8;
+
font-style: italic;
}
-
html[data-theme="light"] .z-support.z-function.z-builtin {
-
color: #fe640b;
-
font-style: italic;
+
.z-support.z-function.z-builtin {
+
color: #f5a97f;
+
font-style: italic;
}
-
html[data-theme="light"] .z-variable.z-other.z-constant {
-
color: #fe640b;
+
.z-variable.z-other.z-constant {
+
color: #f5a97f;
}
-
html[data-theme="light"] .z-keyword {
-
color: #d20f39;
-
font-style: italic;
+
.z-keyword {
+
color: #ed8796;
+
font-style: italic;
}
-
html[data-theme="light"] .z-keyword.z-control.z-loop,
-
html[data-theme="light"] .z-keyword.z-control.z-conditional,
-
html[data-theme="light"] .z-keyword.z-control.z-c\+\+ {
-
color: #8839ef;
-
font-weight: bold;
+
.z-keyword.z-control.z-loop,
+
.z-keyword.z-control.z-conditional,
+
.z-keyword.z-control.z-c\+\+ {
+
color: #c6a0f6;
+
font-weight: bold;
}
-
html[data-theme="light"] .z-keyword.z-control.z-return,
-
html[data-theme="light"] .z-keyword.z-control.z-flow.z-return {
-
color: #ea76cb;
-
font-weight: bold;
+
.z-keyword.z-control.z-return,
+
.z-keyword.z-control.z-flow.z-return {
+
color: #f5bde6;
+
font-weight: bold;
}
-
html[data-theme="light"] .z-support.z-type.z-exception {
-
color: #fe640b;
-
font-style: italic;
+
.z-support.z-type.z-exception {
+
color: #f5a97f;
+
font-style: italic;
}
-
html[data-theme="light"] .z-keyword.z-operator,
-
html[data-theme="light"] .z-punctuation.z-accessor {
-
color: #04a5e5;
-
font-weight: bold;
+
.z-keyword.z-operator,
+
.z-punctuation.z-accessor {
+
color: #91d7e3;
+
font-weight: bold;
}
-
html[data-theme="light"] .z-punctuation.z-separator {
-
color: #179299;
+
.z-punctuation.z-separator {
+
color: #8bd5ca;
}
-
html[data-theme="light"] .z-punctuation.z-terminator {
-
color: #179299;
+
.z-punctuation.z-terminator {
+
color: #8bd5ca;
}
-
html[data-theme="light"] .z-punctuation.z-section {
-
color: #7c7f93;
+
.z-punctuation.z-section {
+
color: #939ab7;
}
-
html[data-theme="light"] .z-keyword.z-control.z-import.z-include {
-
color: #179299;
-
font-style: italic;
+
.z-keyword.z-control.z-import.z-include {
+
color: #8bd5ca;
+
font-style: italic;
}
-
html[data-theme="light"] .z-storage {
-
color: #d20f39;
+
.z-storage {
+
color: #ed8796;
}
-
html[data-theme="light"] .z-storage.z-type {
-
color: #df8e1d;
-
font-style: italic;
+
.z-storage.z-type {
+
color: #eed49f;
+
font-style: italic;
}
-
html[data-theme="light"] .z-storage.z-modifier {
-
color: #d20f39;
+
.z-storage.z-modifier {
+
color: #ed8796;
}
-
html[data-theme="light"] .z-entity.z-name.z-namespace,
-
html[data-theme="light"] .z-meta.z-path {
-
color: #dc8a78;
-
font-style: italic;
+
.z-entity.z-name.z-namespace,
+
.z-meta.z-path {
+
color: #f4dbd6;
+
font-style: italic;
}
-
html[data-theme="light"] .z-storage.z-type.z-class {
-
color: #dc8a78;
-
font-style: italic;
+
.z-storage.z-type.z-class {
+
color: #f4dbd6;
+
font-style: italic;
}
-
html[data-theme="light"] .z-entity.z-name.z-label {
-
color: #1e66f5;
+
.z-entity.z-name.z-label {
+
color: #8aadf4;
}
-
html[data-theme="light"] .z-keyword.z-declaration.z-class {
-
color: #d20f39;
-
font-style: italic;
+
.z-keyword.z-declaration.z-class {
+
color: #ed8796;
+
font-style: italic;
}
-
html[data-theme="light"] .z-entity.z-name.z-class,
-
html[data-theme="light"] .z-meta.z-toc-list.z-full-identifier {
-
color: #04a5e5;
+
.z-entity.z-name.z-class,
+
.z-meta.z-toc-list.z-full-identifier {
+
color: #91d7e3;
}
-
html[data-theme="light"] .z-entity.z-other.z-inherited-class {
-
color: #04a5e5;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-function,
-
html[data-theme="light"] .z-variable.z-function {
-
color: #1e66f5;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-function.z-preprocessor {
-
color: #d20f39;
-
}
-
html[data-theme="light"] .z-keyword.z-control.z-import {
-
color: #d20f39;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-function.z-constructor,
-
html[data-theme="light"] .z-entity.z-name.z-function.z-destructor {
-
color: #7287fd;
-
}
-
html[data-theme="light"] .z-variable.z-parameter.z-function {
-
color: #dc8a78;
-
font-style: italic;
+
.z-entity.z-other.z-inherited-class {
+
color: #91d7e3;
+
font-style: italic;
}
-
html[data-theme="light"] .z-keyword.z-declaration.z-function {
-
color: #e64553;
-
font-style: italic;
+
.z-entity.z-name.z-function,
+
.z-variable.z-function {
+
color: #8aadf4;
+
font-style: italic;
}
-
html[data-theme="light"] .z-support.z-function {
-
color: #04a5e5;
+
.z-entity.z-name.z-function.z-preprocessor {
+
color: #ed8796;
}
-
html[data-theme="light"] .z-support.z-constant {
-
color: #1e66f5;
+
.z-keyword.z-control.z-import {
+
color: #ed8796;
}
-
html[data-theme="light"] .z-support.z-type,
-
html[data-theme="light"] .z-support.z-class {
-
color: #1e66f5;
-
font-style: italic;
+
.z-entity.z-name.z-function.z-constructor,
+
.z-entity.z-name.z-function.z-destructor {
+
color: #b7bdf8;
}
-
html[data-theme="light"] .z-variable.z-function {
-
color: #1e66f5;
-
font-style: italic;
+
.z-variable.z-parameter.z-function {
+
color: #f4dbd6;
+
font-style: italic;
}
-
html[data-theme="light"] .z-variable.z-parameter {
-
color: #dc8a78;
-
font-style: italic;
+
.z-keyword.z-declaration.z-function {
+
color: #ee99a0;
+
font-style: italic;
}
-
html[data-theme="light"] .z-variable.z-other {
-
color: #4c4f69;
-
font-style: italic;
+
.z-support.z-function {
+
color: #91d7e3;
}
-
html[data-theme="light"] .z-variable.z-other.z-member {
-
color: #dc8a78;
+
.z-support.z-constant {
+
color: #8aadf4;
}
-
html[data-theme="light"] .z-variable.z-language {
-
color: #179299;
+
.z-support.z-type,
+
.z-support.z-class {
+
color: #8aadf4;
+
font-style: italic;
}
-
html[data-theme="light"] .z-entity.z-name.z-tag {
-
color: #fe640b;
+
.z-variable.z-function {
+
color: #8aadf4;
+
font-style: italic;
}
-
html[data-theme="light"] .z-entity.z-other.z-attribute-name {
-
color: #8839ef;
-
font-style: italic;
+
.z-variable.z-parameter {
+
color: #f4dbd6;
+
font-style: italic;
}
-
html[data-theme="light"] .z-punctuation.z-definition.z-tag {
-
color: #e64553;
+
.z-variable.z-other {
+
color: #cad3f5;
+
font-style: italic;
}
-
html[data-theme="light"] .z-markup.z-underline.z-link.z-markdown {
-
color: #dc8a78;
-
text-decoration: underline;
-
font-style: italic;
+
.z-variable.z-other.z-member {
+
color: #f4dbd6;
}
-
html[data-theme="light"] .z-meta.z-link.z-inline.z-description {
-
color: #7287fd;
-
font-weight: bold;
+
.z-variable.z-language {
+
color: #8bd5ca;
}
-
html[data-theme="light"] .z-comment.z-block.z-markdown,
-
html[data-theme="light"] .z-meta.z-code-fence,
-
html[data-theme="light"] .z-markup.z-raw.z-code-fence,
-
html[data-theme="light"] .z-markup.z-raw.z-inline {
-
color: #179299;
-
font-style: italic;
+
.z-entity.z-name.z-tag {
+
color: #f5a97f;
}
-
html[data-theme="light"] .z-punctuation.z-definition.z-heading,
-
html[data-theme="light"] .z-entity.z-name.z-section {
-
color: #1e66f5;
-
font-weight: bold;
+
.z-entity.z-other.z-attribute-name {
+
color: #c6a0f6;
+
font-style: italic;
}
-
html[data-theme="light"] .z-markup.z-italic {
-
color: #e64553;
-
font-style: italic;
+
.z-punctuation.z-definition.z-tag {
+
color: #ee99a0;
}
-
html[data-theme="light"] .z-markup.z-bold {
-
color: #e64553;
-
font-weight: bold;
+
.z-markup.z-underline.z-link.z-markdown {
+
color: #f4dbd6;
+
text-decoration: underline;
+
font-style: italic;
}
-
html[data-theme="light"] .z-constant.z-character.z-escape {
-
color: #ea76cb;
+
.z-meta.z-link.z-inline.z-description {
+
color: #b7bdf8;
+
font-weight: bold;
}
-
html[data-theme="light"]
-
.z-source.z-shell.z-bash
-
.z-meta.z-function.z-shell
-
.z-meta.z-compound.z-shell
-
.z-meta.z-function-call.z-identifier.z-shell {
-
color: #ea76cb;
+
.z-comment.z-block.z-markdown,
+
.z-meta.z-code-fence,
+
.z-markup.z-raw.z-code-fence,
+
.z-markup.z-raw.z-inline {
+
color: #8bd5ca;
+
font-style: italic;
}
-
html[data-theme="light"] .z-variable.z-language.z-shell {
-
color: #d20f39;
-
font-style: italic;
+
.z-punctuation.z-definition.z-heading,
+
.z-entity.z-name.z-section {
+
color: #8aadf4;
+
font-weight: bold;
}
-
html[data-theme="light"]
-
.z-source.z-lua
-
.z-meta.z-function.z-lua
-
.z-meta.z-block.z-lua
-
.z-meta.z-mapping.z-value.z-lua
-
.z-meta.z-mapping.z-key.z-lua
-
.z-string.z-unquoted.z-key.z-lua {
-
color: #7287fd;
-
font-style: italic;
+
.z-markup.z-italic {
+
color: #ee99a0;
+
font-style: italic;
}
-
html[data-theme="light"]
-
.z-source.z-lua
-
.z-meta.z-function.z-lua
-
.z-meta.z-block.z-lua
-
.z-meta.z-mapping.z-key.z-lua
-
.z-string.z-unquoted.z-key.z-lua {
-
color: #dd7878;
+
.z-markup.z-bold {
+
color: #ee99a0;
+
font-weight: bold;
}
-
html[data-theme="light"] .z-entity.z-name.z-constant.z-java {
-
color: #179299;
+
.z-constant.z-character.z-escape {
+
color: #f5bde6;
}
-
html[data-theme="light"] .z-support.z-type.z-property-name.z-css {
-
color: #dd7878;
-
font-style: italic;
+
html[data-theme="dark"]
+
.z-source.z-shell.z-bash
+
.z-meta.z-function.z-shell
+
.z-meta.z-compound.z-shell
+
.z-meta.z-function-call.z-identifier.z-shell {
+
color: #f5bde6;
}
-
html[data-theme="light"] .z-support.z-constant.z-property-value.z-css {
-
color: #4c4f69;
-
}
-
html[data-theme="light"] .z-constant.z-numeric.z-suffix.z-css,
-
html[data-theme="light"] .z-keyword.z-other.z-unit.z-css {
-
color: #179299;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-other.z-custom-property.z-name.z-css,
-
html[data-theme="light"] .z-support.z-type.z-custom-property.z-name.z-css,
-
html[data-theme="light"] .z-punctuation.z-definition.z-custom-property.z-css {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-tag.z-css {
-
color: #7287fd;
-
}
-
html[data-theme="light"] .z-variable.z-other.z-sass {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-invalid {
-
color: #4c4f69;
-
background-color: #d20f39;
-
}
-
html[data-theme="light"] .z-invalid.z-deprecated {
-
color: #4c4f69;
-
background-color: #8839ef;
-
}
-
html[data-theme="light"] .z-meta.z-diff,
-
html[data-theme="light"] .z-meta.z-diff.z-header {
-
color: #9ca0b0;
-
}
-
html[data-theme="light"] .z-markup.z-deleted {
-
color: #d20f39;
-
}
-
html[data-theme="light"] .z-markup.z-inserted {
-
color: #40a02b;
-
}
-
html[data-theme="light"] .z-markup.z-changed {
-
color: #df8e1d;
-
}
-
html[data-theme="light"] .z-message.z-error {
-
color: #d20f39;
+
.z-variable.z-language.z-shell {
+
color: #ed8796;
+
font-style: italic;
}
-
/* dark */
-
-
html[data-theme="dark"] .z-code {
-
color: #cad3f5;
-
background-color: #2a2e35;
+
.z-source.z-lua
+
.z-meta.z-function.z-lua
+
.z-meta.z-block.z-lua
+
.z-meta.z-mapping.z-value.z-lua
+
.z-meta.z-mapping.z-key.z-lua
+
.z-string.z-unquoted.z-key.z-lua {
+
color: #b7bdf8;
+
font-style: italic;
}
-
html[data-theme="dark"] .z-comment {
-
color: #6e738d;
-
font-style: italic;
+
.z-source.z-lua
+
.z-meta.z-function.z-lua
+
.z-meta.z-block.z-lua
+
.z-meta.z-mapping.z-key.z-lua
+
.z-string.z-unquoted.z-key.z-lua {
+
color: #f0c6c6;
}
-
html[data-theme="dark"] .z-string {
-
color: #a6da95;
+
.z-entity.z-name.z-constant.z-java {
+
color: #8bd5ca;
}
-
html[data-theme="dark"] .z-string.z-regexp {
-
color: #f5a97f;
+
.z-support.z-type.z-property-name.z-css {
+
color: #f0c6c6;
+
font-style: italic;
}
-
html[data-theme="dark"] .z-constant.z-numeric {
-
color: #f5a97f;
+
.z-support.z-constant.z-property-value.z-css {
+
color: #cad3f5;
}
-
html[data-theme="dark"] .z-constant.z-language.z-boolean {
-
color: #f5a97f;
-
font-weight: bold;
-
font-style: italic;
+
.z-constant.z-numeric.z-suffix.z-css,
+
.z-keyword.z-other.z-unit.z-css {
+
color: #8bd5ca;
+
font-style: italic;
}
-
html[data-theme="dark"] .z-constant.z-language {
-
color: #b7bdf8;
-
font-style: italic;
+
.z-variable.z-other.z-custom-property.z-name.z-css,
+
.z-support.z-type.z-custom-property.z-name.z-css,
+
.z-punctuation.z-definition.z-custom-property.z-css {
+
color: #8bd5ca;
}
-
html[data-theme="dark"] .z-support.z-function.z-builtin {
-
color: #f5a97f;
-
font-style: italic;
+
.z-entity.z-name.z-tag.z-css {
+
color: #b7bdf8;
}
-
html[data-theme="dark"] .z-variable.z-other.z-constant {
-
color: #f5a97f;
+
.z-variable.z-other.z-sass {
+
color: #8bd5ca;
}
-
html[data-theme="dark"] .z-keyword {
-
color: #ed8796;
-
font-style: italic;
+
.z-invalid {
+
color: #cad3f5;
+
background-color: #ed8796;
}
-
html[data-theme="dark"] .z-keyword.z-control.z-loop,
-
html[data-theme="dark"] .z-keyword.z-control.z-conditional,
-
html[data-theme="dark"] .z-keyword.z-control.z-c\+\+ {
-
color: #c6a0f6;
-
font-weight: bold;
+
.z-invalid.z-deprecated {
+
color: #cad3f5;
+
background-color: #c6a0f6;
}
-
html[data-theme="dark"] .z-keyword.z-control.z-return,
-
html[data-theme="dark"] .z-keyword.z-control.z-flow.z-return {
-
color: #f5bde6;
-
font-weight: bold;
+
.z-meta.z-diff,
+
.z-meta.z-diff.z-header {
+
color: #6e738d;
}
-
html[data-theme="dark"] .z-support.z-type.z-exception {
-
color: #f5a97f;
-
font-style: italic;
+
.z-markup.z-deleted {
+
color: #ed8796;
}
-
html[data-theme="dark"] .z-keyword.z-operator,
-
html[data-theme="dark"] .z-punctuation.z-accessor {
-
color: #91d7e3;
-
font-weight: bold;
+
.z-markup.z-inserted {
+
color: #a6da95;
}
-
html[data-theme="dark"] .z-punctuation.z-separator {
-
color: #8bd5ca;
+
.z-markup.z-changed {
+
color: #eed49f;
}
-
html[data-theme="dark"] .z-punctuation.z-terminator {
-
color: #8bd5ca;
-
}
-
html[data-theme="dark"] .z-punctuation.z-section {
-
color: #939ab7;
-
}
-
html[data-theme="dark"] .z-keyword.z-control.z-import.z-include {
-
color: #8bd5ca;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-storage {
-
color: #ed8796;
-
}
-
html[data-theme="dark"] .z-storage.z-type {
-
color: #eed49f;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-storage.z-modifier {
-
color: #ed8796;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-namespace,
-
html[data-theme="dark"] .z-meta.z-path {
-
color: #f4dbd6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-storage.z-type.z-class {
-
color: #f4dbd6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-label {
-
color: #8aadf4;
-
}
-
html[data-theme="dark"] .z-keyword.z-declaration.z-class {
-
color: #ed8796;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-class,
-
html[data-theme="dark"] .z-meta.z-toc-list.z-full-identifier {
-
color: #91d7e3;
-
}
-
html[data-theme="dark"] .z-entity.z-other.z-inherited-class {
-
color: #91d7e3;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-function,
-
html[data-theme="dark"] .z-variable.z-function {
-
color: #8aadf4;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-function.z-preprocessor {
-
color: #ed8796;
-
}
-
html[data-theme="dark"] .z-keyword.z-control.z-import {
-
color: #ed8796;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-function.z-constructor,
-
html[data-theme="dark"] .z-entity.z-name.z-function.z-destructor {
-
color: #b7bdf8;
-
}
-
html[data-theme="dark"] .z-variable.z-parameter.z-function {
-
color: #f4dbd6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-keyword.z-declaration.z-function {
-
color: #ee99a0;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-support.z-function {
-
color: #91d7e3;
-
}
-
html[data-theme="dark"] .z-support.z-constant {
-
color: #8aadf4;
-
}
-
html[data-theme="dark"] .z-support.z-type,
-
html[data-theme="dark"] .z-support.z-class {
-
color: #8aadf4;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-variable.z-function {
-
color: #8aadf4;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-variable.z-parameter {
-
color: #f4dbd6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-variable.z-other {
-
color: #cad3f5;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-variable.z-other.z-member {
-
color: #f4dbd6;
-
}
-
html[data-theme="dark"] .z-variable.z-language {
-
color: #8bd5ca;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-tag {
-
color: #f5a97f;
-
}
-
html[data-theme="dark"] .z-entity.z-other.z-attribute-name {
-
color: #c6a0f6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-punctuation.z-definition.z-tag {
-
color: #ee99a0;
-
}
-
html[data-theme="dark"] .z-markup.z-underline.z-link.z-markdown {
-
color: #f4dbd6;
-
text-decoration: underline;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-meta.z-link.z-inline.z-description {
-
color: #b7bdf8;
-
font-weight: bold;
-
}
-
html[data-theme="dark"] .z-comment.z-block.z-markdown,
-
html[data-theme="dark"] .z-meta.z-code-fence,
-
html[data-theme="dark"] .z-markup.z-raw.z-code-fence,
-
html[data-theme="dark"] .z-markup.z-raw.z-inline {
-
color: #8bd5ca;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-punctuation.z-definition.z-heading,
-
html[data-theme="dark"] .z-entity.z-name.z-section {
-
color: #8aadf4;
-
font-weight: bold;
-
}
-
html[data-theme="dark"] .z-markup.z-italic {
-
color: #ee99a0;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-markup.z-bold {
-
color: #ee99a0;
-
font-weight: bold;
-
}
-
html[data-theme="dark"] .z-constant.z-character.z-escape {
-
color: #f5bde6;
-
}
-
html[data-theme="dark"]
-
.z-source.z-shell.z-bash
-
.z-meta.z-function.z-shell
-
.z-meta.z-compound.z-shell
-
.z-meta.z-function-call.z-identifier.z-shell {
-
color: #f5bde6;
-
}
-
html[data-theme="dark"] .z-variable.z-language.z-shell {
-
color: #ed8796;
-
font-style: italic;
-
}
-
html[data-theme="dark"]
-
.z-source.z-lua
-
.z-meta.z-function.z-lua
-
.z-meta.z-block.z-lua
-
.z-meta.z-mapping.z-value.z-lua
-
.z-meta.z-mapping.z-key.z-lua
-
.z-string.z-unquoted.z-key.z-lua {
-
color: #b7bdf8;
-
font-style: italic;
-
}
-
html[data-theme="dark"]
-
.z-source.z-lua
-
.z-meta.z-function.z-lua
-
.z-meta.z-block.z-lua
-
.z-meta.z-mapping.z-key.z-lua
-
.z-string.z-unquoted.z-key.z-lua {
-
color: #f0c6c6;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-constant.z-java {
-
color: #8bd5ca;
-
}
-
html[data-theme="dark"] .z-support.z-type.z-property-name.z-css {
-
color: #f0c6c6;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-support.z-constant.z-property-value.z-css {
-
color: #cad3f5;
-
}
-
html[data-theme="dark"] .z-constant.z-numeric.z-suffix.z-css,
-
html[data-theme="dark"] .z-keyword.z-other.z-unit.z-css {
-
color: #8bd5ca;
-
font-style: italic;
-
}
-
html[data-theme="dark"] .z-variable.z-other.z-custom-property.z-name.z-css,
-
html[data-theme="dark"] .z-support.z-type.z-custom-property.z-name.z-css,
-
html[data-theme="dark"] .z-punctuation.z-definition.z-custom-property.z-css {
-
color: #8bd5ca;
-
}
-
html[data-theme="dark"] .z-entity.z-name.z-tag.z-css {
-
color: #b7bdf8;
-
}
-
html[data-theme="dark"] .z-variable.z-other.z-sass {
-
color: #8bd5ca;
-
}
-
html[data-theme="dark"] .z-invalid {
-
color: #cad3f5;
-
background-color: #ed8796;
-
}
-
html[data-theme="dark"] .z-invalid.z-deprecated {
-
color: #cad3f5;
-
background-color: #c6a0f6;
-
}
-
html[data-theme="dark"] .z-meta.z-diff,
-
html[data-theme="dark"] .z-meta.z-diff.z-header {
-
color: #6e738d;
-
}
-
html[data-theme="dark"] .z-markup.z-deleted {
-
color: #ed8796;
-
}
-
html[data-theme="dark"] .z-markup.z-inserted {
-
color: #a6da95;
-
}
-
html[data-theme="dark"] .z-markup.z-changed {
-
color: #eed49f;
-
}
-
html[data-theme="dark"] .z-message.z-error {
-
color: #ed8796;
+
.z-message.z-error {
+
color: #ed8796;
}
+20
scripts/build.ts
···
+
#!/usr/bin/env bun
+
+
import { existsSync } from 'fs';
+
+
await Bun.$`rm -rf .zola-build`.quiet();
+
await Bun.$`mkdir -p .zola-build`.quiet();
+
await Bun.$`cp -r content .zola-build/`.quiet();
+
+
const optionalDirs = ['static', 'templates', 'sass', 'syntaxes'];
+
for (const dir of optionalDirs) {
+
if (existsSync(dir)) {
+
await Bun.$`cp -r ${dir} .zola-build/`.quiet();
+
}
+
}
+
+
await Bun.$`cp config.toml .zola-build/`.quiet();
+
await Bun.$`bun run scripts/preprocess.ts .zola-build/content`.quiet();
+
await Bun.$`cd .zola-build && zola build --force --output-dir ../public`;
+
await Bun.$`rm -rf .zola-build`.quiet();
+
+72
scripts/dev.ts
···
+
#!/usr/bin/env bun
+
+
import { watch } from 'fs';
+
import { existsSync } from 'fs';
+
import { spawn } from 'child_process';
+
+
let zolaProcess: any = null;
+
let isRebuilding = false;
+
+
function cleanup() {
+
if (zolaProcess) {
+
zolaProcess.kill();
+
}
+
process.exit(0);
+
}
+
+
process.on('SIGINT', cleanup);
+
process.on('SIGTERM', cleanup);
+
+
async function buildShadow() {
+
if (isRebuilding) return;
+
isRebuilding = true;
+
+
if (zolaProcess) {
+
zolaProcess.kill();
+
zolaProcess = null;
+
}
+
+
await Bun.$`rm -rf .zola-build`.quiet();
+
await Bun.$`mkdir -p .zola-build`.quiet();
+
await Bun.$`cp -r content .zola-build/`.quiet();
+
+
const optionalDirs = ['static', 'templates', 'sass', 'syntaxes'];
+
for (const dir of optionalDirs) {
+
if (existsSync(dir)) {
+
await Bun.$`cp -r ${dir} .zola-build/`.quiet();
+
}
+
}
+
+
await Bun.$`cp config.toml .zola-build/`.quiet();
+
await Bun.$`bun run scripts/preprocess.ts .zola-build/content`.quiet();
+
+
zolaProcess = spawn('zola', ['serve', '--force', '--interface', '0.0.0.0', '--output-dir', '../public'], {
+
cwd: '.zola-build',
+
stdio: 'inherit'
+
});
+
+
zolaProcess.on('error', (err: Error) => {
+
console.error('Failed to start Zola:', err);
+
});
+
+
isRebuilding = false;
+
}
+
+
await buildShadow();
+
+
const watchDirs = ['content', 'templates', 'sass', 'static', 'syntaxes'];
+
+
for (const dir of watchDirs) {
+
if (existsSync(dir)) {
+
watch(dir, { recursive: true }, async (event, filename) => {
+
if (filename && !filename.includes('.zola-build')) {
+
await buildShadow();
+
}
+
});
+
}
+
}
+
+
watch('config.toml', async () => {
+
await buildShadow();
+
});
+
+122
scripts/genOG.ts
···
+
import puppeteer from "puppeteer";
+
import { readdir, mkdir } from "node:fs/promises";
+
+
const template = await Bun.file("tools/og.html").text();
+
+
const browser = await puppeteer.launch({
+
args: ["--no-sandbox"],
+
executablePath: process.env.PUPPETEER_EXEC_PATH, // set by docker container
+
});
+
+
async function og(
+
postname: string,
+
type: string,
+
by: string | undefined,
+
outputPath: string,
+
width = 1200,
+
height = 630,
+
) {
+
const page = await browser.newPage();
+
+
await page.setViewport({ width, height });
+
+
await page.setContent(
+
template
+
.toString()
+
.replace("{{postname}}", postname)
+
.replace("{{type}}", type)
+
.replace("{{by}}", by || ""),
+
);
+
+
await page.screenshot({ path: outputPath });
+
}
+
+
async function fileExists(path: string): Promise<boolean> {
+
try {
+
await Bun.file(path);
+
return true;
+
} catch (e) {
+
return false;
+
}
+
}
+
+
try {
+
// check if the public/blog folder exists
+
// if not exit
+
// if it does, get all the folders and then get the title tag from the index.html
+
+
if (!(await fileExists("public/"))) {
+
console.error("public/ does not exist");
+
process.exit(1);
+
}
+
+
// read all the files in the current directory filtering for index.htmls
+
const files = (await readdir("public/", { recursive: true })).filter((file) =>
+
file.endsWith("index.html"),
+
);
+
+
const directories = new Set(
+
files.map((file) => file.replace("index.html", "")),
+
);
+
+
const existing = (await readdir("static/")).filter((file) =>
+
directories.has(file),
+
);
+
+
// create not existing
+
for (const dir of directories) {
+
if (!existing.includes(dir)) {
+
await mkdir(`static/${dir.split("/").slice(0, -1).join("/")}`, {
+
recursive: true,
+
});
+
}
+
}
+
+
console.log("Generating OG images for", files.length, "files");
+
+
// for each file, get the title tag from the index.html
+
for (const file of files) {
+
const index = await Bun.file(`public/${file}`).text();
+
const title = index.match(/<title>(.*?)<\/title>/)[1];
+
let type = "Page";
+
let by: string | undefined;
+
switch (file.split("/")[0]) {
+
case "blog":
+
type = "Blog";
+
if (file.split("/")[1] !== "index.html") {
+
by = "<p>A post ... yeah thats about it</p>";
+
} else {
+
by = "<p>All authored by me ... or are they???</p>";
+
}
+
break;
+
case "verify":
+
type = "Slash Page";
+
by = "<p>So you can stalk me ๐Ÿ’€</p>";
+
break;
+
case "pfp":
+
type = "Slash Page";
+
by = "<p>Want to stare at my pretty face?</p>";
+
break;
+
case "tags":
+
if (file.split("/")[1] === "index.html") {
+
type = "Tags";
+
by = "<p>A total archive!</p>";
+
} else {
+
type = "Tag";
+
by = "<p>Find more posts like this!</p>";
+
}
+
break;
+
case "index.html":
+
type = "Root";
+
by = "<p>Where it all begins</p>";
+
break;
+
}
+
+
console.log("Generating OG for", file, "title:", title, "with type:", type);
+
await og(title, type, by, `static/${file.replace("index.html", "og.png")}`);
+
}
+
} catch (e) {
+
console.error(e);
+
} finally {
+
await browser.close();
+
}
+111
scripts/og.html
···
+
<!DOCTYPE html>
+
<html>
+
<head>
+
<style>
+
:root,
+
::backdrop {
+
color-scheme: dark;
+
--bg: #222529;
+
--bg-light: #464949;
+
--text: #d6d6d6;
+
--text-light: #c5c0b7;
+
--accent: #78b6ad;
+
--accent-light: #87c9e5;
+
--accent-text: var(--bg);
+
--border: #dbd5bc;
+
--link: #e2c8a2;
+
--gradient-average-light: oklch(86.49% 0.018 73.05);
+
--gradient-average-dark: oklch(27.58% 0.0203 289.13);
+
--nightshade-violet: oklch(22.96% 0.0242 287.67);
+
--purple-night: oklch(18.96% 0.0242 287.67);
+
--dark-crushed-grape: oklch(74.02% 0.0756 311.96);
+
--light-crushed-grape: oklch(73.48% 0.1008 284.99);
+
--reseda-green: oklch(62.33% 0.0475 126.94);
+
--earth-yellow: oklch(87.45% 0.0203 74.93);
+
--sunset: oklch(87.45% 0.0334 74.93);
+
--ultra-violet: oklch(42.21% 0.0676 297.45);
+
--rose-quartz: oklch(65.32% 0.0585 311.96);
+
--pink-puree: oklch(75.65% 0.0555 290.76);
+
--lavendar-breeze: oklch(91.06% 0.0223 290.76);
+
--purple-gray: oklch(25.63% 0.0002 290.76);
+
--alice-blue: oklch(95.38% 0.0118 239.91);
+
}
+
+
body {
+
font-weight: 600;
+
color: var(--lavendar-breeze);
+
background-color: var(--purple-night);
+
font-family: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono",
+
monospace;
+
display: flex;
+
flex-direction: column;
+
text-align: center;
+
}
+
+
div {
+
margin: 0;
+
display: flex;
+
flex-direction: column;
+
align-items: center;
+
justify-content: center;
+
height: 90vh; /* 90% of viewport height */
+
width: 90vw; /* 90% of viewport width */
+
padding: 5vh 5vw; /* 5% border on all sides */
+
box-sizing: border-box;
+
align-self: center;
+
}
+
+
h1 {
+
font-size: calc(2 * 2vw);
+
}
+
h2 {
+
font-size: calc(1.75 * 2vw);
+
}
+
h3 {
+
font-size: calc(1.5 * 2vw);
+
}
+
h4 {
+
font-size: calc(1.25 * 2vw);
+
}
+
h5 {
+
font-size: calc(1 * 2vw);
+
}
+
h6 {
+
font-size: calc(0.75 * 2vw);
+
}
+
+
h1,
+
h2,
+
h3,
+
h4,
+
h5,
+
h6 {
+
margin: 0.5em 0 0.5em 0;
+
padding: 0.22em 0.4em 0.22em 0.4em;
+
border-radius: 0.1em;
+
width: fit-content;
+
color: var(--lavendar-breeze);
+
}
+
+
h1 {
+
background-color: var(--rose-quartz);
+
color: var(--purple-gray);
+
}
+
+
p {
+
margin: 1rem 0;
+
color: var(--pink-puree);
+
font-size: calc(
+
1rem + 1vw
+
); /* Adjust font size based on viewport width */
+
}
+
</style>
+
</head>
+
<body>
+
<div>
+
<h1>{{type}}</h1>
+
<h2>{{postname}}</h2>
+
{{by}}
+
</div>
+
</body>
+
</html>
+142
scripts/preprocess.ts
···
+
#!/usr/bin/env bun
+
+
import fs from 'fs';
+
import path from 'path';
+
import { glob } from 'glob';
+
+
const contentDir = process.argv[2] || 'content';
+
+
function splitByCodeBlocks(content: string): { text: string; isCode: boolean }[] {
+
const parts: { text: string; isCode: boolean }[] = [];
+
const codeBlockRegex = /^(```|~~~)/gm;
+
+
let lastIndex = 0;
+
let inCodeBlock = false;
+
let match;
+
+
codeBlockRegex.lastIndex = 0;
+
+
while ((match = codeBlockRegex.exec(content)) !== null) {
+
const segment = content.slice(lastIndex, match.index);
+
if (segment) {
+
parts.push({ text: segment, isCode: inCodeBlock });
+
}
+
inCodeBlock = !inCodeBlock;
+
lastIndex = match.index;
+
}
+
+
// Add remaining content
+
if (lastIndex < content.length) {
+
parts.push({ text: content.slice(lastIndex), isCode: inCodeBlock });
+
}
+
+
return parts;
+
}
+
+
function transformCallouts(content: string): string {
+
return content.replace(
+
/^> \[!(INFO|WARNING|WARN|DANGER|ERROR|TIP|HINT|NOTE)\]\n((?:> .*\n?)*)/gm,
+
(match, type, body) => {
+
const cleanBody = body.replace(/^> /gm, '').trim();
+
const normalizedType = type.toLowerCase() === 'warn' ? 'warning' :
+
type.toLowerCase() === 'error' ? 'danger' :
+
type.toLowerCase() === 'hint' ? 'tip' :
+
type.toLowerCase();
+
return `{% callout(type="${normalizedType}") %}\n${cleanBody}\n{% end %}\n`;
+
}
+
);
+
}
+
+
function transformImages(content: string): string {
+
// Transform multiple images: !![alt1](url1)[alt2](url2){attrs}
+
content = content.replace(
+
/!!(\[([^\]]*)\]\(([^)]+)\))+(?:\{([^}]+)\})?/g,
+
(match) => {
+
// Extract all [alt](url) pairs
+
const pairs = [...match.matchAll(/\[([^\]]*)\]\(([^)]+)\)/g)];
+
const urls = pairs.map(p => p[2]).join(', ');
+
const alts = pairs.map(p => p[1]).join(', ');
+
+
// Extract attrs if present
+
const attrsMatch = match.match(/\{([^}]+)\}$/);
+
const attrs = attrsMatch ? attrsMatch[1] : '';
+
+
const params: string[] = [`id="${urls}"`];
+
+
if (alts.trim()) {
+
params.push(`alt="${alts}"`);
+
}
+
+
if (attrs) {
+
const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || [];
+
if (classes.length) {
+
params.push(`class="${classes.join(' ')}"`);
+
}
+
+
const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g);
+
for (const [, key, value] of keyValueMatches) {
+
if (key !== 'class') {
+
params.push(`${key}="${value.replace(/["']/g, '')}"`);
+
}
+
}
+
}
+
+
return `{{ imgs(${params.join(', ')}) }}`;
+
}
+
);
+
+
// Transform single images: ![alt](url){attrs}
+
content = content.replace(
+
/!\[([^\]]*)\]\(([^)]+)\)(?:\{([^}]+)\})?/g,
+
(match, alt, url, attrs) => {
+
const params: string[] = [`id="${url}"`];
+
+
if (alt) {
+
params.push(`alt="${alt}"`);
+
}
+
+
if (attrs) {
+
const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || [];
+
if (classes.length) {
+
params.push(`class="${classes.join(' ')}"`);
+
}
+
+
const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g);
+
for (const [, key, value] of keyValueMatches) {
+
if (key !== 'class') {
+
params.push(`${key}="${value.replace(/["']/g, '')}"`);
+
}
+
}
+
}
+
+
return `{{ img(${params.join(', ')}) }}`;
+
}
+
);
+
+
return content;
+
}
+
+
function processFile(filePath: string): void {
+
let content = fs.readFileSync(filePath, 'utf8');
+
const originalContent = content;
+
+
// Split by code blocks and only transform non-code parts
+
const parts = splitByCodeBlocks(content);
+
content = parts.map(part => {
+
if (part.isCode) {
+
return part.text; // Don't transform code blocks
+
}
+
let text = part.text;
+
text = transformCallouts(text);
+
text = transformImages(text);
+
return text;
+
}).join('');
+
+
if (content !== originalContent) {
+
fs.writeFileSync(filePath, content);
+
}
+
}
+
+
const files = glob.sync(`${contentDir}/**/*.md`);
+
files.forEach(processFile);
+
+180
scripts/rehost-cdn.sh
···
+
#!/usr/bin/env bash
+
set -euo pipefail
+
+
API_URL="https://cdn.hackclub.com/api/v3/new"
+
TOKEN="${HACKCLUB_CDN_TOKEN:-}"
+
if [[ -z "${TOKEN}" ]]; then
+
TOKEN="${1:-}"
+
fi
+
if [[ -z "${TOKEN}" ]]; then
+
echo "Usage: HACKCLUB_CDN_TOKEN=... $0 [token] [--dry-run] [paths...]" >&2
+
exit 1
+
fi
+
+
DRY_RUN=false
+
SKIP_CHECK=false
+
CACHED_URLS=()
+
ARGS=()
+
for a in "$@"; do
+
case "$a" in
+
--dry-run) DRY_RUN=true ;;
+
--skip-check) SKIP_CHECK=true ;;
+
*) ARGS+=("$a") ;;
+
esac
+
done
+
# remove token if passed as first arg
+
if [[ ${#ARGS[@]} -gt 0 && "${ARGS[0]}" != "--dry-run" ]]; then
+
ARGS=("${ARGS[@]:1}")
+
fi
+
+
PATHS=("content")
+
if [[ ${#ARGS[@]} -gt 0 ]]; then PATHS=("${ARGS[@]}"); fi
+
+
TMP_DIR=".crush/rehost-cdn"
+
MAP_FILE="$TMP_DIR/map.tsv"
+
mkdir -p "$TMP_DIR"
+
touch "$MAP_FILE"
+
+
collect_urls() {
+
# Markdown images: ![alt](URL)
+
rg -n --no-heading -e '!\[[^\]]*\]\((https?://[^)\s]+)\)' -g '!**/*.map' -g '!**/*.lock' "${PATHS[@]}" 2>/dev/null |
+
awk -F: '{file=$1; sub(/^[^:]*:/, "", $0); match($0, /!\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/, m); if(m[1]!="") print file"\t"m[1]}' |
+
# Zola shortcode variants:
+
# - {% img(id="URL", ...) %}
+
# - {{ img(id="URL", ...) }}
+
cat <( rg -n --no-heading -e '\{[%{]\s*img[^}%]*[}%]\}' "${PATHS[@]}" 2>/dev/null | \
+
awk -F: '{file=$1; sub(/^[^:]*:/, "", $0); if (match($0, /(id|src)[[:space:]]*=[[:space:]]*"(https?:\/\/[^"[:space:]]+)"/, m)) print file"\t"m[2]}' ) |
+
awk -F'\t' '{print $1"\t"$2}' |
+
grep -E '\.(png|jpe?g|gif|webp|svg|bmp|tiff?|avif)(\?.*)?$' -i |
+
grep -vE 'hc-cdn\.|cdn\.hackclub\.com'
+
}
+
+
batch_upload() {
+
payload=$(jq -sR 'split("\n")|map(select(length>0))' <(printf "%s\n" "$@"))
+
for attempt in 1 2 3; do
+
resp=$(curl -sS -w "\n%{http_code}" -X POST "$API_URL" \
+
-H "Authorization: Bearer $TOKEN" \
+
-H 'Content-Type: application/json' \
+
--data-raw "$payload" 2>&1) || true
+
body=$(printf "%s" "$resp" | sed '$d')
+
code=$(printf "%s" "$resp" | tail -n1)
+
if [[ "$code" == "200" ]]; then
+
printf "%s" "$body" | jq -r '.files[] | .sourceUrl? as $s | .deployedUrl + "\t" + ( $s // "" )'
+
return 0
+
fi
+
echo "Upload attempt $attempt failed with $code" >&2
+
echo "Response body:" >&2
+
printf "%s\n" "$body" >&2
+
echo "Payload:" >&2
+
printf "%s\n" "$payload" >&2
+
sleep $((attempt*2))
+
done
+
echo "Upload failed after retries" >&2
+
return 1
+
}
+
+
mapfile -t LINES < <(collect_urls | sort -u)
+
+
URLS_TO_SEND=()
+
FILES=()
+
total=${#LINES[@]}
+
idx=0
+
for line in "${LINES[@]}"; do
+
idx=$((idx+1))
+
file="${line%%$'\t'*}"
+
url="${line#*$'\t'}"
+
if grep -Fq "${url}" "$MAP_FILE" 2>/dev/null; then
+
echo "[$idx/$total] cached: $url -> will rewrite only"
+
CACHED_URLS+=("$url")
+
continue
+
fi
+
if $DRY_RUN; then
+
echo "[$idx/$total] queued: $url"
+
URLS_TO_SEND+=("$url")
+
FILES+=("$file")
+
else
+
if $SKIP_CHECK; then
+
echo "[$idx/$total] no-check: $url"
+
URLS_TO_SEND+=("$url")
+
FILES+=("$file")
+
else
+
echo -n "[$idx/$total] checking: $url ... "
+
code=$(curl -sS -o /dev/null -w '%{http_code}' -L "$url" || echo 000)
+
if [[ "$code" =~ ^2|3 ]]; then
+
echo "ok ($code)"
+
URLS_TO_SEND+=("$url")
+
FILES+=("$file")
+
else
+
echo "fail ($code)"; echo "Skipping: $url" >&2
+
fi
+
fi
+
fi
+
done
+
+
if [[ ${#URLS_TO_SEND[@]} -eq 0 ]]; then
+
echo "No new image URLs to process"; exit 0
+
fi
+
+
BATCH=50
+
start=0
+
# Rewrites for cached URLs from map without uploading
+
if [ "${#CACHED_URLS[@]}" -gt 0 ] 2>/dev/null; then
+
echo "Rewriting cached URLs from map without upload..."
+
for src in "${CACHED_URLS[@]}"; do
+
dst=$(awk -F'\t' -v s="$src" '$1==s{print $2}' "$MAP_FILE" | head -n1)
+
[[ -z "$dst" ]] && continue
+
rg -l --fixed-strings -- "$src" "${PATHS[@]}" 2>/dev/null | while read -r f; do
+
mkdir -p "$TMP_DIR/backup"
+
if [[ ! -e "$TMP_DIR/backup/$f" ]]; then
+
mkdir -p "$TMP_DIR/backup/$(dirname "$f")"
+
cp "$f" "$TMP_DIR/backup/$f"
+
fi
+
sed -i "s#$(printf '%s' "$src" | sed -e 's/[.[\*^$]/\\&/g' -e 's#/#\\/#g')#$(printf '%s' "$dst" | sed -e 's/[&]/\\&/g' -e 's#/#\\/#g')#g" "$f"
+
echo "Rewrote (cached): $f"
+
done
+
done
+
fi
+
+
while [[ $start -lt ${#URLS_TO_SEND[@]} ]]; do
+
end=$(( start + BATCH ))
+
if [[ $end -gt ${#URLS_TO_SEND[@]} ]]; then end=${#URLS_TO_SEND[@]}; fi
+
chunk=("${URLS_TO_SEND[@]:start:end-start}")
+
if $DRY_RUN; then
+
for u in "${chunk[@]}"; do echo "DRY: would upload $u"; done
+
else
+
echo "Uploading ${#chunk[@]} URLs..."
+
resp=$(batch_upload "${chunk[@]}") || { echo "Upload failed" >&2; exit 1; }
+
echo "Upload response:"; printf "%s\n" "$resp"
+
mapfile -t deployed_arr < <(printf "%s\n" "$resp" | awk '{print $0}')
+
for i in "${!chunk[@]}"; do
+
src="${chunk[$i]}"
+
dst="${deployed_arr[$i]:-}"
+
if [[ -n "$dst" ]]; then
+
printf "%s\t%s\n" "$src" "$dst" | tee -a "$MAP_FILE"
+
fi
+
done
+
fi
+
start=$end
+
done
+
+
if $DRY_RUN; then echo "DRY: skipping replacements"; exit 0; fi
+
+
# Replace in-place using map
+
if [[ -s "$MAP_FILE" ]]; then
+
cp "$MAP_FILE" "$TMP_DIR/map-$(date +%s).tsv"
+
while IFS=$'\t' read -r src dst; do
+
[[ -z "$src" || -z "$dst" ]] && continue
+
rg -l --fixed-strings -- "$src" "${PATHS[@]}" 2>/dev/null | while read -r f; do
+
mkdir -p "$TMP_DIR/backup"
+
if [[ ! -e "$TMP_DIR/backup/$f" ]]; then
+
mkdir -p "$TMP_DIR/backup/$(dirname "$f")"
+
cp "$f" "$TMP_DIR/backup/$f"
+
fi
+
sed -i "s#$(printf '%s' "$src" | sed -e 's/[.[\*^$]/\\&/g' -e 's#/#\\/#g')#$(printf '%s' "$dst" | sed -e 's/[&]/\\&/g' -e 's#/#\\/#g')#g" "$f"
+
echo "Rewrote: $f"
+
done
+
done < "$MAP_FILE"
+
echo "Backups in $TMP_DIR/backup"
+
fi
+
+
echo "Done"
static/android-chrome-192x192.png

This is a binary file and will not be displayed.

static/android-chrome-512x512.png

This is a binary file and will not be displayed.

static/apple-touch-icon.png

This is a binary file and will not be displayed.

+1
static/badges/MadeByAHuman_04.svg
···
+
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="88" height="31" viewBox="0 0 88 31"><g id="Background"><rect width="88" height="31" fill="#b39ddb"/></g><g id="_3D"><polygon points="88 0 88 31 0 31 2 29 86 29 86 2 88 0" fill="#263238" opacity=".5"/><polygon points="88 0 86 2 2 2 2 29 0 31 0 0 88 0" fill="#fff" opacity=".5"/></g><g id="Text"><path d="m40.86,9.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.04-.77,1.75-.77.93,0,1.72.4,2.13,1.17.37-.72,1.2-1.17,2.06-1.17,1.39,0,2.44.87,2.44,2.52v3.49h-1.21v-3.31Z" fill="#263238"/><path d="m46.01,6.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m53.31,6.94c.77,0,1.59.37,2.04.92v-2.86h1.24v7.95h-1.24v-.89c-.38.54-1.08.99-2.05.99-1.56,0-2.79-1.26-2.79-3.08s1.24-3.03,2.8-3.03Zm.25,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m60.73,13.05c-1.7,0-2.95-1.2-2.95-3.06s1.2-3.05,2.95-3.05,2.88,1.17,2.88,2.91c0,.2-.01.4-.04.6h-4.52c.09.98.78,1.57,1.69,1.57.75,0,1.17-.37,1.4-.83h1.32c-.33,1.03-1.27,1.86-2.72,1.86Zm-1.68-3.59h3.28c-.02-.91-.74-1.49-1.65-1.49-.83,0-1.49.56-1.62,1.49Z" fill="#263238"/><path d="m70.89,6.94c1.57,0,2.78,1.2,2.78,3.03s-1.22,3.08-2.78,3.08c-.98,0-1.68-.44-2.06-.96v.86h-1.22v-7.95h1.22v2.91c.39-.54,1.13-.98,2.06-.98Zm-.27,1.06c-.91,0-1.79.72-1.79,1.99s.88,2,1.79,2,1.8-.74,1.8-2.02-.88-1.97-1.8-1.97Z" fill="#263238"/><path d="m78.95,7.04h1.27l-3.63,8.7h-1.27l1.2-2.88-2.33-5.82h1.36l1.67,4.51,1.73-4.51Z" fill="#263238"/><path d="m34.48,17.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m42.17,16.01h1.22v2.72c.41-.49,1.07-.78,1.84-.78,1.32,0,2.35.87,2.35,2.52v3.49h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-7.95Z" fill="#263238"/><path d="m54.5,23.96h-1.22v-.71c-.39.5-1.05.79-1.75.79-1.39,0-2.44-.87-2.44-2.52v-3.48h1.21v3.3c0,1.08.59,1.64,1.48,1.64s1.49-.56,1.49-1.64v-3.3h1.22v5.92Z" fill="#263238"/><path d="m64.5,20.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.04-.77,1.75-.77.93,0,1.72.4,2.13,1.17.37-.72,1.2-1.17,2.06-1.17,1.39,0,2.44.87,2.44,2.52v3.49h-1.21v-3.31Z" fill="#263238"/><path d="m69.65,17.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m78.74,20.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.05-.77,1.76-.77,1.39,0,2.43.87,2.43,2.52v3.49h-1.21v-3.31Z" fill="#263238"/></g><g id="Image"><circle cx="15.37" cy="15.5" r="10.3" fill="#ffcc80" stroke="#263238" stroke-miterlimit="10" stroke-width="1.5"/><rect x="7.55" y="13.8" width="2.54" height="3.4" rx="1.27" ry="1.27" fill="#263238"/><rect x="20.65" y="13.8" width="2.54" height="3.4" rx="1.27" ry="1.27" fill="#263238"/><path d="m18.44,15.5c0,1.7-1.37,3.07-3.07,3.07s-3.07-1.37-3.07-3.07" fill="none" stroke="#263238" stroke-miterlimit="10" stroke-width="1.5"/></g></svg>
static/badges/get-netscape.gif

This is a binary file and will not be displayed.

static/badges/green-team.gif

This is a binary file and will not be displayed.

static/badges/hackclub.png

This is a binary file and will not be displayed.

static/badges/made-with-neovim.png

This is a binary file and will not be displayed.

static/badges/no-web3.gif

This is a binary file and will not be displayed.

static/badges/powered-by-nix.gif

This is a binary file and will not be displayed.

static/badges/tangled.png

This is a binary file and will not be displayed.

static/blog/adding-a-copy-button/og.png

This is a binary file and will not be displayed.

static/blog/airbuds/og.png

This is a binary file and will not be displayed.

static/blog/analyzing-implications-of-online-safety-legislation/og.png

This is a binary file and will not be displayed.

static/blog/atuin/og.png

This is a binary file and will not be displayed.

static/blog/degraded-zpool-proxmox/og.png

This is a binary file and will not be displayed.

static/blog/exporting-from-plausible/og.png

This is a binary file and will not be displayed.

static/blog/garmin-vivoactive-homeassistant/og.png

This is a binary file and will not be displayed.

static/blog/hilton-tomfoolery/og.png

This is a binary file and will not be displayed.

static/blog/install-truenas-core-proxmox/og.png

This is a binary file and will not be displayed.

static/blog/mega/og.png

This is a binary file and will not be displayed.

static/blog/monaspace-vs-code-install/og.png

This is a binary file and will not be displayed.

static/blog/my-animations/og.png

This is a binary file and will not be displayed.

static/blog/my-life-story-with-tech/og.png

This is a binary file and will not be displayed.

static/blog/og.png

This is a binary file and will not be displayed.

static/blog/remove-exif-git-hook/og.png

This is a binary file and will not be displayed.

static/blog/spherical-ray-diagrams/og.png

This is a binary file and will not be displayed.

static/blog/spotify-to-apple-music/og.png

This is a binary file and will not be displayed.

static/blog/ssd-removal-mbp-2017/og.png

This is a binary file and will not be displayed.

static/blog/tangled-sync/og.png

This is a binary file and will not be displayed.

static/blog/test-post/og.png

This is a binary file and will not be displayed.

static/favicon/apple-touch-icon.png

This is a binary file and will not be displayed.

static/favicon/favicon-96x96.png

This is a binary file and will not be displayed.

static/favicon/favicon.ico

This is a binary file and will not be displayed.

+21
static/favicon/site.webmanifest
···
+
{
+
"name": "site@zera",
+
"short_name": "site@zera",
+
"icons": [
+
{
+
"src": "/favicon/web-app-manifest-192x192.png",
+
"sizes": "192x192",
+
"type": "image/png",
+
"purpose": "maskable"
+
},
+
{
+
"src": "/favicon/web-app-manifest-512x512.png",
+
"sizes": "512x512",
+
"type": "image/png",
+
"purpose": "maskable"
+
}
+
],
+
"theme_color": "#272631",
+
"background_color": "#272631",
+
"display": "standalone"
+
}
static/favicon/web-app-manifest-192x192.png

This is a binary file and will not be displayed.

static/favicon/web-app-manifest-512x512.png

This is a binary file and will not be displayed.

static/favicon-16x16.png

This is a binary file and will not be displayed.

static/favicon-32x32.png

This is a binary file and will not be displayed.

static/favicon.ico

This is a binary file and will not be displayed.

-21
static/icons.svg
···
-
<svg xmlns="http://www.w3.org/2000/svg">
-
<symbol id="rss" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1Z"/>
-
</symbol>
-
-
<symbol id="darkMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z"/>
-
</symbol>
-
-
<symbol id="lightMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z"/>
-
</symbol>
-
-
<symbol id="chevronLeft" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M15.41 16.58L10.83 12l4.58-4.59L14 6l-6 6l6 6l1.41-1.42Z"/>
-
</symbol>
-
-
<symbol id="chevronRight" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.42Z"/>
-
</symbol>
-
</svg>
+149
static/js/404-matcher.js
···
+
// Taken from Vale's 404 Guesser
+
// https://vale.rocks/assets/scripts/404-guesser.js
+
// which was based on Gwern's 404 Error Page URL Suggester
+
// https://gwern.net/static/js/404-guesser.js
+
+
class URLSuggester {
+
constructor() {
+
this.maxDistance = 8;
+
this.urls = [];
+
}
+
+
async initialize() {
+
try {
+
const sitemapText = await this.fetchSitemap();
+
if (sitemapText) {
+
this.urls = this.parseUrls(sitemapText);
+
const currentPath = window.location.pathname;
+
if (!currentPath.endsWith("/404")) {
+
const suggestions = this.findSimilarUrls(currentPath);
+
this.injectSuggestions(currentPath, suggestions);
+
}
+
}
+
} catch (error) {
+
console.error("Error initializing URL suggester:", error);
+
}
+
}
+
+
async fetchSitemap() {
+
try {
+
const response = await fetch("/sitemap.xml");
+
return await response.text();
+
} catch (error) {
+
console.error("Error fetching sitemap:", error);
+
return null;
+
}
+
}
+
+
parseUrls(sitemapText) {
+
const parser = new DOMParser();
+
const xmlDoc = parser.parseFromString(sitemapText, "text/xml");
+
const urlNodes = xmlDoc.getElementsByTagName("url");
+
return Array.from(urlNodes).map(
+
(node) =>
+
new URL(node.getElementsByTagName("loc")[0].textContent).pathname,
+
);
+
}
+
+
boundedLevenshteinDistance(a, b, maxDistance) {
+
if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1;
+
const matrix = Array(b.length + 1)
+
.fill(null)
+
.map((_, i) => [i]);
+
for (let j = 1; j <= a.length; j++) {
+
matrix[0][j] = j;
+
}
+
for (let i = 1; i <= b.length; i++) {
+
let minDistance = maxDistance + 1;
+
for (let j = 1; j <= a.length; j++) {
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
+
matrix[i][j] = matrix[i - 1][j - 1];
+
} else {
+
matrix[i][j] = Math.min(
+
matrix[i - 1][j - 1] + 1,
+
matrix[i][j - 1] + 1,
+
matrix[i - 1][j] + 1,
+
);
+
}
+
minDistance = Math.min(minDistance, matrix[i][j]);
+
}
+
if (minDistance > maxDistance) {
+
return maxDistance + 1;
+
}
+
}
+
return matrix[b.length][a.length];
+
}
+
+
findSimilarUrls(targetUrl) {
+
const targetPath = new URL(targetUrl, location.origin).pathname;
+
+
if (targetPath.startsWith("/posts/")) {
+
const exactMatch = this.urls.find((url) => url === targetPath);
+
if (exactMatch) {
+
return [location.origin + exactMatch];
+
}
+
}
+
+
const potentialMatches = this.urls.filter(
+
(url) =>
+
Math.abs(url.length - targetPath.length) <= this.maxDistance &&
+
!url.endsWith("/404.html"),
+
);
+
+
const similarUrls = potentialMatches
+
.map((url) => ({
+
url,
+
distance: this.boundedLevenshteinDistance(
+
url,
+
targetPath,
+
this.maxDistance,
+
),
+
}))
+
.filter((item) => item.distance <= this.maxDistance)
+
.sort((a, b) => a.distance - b.distance);
+
+
const seenUrls = new Set();
+
const uniqueSimilarUrls = similarUrls
+
.filter((item) => {
+
if (seenUrls.has(item.url)) return false;
+
seenUrls.add(item.url);
+
return true;
+
})
+
.slice(0, 10);
+
+
return uniqueSimilarUrls.map((item) => location.origin + item.url);
+
}
+
+
injectSuggestions(currentPath, suggestions) {
+
const app = document.querySelector("#suggestions");
+
if (!app) return;
+
+
if (suggestions.length > 0) {
+
const p = document.createElement("p");
+
+
p.innerHTML = "I did however find some URLs that might be relevant?";
+
app.appendChild(p);
+
+
for (const url of suggestions) {
+
const a = document.createElement("a");
+
const cleanUrl = url.replace(/\.html$/, "");
+
a.href = cleanUrl;
+
a.textContent = cleanUrl;
+
app.appendChild(a);
+
}
+
+
const endText = document.createElement("p");
+
app.appendChild(endText);
+
} else {
+
const p = document.createElement("p");
+
p.innerHTML = `Couldn't find any URLs similar to <code>${currentPath}</code>. I guess it's time to find something new`;
+
app.appendChild(p);
+
}
+
+
app.className = "url-suggestions";
+
}
+
}
+
+
document.addEventListener("DOMContentLoaded", () => {
+
new URLSuggester().initialize();
+
});
+82
static/js/copy-button.js
···
+
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
+
+
function initCopyButtons() {
+
const blocks = document.querySelectorAll("pre[class^='language-']");
+
+
for (const block of blocks) {
+
// Code block header title
+
const title = document.createElement("span");
+
title.style.color = "var(--accent-text)";
+
const lang = block.getAttribute("data-lang");
+
const comment =
+
block.previousElementSibling &&
+
(block.previousElementSibling.tagName === "blockquote" ||
+
block.previousElementSibling.nodeName === "BLOCKQUOTE")
+
? block.previousElementSibling
+
: null;
+
if (comment) block.previousElementSibling.remove();
+
title.innerHTML =
+
lang + (comment ? ` (${comment.textContent.trim()})` : "");
+
+
// Copy button icon
+
const icon = document.createElement("i");
+
icon.classList.add("icon");
+
+
// Copy button
+
const button = document.createElement("button");
+
const copyCodeText = "Copy code";
+
button.setAttribute("title", copyCodeText);
+
button.appendChild(icon);
+
+
// Code block header
+
const header = document.createElement("div");
+
header.classList.add("header");
+
header.appendChild(title);
+
header.appendChild(button);
+
+
// Container that holds header and the code block itself
+
const container = document.createElement("div");
+
container.classList.add("pre-container");
+
container.appendChild(header);
+
+
// Move code block into the container
+
block.parentNode.insertBefore(container, block);
+
container.appendChild(block);
+
+
button.addEventListener("click", async () => {
+
await copyCode(block, header, button);
+
});
+
}
+
+
async function copyCode(block, header, button) {
+
const code = block.querySelector("code");
+
const text = code.innerText;
+
+
// Only try to copy if clipboard API is available
+
if (navigator.clipboard) {
+
try {
+
await navigator.clipboard.writeText(text);
+
header.classList.add("active");
+
button.setAttribute("disabled", true);
+
+
header.addEventListener(
+
"animationend",
+
() => {
+
header.classList.remove("active");
+
button.removeAttribute("disabled");
+
},
+
{ once: true },
+
);
+
} catch (err) {
+
console.error("Failed to copy:", err);
+
}
+
}
+
}
+
}
+
+
// Since the script has defer attribute, the DOM is already loaded when this runs
+
if (document.readyState === 'loading') {
+
document.addEventListener('DOMContentLoaded', initCopyButtons);
+
} else {
+
initCopyButtons();
+
}
+65
static/js/emoji-replace.js
···
+
document.addEventListener("DOMContentLoaded", () => {
+
const content = document.querySelector("main");
+
if (!content) return;
+
+
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, {
+
acceptNode: (node) => {
+
// Skip code blocks, pre tags, and script/style tags
+
let parent = node.parentElement;
+
while (parent) {
+
const tag = parent.tagName.toLowerCase();
+
if (
+
tag === "code" ||
+
tag === "pre" ||
+
tag === "script" ||
+
tag === "style"
+
) {
+
return NodeFilter.FILTER_REJECT;
+
}
+
parent = parent.parentElement;
+
}
+
return NodeFilter.FILTER_ACCEPT;
+
},
+
});
+
+
const nodesToReplace = [];
+
while (walker.nextNode()) {
+
const node = walker.currentNode;
+
if (/:[\w-]+:/.test(node.textContent)) {
+
nodesToReplace.push(node);
+
}
+
}
+
+
nodesToReplace.forEach((node) => {
+
const frag = document.createDocumentFragment();
+
const parts = node.textContent.split(/(:[\w-]+:)/);
+
+
parts.forEach((part) => {
+
if (/^:[\w-]+:$/.test(part)) {
+
const name = part.slice(1, -1);
+
+
const span = document.createElement("span");
+
span.className = "emoji-inline--wrapper";
+
+
const img = document.createElement("img");
+
img.src = `https://cachet.dunkirk.sh/emojis/${name}/r`;
+
img.alt = part;
+
img.className = "emoji-inline";
+
img.loading = "lazy";
+
img.setAttribute("aria-label", `${name} emoji`);
+
+
// Fallback: if image fails to load, show original text
+
img.onerror = () => {
+
span.replaceWith(document.createTextNode(part));
+
};
+
+
span.appendChild(img);
+
frag.appendChild(span);
+
} else if (part) {
+
frag.appendChild(document.createTextNode(part));
+
}
+
});
+
+
node.replaceWith(frag);
+
});
+
});
-54
static/js/script.js
···
-
const toggleButton = document.getElementById("theme-toggle");
-
const themeIcon = document.getElementById("theme-icon");
-
const themeSound = document.getElementById("theme-sound");
-
-
// Function to update the theme icon based on the current theme
-
const updateThemeIcon = (isDarkMode) => {
-
const themeMode = isDarkMode ? "darkMode" : "lightMode";
-
const iconPath = themeIcon
-
.querySelector("use")
-
.getAttribute("href")
-
.replace(/#.*$/, `#${themeMode}`);
-
themeIcon.querySelector("use").setAttribute("href", iconPath);
-
};
-
-
// Function to update the theme based on the current mode
-
const updateTheme = (isDarkMode) => {
-
const theme = isDarkMode ? "dark" : "light";
-
document.documentElement.setAttribute("data-theme", theme);
-
updateThemeIcon(isDarkMode);
-
};
-
-
// Function to toggle the theme
-
const toggleTheme = () => {
-
const isDarkMode = toggleButton.checked;
-
updateTheme(isDarkMode);
-
themeSound.play();
-
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
-
-
// Add transition class to body for smooth transition
-
document.body.classList.add("theme-transition");
-
setTimeout(() => {
-
document.body.classList.remove("theme-transition");
-
}, 300);
-
};
-
-
// Event listener for theme toggle
-
toggleButton.addEventListener("change", toggleTheme);
-
-
// Function to initialize the theme based on the stored preference
-
const initializeTheme = () => {
-
const storedTheme = localStorage.getItem("theme");
-
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
-
const isDarkMode = storedTheme === "dark" || (!storedTheme && prefersDark);
-
toggleButton.checked = isDarkMode;
-
updateTheme(isDarkMode);
-
};
-
-
// Initialize the theme
-
initializeTheme();
-
-
// Listen for changes in system preference
-
window
-
.matchMedia("(prefers-color-scheme: dark)")
-
.addEventListener("change", initializeTheme);
+92
static/lightbox.js
···
+
let currentLightboxImages = [];
+
let currentLightboxIndex = 0;
+
+
function openLightbox(src) {
+
currentLightboxImages = [src];
+
currentLightboxIndex = 0;
+
showLightbox();
+
}
+
+
function openLightboxGroup(element) {
+
const group = element.closest('.img-group');
+
const images = Array.from(group.querySelectorAll('img')).map(img => img.src);
+
const clickedImg = element.querySelector('img');
+
+
currentLightboxImages = images;
+
currentLightboxIndex = images.indexOf(clickedImg.src);
+
showLightbox();
+
}
+
+
function showLightbox() {
+
let lightbox = document.getElementById('lightbox');
+
+
if (!lightbox) {
+
lightbox = document.createElement('div');
+
lightbox.id = 'lightbox';
+
lightbox.innerHTML = `
+
<div class="lightbox-content">
+
<button class="lightbox-close" onclick="closeLightbox()">&times;</button>
+
<img id="lightbox-img" src="" alt="">
+
<div class="lightbox-controls">
+
<button class="lightbox-prev" onclick="prevImage()">โ†</button>
+
<button class="lightbox-next" onclick="nextImage()">โ†’</button>
+
</div>
+
</div>
+
`;
+
document.body.appendChild(lightbox);
+
+
lightbox.addEventListener('click', (e) => {
+
if (e.target === lightbox) closeLightbox();
+
});
+
+
document.addEventListener('keydown', handleKeyPress);
+
}
+
+
updateLightboxImage();
+
lightbox.style.display = 'flex';
+
document.body.style.overflow = 'hidden';
+
}
+
+
function closeLightbox() {
+
const lightbox = document.getElementById('lightbox');
+
if (lightbox) {
+
lightbox.style.display = 'none';
+
document.body.style.overflow = '';
+
}
+
}
+
+
function updateLightboxImage() {
+
const img = document.getElementById('lightbox-img');
+
const controls = document.querySelector('.lightbox-controls');
+
+
img.src = currentLightboxImages[currentLightboxIndex];
+
+
if (currentLightboxImages.length === 1) {
+
controls.style.display = 'none';
+
} else {
+
controls.style.display = 'flex';
+
}
+
}
+
+
function prevImage() {
+
currentLightboxIndex = (currentLightboxIndex - 1 + currentLightboxImages.length) % currentLightboxImages.length;
+
updateLightboxImage();
+
}
+
+
function nextImage() {
+
currentLightboxIndex = (currentLightboxIndex + 1) % currentLightboxImages.length;
+
updateLightboxImage();
+
}
+
+
function handleKeyPress(e) {
+
const lightbox = document.getElementById('lightbox');
+
if (!lightbox || lightbox.style.display !== 'flex') return;
+
+
if (e.key === 'Escape') {
+
closeLightbox();
+
} else if (e.key === 'ArrowLeft') {
+
prevImage();
+
} else if (e.key === 'ArrowRight') {
+
nextImage();
+
}
+
}
static/now/og.png

This is a binary file and will not be displayed.

static/og.png

This is a binary file and will not be displayed.

static/pfp/og.png

This is a binary file and will not be displayed.

static/pfps/current.webp

This is a binary file and will not be displayed.

static/pfps/fall.jpg

This is a binary file and will not be displayed.

static/pfps/hands.jpg

This is a binary file and will not be displayed.

static/pfps/instsqc-rat-pfp.webp

This is a binary file and will not be displayed.

static/pfps/kieranrat.webp

This is a binary file and will not be displayed.

static/pfps/kitty.webp

This is a binary file and will not be displayed.

static/pfps/starry.webp

This is a binary file and will not be displayed.

-19
static/site.webmanifest
···
-
{
-
"name": "",
-
"short_name": "",
-
"icons": [
-
{
-
"src": "/android-chrome-192x192.png",
-
"sizes": "192x192",
-
"type": "image/png"
-
},
-
{
-
"src": "/android-chrome-512x512.png",
-
"sizes": "512x512",
-
"type": "image/png"
-
}
-
],
-
"theme_color": "#ffffff",
-
"background_color": "#ffffff",
-
"display": "standalone"
-
}
static/tags/accessibility/og.png

This is a binary file and will not be displayed.

static/tags/apple/og.png

This is a binary file and will not be displayed.

static/tags/archival/og.png

This is a binary file and will not be displayed.

static/tags/atproto/og.png

This is a binary file and will not be displayed.

static/tags/biography/og.png

This is a binary file and will not be displayed.

static/tags/cool-stuff/og.png

This is a binary file and will not be displayed.

static/tags/essays/og.png

This is a binary file and will not be displayed.

static/tags/fancy/og.png

This is a binary file and will not be displayed.

static/tags/graphql/og.png

This is a binary file and will not be displayed.

static/tags/hilton/og.png

This is a binary file and will not be displayed.

static/tags/homelab/og.png

This is a binary file and will not be displayed.

static/tags/meta/og.png

This is a binary file and will not be displayed.

static/tags/mildrant/og.png

This is a binary file and will not be displayed.

static/tags/music/og.png

This is a binary file and will not be displayed.

static/tags/nix/og.png

This is a binary file and will not be displayed.

static/tags/og.png

This is a binary file and will not be displayed.

static/tags/physics/og.png

This is a binary file and will not be displayed.

static/tags/project/og.png

This is a binary file and will not be displayed.

static/tags/reverse-engineering/og.png

This is a binary file and will not be displayed.

static/tags/shell/og.png

This is a binary file and will not be displayed.

static/tags/teardown/og.png

This is a binary file and will not be displayed.

static/tags/tool/og.png

This is a binary file and will not be displayed.

static/tags/tutorial/og.png

This is a binary file and will not be displayed.

static/tags/yap-fest/og.png

This is a binary file and will not be displayed.

static/verify/og.png

This is a binary file and will not be displayed.

+433
syntaxes/authorized-keys.sublime-syntax
···
+
%YAML 1.2
+
---
+
# https://www.sublimetext.com/docs/syntax.html
+
# https://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT
+
# https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
+
name: Authorized Keys
+
scope: text.authorized_keys
+
version: 2
+
extends: SSH Crypto.sublime-syntax
+
+
file_extensions:
+
- authorized_keys
+
- pub
+
+
hidden_file_extensions:
+
- authorized_keys2
+
+
contexts:
+
main:
+
- include: comments-number-sign
+
- match: ^
+
push:
+
- meta_scope: meta.line.authorized-key.authorized_keys
+
- include: pop-before-nl
+
- include: pop-nl
+
- include: ssh-key-types
+
- include: ssh-fingerprint-with-label
+
- include: flag-options
+
- include: value-options
+
- include: strings
+
- match: =
+
scope: keyword.operator.assignment.authorized_keys
+
- include: punctuation-comma-sequence
+
+
flag-options:
+
- match: (?:no-)?(?:pty|user-rc|(?:agent|port|X11)-forwarding)
+
scope: keyword.other.authorized_keys
+
- match: (?:no-touch-required|verify-required|cert-authority|restrict)
+
scope: keyword.other.authorized_keys
+
+
value-options:
+
- match: (principals)(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- include: punctuation-comma-sequence
+
push: value-option-body
+
+
- match: (tunnel)(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- match: \d{1,3}
+
scope: meta.number.integer.decimal.authorized_keys
+
constant.numeric.value.authorized_keys
+
push: value-option-body
+
+
- match: (?:(expiry-time)|(valid-before))(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: invalid.deprecated.authorized_keys
+
3: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- match: |-
+
(?x:
+
\d{4} # Year
+
(?:0\d|1[12]) # Month
+
(?:[0-2]\d|3[01]) # Day
+
(?: # Optionally:
+
(?:[01]\d|2[0-3]) # HH
+
(?:[0-5]\d){1,2} # MM and maybe SS
+
)?
+
Z? # Optional UTC
+
)
+
scope: meta.constant.date.authorized_keys
+
constant.numeric.integer.date.authorized_keys
+
push: value-option-body
+
+
# Technically, permitopen requires a host, but let's be lenient
+
- match: (permitlisten|permitopen)(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- include: ipv4
+
- include: ipv6-square-bracket
+
- match: (?:([^"]*)(:))?(?:({{zero_to_65535}})|(\*))
+
captures:
+
1: meta.string.host.authorized_keys
+
2: punctuation.separator.sequence.authorized_keys
+
3: meta.number.integer.decimal.authorized_keys
+
constant.numeric.port-number.authorized_keys
+
4: constant.other.wildcard.asterisk.authorized_keys
+
push: value-option-body
+
+
- match: (from)(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- include: operator-exclamation
+
- include: punctuation-comma-sequence
+
- include: punctuation-dot-sequence
+
- include: wildcards
+
push: value-option-body
+
+
- match: (environment)(=)
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
with_prototype:
+
- match: (\w+)(=)
+
captures:
+
1: variable.other.readwrite.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
push: value-option-body
+
+
- match: (command)(=)(")
+
captures:
+
1: keyword.other.authorized_keys
+
2: keyword.operator.assignment.authorized_keys
+
3: string.quoted.double.authorized_keys
+
punctuation.definition.string.begin.authorized_keys
+
# TODO: Allow escaped double-quote
+
embed: scope:source.shell.bash
+
embed_scope: source.shell.embedded
+
escape: '"|(?=$)'
+
escape_captures:
+
0: string.quoted.double.authorized_keys
+
punctuation.definition.string.end.authorized_keys
+
+
value-option-body:
+
- include: strings
+
- match: (?=,|\s)
+
pop: 1
+
- match: .
+
scope: invalid.illegal.authorized_keys
+
pop: 1
+
+
strings:
+
- match: '"'
+
scope: punctuation.definition.string.begin.authorized_keys
+
push:
+
- meta_scope: string.quoted.double.authorized_keys
+
- match: \\"
+
scope: constant.character.escape.authorized_keys
+
- match: '"'
+
scope: punctuation.definition.string.end.authorized_keys
+
pop: 1
+
+
+
+
comments:
+
- include: comments-number-sign
+
- include: comments-semicolon
+
+
comments-number-sign:
+
- match: ^\s*(#+)
+
captures:
+
1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.number-sign.ssh.common
+
- match: \n
+
scope: comment.line.number-sign.ssh.common
+
pop: true
+
+
comments-semicolon:
+
- match: ^\s*(;+)
+
captures:
+
1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.semi-colon.ssh.common
+
- include: pop-nl
+
+
###[ COMPONENTS ]##############################################################
+
+
operator-exclamation:
+
- match: '!'
+
scope: keyword.operator.logical.ssh.common
+
+
wildcards:
+
- match: \*
+
scope: constant.other.wildcard.asterisk.ssh.common
+
- match: \?
+
scope: constant.other.wildcard.questionmark.ssh.common
+
+
punctuation-comma-sequence:
+
- match: ','
+
scope: punctuation.separator.sequence.ssh.common
+
+
punctuation-dot-sequence:
+
- match: \.
+
scope: punctuation.separator.sequence.ssh.common
+
+
punctuation-at:
+
- match: '@'
+
scope: punctuation.separator.sequence.ssh.common
+
+
ssh-fingerprint:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
+
ssh-fingerprint-with-label:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
push: expect-fingerprint-label
+
+
expect-fingerprint-label:
+
- include: pop-before-nl
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.annotation.identifier.ssh.common
+
string.unquoted.ssh.common
+
- match: '(?=[ \t]*$)'
+
pop: 1
+
- include: punctuation-at
+
+
time-values:
+
# https://man.openbsd.org/sshd_config.5#TIME_FORMATS
+
# seconds, minutes, hours, days, weeks
+
- match: \b(?=[\dsmhdw]*\d[smhdw][\s,"])
+
push:
+
- meta_scope: meta.constant.time.ssh.common
+
meta.number.integer.decimal.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: (\d+)([smhdw])
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
+
bytes-values:
+
- match: \b(\d+)([KMG])(?=[\s,"])
+
scope: meta.constant.bytes.ssh.common
+
meta.number.integer.other.ssh.common
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
+
mac-addresses:
+
- match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2})
+
scope: entity.name.constant.mac-address.ssh.common
+
+
ipv4:
+
- match: '\b{{ipv4}}\b'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
+
ipv6:
+
- match: '{{ipv6}}'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
+
ipv6-square-bracket:
+
- match: (\[){{ipv6}}(\])
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
captures:
+
1: punctuation.definition.constant.begin.ssh.common
+
2: punctuation.definition.constant.end.ssh.common
+
+
ip-addresses:
+
- include: ipv6
+
- include: ipv4
+
+
ipv4-with-cidr:
+
- match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
+
ipv6-with-cidr:
+
- match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)?
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
+
ip-addresses-with-cidr:
+
- include: ipv6-with-cidr
+
- include: ipv4-with-cidr
+
+
port-numbers:
+
- match: \b{{zero_to_65535}}(?![\w:])
+
scope: meta.number.integer.decimal.ssh.common
+
constant.numeric.port-number.ssh.common
+
+
match-all:
+
- match: '\b(?xi: all )\b'
+
scope: constant.language.boolean.true.ssh.common
+
+
none:
+
- match: \bnone\b
+
scope: constant.language.null.ssh.common
+
+
any:
+
- match: \bany\b
+
scope: constant.language.set.ssh.common
+
+
boolean:
+
- match: \byes\b
+
scope: constant.language.boolean.true.ssh.common
+
- match: \bno\b
+
scope: constant.language.boolean.false.ssh.common
+
+
boolean-with-typing:
+
- include: boolean
+
# Consume while typing as well, but unscoped
+
- match: \b(?:ye?|n)\b
+
+
log-level:
+
- match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b'
+
scope: constant.language.log-level.ssh.common
+
+
possibly-quoted-value:
+
- meta_content_scope: meta.mapping.value.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_scope: string.quoted.double.ssh.common
+
- match: (")(?:\s*(\S.*))?
+
captures:
+
1: punctuation.definition.string.end.ssh.common
+
2: invalid.illegal.ssh.common
+
pop: 1
+
- match: \n|$
+
scope: invalid.illegal.unclosed-string.ssh.common
+
pop: 2
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- include: pop-before-nl
+
- include: pop-nl
+
+
string-patterns:
+
# https://man7.org/linux/man-pages/man5/ssh_config.5.html#PATTERNS
+
# https://man.openbsd.org/ssh_config.5#PATTERNS
+
# https://man7.org/linux/man-pages/man5/sshd_config.5.html#PATTERNS
+
# https://man.openbsd.org/sshd_config.5#PATTERNS
+
- include: punctuation-comma-sequence
+
- include: operator-exclamation
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_content_scope: string.quoted.double.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.end.ssh.common
+
pop: 1
+
- include: wildcards
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- match: (?=[,!\s])
+
pop: 1
+
- include: wildcards
+
+
paths:
+
# This is just heuristic. Expect failures.
+
- match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?)
+
push:
+
- meta_scope: meta.path.ssh.common
+
entity.name.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: ~[\w\-.]*
+
scope: variable.language.home.ssh.common
+
- match: (/)(?:(\.{1,2})(?=/)|\.(?!/))?
+
captures:
+
1: punctuation.separator.path.ssh.common
+
2: constant.other.placeholder.ssh.common
+
- match: \.(?=[\w*?%])
+
scope: punctuation.separator.sequence.ssh.common
+
- include: wildcards
+
- include: tokens
+
- include: environment-variables
+
+
none-command-values:
+
- match: \s*(none)\b[ \t]*$
+
captures:
+
1: constant.language.null.ssh.common
+
- match: \s*((")(none)("))[ \t]*$
+
captures:
+
1: string.quoted.double.ssh.common
+
2: punctuation.definition.string.begin.ssh.common
+
3: constant.language.null.ssh.common
+
4: punctuation.definition.string.end.ssh.common
+
+
tokens: []
+
environment-variables: []
+
+
###[ PROTOTYPE ]###############################################################
+
+
pop-nl:
+
- match: \n
+
pop: 1
+
+
pop-before-nl:
+
- match: (?=\n)
+
pop: 1
+
+
###############################################################################
+
+
+
+
ssh-ciphers:
+
- match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.cipher.ssh.crypto
+
- match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"])
+
scope: invalid.deprecated.cipher.ssh.crypto
+
ssh-kex-algorithms:
+
- match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"])
+
scope: support.function.kex-algorithm.ssh.crypto
+
- match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"])
+
scope: invalid.deprecated.kex-algorithm.ssh.crypto
+
ssh-key-types:
+
- match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"])
+
scope: support.type.key-type.ssh.crypto
+
- match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"])
+
scope: invalid.deprecated.key-type.ssh.crypto
+
ssh-mac-algorithms:
+
- match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.mac-algorithm.ssh.crypto
+
- match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"])
+
scope: invalid.deprecated.mac-algorithm.ssh.crypto
+
extends: SSH Common.sublime-syntax
+
hidden: true
+
hidden_file_extensions:
+
- syntax_test_crypto
+
name: SSH Crypto
+
scope: text.ssh.crypto
+
version: 2
+
variables:
+
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+243
syntaxes/gleam.sublime-syntax
···
+
%YAML 1.2
+
---
+
version: 2
+
+
file_extensions:
+
- gleam
+
+
scope: source.gleam
+
+
variables:
+
lower_ident: '[[:lower:]][[:word:]]*'
+
upper_ident: '[[:upper:]][[:word:]]*'
+
+
contexts:
+
main:
+
- include: base
+
base:
+
- include: attribute
+
- include: bitstring
+
- include: block
+
- include: comment
+
- include: constant_def
+
- include: function_def
+
- include: keyword
+
- include: function_call
+
- include: record
+
- include: import
+
- include: number
+
- include: operator
+
- include: punctuation
+
- include: string
+
- include: unused_name
+
- include: type_name
+
+
# Attributes (annotations)
+
attribute:
+
- match: ^\s*(@{{lower_ident}})\(
+
captures:
+
1: variable.other.constant.gleam
+
push:
+
- include: arguments
+
- meta_scope: meta.annotation.gleam
+
- match: ^\s*(@{{lower_ident}})
+
scope: meta.annotation.gleam
+
captures:
+
1: variable.other.constant.gleam
+
+
# Arguments (to a function call, record constructor, or attribute)
+
arguments:
+
- include: bitstring
+
- include: block
+
- include: comment
+
- include: function_def
+
- include: function_call
+
- include: record
+
- include: number
+
- include: operator
+
- include: punctuation
+
- include: string
+
- include: unused_name
+
- include: type_name
+
- match: '\b{{lower_ident}}:'
+
scope: constant.other.gleam
+
- match: \)
+
pop: true
+
+
# Bitstrings
+
bitstring:
+
- match: '<<'
+
scope: punctuation.definition.generic.begin.gleam
+
push:
+
- include: number
+
- include: string
+
- match: \b(bytes|int|float|bits|utf8|utf16|utf32|utf8_codepoint|utf16_codepoint|utf32_codepoint|signed|unsigned|big|little|native|unit|size)\b
+
scope: keyword.other.gleam
+
- match: '>>'
+
scope: punctuation.definition.generic.end.gleam
+
pop: true
+
+
# Blocks
+
block:
+
- match: '{'
+
scope: punctuation.section.block.begin.gleam
+
push: base
+
- match: '}'
+
scope: punctuation.section.block.end.gleam
+
pop: true
+
+
# Comments
+
comment:
+
- match: ///?/?
+
scope: punctuation.definition.comment.line.gleam
+
push:
+
- meta_scope: comment.line.gleam
+
- match: $
+
pop: true
+
+
# Constant definitions
+
constant_def:
+
- match: \b(const)\s+({{lower_ident}})\b
+
captures:
+
1: keyword.other.gleam
+
2: entity.name.constant.gleam
+
+
# Function calls
+
function_call:
+
- match: \b(?:{{lower_ident}}\.)*({{lower_ident}})\(
+
captures:
+
1: variable.function.gleam
+
push: arguments
+
+
# Function definitions
+
function_def:
+
- match: \b(fn)(?:[[:space:]]+({{lower_ident}}))?[[:space:]]*\(
+
captures:
+
1: storage.type.function.gleam
+
2: entity.name.function.gleam
+
push: function_def_args
+
+
# Function arguments
+
function_def_args:
+
- include: function_def
+
- include: punctuation
+
- include: type_name
+
- include: unused_name
+
- match: ->
+
scope: keyword.operator.gleam
+
- match: \b(?:({{lower_ident}})[[:space:]]+)?({{lower_ident}}:)
+
captures:
+
1: constant.other.gleam
+
2: variable.parameter.gleam
+
- match: \(
+
push: function_def_args
+
- match: \)
+
pop: true
+
+
# Imports
+
import:
+
- match: ^import\b
+
scope: keyword.control.import.gleam
+
push:
+
- match: \bas\b
+
scope: keyword.control.import.gleam
+
- match: \b(?:{{lower_ident}}/)*{{lower_ident}}\b
+
scope: entity.name.namespace.gleam
+
- match: (\.)({)
+
captures:
+
1: punctuation.accessor.gleam
+
2: punctuation.definition.generic.begin.gleam
+
push:
+
- include: punctuation
+
- include: type_name
+
- match: \bas\b
+
scope: keyword.control.import.gleam
+
- match: \btype\b
+
scope: storage.type.gleam
+
- match: '}'
+
scope: punctuation.definition.generic.end.gleam
+
pop: true
+
- match: $
+
pop: true
+
+
# Keywords
+
keyword:
+
- match: \b(as|assert|case|const|echo|if|let|panic|todo|use)\b
+
scope: keyword.other.gleam
+
- match: \b(opaque|pub)\b
+
scope: storage.modifier.gleam
+
- match: \btype\b
+
scope: storage.type.gleam
+
- match: \bfn\b
+
scope: storage.type.function.gleam
+
# Reserved for future use
+
- match: \b(auto|delegate|derive|else|implement|macro|test)\b
+
scope: invalid.illegal.gleam
+
+
# Numbers
+
number:
+
- match: \b0b[01][01_]*\b
+
scope: constant.numeric.binary.gleam
+
- match: \b0o[0-7][0-7_]*\b
+
scope: constant.numeric.octal.gleam
+
- match: \b[0-9][0-9_]*(\.[0-9_]*(e-?[0-9][0-9_]*)?)?\b
+
scope: constant.numeric.decimal.gleam
+
- match: \b0x[[:xdigit:]][[:xdigit:]_]*\b
+
scope: constant.numeric.hexadecimal.gleam
+
+
# Operators
+
operator:
+
- match: <-
+
scope: keyword.operator.assignment.gleam
+
- match: (\|>|\.\.|<=\.|>=\.|==\.|!=\.|<\.|>\.|<=|>=|==|!=|<|>|<>)
+
scope: keyword.operator.gleam
+
- match: '='
+
scope: keyword.operator.assignment.gleam
+
- match: ->
+
scope: keyword.operator.gleam
+
- match: (\+\.|\-\.|/\.|\*\.|%\.|\+|\-|/|\*|%)
+
scope: keyword.operator.arithmetic.gleam
+
- match: (&&|\|\|)
+
scope: keyword.operator.logical.gleam
+
- match: \|
+
scope: keyword.operator.gleam
+
+
# Punctuation (separators, accessors)
+
punctuation:
+
- match: \.
+
scope: punctuation.accessor.gleam
+
- match: ','
+
scope: punctuation.separator.gleam
+
+
# Records (constructors with arguments)
+
record:
+
- match: \b((?:{{lower_ident}}\.)*{{upper_ident}})\(
+
captures:
+
1: entity.name.type.gleam
+
push: arguments
+
+
# Strings
+
string:
+
- match: '"'
+
scope: punctuation.definition.string.begin.gleam
+
push:
+
- meta_scope: string.quoted.double.gleam
+
- match: \\[fnrt"\\]
+
scope: constant.character.escape.gleam
+
- match: \\u\{[[:xdigit:]]{1,6}\}
+
scope: constant.character.escape.gleam
+
- match: \\
+
scope: invalid.illegal.gleam
+
- match: '"'
+
scope: punctuation.definition.string.end.gleam
+
pop: true
+
+
# Types and constructors
+
type_name:
+
- match: \b(?:{{lower_ident}}\.)*{{upper_ident}}\b
+
scope: entity.name.type.gleam
+
+
# Unused bindings
+
unused_name:
+
- match: \b_{{lower_ident}}\b
+
scope: comment.line.gleam
+305
syntaxes/known-hosts.sublime-syntax
···
+
%YAML 1.2
+
---
+
# Standalone version of known-hosts.sublime-syntax
+
# Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax
+
+
name: Known Hosts
+
scope: text.known_hosts
+
version: 2
+
file_extensions:
+
- known_hosts
+
hidden_file_extensions:
+
- known_hosts.old
+
variables:
+
base64_char: '[a-zA-Z0-9+/]'
+
ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3})
+
zero_to_32: (?:3[0-2]|[12][0-9]|[0-9])
+
zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9])
+
zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9])
+
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+
ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}})
+
ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\
+
\ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\
+
\ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \
+
\ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \
+
\ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\
+
\ # fe80::7:8%eth0 fe80::7:8%1 \
+
\ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\
+
\ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\
+
\ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\
+
\ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\
+
\ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\
+
\ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\
+
\ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\
+
\ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\
+
\ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \
+
\ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \
+
\ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)"
+
contexts:
+
main:
+
- include: comments-number-sign
+
- match: ^((@)(?:revoked|cert-authority))?
+
captures:
+
1: meta.annotation.known_hosts variable.annotation.known_hosts
+
2: punctuation.definition.annotation.known_hosts
+
push:
+
- meta_scope: meta.line.known-host.known_hosts
+
- include: pop-before-nl
+
- include: pop-nl
+
- include: punctuation-comma-sequence
+
- include: ssh-fingerprint-with-label
+
- include: ssh-key-types
+
- include: hostname-or-ip-value
+
comments:
+
- include: comments-number-sign
+
- include: comments-semicolon
+
comments-number-sign:
+
- match: ^\s*(#+)
+
captures:
+
1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.number-sign.ssh.common
+
- match: \n
+
scope: comment.line.number-sign.ssh.common
+
pop: true
+
comments-semicolon:
+
- match: ^\s*(;+)
+
captures:
+
1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.semi-colon.ssh.common
+
- include: pop-nl
+
operator-exclamation:
+
- match: '!'
+
scope: keyword.operator.logical.ssh.common
+
wildcards:
+
- match: \*
+
scope: constant.other.wildcard.asterisk.ssh.common
+
- match: \?
+
scope: constant.other.wildcard.questionmark.ssh.common
+
punctuation-comma-sequence:
+
- match: ','
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-dot-sequence:
+
- match: \.
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-at:
+
- match: '@'
+
scope: punctuation.separator.sequence.ssh.common
+
ssh-fingerprint:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
ssh-fingerprint-with-label:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
push: expect-fingerprint-label
+
expect-fingerprint-label:
+
- include: pop-before-nl
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common
+
- match: (?=[ \t]*$)
+
pop: 1
+
- include: punctuation-at
+
time-values:
+
- match: \b(?=[\dsmhdw]*\d[smhdw][\s,"])
+
push:
+
- meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: (\d+)([smhdw])
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
bytes-values:
+
- match: \b(\d+)([KMG])(?=[\s,"])
+
scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
mac-addresses:
+
- match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2})
+
scope: entity.name.constant.mac-address.ssh.common
+
ipv4:
+
- match: \b{{ipv4}}\b
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
ipv6:
+
- match: '{{ipv6}}'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
ipv6-square-bracket:
+
- match: (\[){{ipv6}}(\])
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
captures:
+
1: punctuation.definition.constant.begin.ssh.common
+
2: punctuation.definition.constant.end.ssh.common
+
ip-addresses:
+
- include: ipv6
+
- include: ipv4
+
ipv4-with-cidr:
+
- match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ipv6-with-cidr:
+
- match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)?
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ip-addresses-with-cidr:
+
- include: ipv6-with-cidr
+
- include: ipv4-with-cidr
+
port-numbers:
+
- match: \b{{zero_to_65535}}(?![\w:])
+
scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common
+
match-all:
+
- match: '\b(?xi: all )\b'
+
scope: constant.language.boolean.true.ssh.common
+
none:
+
- match: \bnone\b
+
scope: constant.language.null.ssh.common
+
any:
+
- match: \bany\b
+
scope: constant.language.set.ssh.common
+
boolean:
+
- match: \byes\b
+
scope: constant.language.boolean.true.ssh.common
+
- match: \bno\b
+
scope: constant.language.boolean.false.ssh.common
+
boolean-with-typing:
+
- include: boolean
+
- match: \b(?:ye?|n)\b
+
log-level:
+
- match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b'
+
scope: constant.language.log-level.ssh.common
+
possibly-quoted-value:
+
- meta_content_scope: meta.mapping.value.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_scope: string.quoted.double.ssh.common
+
- match: (")(?:\s*(\S.*))?
+
captures:
+
1: punctuation.definition.string.end.ssh.common
+
2: invalid.illegal.ssh.common
+
pop: 1
+
- match: \n|$
+
scope: invalid.illegal.unclosed-string.ssh.common
+
pop: 2
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- include: pop-before-nl
+
- include: pop-nl
+
string-patterns:
+
- include: punctuation-comma-sequence
+
- include: operator-exclamation
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_content_scope: string.quoted.double.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.end.ssh.common
+
pop: 1
+
- include: wildcards
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- match: (?=[,!\s])
+
pop: 1
+
- include: wildcards
+
paths:
+
- match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?)
+
push:
+
- meta_scope: meta.path.ssh.common entity.name.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: ~[\w\-.]*
+
scope: variable.language.home.ssh.common
+
- match: (/)(?:(\.{1,2})(?=/)|\.(?!/))?
+
captures:
+
1: punctuation.separator.path.ssh.common
+
2: constant.other.placeholder.ssh.common
+
- match: \.(?=[\w*?%])
+
scope: punctuation.separator.sequence.ssh.common
+
- include: wildcards
+
- include: tokens
+
- include: environment-variables
+
none-command-values:
+
- match: \s*(none)\b[ \t]*$
+
captures:
+
1: constant.language.null.ssh.common
+
- match: \s*((")(none)("))[ \t]*$
+
captures:
+
1: string.quoted.double.ssh.common
+
2: punctuation.definition.string.begin.ssh.common
+
3: constant.language.null.ssh.common
+
4: punctuation.definition.string.end.ssh.common
+
tokens: []
+
environment-variables: []
+
pop-nl:
+
- match: \n
+
pop: 1
+
pop-before-nl:
+
- match: (?=\n)
+
pop: 1
+
ssh-ciphers:
+
- match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.cipher.ssh.crypto
+
- match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"])
+
scope: invalid.deprecated.cipher.ssh.crypto
+
ssh-kex-algorithms:
+
- match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"])
+
scope: support.function.kex-algorithm.ssh.crypto
+
- match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"])
+
scope: invalid.deprecated.kex-algorithm.ssh.crypto
+
ssh-key-types:
+
- match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"])
+
scope: support.type.key-type.ssh.crypto
+
- match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"])
+
scope: invalid.deprecated.key-type.ssh.crypto
+
ssh-mac-algorithms:
+
- match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.mac-algorithm.ssh.crypto
+
- match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"])
+
scope: invalid.deprecated.mac-algorithm.ssh.crypto
+
hostname-or-ip-value:
+
- include: operator-exclamation
+
- match: \[
+
scope: punctuation.definition.string.begin.known_hosts
+
push:
+
- meta_scope: meta.brackets.host.known_hosts
+
- match: (\])(?:(:)({{zero_to_65535}}))?
+
captures:
+
1: punctuation.definition.string.end.known_hosts
+
2: punctuation.separator.sequence.known_hosts
+
3: meta.number.integer.decimal.known_hosts constant.numeric.port-number.known_hosts
+
pop: 1
+
- include: operator-exclamation
+
- include: ip-addresses
+
- match: ''
+
push:
+
- meta_scope: meta.string.host.known_hosts string.quoted.other.known_hosts
+
- match: (?=,|\])
+
pop: 1
+
- include: wildcards
+
- include: punctuation-dot-sequence
+
- include: ip-addresses
+
- match: (\|)(\d+)(\|)({{base64_char}}{27}=)(\|)({{base64_char}}{27}=)
+
scope: meta.string.host.obfuscated.known_hosts
+
captures:
+
1: punctuation.definition.known_hosts
+
2: constant.numeric.integer.algorithm.known_hosts
+
3: punctuation.definition.known_hosts
+
4: string.unquoted.salt.known_hosts
+
5: punctuation.definition.known_hosts
+
6: string.unquoted.hash.known_hosts
+
- match: (?=\S)
+
push: hostname
+
hostname:
+
- meta_content_scope: meta.string.host.known_hosts string.unquoted.known_hosts
+
- match: (?=[,\[\s])
+
pop: 1
+
- include: wildcards
+
- include: punctuation-dot-sequence
+151
syntaxes/pem.sublime-syntax
···
+
%YAML 1.2
+
---
+
# Not strictly just PEM. Includes some other stuff, just to be helpful.
+
+
# https://www.sublimetext.com/docs/syntax.html
+
# https://datatracker.ietf.org/doc/html/rfc7468 (PEM)
+
# https://datatracker.ietf.org/doc/html/rfc4716 (OpenSSH)
+
# https://datatracker.ietf.org/doc/html/rfc4880 (OpenPGP)
+
+
name: Private Encrypted Mail (PEM) Key
+
scope: source.pem
+
version: 2
+
extends: SSH Common.sublime-syntax
+
+
file_extensions:
+
- pem
+
+
hidden_file_extensions:
+
- cer
+
- cert
+
- crt
+
- id_dsa
+
- id_ed25519
+
- id_ed448
+
- id_eddsa
+
- id_rsa
+
+
first_line_match: |-
+
^(?x:
+
(-{4}[ -])
+
BEGIN [ ]
+
( (?:[0-9A-Z -]+[ ])? (?: PUBLIC | PRIVATE ) [ ] KEY
+
| (?:[0-9A-Z -]+[ ])? CERTIFICATE (?:[ ] REQUEST )?
+
| (?:[0-9A-Z -]+[ ])? PARAMETERS
+
| X509 [ ] CRL
+
| PKCS7
+
| PKCS [ ] \#7 [ ] SIGNED [ ] DATA
+
| CMS
+
| PGP [ ] MESSAGE (?:,[ ] PART [ ] \d+(?:/\d+)?)?
+
| PGP [ ] (?: PUBLIC | PRIVATE ) [ ] KEY [ ] BLOCK
+
| PGP [ ] SIGNATURE
+
)
+
([ -]-{4})
+
)
+
+
contexts:
+
main:
+
- include: comments-number-sign
+
- match: |-
+
^(?x:
+
(-{4}[ -])
+
BEGIN [ ]
+
( (?:[0-9A-Z -]+[ ])? (?: PUBLIC | PRIVATE ) [ ] KEY
+
| (?:[0-9A-Z -]+[ ])? CERTIFICATE (?:[ ] REQUEST )?
+
| (?:[0-9A-Z -]+[ ])? PARAMETERS
+
| X509 [ ] CRL
+
| PKCS7
+
| PKCS [ ] \#7 [ ] SIGNED [ ] DATA
+
| CMS
+
| PGP [ ] MESSAGE (?:,[ ] PART [ ] \d+(?:/\d+)?)?
+
| PGP [ ] (?: PUBLIC | PRIVATE ) [ ] KEY [ ] BLOCK
+
| PGP [ ] SIGNATURE
+
)
+
([ -]-{4})
+
)
+
scope: punctuation.section.block.begin.pem
+
push: pem-key
+
- include: setext-headings
+
+
pem-key:
+
- meta_scope: meta.block.pem
+
- match: ^\1END \2\3
+
scope: punctuation.section.block.end.pem
+
pop: 1
+
- include: comments-number-sign
+
- match: ^{{base64_char}}{1,100}(={0,3})?$
+
scope: string.unquoted.pem
+
captures:
+
1: punctuation.definition.string.end.pem
+
- include: headers
+
+
headers:
+
- match: ^(?i:(Comment))(:)
+
captures:
+
1: keyword.other.comment.pem
+
2: punctuation.separator.key-value.pem
+
push:
+
- meta_content_scope: comment.line.pem
+
- include: header-end
+
- match: ^((x-)?[\w-]+)(:)
+
captures:
+
1: meta.mapping.key.pem keyword.other.pem
+
2: variable.annotation.pem
+
3: punctuation.separator.key-value.pem
+
push: header-value
+
+
header-value:
+
- meta_scope: meta.mapping.pem
+
- meta_content_scope: meta.mapping.value.pem
+
- include: header-end
+
- include: punctuation-comma-sequence
+
- match: =
+
scope: punctuation.separator.key-value.pem
+
- match: '\b(?x: ENCRYPTED | MIC-ONLY | MIC-CLEAR )\b'
+
scope: storage.modifier.pem
+
- match: |-
+
\b(?x:
+
( AES-(?:128|256)-CBC
+
| DES-(?:EDE3-)?CBC
+
)\b
+
( (,) .+ )?
+
)
+
captures:
+
1: meta.function-call.identifier.pem
+
support.function.cipher.ssh.crypto
+
2: meta.function-call.arguments.pem
+
3: punctuation.section.arguments.begin.pem
+
+
header-end:
+
- match: \\\r?\n
+
scope: punctuation.separator.continuation.line.pem
+
push:
+
- match: ^
+
pop: 1
+
- match: (?=$)
+
pop: 1
+
+
setext-headings:
+
- match: ^(?:=+|(?=\S))
+
branch_point: maybe-heading
+
branch:
+
- setext-heading
+
- not-heading
+
+
setext-heading:
+
- meta_scope: markup.heading.pem
+
- meta_content_scope: entity.name.section.pem
+
- match: ^(={5,})[ \t]*$(\n?)
+
captures:
+
1: punctuation.definition.heading.setext.pem
+
2: meta.whitespace.newline.pem
+
pop: 1
+
- match: ^(?!=+)$
+
fail: maybe-heading
+
+
not-heading:
+
- match: ''
+
pop: 1
+
+
variables:
+
base64_char: '[a-zA-Z0-9+/]'
+288
syntaxes/ssh-common.sublime-syntax
···
+
%YAML 1.2
+
---
+
# This file is some kind of internal library which is used to store
+
# common rules which are used by the visible syntax files.
+
name: SSH Common
+
scope: text.ssh.common
+
version: 2
+
hidden: true
+
+
contexts:
+
main:
+
- include: comments-number-sign
+
+
###[ COMMENTS ]################################################################
+
+
comments:
+
- include: comments-number-sign
+
- include: comments-semicolon
+
+
comments-number-sign:
+
- match: ^\s*(#+)
+
captures:
+
1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.number-sign.ssh.common
+
- match: \n
+
scope: comment.line.number-sign.ssh.common
+
pop: true
+
+
comments-semicolon:
+
- match: ^\s*(;+)
+
captures:
+
1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.semi-colon.ssh.common
+
- include: pop-nl
+
+
###[ COMPONENTS ]##############################################################
+
+
operator-exclamation:
+
- match: '!'
+
scope: keyword.operator.logical.ssh.common
+
+
wildcards:
+
- match: \*
+
scope: constant.other.wildcard.asterisk.ssh.common
+
- match: \?
+
scope: constant.other.wildcard.questionmark.ssh.common
+
+
punctuation-comma-sequence:
+
- match: ','
+
scope: punctuation.separator.sequence.ssh.common
+
+
punctuation-dot-sequence:
+
- match: \.
+
scope: punctuation.separator.sequence.ssh.common
+
+
punctuation-at:
+
- match: '@'
+
scope: punctuation.separator.sequence.ssh.common
+
+
ssh-fingerprint:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
+
ssh-fingerprint-with-label:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
push: expect-fingerprint-label
+
+
expect-fingerprint-label:
+
- include: pop-before-nl
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.annotation.identifier.ssh.common
+
string.unquoted.ssh.common
+
- match: '(?=[ \t]*$)'
+
pop: 1
+
- include: punctuation-at
+
+
time-values:
+
# https://man.openbsd.org/sshd_config.5#TIME_FORMATS
+
# seconds, minutes, hours, days, weeks
+
- match: \b(?=[\dsmhdw]*\d[smhdw][\s,"])
+
push:
+
- meta_scope: meta.constant.time.ssh.common
+
meta.number.integer.decimal.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: (\d+)([smhdw])
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
+
bytes-values:
+
- match: \b(\d+)([KMG])(?=[\s,"])
+
scope: meta.constant.bytes.ssh.common
+
meta.number.integer.other.ssh.common
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
+
mac-addresses:
+
- match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2})
+
scope: entity.name.constant.mac-address.ssh.common
+
+
ipv4:
+
- match: '\b{{ipv4}}\b'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
+
ipv6:
+
- match: '{{ipv6}}'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
+
ipv6-square-bracket:
+
- match: (\[){{ipv6}}(\])
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
captures:
+
1: punctuation.definition.constant.begin.ssh.common
+
2: punctuation.definition.constant.end.ssh.common
+
+
ip-addresses:
+
- include: ipv6
+
- include: ipv4
+
+
ipv4-with-cidr:
+
- match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
+
ipv6-with-cidr:
+
- match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)?
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
+
ip-addresses-with-cidr:
+
- include: ipv6-with-cidr
+
- include: ipv4-with-cidr
+
+
port-numbers:
+
- match: \b{{zero_to_65535}}(?![\w:])
+
scope: meta.number.integer.decimal.ssh.common
+
constant.numeric.port-number.ssh.common
+
+
match-all:
+
- match: '\b(?xi: all )\b'
+
scope: constant.language.boolean.true.ssh.common
+
+
none:
+
- match: \bnone\b
+
scope: constant.language.null.ssh.common
+
+
any:
+
- match: \bany\b
+
scope: constant.language.set.ssh.common
+
+
boolean:
+
- match: \byes\b
+
scope: constant.language.boolean.true.ssh.common
+
- match: \bno\b
+
scope: constant.language.boolean.false.ssh.common
+
+
boolean-with-typing:
+
- include: boolean
+
# Consume while typing as well, but unscoped
+
- match: \b(?:ye?|n)\b
+
+
log-level:
+
- match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b'
+
scope: constant.language.log-level.ssh.common
+
+
possibly-quoted-value:
+
- meta_content_scope: meta.mapping.value.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_scope: string.quoted.double.ssh.common
+
- match: (")(?:\s*(\S.*))?
+
captures:
+
1: punctuation.definition.string.end.ssh.common
+
2: invalid.illegal.ssh.common
+
pop: 1
+
- match: \n|$
+
scope: invalid.illegal.unclosed-string.ssh.common
+
pop: 2
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- include: pop-before-nl
+
- include: pop-nl
+
+
string-patterns:
+
# https://man7.org/linux/man-pages/man5/ssh_config.5.html#PATTERNS
+
# https://man.openbsd.org/ssh_config.5#PATTERNS
+
# https://man7.org/linux/man-pages/man5/sshd_config.5.html#PATTERNS
+
# https://man.openbsd.org/sshd_config.5#PATTERNS
+
- include: punctuation-comma-sequence
+
- include: operator-exclamation
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_content_scope: string.quoted.double.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.end.ssh.common
+
pop: 1
+
- include: wildcards
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- match: (?=[,!\s])
+
pop: 1
+
- include: wildcards
+
+
paths:
+
# This is just heuristic. Expect failures.
+
- match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?)
+
push:
+
- meta_scope: meta.path.ssh.common
+
entity.name.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: ~[\w\-.]*
+
scope: variable.language.home.ssh.common
+
- match: (/)(?:(\.{1,2})(?=/)|\.(?!/))?
+
captures:
+
1: punctuation.separator.path.ssh.common
+
2: constant.other.placeholder.ssh.common
+
- match: \.(?=[\w*?%])
+
scope: punctuation.separator.sequence.ssh.common
+
- include: wildcards
+
- include: tokens
+
- include: environment-variables
+
+
none-command-values:
+
- match: \s*(none)\b[ \t]*$
+
captures:
+
1: constant.language.null.ssh.common
+
- match: \s*((")(none)("))[ \t]*$
+
captures:
+
1: string.quoted.double.ssh.common
+
2: punctuation.definition.string.begin.ssh.common
+
3: constant.language.null.ssh.common
+
4: punctuation.definition.string.end.ssh.common
+
+
tokens: []
+
environment-variables: []
+
+
###[ PROTOTYPE ]###############################################################
+
+
pop-nl:
+
- match: \n
+
pop: 1
+
+
pop-before-nl:
+
- match: (?=\n)
+
pop: 1
+
+
###############################################################################
+
+
variables:
+
base64_char: '[a-zA-Z0-9+/]'
+
ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3})
+
# ipv4_basic: (?:(?:\d{1,3}\.){3}\d{1,3})
+
# ipv6_basic: (?i:(?:[a-f0-9:]+:+)+[a-f0-9]+)
+
zero_to_32: (?:3[0-2]|[12][0-9]|[0-9])
+
zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9])
+
zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9])
+
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+
ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}})
+
ipv6: |-
+
(?xi:
+
(?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses)
+
|(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address)
+
|(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,}) # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index)
+
|(?:(?:[0-9a-f]{1,4}:){7,7} [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8
+
| (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6}) # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8
+
|(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}) # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8
+
|(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}) # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8
+
|(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}) # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8
+
|(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}) # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8
+
|(?:(?:[0-9a-f]{1,4}:){1,6} :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8
+
|(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: 1:2:3:4:5:6:7::
+
|(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::
+
)
+556
syntaxes/ssh-config.sublime-syntax
···
+
%YAML 1.2
+
---
+
# Standalone version of ssh-config.sublime-syntax
+
# Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax
+
+
name: SSH Config
+
scope: source.ssh_config
+
version: 2
+
file_extensions:
+
- ssh_config
+
variables:
+
base64_char: '[a-zA-Z0-9+/]'
+
ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3})
+
zero_to_32: (?:3[0-2]|[12][0-9]|[0-9])
+
zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9])
+
zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9])
+
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+
ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}})
+
ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\
+
\ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\
+
\ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \
+
\ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \
+
\ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\
+
\ # fe80::7:8%eth0 fe80::7:8%1 \
+
\ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\
+
\ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\
+
\ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\
+
\ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\
+
\ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\
+
\ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\
+
\ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\
+
\ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\
+
\ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \
+
\ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \
+
\ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)"
+
tokens_standard: (?:%[%CdhikLlnpru])
+
tokens_knownhosts: (?:{{tokens_standard}}|%[%fHIKt])
+
tokens_hostname: (?:%[%h])
+
tokens_proxycommand: (?:%[%hnpr])
+
tokens_all: (?:{{tokens_knownhosts}}|%T)
+
tokens_localcommand: '{{tokens_all}}'
+
match_parameters: "\\b(?xi:\n all | canonical | final | exec | localnetwork | host\
+
\ | originalhost\n| tagged | command | user | localuser | version | sessiontype\n\
+
)\\b"
+
contexts:
+
main:
+
- include: comments
+
- include: host-block
+
- include: match
+
- include: naked-parameters
+
comments:
+
- include: comments-number-sign
+
- include: comments-semicolon
+
comments-number-sign:
+
- match: ^\s*(#+)
+
captures:
+
1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.number-sign.ssh.common
+
- match: \n
+
scope: comment.line.number-sign.ssh.common
+
pop: true
+
comments-semicolon:
+
- match: ^\s*(;+)
+
captures:
+
1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.semi-colon.ssh.common
+
- include: pop-nl
+
operator-exclamation:
+
- match: '!'
+
scope: keyword.operator.logical.ssh.common
+
wildcards:
+
- match: \*
+
scope: constant.other.wildcard.asterisk.ssh.common
+
- match: \?
+
scope: constant.other.wildcard.questionmark.ssh.common
+
punctuation-comma-sequence:
+
- match: ','
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-dot-sequence:
+
- match: \.
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-at:
+
- match: '@'
+
scope: punctuation.separator.sequence.ssh.common
+
ssh-fingerprint:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
ssh-fingerprint-with-label:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
push: expect-fingerprint-label
+
expect-fingerprint-label:
+
- include: pop-before-nl
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common
+
- match: (?=[ \t]*$)
+
pop: 1
+
- include: punctuation-at
+
time-values:
+
- match: \b(?=[\dsmhdw]*\d[smhdw][\s,"])
+
push:
+
- meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: (\d+)([smhdw])
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
bytes-values:
+
- match: \b(\d+)([KMG])(?=[\s,"])
+
scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
mac-addresses:
+
- match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2})
+
scope: entity.name.constant.mac-address.ssh.common
+
ipv4:
+
- match: \b{{ipv4}}\b
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
ipv6:
+
- match: '{{ipv6}}'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
ipv6-square-bracket:
+
- match: (\[){{ipv6}}(\])
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
captures:
+
1: punctuation.definition.constant.begin.ssh.common
+
2: punctuation.definition.constant.end.ssh.common
+
ip-addresses:
+
- include: ipv6
+
- include: ipv4
+
ipv4-with-cidr:
+
- match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ipv6-with-cidr:
+
- match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)?
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ip-addresses-with-cidr:
+
- include: ipv6-with-cidr
+
- include: ipv4-with-cidr
+
port-numbers:
+
- match: \b{{zero_to_65535}}(?![\w:])
+
scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common
+
match-all:
+
- match: '\b(?xi: all )\b'
+
scope: constant.language.boolean.true.ssh.common
+
none:
+
- match: \bnone\b
+
scope: constant.language.null.ssh.common
+
any:
+
- match: \bany\b
+
scope: constant.language.set.ssh.common
+
boolean:
+
- match: \byes\b
+
scope: constant.language.boolean.true.ssh.common
+
- match: \bno\b
+
scope: constant.language.boolean.false.ssh.common
+
boolean-with-typing:
+
- include: boolean
+
- match: \b(?:ye?|n)\b
+
log-level:
+
- match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b'
+
scope: constant.language.log-level.ssh.common
+
possibly-quoted-value:
+
- meta_content_scope: meta.mapping.value.ssh_config
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh_config
+
push:
+
- meta_scope: string.quoted.double.ssh_config
+
- match: (")(?:\s*(\S.*))?
+
captures:
+
1: punctuation.definition.string.end.ssh_config
+
2: invalid.illegal.ssh_config
+
pop: 1
+
- match: \n|$
+
scope: invalid.illegal.unclosed-string.ssh_config
+
pop: 2
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh_config
+
- include: pop-before-nl
+
- include: pop-nl
+
string-patterns:
+
- include: punctuation-comma-sequence
+
- include: operator-exclamation
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_content_scope: string.quoted.double.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.end.ssh.common
+
pop: 1
+
- include: wildcards
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- match: (?=[,!\s])
+
pop: 1
+
- include: wildcards
+
paths:
+
- match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?)
+
push:
+
- meta_scope: meta.path.ssh.common entity.name.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: ~[\w\-.]*
+
scope: variable.language.home.ssh.common
+
- match: (/)(?:(\.{1,2})(?=/)|\.(?!/))?
+
captures:
+
1: punctuation.separator.path.ssh.common
+
2: constant.other.placeholder.ssh.common
+
- match: \.(?=[\w*?%])
+
scope: punctuation.separator.sequence.ssh.common
+
- include: wildcards
+
- include: tokens
+
- include: environment-variables
+
none-command-values:
+
- match: \s*(none)\b[ \t]*$
+
captures:
+
1: constant.language.null.ssh.common
+
- match: \s*((")(none)("))[ \t]*$
+
captures:
+
1: string.quoted.double.ssh.common
+
2: punctuation.definition.string.begin.ssh.common
+
3: constant.language.null.ssh.common
+
4: punctuation.definition.string.end.ssh.common
+
tokens:
+
- match: '%%'
+
scope: constant.character.escape.ssh_config
+
- match: '{{tokens_standard}}'
+
scope: constant.other.placeholder.ssh_config
+
environment-variables:
+
- include: scope:source.shell#parameter-expansions
+
pop-nl:
+
- match: \n
+
pop: 1
+
pop-before-nl:
+
- match: (?=\n)
+
pop: 1
+
ssh-ciphers:
+
- match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.cipher.ssh.crypto
+
- match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"])
+
scope: invalid.deprecated.cipher.ssh.crypto
+
ssh-kex-algorithms:
+
- match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"])
+
scope: support.function.kex-algorithm.ssh.crypto
+
- match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"])
+
scope: invalid.deprecated.kex-algorithm.ssh.crypto
+
ssh-key-types:
+
- match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"])
+
scope: support.type.key-type.ssh.crypto
+
- match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"])
+
scope: invalid.deprecated.key-type.ssh.crypto
+
ssh-mac-algorithms:
+
- match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.mac-algorithm.ssh.crypto
+
- match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"])
+
scope: invalid.deprecated.mac-algorithm.ssh.crypto
+
parameters:
+
- include: comments
+
- include: parameter-hostname
+
- include: parameter-localcommand
+
- include: parameter-proxycommand
+
- include: parameter-proxyjump
+
- include: parameter-knownhostscommand
+
- include: parameter-with-boolean-values
+
- include: parameter-with-boolean-values-plus-ask
+
- include: parameter-generic
+
pop-before-match-parameter:
+
- include: pop-before-nl
+
- match: (?=\s*{{match_parameters}})
+
pop: 1
+
pop-before-next-host:
+
- match: '(?=^\s*(?xi: Host | Match )\b)'
+
pop: 1
+
naked-parameters:
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.block.naked.ssh_config
+
- include: pop-before-next-host
+
- include: parameters
+
host-pattern:
+
- meta_content_scope: entity.name.label.host-alias.ssh_config
+
- include: punctuation-dot-sequence
+
- include: wildcards
+
- match: (?=\s|,)
+
pop: 1
+
host-patterns:
+
- include: operator-exclamation
+
- include: punctuation-comma-sequence
+
- match: (?=\S)
+
push: host-pattern
+
host-block:
+
- match: ^\s*((?i:Host))\b
+
captures:
+
1: keyword.declaration.host-alias.ssh_config
+
set: host-aliases
+
host-aliases:
+
- meta_scope: meta.block.host.ssh_config
+
- match: (?=\n)
+
set: host-body
+
- include: host-patterns
+
host-body:
+
- meta_scope: meta.block.host.ssh_config
+
- include: pop-before-next-host
+
- include: parameters
+
match:
+
- match: ^\s*((?i:Match))\b
+
captures:
+
1: keyword.control.conditional.ssh_config
+
set: match-conditions
+
match-conditions:
+
- meta_scope: meta.block.match.ssh_config
+
- meta_content_scope: meta.statement.conditional.ssh_config
+
- match: \n
+
set: match-body
+
- include: operator-exclamation
+
- include: match-all
+
- match: '\b(?xi: canonical | final )\b'
+
scope: keyword.other.ssh_config
+
- match: '\b((?xi: exec ))\b\s*(\")'
+
captures:
+
1: keyword.other.ssh_config
+
2: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config
+
escape: (?<!\\)\"(?=\s*(?:#.*)?)
+
escape_captures:
+
0: meta.block.match.ssh_config meta.statement.conditional.ssh_config string.quoted.double.ssh_config
+
punctuation.definition.string.end.ssh_config
+
embed_scope: string.quoted.double.ssh_config
+
embed: scope:source.shell.embedded.ssh
+
- match: '\b((?xi: exec ))\b[ \t]+'
+
captures:
+
1: keyword.other.ssh_config
+
embed: scope:source.shell.embedded.ssh
+
escape: (?=\s*(?:{{match_parameters}}|$))
+
- match: '\b(?xi: (?:original)? host )\b'
+
scope: keyword.other.ssh_config
+
push:
+
- include: pop-before-match-parameter
+
- include: punctuation-comma-sequence
+
- include: host-patterns
+
- match: '\b(?xi: (?:local)? user | tagged | version | command | sessiontype )\b'
+
scope: keyword.other.ssh_config
+
push:
+
- include: pop-before-match-parameter
+
- include: string-patterns
+
- match: '\b(?xi: localnetwork )\b'
+
scope: keyword.other.ssh_config
+
push:
+
- include: pop-before-match-parameter
+
- include: punctuation-comma-sequence
+
- include: ip-addresses-with-cidr
+
match-body:
+
- meta_content_scope: meta.block.match.ssh_config
+
- include: pop-before-next-host
+
- include: parameters
+
parameter-hostname:
+
- match: ^\s*((?i:HostName))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.declaration.host.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
push:
+
- meta_content_scope: meta.string.host.ssh_config
+
- include: pop-nl
+
- include: ip-addresses
+
- match: (?=\S+)
+
push:
+
- meta_content_scope: string.unquoted.hostname.ssh_config
+
- include: pop-before-nl
+
- include: punctuation-dot-sequence
+
- match: '{{tokens_hostname}}'
+
scope: constant.character.escape.ssh_config
+
- match: \s+(\S+)
+
captures:
+
1: invalid.illegal.ssh_config
+
parameter-proxyjump:
+
- match: ^\s*((?i:ProxyJump))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
push:
+
- meta_content_scope: meta.mapping.value.ssh_config
+
- include: pop-nl
+
- include: none-command-values
+
- match: '"'
+
scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config
+
escape: (")|(?=\n|$)
+
escape_captures:
+
1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config
+
embed_scope: string.quoted.double.ssh_config
+
embed: proxyjump-values
+
- match: (?=\S)
+
escape: (?=\n|$)
+
embed: proxyjump-values
+
proxyjump-values:
+
- include: ip-addresses
+
- include: punctuation-comma-sequence
+
- match: (?=[\w%]+@)
+
push:
+
- meta_content_scope: meta.string.user.ssh_config string.unquoted.ssh_config
+
- match: '%%'
+
scope: constant.character.escape.ssh_config
+
- match: '{{tokens_proxycommand}}'
+
scope: constant.other.placeholder.ssh_config
+
- match: '@'
+
scope: punctuation.separator.ssh_config
+
pop: 1
+
- match: :(?={{zero_to_65535}}(?![\w:]))
+
scope: punctuation.separator.ssh_config
+
push:
+
- match: (?=\D)
+
pop: 1
+
- include: port-numbers
+
- match: (?=\S+)
+
push:
+
- meta_content_scope: string.unquoted.hostname.ssh_config
+
- match: (?=[\s,:"])
+
pop: 1
+
- include: punctuation-dot-sequence
+
- match: '%%'
+
scope: constant.character.escape.ssh_config
+
- match: '{{tokens_proxycommand}}'
+
scope: constant.other.placeholder.ssh_config
+
parameter-proxycommand:
+
- match: ^\s*((?i:ProxyCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
push:
+
- meta_content_scope: meta.mapping.value.ssh_config
+
- include: pop-nl
+
- include: none-command-values
+
- match: '"'
+
scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config
+
escape: (")|(?=\n|$)
+
escape_captures:
+
1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config
+
embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.proxycommand
+
embed: scope:source.shell.embedded.ssh.proxycommand
+
- match: (?=\S)
+
escape: (?=\n|$)
+
embed: scope:source.shell.embedded.ssh.proxycommand
+
parameter-localcommand:
+
- match: ^\s*((?i:LocalCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
push:
+
- meta_content_scope: meta.mapping.value.ssh_config
+
- include: pop-nl
+
- include: none-command-values
+
- match: '"'
+
scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config
+
escape: (")|(?=$)
+
escape_captures:
+
1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config
+
embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.localcommand
+
embed: scope:source.shell.embedded.ssh.localcommand
+
- match: (?=\S)
+
escape: (?=$)
+
embed: scope:source.shell.embedded.ssh.localcommand
+
parameter-knownhostscommand:
+
- match: ^\s*((?i:KnownHostsCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
push:
+
- meta_content_scope: meta.mapping.value.ssh_config
+
- include: pop-nl
+
- include: none-command-values
+
- match: '"'
+
scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config
+
escape: (")|(?=$)
+
escape_captures:
+
1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config
+
embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.knownhostscommand
+
embed: scope:source.shell.embedded.ssh.knownhostscommand
+
- match: (?=\S)
+
escape: (?=$)
+
embed: scope:source.shell.embedded.ssh.knownhostscommand
+
parameter-with-boolean-values:
+
- match: "(?xi:\n ^\\s*\n (\n (?: Pubkey | HostBased | Password | ChallengeResponse\n\
+
\ | KbdInteractive | (?:Rhosts)? RSA\n ) Authentication # Auth\n | ForwardAgent\
+
\ | ForwardX11(?:Trusted)? | ClearAllForwardings\n | ExitOnForwardFailure #\
+
\ Fwds\n | BatchMode | CanonicalizeFallbackLocal | CheckHostIP | Compression\n\
+
\ | EnableEscapeCommandLine | EnableSSHKeySign\n | ForkAfterAuthentication\
+
\ | GatewayPorts | HashKnownHosts\n | IdentitiesOnly | NoHostAuthenticationForLocalhost\n\
+
\ | PermitLocalCommand | ProxyUseFdpass | RefuseConnection | StdinNull\n |\
+
\ StreamLocalBindUnlink | TCPKeepAlive\n | UseKeychain | UsePrivilegedPort\
+
\ | VisualHostKey\n | GSSAPI (?:\n Authentication | KeyExchange | DelegateCredentials\n\
+
\ | RenewalForcesRekey | TrustDNS ) # GSSAPI\n )\n \\b[ \\t]*(=)?\n)"
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
with_prototype:
+
- include: boolean-with-typing
+
- match: '[^"\s]+'
+
scope: invalid.illegal.sshd_config
+
push: possibly-quoted-value
+
parameter-with-boolean-values-plus-ask:
+
- match: "(?xi:\n ^\\s*\n ( ControlMaster | StrictHostKeyChecking | UpdateHostKeys\n\
+
\ | VerifyHostKeyDNS\n )\n \\b[ \\t]*(=)?\n)"
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
with_prototype:
+
- include: boolean-with-typing
+
- include: ask
+
- match: \bas?\b
+
- match: '[^"\s]+'
+
scope: invalid.illegal.sshd_config
+
push: possibly-quoted-value
+
parameter-generic:
+
- match: ^\s*([a-zA-Z1]+)\b[ \t]*(=)?
+
captures:
+
1: meta.mapping.key.ssh_config keyword.other.ssh_config
+
2: keyword.operator.assignment.ssh_config
+
with_prototype:
+
- include: generic-parameter-values
+
push: possibly-quoted-value
+
ask:
+
- match: \bask\b
+
scope: constant.language.ssh_config
+
generic-parameter-values:
+
- include: environment-variables
+
- include: none
+
- include: boolean
+
- include: any
+
- include: ask
+
- include: tokens
+
- include: wildcards
+
- include: operator-exclamation
+
- include: punctuation-comma-sequence
+
- include: ssh-key-types
+
- include: ssh-ciphers
+
- include: ssh-kex-algorithms
+
- include: ssh-mac-algorithms
+
- include: ipv6-square-bracket
+
- include: ip-addresses-with-cidr
+
- include: time-values
+
- include: bytes-values
+
- include: log-level
+
- include: paths
+
- match: \b\d+(?=[\s,"])
+
scope: meta.number.integer.ssh_config constant.numeric.value.ssh_config
+48
syntaxes/ssh-crypto.sublime-syntax
···
+
%YAML 1.2
+
---
+
contexts:
+
main:
+
- include: comments
+
- match: '^key type:'
+
push:
+
- include: pop-before-nl
+
- include: ssh-key-types
+
- match: '^cipher:'
+
push:
+
- include: pop-before-nl
+
- include: ssh-ciphers
+
- match: '^kex:'
+
push:
+
- include: pop-before-nl
+
- include: ssh-kex-algorithms
+
- match: '^mac:'
+
push:
+
- include: pop-before-nl
+
- include: ssh-mac-algorithms
+
ssh-ciphers:
+
- match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.cipher.ssh.crypto
+
- match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"])
+
scope: invalid.deprecated.cipher.ssh.crypto
+
ssh-kex-algorithms:
+
- match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"])
+
scope: support.function.kex-algorithm.ssh.crypto
+
- match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"])
+
scope: invalid.deprecated.kex-algorithm.ssh.crypto
+
ssh-key-types:
+
- match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"])
+
scope: support.type.key-type.ssh.crypto
+
- match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"])
+
scope: invalid.deprecated.key-type.ssh.crypto
+
ssh-mac-algorithms:
+
- match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.mac-algorithm.ssh.crypto
+
- match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"])
+
scope: invalid.deprecated.mac-algorithm.ssh.crypto
+
extends: SSH Common.sublime-syntax
+
hidden: true
+
hidden_file_extensions:
+
- syntax_test_crypto
+
name: SSH Crypto
+
scope: text.ssh.crypto
+
version: 2
+496
syntaxes/sshd-config.sublime-syntax
···
+
%YAML 1.2
+
---
+
# Standalone version of sshd-config.sublime-syntax
+
# Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax
+
+
name: SSHD Config
+
scope: source.sshd_config
+
version: 2
+
file_extensions:
+
- sshd_config
+
variables:
+
base64_char: '[a-zA-Z0-9+/]'
+
ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3})
+
zero_to_32: (?:3[0-2]|[12][0-9]|[0-9])
+
zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9])
+
zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9])
+
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+
ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}})
+
ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\
+
\ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\
+
\ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \
+
\ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \
+
\ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\
+
\ # fe80::7:8%eth0 fe80::7:8%1 \
+
\ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\
+
\ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\
+
\ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\
+
\ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\
+
\ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\
+
\ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\
+
\ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\
+
\ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\
+
\ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \
+
\ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \
+
\ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)"
+
all_parameters: "\\b(?xi:\n AcceptEnv | AddressFamily\n | Allow (?: AgentForwarding\
+
\ | Groups | StreamLocalForwarding\n | TcpForwarding | Users)\n | AuthenticationMethods\n\
+
\ | Authorized (?: Keys | Principals )(?: Command | CommandUser | File )\n |\
+
\ Banner\n | CASignatureAlgorithms | ChallengeResponseAuthentication\n | ChannelTimeout\
+
\ | ChrootDirectory | Ciphers | ClientAliveCountMax\n | ClientAliveInterval |\
+
\ Compression\n | DenyGroups | DenyUsers | DisableForwarding\n | ExposeAuthInfo\n\
+
\ | FingerprintHash | ForceCommand\n | GatewayPorts | GSSAPIAuthentication |\
+
\ GSSAPICleanupCredentials\n | GSSAPIStrictAcceptorCheck\n | Hostbased (?: AcceptedAlgorithms\
+
\ | AcceptedKeyTypes | Authentication\n | UsesNameFromPacketOnly\
+
\ )\n | HostCertificate | HostKey | HostKeyAgent | HostKeyAlgorithms\n | IgnoreRhosts\
+
\ | IgnoreUserKnownHosts | Include | IPQoS\n | KbdInteractiveAuthentication\n\
+
\ | Kerberos (?: Authentication | GetAFSToken | OrLocalPasswd\n |\
+
\ TicketCleanup )\n | KexAlgorithms | KeyRegenerationInterval\n | ListenAddress\
+
\ | LoginGraceTime | LogLevel | LogVerbose\n | MACs | Match | MaxAuthTries |\
+
\ MaxSessions | MaxStartups | ModuliFile\n | PasswordAuthentication | PAMServiceName\n\
+
\ | Permit (?: EmptyPasswords | Listen | Open | RootLogin | TTY | Tunnel\n \
+
\ | UserEnvironment | UserRC )\n | PerSource (?: MaxStartups | NetBlockSize\
+
\ | Penalties\n | PenaltyExemptList )\n | PidFile | Port | PrintLastLog\
+
\ | PrintMotd | Protocol\n | Pubkey (?: AcceptedAlgorithms | AcceptedKeyTypes\
+
\ | AuthOptions\n | Authentication )\n | RefuseConnection | RekeyLimit\
+
\ | RequiredRSASize | RevokedKeys | RDomain\n | RhostsRSAAuthentication | RSAAuthentication\n\
+
\ | SecurityKeyProvider | ServerKeyBits | SetEnv | ShowPatchLevel\n # SshdAuthPath\
+
\ and SshSessionPath are just for tests\n | StreamLocalBindMask | StreamLocalBindUnlink\n\
+
\ | StrictModes | Subsystem | SyslogFacility\n | TCPKeepAlive | TrustedUserCAKeys\n\
+
\ | UnusedConnectionTimeout | UseDNS | UseLogin | UsePAM\n | UsePrivilegeSeparation\n\
+
\ | VersionAddendum\n | X11DisplayOffset | X11Forwarding | X11UseLocalhost |\
+
\ XAuthLocation\n)\\b"
+
parameters_boolean: "\\b(?xi:\n AllowAgentForwarding\n | ChallengeResponseAuthentication\
+
\ | Compression\n | ExposeAuthInfo\n | GSSAPIAuthentication | GSSAPICleanupCredentials\n\
+
\ | GSSAPIStrictAcceptorCheck\n | HostbasedAuthentication | HostbasedUsesNameFromPacketOnly\n\
+
\ | IgnoreRhosts | IgnoreUserKnownHosts\n | KbdInteractiveAuthentication | KerberosAuthentication\n\
+
\ | KerberosGetAFSToken | KerberosOrLocalPasswd\n | KerberosTicketCleanup\n\
+
\ | PasswordAuthentication | PermitEmptyPasswords | PermitTTY\n | PermitUserEnvironment\
+
\ | PermitUserRC | PrintLastLog | PrintMotd\n | PubkeyAuthentication\n | RefuseConnection\n\
+
\ | StreamLocalBindUnlink | StrictModes\n | TCPKeepAlive\n | UseDNS | UsePAM\n\
+
\ | X11Forwarding | X11UseLocalhost\n)\\b"
+
contexts:
+
main:
+
- include: comments
+
- include: match
+
- include: parameters
+
comments:
+
- match: (#+)(?:\s*({{all_parameters}}))?
+
captures:
+
1: punctuation.definition.comment.sshd_config
+
2: meta.keyword.comment.sshd_config
+
push:
+
- meta_scope: comment.line.number-sign.sshd_config
+
- include: pop-nl
+
- match: (;+)(?:\s*({{all_parameters}}))?
+
captures:
+
1: punctuation.definition.comment.sshd_config
+
2: meta.keyword.comment.sshd_config
+
push:
+
- meta_scope: comment.line.semi-colon.sshd_config
+
- include: pop-nl
+
comments-number-sign:
+
- match: ^\s*(#+)
+
captures:
+
1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.number-sign.ssh.common
+
- match: \n
+
scope: comment.line.number-sign.ssh.common
+
pop: true
+
comments-semicolon:
+
- match: ^\s*(;+)
+
captures:
+
1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common
+
push:
+
- meta_content_scope: comment.line.semi-colon.ssh.common
+
- include: pop-nl
+
operator-exclamation:
+
- match: '!'
+
scope: keyword.operator.logical.ssh.common
+
wildcards:
+
- match: \*
+
scope: constant.other.wildcard.asterisk.ssh.common
+
- match: \?
+
scope: constant.other.wildcard.questionmark.ssh.common
+
punctuation-comma-sequence:
+
- match: ','
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-dot-sequence:
+
- match: \.
+
scope: punctuation.separator.sequence.ssh.common
+
punctuation-at:
+
- match: '@'
+
scope: punctuation.separator.sequence.ssh.common
+
ssh-fingerprint:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
ssh-fingerprint-with-label:
+
- match: '{{ssh_fingerprint}}'
+
scope: variable.other.fingerprint.ssh.common
+
push: expect-fingerprint-label
+
expect-fingerprint-label:
+
- include: pop-before-nl
+
- match: (?=\S)
+
push:
+
- meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common
+
- match: (?=[ \t]*$)
+
pop: 1
+
- include: punctuation-at
+
time-values:
+
- match: \b(?=[\dsmhdw]*\d[smhdw][\s,"])
+
push:
+
- meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: (\d+)([smhdw])
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
bytes-values:
+
- match: \b(\d+)([KMG])(?=[\s,"])
+
scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common
+
captures:
+
1: constant.numeric.value.ssh.common
+
2: constant.numeric.suffix.ssh.common
+
mac-addresses:
+
- match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2})
+
scope: entity.name.constant.mac-address.ssh.common
+
ipv4:
+
- match: \b{{ipv4}}\b
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
ipv6:
+
- match: '{{ipv6}}'
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
ipv6-square-bracket:
+
- match: (\[){{ipv6}}(\])
+
scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
captures:
+
1: punctuation.definition.constant.begin.ssh.common
+
2: punctuation.definition.constant.end.ssh.common
+
ip-addresses:
+
- include: ipv6
+
- include: ipv4
+
ipv4-with-cidr:
+
- match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ipv6-with-cidr:
+
- match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)?
+
captures:
+
1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common
+
2: punctuation.separator.sequence.ssh.common
+
3: constant.other.range.ssh.common
+
ip-addresses-with-cidr:
+
- include: ipv6-with-cidr
+
- include: ipv4-with-cidr
+
port-numbers:
+
- match: \b{{zero_to_65535}}(?![\w:])
+
scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common
+
match-all:
+
- match: '\b(?xi: all )\b'
+
scope: constant.language.boolean.true.ssh.common
+
none:
+
- match: \bnone\b
+
scope: constant.language.null.ssh.common
+
any:
+
- match: \bany\b
+
scope: constant.language.set.ssh.common
+
boolean:
+
- match: \byes\b
+
scope: constant.language.boolean.true.ssh.common
+
- match: \bno\b
+
scope: constant.language.boolean.false.ssh.common
+
boolean-with-typing:
+
- include: boolean
+
- match: \b(?:ye?|n)\b
+
log-level:
+
- match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b'
+
scope: constant.language.log-level.ssh.common
+
possibly-quoted-value:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- match: '"'
+
scope: punctuation.definition.string.begin.sshd_config
+
push:
+
- meta_scope: string.quoted.double.sshd_config
+
- match: (")(?:\s*(\S.*))?
+
captures:
+
1: punctuation.definition.string.end.sshd_config
+
2: invalid.illegal.sshd_config
+
pop: 1
+
- match: \n|$
+
scope: invalid.illegal.unclosed-string.sshd_config
+
pop: 2
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.sshd_config
+
- include: pop-before-nl
+
- include: pop-nl
+
string-patterns:
+
- include: punctuation-comma-sequence
+
- include: operator-exclamation
+
- match: '"'
+
scope: punctuation.definition.string.begin.ssh.common
+
push:
+
- meta_content_scope: string.quoted.double.ssh.common
+
- match: '"'
+
scope: punctuation.definition.string.end.ssh.common
+
pop: 1
+
- include: wildcards
+
- match: (?=\S)
+
push:
+
- meta_content_scope: string.unquoted.ssh.common
+
- match: (?=[,!\s])
+
pop: 1
+
- include: wildcards
+
paths:
+
- match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?)
+
push:
+
- meta_scope: meta.path.ssh.common entity.name.ssh.common
+
- match: (?=[\s,"])
+
pop: 1
+
- match: ~[\w\-.]*
+
scope: variable.language.home.ssh.common
+
- match: (/)(?:(\.{1,2})(?=/)|\.(?!/))?
+
captures:
+
1: punctuation.separator.path.ssh.common
+
2: constant.other.placeholder.ssh.common
+
- match: \.(?=[\w*?%])
+
scope: punctuation.separator.sequence.ssh.common
+
- include: wildcards
+
- include: tokens
+
- include: environment-variables
+
none-command-values:
+
- match: \s*(none)\b[ \t]*$
+
captures:
+
1: constant.language.null.ssh.common
+
- match: \s*((")(none)("))[ \t]*$
+
captures:
+
1: string.quoted.double.ssh.common
+
2: punctuation.definition.string.begin.ssh.common
+
3: constant.language.null.ssh.common
+
4: punctuation.definition.string.end.ssh.common
+
tokens:
+
- match: '%%'
+
scope: constant.character.escape.sshd_config
+
- match: '%[hUu]'
+
scope: constant.other.placeholder.sshd_config
+
environment-variables: []
+
pop-nl:
+
- match: \n
+
pop: 1
+
pop-before-nl:
+
- match: (?=\n)
+
pop: 1
+
ssh-ciphers:
+
- match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.cipher.ssh.crypto
+
- match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"])
+
scope: invalid.deprecated.cipher.ssh.crypto
+
ssh-kex-algorithms:
+
- match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"])
+
scope: support.function.kex-algorithm.ssh.crypto
+
- match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"])
+
scope: invalid.deprecated.kex-algorithm.ssh.crypto
+
ssh-key-types:
+
- match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"])
+
scope: support.type.key-type.ssh.crypto
+
- match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"])
+
scope: invalid.deprecated.key-type.ssh.crypto
+
ssh-mac-algorithms:
+
- match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"])
+
scope: support.function.mac-algorithm.ssh.crypto
+
- match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"])
+
scope: invalid.deprecated.mac-algorithm.ssh.crypto
+
parameters:
+
- include: comments
+
- include: parameter-forcecommand
+
- include: parameter-authorizedkeyscommand
+
- include: parameter-authorizedprincipalscommand
+
- include: parameter-path-with-tokens
+
- include: parameter-routingdomain
+
- include: parameter-with-boolean-values
+
- include: parameter-generic
+
pop-before-match-option:
+
- include: pop-before-nl
+
- match: '(?=\s*(?xi: all | user | group | host | (?:local)? address | localport
+
)\b)'
+
pop: 1
+
pop-before-next-match:
+
- match: (?=^\s*(?i:Match)\b)
+
pop: 1
+
match:
+
- match: ^\s*((?i:Match))\b
+
captures:
+
1: keyword.control.conditional.sshd_config
+
set: match-conditions
+
match-conditions:
+
- meta_scope: meta.block.match.sshd_config
+
- meta_content_scope: meta.statement.conditional.sshd_config
+
- match: \n
+
set: match-body
+
- include: operator-exclamation
+
- include: match-all
+
- match: '\b(?xi: invalid-user )\b'
+
scope: constant.language.null.sshd_config
+
- match: '\b(?xi: host )\b'
+
scope: meta.mapping.key.sshd_config keyword.other.sshd_config
+
with_prototype:
+
- include: punctuation-dot-sequence
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-before-match-option
+
- include: string-patterns
+
- match: '\b(?xi: user | group )\b'
+
scope: meta.mapping.key.sshd_config keyword.other.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-before-match-option
+
- include: string-patterns
+
- match: '\b(?xi: (?:local)? address )\b'
+
scope: meta.mapping.key.sshd_config keyword.other.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-before-match-option
+
- include: operator-exclamation
+
- include: wildcards
+
- include: punctuation-comma-sequence
+
- include: ip-addresses-with-cidr
+
- match: '\b(?xi: localport )\b'
+
scope: meta.mapping.key.sshd_config keyword.other.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-before-match-option
+
- include: port-numbers
+
- match: '\b(?xi: rdomain )\b'
+
scope: meta.mapping.key.sshd_config keyword.other.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-before-match-option
+
- match: \b{{zero_to_255}}\b
+
scope: meta.number.integer.decimal.sshd_config constant.numeric.value.sshd_config
+
match-body:
+
- meta_content_scope: meta.block.match.sshd_config
+
- include: pop-before-next-match
+
- include: parameters
+
parameter-forcecommand:
+
- match: ^\s*((?i:ForceCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-nl
+
- include: none-command-values
+
- match: '"'
+
scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config
+
escape: (")|(?=$)
+
escape_captures:
+
1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config
+
embed_scope: string.quoted.double.sshd_config
+
embed: scope:source.shell
+
- match: (?=\S)
+
escape: (?=$)
+
embed: scope:source.shell
+
parameter-authorizedkeyscommand:
+
- match: ^\s*((?i:AuthorizedKeysCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-nl
+
- match: '"'
+
scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config
+
escape: (")|(?=$)
+
escape_captures:
+
1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config
+
embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.authorizedkeyscommand
+
embed: scope:source.shell.embedded.ssh.authorizedkeyscommand
+
- match: (?=\S)
+
escape: (?=$)
+
embed: scope:source.shell.embedded.ssh.authorizedkeyscommand
+
parameter-authorizedprincipalscommand:
+
- match: ^\s*((?i:AuthorizedPrincipalsCommand))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
push:
+
- meta_content_scope: meta.mapping.value.sshd_config
+
- include: pop-nl
+
- match: '"'
+
scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config
+
escape: (")|(?=$)
+
escape_captures:
+
1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config
+
embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.authorizedprincipalscommand
+
embed: scope:source.shell.embedded.ssh.authorizedprincipalscommand
+
- match: (?=\S)
+
escape: (?=$)
+
embed: scope:source.shell.embedded.ssh.authorizedprincipalscommand
+
parameter-path-with-tokens:
+
- match: '^\s*((?ix: AuthorizedKeysFile | AuthorizedPrincipalsFile | ChrootDirectory
+
))\b\s*(=)?'
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
with_prototype:
+
- include: tokens
+
- include: none
+
- include: paths
+
push: possibly-quoted-value
+
parameter-routingdomain:
+
- match: ^\s*((?i:RoutingDomain))\b\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
with_prototype:
+
- match: '%D'
+
scope: constant.other.placeholder.sshd_config
+
- include: numeric-values
+
push: possibly-quoted-value
+
parameter-with-boolean-values:
+
- match: ^\s*({{parameters_boolean}})\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
with_prototype:
+
- include: boolean-with-typing
+
- match: '[^"\s]+'
+
scope: invalid.illegal.sshd_config
+
push: possibly-quoted-value
+
parameter-generic:
+
- match: ^\s*([a-zA-Z1]+)\b\s*(=)?
+
captures:
+
1: meta.mapping.key.sshd_config keyword.other.sshd_config
+
2: keyword.operator.assignment.sshd_config
+
with_prototype:
+
- include: generic-parameter-values
+
push: possibly-quoted-value
+
generic-parameter-values:
+
- include: boolean
+
- include: none
+
- include: any
+
- match: '\b(?xi: default )\b'
+
scope: constant.language.default.sshd_config
+
- include: ssh-key-types
+
- include: ssh-ciphers
+
- include: ssh-kex-algorithms
+
- include: ssh-mac-algorithms
+
- include: ipv6-square-bracket
+
- include: ip-addresses-with-cidr
+
- include: time-values
+
- include: bytes-values
+
- include: operator-exclamation
+
- include: wildcards
+
- include: punctuation-comma-sequence
+
- include: log-level
+
- include: paths
+
- include: numeric-values
+
- match: ':'
+
scope: punctuation.separator.sequence.sshd_config
+
numeric-values:
+
- match: \b\d+(?=[\s,:"])
+
scope: constant.numeric.sshd_config
+28 -22
templates/404.html
···
{% extends "base.html" %} {% block content %}
<div
-
style="
-
display: flex;
-
flex-direction: column;
-
justify-content: center;
-
align-items: center; /* Center vertically */
-
height: 100%; /* Adjust height as needed */
-
"
+
id="suggestions"
+
style="
+
display: flex;
+
flex-direction: column;
+
justify-content: center;
+
align-items: center; /* Center vertically */
+
height: 100%; /* Adjust height as needed */
+
"
>
-
<p><strong>I think you stumbled on something non existent :)</strong></p>
-
<p><i id="redirect">Redirecting you back home in 5</i></p>
+
<p><strong>I think you stumbled on something non existent :)</strong></p>
</div>
-
<script>
-
const link = document.getElementById("redirect");
+
{% set jsHash = get_hash(path="js/404-matcher.js", sha_type=256, base64=true) %}
+
<script
+
src="{{ get_url(path='js/404-matcher.js?' ~ jsHash, trailing_slash=false) | safe }}"
+
defer
+
></script>
-
// count down to redirect
-
let count = 5;
-
const interval = setInterval(() => {
-
count--;
-
link.innerText = `Redirecting you back home in ${count}`;
-
if (count === 0) {
-
clearInterval(interval);
-
window.location.href = "/";
-
}
-
}, 1000);
-
</script>
+
<!-- <script>
+
const link = document.getElementById("redirect");
+
+
// count down to redirect
+
let count = 5;
+
const interval = setInterval(() => {
+
count--;
+
link.innerText = `Redirecting you back home in ${count}`;
+
if (count === 0) {
+
clearInterval(interval);
+
window.location.href = "/";
+
}
+
}, 1000);
+
</script> -->
{% endblock content %}
+13 -18
templates/base.html
···
-
<!DOCTYPE html>
-
<html lang="{% if page %}{{ page.lang }}{% else %}{{ config.default_language }}{% endif %}">
-
<head>
-
{% include "head.html" %}
-
</head>
-
<body>
-
<header>
-
{% include "header.html" %}
-
</header>
-
<main>
-
{% block content %}
-
{% endblock content %}
-
</main>
-
<footer>
-
{% include "footer.html" %}
-
</footer>
-
</body>
-
</html>
+
<!doctype html>
+
<html
+
lang="{% if page %}{{ page.lang }}{% else %}{{ config.default_language }}{% endif %}"
+
>
+
<head>
+
{% include "head.html" %}
+
</head>
+
<body>
+
<header>{% include "header.html" %}</header>
+
<main>{% block content %} {% endblock content %}</main>
+
<footer>{% include "footer.html" %}</footer>
+
</body>
+
</html>
+47 -40
templates/blog-page.html
···
-
{% extends "base.html" %}
-
-
{% block content %}
+
{% extends "base.html" %} {% block content %}
<div><a href="..">..</a>/<span class="accent-data">{{ page.slug }}</span></div>
-
<time datetime="{{ page.date }}">Published on: <span class="accent-data">{{ page.date }}</span></time>
-
{% if config.extra.author and config.extra.display_author == true %}
-
<address rel="author">By <span class="accent-data">{{config.extra.author}}</span></address>
-
{% endif %}
-
<h1>{{ page.title }}</h1>
+
<article class="h-entry">
+
<a class="u-url" href="{{ page.permalink }}" style="display: none"> </a>
+
<time
+
datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S%z') }}"
+
class="dt-published"
+
>Published on:
+
<span class="accent-data"
+
>{{ page.date | split(pat="T") | first }}</span
+
></time
+
>
+
{% if config.extra.author and config.extra.display_author == true %}
+
<address rel="author">
+
By
+
<a
+
rel="author"
+
class="accent-data p-author h-card text-glow"
+
href="https://dunkirk.sh"
+
>{{config.extra.author}}</a
+
>
+
</address>
+
{% endif %}
+
<h1>{{ page.title }}</h1>
-
{% if page.toc and page.extra.toc %}
-
<h2>Table of contents</h2>
-
<ul>
-
{% for h1 in page.toc %}
-
<li>
-
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
-
{% if h1.children %}
+
{% if page.toc and page.extra["has_toc"] %}
+
<h2>Table of contents</h2>
<ul>
-
{% for h2 in h1.children %}
-
<li>
-
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
-
<ul>
-
{% for h3 in h2.children %}
-
<li>
-
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
-
</li>
-
{% endfor %}
-
</ul>
-
</li>
-
{% endfor %}
+
{% for h2 in page.toc %}
+
<li>
+
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+
<ul>
+
{% for h3 in h2.children %}
+
<li>
+
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
+
</li>
+
{% endfor %}
+
</ul>
+
</li>
+
{% endfor %}
</ul>
{% endif %}
-
</li>
-
{% endfor %}
-
</ul>
-
{% endif %}
-
{{ page.content | safe }}
+
<div class="e-content p-name">{{ page.content | safe }}</div>
-
<p class="tags-data">
-
{% if page.taxonomies.tags %}
-
{% for tag in page.taxonomies.tags %}
-
<a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a>
-
{% endfor %}
-
{% endif %}
-
</p>
-
{% endblock content %}
+
<p class="tags-data">
+
{% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %}
+
<a href="/tags/{{ tag | slugify }}" class="p-category text-glow"
+
>|{{ tag }}|</a
+
>
+
{% endfor %} {% endif %}
+
</p>
+
</article>
+
{% endblock content %}
+25 -13
templates/blog.html
···
{% extends "base.html" %} {% block content %}
<h1 class="title">{{ section.title }}</h1>
-
<p role="heading">--- <span style="letter-spacing: 0.1em;">Main Blog</span> ---</p>
+
<p role="heading">
+
--- <span style="letter-spacing: 0.1em">Main Blog</span> ---
+
</p>
<ul>
-
<!-- If you are using pagination, section.pages will be empty.
+
<!-- If you are using pagination, section.pages will be empty.
You need to use the paginator object -->
-
{% for page in section.pages %}
-
{% if "archival" not in page.taxonomies.tags %}
-
<li>{{ page.date }} &mdash; <a href="{{ page.permalink | safe }}">{{ page.title }}</a></li>
-
{% endif %}
-
{% endfor %}
+
{% for page in section.pages %} {% if "archival" not in page.taxonomies.tags
+
%}
+
<li>
+
{{ page.date | split(pat="T") | first }} &mdash;
+
<a href="{{ page.permalink | safe }}" class="text-glow"
+
>{{ page.title }}</a
+
>
+
</li>
+
{% endif %} {% endfor %}
</ul>
-
<p role="heading" >--- <span style="letter-spacing: 0.213em;">Archival</span> ---</p>
+
<p role="heading">
+
--- <span style="letter-spacing: 0.213em">Archival</span> ---
+
</p>
<ul>
-
{% for page in section.pages %}
-
{% if "archival" in page.taxonomies.tags %}
-
<li>{{ page.date }} &mdash; <a href="{{ page.permalink | safe }}">{{ page.title }}</a> (archival)</li>
-
{% endif %}
-
{% endfor %}
+
{% for page in section.pages %} {% if "archival" in page.taxonomies.tags %}
+
<li>
+
{{ page.date }} &mdash;
+
<a href="{{ page.permalink | safe }}" class="text-glow"
+
>{{ page.title }}</a
+
>
+
(archival)
+
</li>
+
{% endif %} {% endfor %}
</ul>
{% endblock content %}
+57 -22
templates/footer.html
···
<hr />
<div id="footer-container">
-
<div>
-
<p>&copy; {{ now() | date(format="%Y") }} Kieran Klukas</p>
-
<p>
-
Content licensed under
-
<a
-
target="_blank"
-
rel="noopener noreferrer"
-
href="https://creativecommons.org/licenses/by-nc-sa/4.0/"
-
>CC BY-NC-SA 4.0</a
-
>
-
</p>
-
<p>
-
Code licensed under
-
<a
-
target="_blank"
-
rel="noopener noreferrer"
-
href="https://github.com/kcoderhtml/zera/blob/master/LICENSE.md"
-
>AGPL 3.0</a
-
>
-
</p>
-
</div>
-
</div>
+
<p class="badge-row">
+
<a href="https://512kb.club"><img src="/badges/green-team.gif"
+
alt="a proud member of the green team of 512KB club" /></a>
+
<a href="https://hackclub.com"><img src="/badges/hackclub.png" alt="linux powered" /></a>
+
<a href="https://dunkirk.sh/ai"><img src="/badges/MadeByAHuman_04.svg" alt="made by a human" /></a>
+
<a href="https://tangled.org"><img src="/badges/tangled.png" alt="tangled beta" /></a>
+
<a href="https://www.netscape-communications.com/download/"><img src="/badges/get-netscape.gif"
+
alt="get netscape" /></a>
+
<a href="https://tangled.org/@dunkirk.sh/dots"><img src="/badges/powered-by-nix.gif" alt="powered by nix" /></a>
+
<a href="https://tangled.org/@dunkirk.sh/nixvim"><img src="/badges/made-with-neovim.png" /></a>
+
</p>
+
<p style="margin-bottom: 0.5rem">
+
&copy; {{ now() | date(format="%Y") }} Kieran Klukas ||
+
<code id="visits">0</code> page visits || {% set hash =
+
get_env(name="CF_PAGES_COMMIT_SHA", default=load_data(path=".git/refs/heads/main", required=false))%}{% if hash is
+
not string %}{% set hash = "unknown" %}{% endif %}<a href=https://tangled.sh/@dunkirk.sh/zera/commit/{{ hash
+
}}>zera@{{ hash |
+
truncate(length=7, end="")}}</a>
+
</p>
+
<p style="margin-bottom: 0.5rem">
+
Webrings:
+
<a href="https://w.elr.sh">elr</a>
+
[<a href='javascript:void(0)' onclick='randomSite()'>random</a> |
+
<a href='#' id='prev-link'>prev</a> |
+
<a href='#' id='next-link'>next</a>] โ€ข
+
<a href="https://ctp-webr.ing">ctp</a>
+
[<a href="https://ctp-webr.ing/dunkirk/previous">prev</a> |
+
<a href="https://ctp-webr.ing/dunkirk/next">next</a>]
+
</p>
+
+
<script type="text/javascript" src="https://w.elr.sh/onionring-variables.js"></script>
+
<script>
+
thisSite = "https://dunkirk.sh"
+
thisIndex = null;
+
+
for (i = 0; i < sites.length; i++) {
+
if (thisSite.startsWith(sites[i])) {
+
thisIndex = i;
+
break;
+
}
+
}
+
+
function randomSite() {
+
otherSites = sites.slice();
+
otherSites.splice(thisIndex, 1);
+
randomIndex = Math.floor(Math.random() * otherSites.length);
+
location.href = otherSites[randomIndex];
+
}
+
+
+
previousIndex = (thisIndex - 1 < 0) ? sites.length - 1 : thisIndex - 1;
+
nextIndex = (thisIndex + 1 >= sites.length) ? 0 : thisIndex + 1;
+
+
document.getElementById('prev-link').href = sites[previousIndex];
+
document.getElementById('next-link').href = sites[nextIndex];
+
</script>
+
</div>
+98 -26
templates/head.html
···
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="index, follow" />
+
<link
+
rel="sitemap"
+
type="application/xml"
+
title="Sitemap"
+
href="/sitemap.xml"
+
/>
{% if page.title %} {% set title = page.title %} {% elif section.title %} {% set
-
title = section.title %} {% elif config.title %} {% set title = config.title %}
-
{% endif %} {% if page.extra.author %} {% set author = page.extra.author %} {%
-
elif section.extra.author %} {% set author = section.extra.author %} {% elif
+
title = section.title %} {% elif term %} {% set title = "|" ~ term.name ~ "|" %}
+
{% elif current_path and "tags" in current_path %} {% set title = "Root Index"
+
%} {% elif config.title %} {% set title = config.title %} {% endif %} {% if
+
page.extra.author %} {% set author = page.extra.author %} {% elif
+
section.extra.author %} {% set author = section.extra.author %} {% elif
config.extra.author %} {% set author = config.extra.author %} {% endif %} {% if
page.description %} {% set description = page.description | truncate(length=150)
%} {% elif section.description %} {% set description = section.description |
truncate(length=150) %} {% elif config.description %} {% set description =
config.description | truncate(length=150) %} {% endif %} {% if page.extra.image
-
%} {% set image = get_url(path="og.png", trailing_slash=false) %} {% elif
-
section.extra.image %} {% set image = get_url(path=section.extra.image,
+
%} {% set image = get_url(path=page.extra.image, trailing_slash=false) %} {%
+
elif section.extra.image %} {% set image = get_url(path=section.extra.image,
trailing_slash=false) %} {% elif page.path %} {% set image =
-
get_url(path=page.path ~ "og.png", trailing_slash=false) %} {% else %} {% set
-
image = get_url(path="og.png", trailing_slash=false) %} {% endif %} {% if
-
page.permalink %} {% set url = page.permalink %} {% elif section.permalink %} {%
-
set url = section.permalink %} {% elif config.base_url %} {% set url =
-
config.base_url %} {% endif %} {% if title %}
+
get_url(path=page.path ~ "og.png", trailing_slash=false) %} {% elif current_path
+
%} {% set image = get_url(path=current_path ~ "og.png", trailing_slash=false) %}
+
{% else %} {% set image = get_url(path="og.png", trailing_slash=false) %} {%
+
endif %} {% if page.permalink %} {% set url = page.permalink %} {% elif
+
section.permalink %} {% set url = section.permalink %} {% elif config.base_url
+
%} {% set url = config.base_url %} {% endif %} {% if title %} {% if current_url
+
and url != current_url %} {% set url = get_url(path=current_path,
+
trailing_slash=true) %} {% endif %}
<title>{{ title }}</title>
{% endif %} {% block metatags %} {% if title %}
<meta name="title" content="{{ title }}" />
···
<meta property="twitter:image" content="{{ image }}" />
{% endif %} {% endif %}
<link rel="canonical" href="{{ url | safe }}" />
-
{% if image %}
-
<link
-
rel="shortcut icon"
-
type="image/x-icon"
-
href="{{ get_url(path=config.extra.favicon, trailing_slash=false) }}"
-
/>
-
{% endif %} {% endblock metatags %} {% if config.generate_feeds %} {% block feed
-
%} <link rel="alternate" type="application/atom+xml" title="RSS" href="{{
-
get_url(path="atom.xml", trailing_slash=false) }}"> {% endblock feed %} {% endif
-
%} {% block css %} {% set cssHash = get_hash(path="css/main.css", sha_type=256,
-
base64=true) %}
+
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
+
<link rel="shortcut icon" href="/favicon/favicon.ico" />
+
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
+
<meta name="apple-mobile-web-app-title" content="site@zera" />
+
<link rel="manifest" href="/favicon/site.webmanifest" />
+
{% endblock metatags %} {% if config.generate_feeds %} {% block
+
feed%} <link rel="alternate" type="application/atom+xml" title="Kieran Klukas' Atom feed" href="{{
+
get_url(path="atom.xml", trailing_slash=false) }}"> <link rel="alternate"
+
type="application/rss+xml" title="Kieran Klukas' RSS feed" href="{{ get_url(path="rss.xml",
+
trailing_slash=false) }}"> {% endblock feed %} {% endif%} {% block css %} {% set
+
cssHash = get_hash(path="css/main.css", sha_type=256, base64=true) %}
<link
rel="stylesheet"
type="text/css"
href="{{ get_url(path='css/main.css?' ~ cssHash, trailing_slash=false) | safe }}"
/>
-
{% endblock css %} {% set jsHash = get_hash(path="js/script.js", sha_type=256,
+
{% endblock css %}
+
+
{% set jsHash = get_hash(path="js/copy-button.js", sha_type=256,
base64=true) %}
<script
-
src="{{ get_url(path='js/script.js?' ~ jsHash, trailing_slash=false) | safe }}"
+
src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}"
defer
></script>
+
+
{% set emojiJsHash = get_hash(path="js/emoji-replace.js", sha_type=256,
+
base64=true) %}
<script
+
src="{{ get_url(path='js/emoji-replace.js?' ~ emojiJsHash, trailing_slash=false) | safe }}"
defer
-
data-domain="dunkirk.sh"
-
src="https://nexus.kieranklukas.com/js/script.outbound-links.file-downloads.js"
-
></script>
+
></script>
+
+
{% set lightboxJsHash = get_hash(path="lightbox.js", sha_type=256,
+
base64=true) %}
+
<script
+
src="{{ get_url(path='lightbox.js?' ~ lightboxJsHash, trailing_slash=false) | safe }}"
+
defer
+
></script>
+
+
<script type="speculationrules">
+
{
+
"prerender": [
+
{
+
"where": {
+
"selector_matches": "a"
+
}
+
}
+
]
+
}
+
</script>
+
+
<script>
+
function cb(res) {
+
const fmt = new Intl.NumberFormat('en', { notation: 'compact' });
+
const elements = document.querySelectorAll("[id='visits']");
+
elements.forEach(el => {
+
el.innerText = fmt.format(res.value);
+
el.title = res.value + " visits";
+
});
+
}
+
</script>
+
<script async src="https://abacus.jasoncameron.dev/hit/dunkirk.sh/counter{%- if url | split(pat='/') | slice(start=3) | join != '' -%}-{{url | split(pat='/') | slice(start=3) | join(sep=' ') | slugify}}{%- endif -%}?callback=cb"></script>
+
+
<link rel="me" href="https://social.dino.icu/@taciturnaxoltol" />
+
<link rel="me" href="https://github.com/taciturnaxolotl" />
+
<link rel="me" href="https://bsky.app/profile/dunkirk.sh" />
+
<link rel="me" href="https://www.youtube.com/@kieran.rambles" />
+
<link rel="me" href="https://keyoxide.org/aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A" />
+
<link rel="me" href="https://serif.blue" />
+
<meta name="fediverse:creator" content="@taciturnaxoltol@social.dino.icu" />
+
+
<div class="h-card" style="display:none">
+
<a class="u-url" rel="me home" href="https://dunkirk.sh">
+
<span class="p-name">Kieran Klukas</span>
+
</a>
+
<p class="p-given-name">Kieran</p>
+
<p class="p-family-name">Klukas</p>
+
<p class="dt-bday">2008-04-27</p>
+
<p class="p-sex">male</p>
+
<p class="p-note">
+
{% set time = now() | date(format="%s") | int - 1209254400 %}{{ time / 31536000 | round(method="floor") }}, typescript nerd, videographer, frc programmer, semi retired fpv pilot
+
</p>
+
<a class="u-email" href="mailto:me@dunkirk.sh" rel="me">me@dunkirk.sh</a>
+
<div class="p-adr h-adr">
+
<span class="p-country-name">United States of America</span>
+
</div>
+
<img class="u-photo" src="/pfps/fall.jpg" alt="kieran wearing a robotics sweatshirt and standing in front of a tree with fall leaves" />
+
</div>
+13 -22
templates/header.html
···
-
{% if config.extra.header_nav %} {% if not current_url %} {% set current_url =
-
"" %} {% endif %}
+
{% if config.extra.header_nav %}
+
{% if page %}
+
{% set active_path = page.path | trim_end_matches(pat="/") %}
+
{% elif section %}
+
{% set active_path = section.path | trim_end_matches(pat="/") %}
+
{% elif current_path %}
+
{% set active_path = current_path | trim_end_matches(pat="/") %}
+
{% else %}
+
{% set active_path = "" %}
+
{% endif %}
<nav id="nav-bar">
{% for nav_item in config.extra.header_nav %}
-
<a
-
href="{{ nav_item.url }}"
-
class="{% if nav_item.url == current_url %}active{% endif %}"
-
>
+
<a href="{{ nav_item.url }}"
+
class="{% if nav_item.url == active_path or (nav_item.url == '/' and active_path == '') %}active{% endif %}">
{{ nav_item.name }}
</a>
{% endfor %}
-
<div>
-
<input type="checkbox" id="theme-toggle" style="display: none" />
-
<label for="theme-toggle" id="theme-toggle-label"
-
><svg id="theme-icon" class="icons">
-
<use
-
href="{{ get_url(path='/icons.svg#lightMode', trailing_slash=false) | safe }}"
-
></use></svg
-
></label>
-
<audio id="theme-sound">
-
<source
-
src="{{ get_url(path='click.ogg', trailing_slash=false) | safe }}"
-
type="audio/ogg"
-
/>
-
</audio>
-
</div>
</nav>
-
{% endif %}
+
{% endif %}
+12 -12
templates/index.html
···
{% else %} {% set pages = section.pages %} {% endif %}
<ul class="titleList">
-
{% for page in pages %}
-
<li>
-
<a href="{{ page.permalink | safe }}">{{ page.title }}</a>
-
<br />
-
{{ page.description }}
-
</li>
-
{% endfor %}
+
{% for page in pages %}
+
<li>
+
<a href="{{ page.permalink | safe }}">{{ page.title }}</a>
+
<br />
+
{{ page.description }}
+
</li>
+
{% endfor %}
</ul>
{% if paginator %}
<div class="metaData">
-
{% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp
-
<a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{
-
paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if
-
paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp
-
<a href="{{ paginator.last }}">โฅธ</a>{% endif %}
+
{% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp
+
<a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{
+
paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if
+
paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp
+
<a href="{{ paginator.last }}">โฅธ</a>{% endif %}
</div>
{% endif %} {% endif %} {% endblock content %}
+22 -22
templates/page.html
···
{% if page.toc and page.extra.toc %}
<h2>Table of contents</h2>
<ul>
-
{% for h1 in page.toc %}
-
<li>
-
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
-
{% if h1.children %}
-
<ul>
-
{% for h2 in h1.children %}
-
<li>
-
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+
{% for h1 in page.toc %}
+
<li>
+
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
+
{% if h1.children %}
<ul>
-
{% for h3 in h2.children %}
-
<li>
-
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
-
</li>
-
{% endfor %}
+
{% for h2 in h1.children %}
+
<li>
+
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+
<ul>
+
{% for h3 in h2.children %}
+
<li>
+
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
+
</li>
+
{% endfor %}
+
</ul>
+
</li>
+
{% endfor %}
</ul>
-
</li>
-
{% endfor %}
-
</ul>
-
{% endif %}
-
</li>
-
{% endfor %}
+
{% endif %}
+
</li>
+
{% endfor %}
</ul>
{% endif %} {{ page.content | safe }}
<p class="tags-data">
-
{% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %}
-
<a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a>
-
{% endfor %} {% endif %}
+
{% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %}
+
<a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a>
+
{% endfor %} {% endif %}
</p>
{% endblock content %}
+16 -18
templates/section.html
···
-
{% extends "base.html" %}
-
-
{% block content %}
+
{% extends "base.html" %} {% block content %}
<h1>{{ section.title }}</h1>
-
{{ section.content | safe }}
-
-
{% if paginator %}
-
{% set pages = paginator.pages %}
-
{% else %}
-
{% set pages = section.pages %}
-
{% endif %}
+
{{ section.content | safe }} {% if paginator %} {% set pages = paginator.pages
+
%} {% else %} {% set pages = section.pages %} {% endif %}
<ul class="title-list">
-
{% for page in pages %}
-
<li>
-
<a href="{{ page.permalink | safe }}">{{ page.title }}</a>
-
</li>
-
{% endfor %}
+
{% for page in pages %}
+
<li>
+
<a href="{{ page.permalink | safe }}">{{ page.title }}</a>
+
</li>
+
{% endfor %}
</ul>
{% if paginator %}
-
<div class="accent-data">{% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp <a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{ paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp <a href="{{ paginator.last }}">โฅธ</a>{% endif %}</div>
-
{% endif %}
-
{% endblock content %}
+
<div class="accent-data">
+
{% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp
+
<a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{
+
paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if
+
paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp
+
<a href="{{ paginator.last }}">โฅธ</a>{% endif %}
+
</div>
+
{% endif %} {% endblock content %}
+1 -1
templates/shortcodes/age.md
···
-
{% set result = 1 %}{% for _ in range(end=length) %}{% set_global result = result * 10 %}{% endfor %}{% set time = now() | date(format="%s") | int - 1209254400 %}{{ time / 31536000 * result | round() / result }}
+
{% set time = now() | date(format="%s") | int - 1209254400 %}{{ (time / 31536000) | round(method="floor", precision=length) }}{% if comma %},{% endif %}
+70
templates/shortcodes/bluesky.html
···
+
{% set profile_part = post | split(pat="profile/") | last %} {% set parts =
+
profile_part | split(pat="/") %} {% set handle = parts[0] %} {% set post_id =
+
parts[2] %} {% set api_url =
+
"https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://" ~
+
handle ~ "/app.bsky.feed.post/" ~ post_id %} {% set response =
+
load_data(url=api_url, format="json") %} {% if response.thread and
+
response.thread.post %} {% set post_data = response.thread.post %} {% set author
+
= post_data.author.displayName %} {% set handle = post_data.author.handle %} {%
+
set content = post_data.record.text %} {% set has_embed = post_data.embed is
+
defined and post_data.embed %}
+
<blockquote>
+
{{ content }} {% if has_embed %} {% if post_data.embed["$type"] ==
+
"app.bsky.embed.video#view" %}
+
<video controls poster="{{ post_data.embed.thumbnail }}">
+
<source
+
src="{{ post_data.embed.playlist }}"
+
type="application/x-mpegURL"
+
/>
+
</video>
+
{% elif post_data.embed["$type"] == "app.bsky.embed.images#view" %} {% if
+
post_data.embed.images | length > 3 %}
+
<div class="image-gallery gallery-grid">
+
{% for image in post_data.embed.images %}
+
<img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" />
+
{% endfor %}
+
</div>
+
{% elif post_data.embed.images | length == 2 %}
+
<div class="image-gallery side-by-side">
+
{% for image in post_data.embed.images %}
+
<img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" />
+
{% endfor %}
+
</div>
+
{% else %}
+
<div class="image-gallery">
+
{% for image in post_data.embed.images %}
+
<img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" />
+
{% endfor %}
+
</div>
+
{% endif %} {% endif %} {% endif %}
+
</blockquote>
+
<p>
+
<cite>
+
<a href="{{ post }}" target="_blank" rel="noopener"
+
><img
+
src="{{ post_data.author.avatar }}"
+
alt="{{ author }}'s avatar"
+
class="avatar"
+
/>@{{ handle }}</a
+
></cite
+
>
+
</p>
+
{% else %}
+
<blockquote>
+
<div class="bsky-post">
+
<div class="bsky-post-content">"Failed to render Bluesky post"</div>
+
<div class="bsky-post-footer">
+
<cite
+
><img
+
src="/img/bluesky-logo.png"
+
alt="Bluesky logo"
+
class="avatar"
+
/>
+
<a href="{{ post }}" target="_blank" rel="noopener"
+
>View on Bluesky</a
+
></cite
+
>
+
</div>
+
</div>
+
</blockquote>
+
{% endif %}
+42
templates/shortcodes/callout.html
···
+
{%- set type = type | default(value="info") | lower -%}
+
{%- set title = title | default(value="") -%}
+
+
{%- if type == "info" -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' -%}
+
{%- set color = "blue" -%}
+
{%- set default_title = "Info" -%}
+
{%- elif type == "warning" or type == "warn" -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>' -%}
+
{%- set color = "yellow" -%}
+
{%- set default_title = "Warning" -%}
+
{%- elif type == "danger" or type == "error" -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' -%}
+
{%- set color = "red" -%}
+
{%- set default_title = "Danger" -%}
+
{%- elif type == "tip" or type == "hint" -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path><path d="M9 18h6"></path><path d="M10 22h4"></path></svg>' -%}
+
{%- set color = "green" -%}
+
{%- set default_title = "Tip" -%}
+
{%- elif type == "note" -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>' -%}
+
{%- set color = "gray" -%}
+
{%- set default_title = "Note" -%}
+
{%- else -%}
+
{%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' -%}
+
{%- set color = "blue" -%}
+
{%- set default_title = "Info" -%}
+
{%- endif -%}
+
+
{%- if title == "" -%}
+
{%- set title = default_title -%}
+
{%- endif -%}
+
+
<div class="callout callout-{{ color }}">
+
<div class="callout-title">
+
<span class="callout-icon">{{ icon | safe }}</span>
+
<strong>{{ title }}</strong>
+
</div>
+
<div class="callout-content">
+
{{ body | markdown | safe }}
+
</div>
+
</div>
+6 -4
templates/shortcodes/img.html
···
<figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}>
-
<img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} />
-
{% if caption %}
-
<figcaption>{{caption}}</figcaption>
-
{% endif %}
+
<div class="img-container" onclick="openLightbox('{{id}}')">
+
<img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} />
+
</div>
+
{% if caption %}
+
<figcaption>{{caption | markdown | safe}}</figcaption>
+
{% endif %}
</figure>
+14
templates/shortcodes/imgs.html
···
+
<figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}>
+
<div class="img-group" data-images="{{id}}" data-alts="{{alt | default(value='')}}">
+
{% set images = id | split(pat=",") %}
+
{% set alts = alt | default(value="") | split(pat=",") %}
+
{% for image in images %}
+
<div class="img-container" onclick="openLightboxGroup(this)">
+
<img src="{{image | trim}}" {% if alts[loop.index0] %}alt="{{alts[loop.index0] | trim}}" {% endif %} />
+
</div>
+
{% endfor %}
+
</div>
+
{% if caption %}
+
<figcaption>{{caption | markdown | safe}}</figcaption>
+
{% endif %}
+
</figure>
+104
templates/shortcodes/is.md
···
+
<div class="bubble" style="visibility: hidden; opacity: 0;">
+
<span><a href="https://bsky.app/@doing.dunkirk.sh" id="verb-link">Kieran is</a> <i id="status-text"></i> - <span id="time-ago"></span></span>
+
</div>
+
+
<script>
+
document.addEventListener("DOMContentLoaded", () => {
+
// Initialize RelativeTimeFormat with user's locale
+
const rtf = new Intl.RelativeTimeFormat(navigator.language, {
+
numeric: "auto",
+
style: "long"
+
});
+
+
fetch(
+
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update",
+
)
+
.then((response) => {
+
if (!response.ok) {
+
throw new Error("Network response was not ok");
+
}
+
return response.json();
+
})
+
.then((statusData) => {
+
if (statusData.records && statusData.records.length > 0) {
+
// Calculate time difference
+
if (statusData.records[0].value.createdAt) {
+
const createdDate = new Date(statusData.records[0].value.createdAt);
+
const now = new Date();
+
const diffInMs = now - createdDate;
+
const diffInMins = Math.floor(diffInMs / (1000 * 60));
+
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
+
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
+
+
// Ignore if older than 12 hours
+
if (diffInHours > 12) {
+
return;
+
}
+
+
const latestStatus = `"${statusData.records[0].value.text}"`;
+
document.getElementById("status-text").textContent = latestStatus;
+
+
// Format time contextually using Intl.RelativeTimeFormat
+
let timeAgoText;
+
const createdHour = createdDate.getHours();
+
const isToday = diffInHours < 24 && now.getDate() === createdDate.getDate();
+
const isYesterday = diffInHours >= 24 && diffInHours < 48 &&
+
(now.getDate() - createdDate.getDate() === 1 ||
+
(now.getDate() === 1 && new Date(now.getFullYear(), now.getMonth(), 0).getDate() === createdDate.getDate()));
+
+
if (diffInMins < 1) {
+
timeAgoText = rtf.format(0, "minute"); // "now" in the user's language
+
} else if (diffInMins < 5) {
+
timeAgoText = rtf.format(-1, "minute"); // "1 minute ago" in the user's language
+
} else if (diffInMins < 60) {
+
timeAgoText = rtf.format(-diffInMins, "minute");
+
} else if (diffInHours < 3) {
+
timeAgoText = rtf.format(-diffInHours, "hour");
+
} else if (isToday) {
+
// Time of day context, but still localized
+
if (createdHour < 12) {
+
timeAgoText = "this morning";
+
} else if (createdHour < 17) {
+
timeAgoText = "this afternoon";
+
} else {
+
timeAgoText = "this evening";
+
}
+
} else if (isYesterday) {
+
timeAgoText = rtf.format(-1, "day"); // "yesterday" in the user's language
+
} else if (diffInDays < 7) {
+
timeAgoText = rtf.format(-diffInDays, "day");
+
} else {
+
// For older posts, use a date formatter
+
const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
+
month: 'short',
+
day: 'numeric'
+
});
+
timeAgoText = `on ${dateFormatter.format(createdDate)}`;
+
}
+
+
document.getElementById("time-ago").textContent = timeAgoText;
+
+
// Change "is" to "was" based on recency
+
const verbLink = document.getElementById("verb-link");
+
if (diffInMins > 30) {
+
verbLink.textContent = "Kieran was";
+
}
+
+
// Show and animate the bubble since we have a valid status
+
const bubble = document.querySelector(".bubble");
+
bubble.style.visibility = "visible";
+
bubble.classList.add("animate-in");
+
+
// For reduced motion preferences, ensure the bubble is always visible
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+
bubble.style.transform = "none"; // Ensure no transform is applied
+
bubble.style.opacity = "1"; // Ensure content is visible
+
}
+
}
+
}
+
})
+
.catch((error) => {
+
console.error("Error fetching status update:", error);
+
});
+
});
+
</script>
+655
templates/shortcodes/lensDiagram.html
···
+
<div
+
id="rayTracer"
+
style="display: flex; flex-direction: column; min-height: 40rem"
+
>
+
<div class="controls" style="display: flex; flex-direction: column">
+
<div style="display: flex; gap: 20px; align-items: center">
+
<div>
+
<label>Mirror Type:</label>
+
<select id="mirrorType">
+
<option value="concave">Concave Mirror</option>
+
<option value="convex">Convex Mirror</option>
+
</select>
+
</div>
+
<div>
+
<label>Radius of Curvature:</label>
+
<input
+
type="number"
+
id="radius"
+
value="20"
+
min="0.2"
+
step="0.2"
+
/>
+
</div>
+
<div>
+
<label>Object Distance:</label>
+
<input
+
type="number"
+
id="objectDist"
+
value="30"
+
min="0.2"
+
step="0.2"
+
/>
+
</div>
+
</div>
+
<div style="display: flex; gap: 20px; align-items: center; width: 100%">
+
<div>
+
<label>Object Height:</label>
+
<input
+
type="number"
+
id="objectHeight"
+
value="20"
+
min="0.1"
+
step="0.1"
+
/>
+
</div>
+
<div style="flex: 1">
+
<label>Zoom:</label>
+
<input
+
type="range"
+
id="zoom"
+
min="0.01"
+
max="8"
+
step="0.01"
+
value="1"
+
style="width: 100%"
+
/>
+
</div>
+
</div>
+
</div>
+
<canvas id="canvas" style="flex: 1; cursor: move"></canvas>
+
</div>
+
+
<style>
+
#rayTracer {
+
padding: 20px;
+
}
+
.controls {
+
margin-bottom: 20px;
+
}
+
.controls div {
+
margin: 0.2rem 0;
+
}
+
#canvas {
+
border: 1px solid #ccc;
+
width: 100%;
+
}
+
</style>
+
+
<script>
+
const canvas = document.getElementById("canvas");
+
const ctx = canvas.getContext("2d");
+
const mirrorType = document.getElementById("mirrorType");
+
const radiusInput = document.getElementById("radius");
+
const objectDistInput = document.getElementById("objectDist");
+
const objectHeightInput = document.getElementById("objectHeight");
+
const zoomInput = document.getElementById("zoom");
+
+
let offsetX = 0;
+
let offsetY = 0;
+
let isDragging = false;
+
let lastX = 0;
+
let lastY = 0;
+
+
canvas.addEventListener("mousedown", (e) => {
+
isDragging = true;
+
lastX = e.clientX;
+
lastY = e.clientY;
+
});
+
+
canvas.addEventListener("mousemove", (e) => {
+
if (isDragging) {
+
offsetX += e.clientX - lastX;
+
offsetY += e.clientY - lastY;
+
lastX = e.clientX;
+
lastY = e.clientY;
+
update();
+
}
+
});
+
+
canvas.addEventListener("mouseup", () => {
+
isDragging = false;
+
});
+
+
canvas.addEventListener("mouseleave", () => {
+
isDragging = false;
+
});
+
+
canvas.addEventListener("wheel", (e) => {
+
e.preventDefault();
+
const zoomSpeed = 0.001;
+
const newZoom = parseFloat(zoomInput.value) - e.deltaY * zoomSpeed;
+
zoomInput.value = Math.min(Math.max(newZoom, 0.01), 8);
+
update();
+
});
+
+
function calculateReflectedRay(
+
startX,
+
startY,
+
incidentX,
+
incidentY,
+
centerX,
+
centerY,
+
radius,
+
) {
+
// Calculate normal vector at intersection point
+
const nx = (incidentX - centerX) / radius;
+
const ny = (incidentY - centerY) / radius;
+
+
// Calculate incident vector
+
const ix = incidentX - startX;
+
const iy = incidentY - startY;
+
const iLen = Math.sqrt(ix * ix + iy * iy);
+
const dirX = ix / iLen;
+
const dirY = iy / iLen;
+
+
// Calculate reflection using r = i - 2(iยทn)n
+
const dot = dirX * nx + dirY * ny;
+
const reflectX = dirX - 2 * dot * nx;
+
const reflectY = dirY - 2 * dot * ny;
+
+
// Extend reflected ray to edge of canvas
+
const t = Math.max(
+
Math.abs((0 - incidentX) / reflectX),
+
Math.abs((canvas.width - incidentX) / reflectX),
+
Math.abs((0 - incidentY) / reflectY),
+
Math.abs((canvas.height - incidentY) / reflectY),
+
);
+
+
return {
+
x: incidentX + reflectX * t,
+
y: incidentY + reflectY * t,
+
};
+
}
+
+
function drawMirror(isConcave, R) {
+
const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value);
+
const centerX = canvas.width / 2 + R * scale * isConcave + offsetX;
+
const centerY = canvas.height / 2 + offsetY;
+
+
ctx.beginPath();
+
ctx.strokeStyle = "black";
+
if (isConcave) {
+
ctx.arc(
+
centerX - R * scale,
+
centerY,
+
R * scale,
+
-Math.PI / 2.75,
+
Math.PI / 2.75,
+
);
+
} else {
+
ctx.arc(
+
centerX + R * scale,
+
centerY,
+
R * scale,
+
-Math.PI / 2.75 + Math.PI,
+
Math.PI / 2.75 + Math.PI,
+
);
+
}
+
ctx.stroke();
+
}
+
+
function drawArrow(x, y, height) {
+
const arrowHeadSize = height * 0.1; // Scale arrow head with height
+
ctx.lineWidth = 1;
+
ctx.beginPath();
+
+
// Draw the main shaft
+
ctx.moveTo(x, y);
+
ctx.lineTo(x, y - height * 0.9);
+
+
// Draw the arrow head
+
ctx.moveTo(x, y - height);
+
ctx.lineTo(x - arrowHeadSize, y - height + arrowHeadSize);
+
ctx.moveTo(x, y - height);
+
ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize);
+
ctx.moveTo(x - arrowHeadSize, y - height + arrowHeadSize);
+
ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize);
+
+
ctx.stroke();
+
}
+
+
function extendRayToCanvasEdge(x1, y1, x2, y2) {
+
const rayDirX = x2 - x1;
+
const rayDirY = y2 - y1;
+
const t = Math.max(
+
Math.abs((0 - x2) / rayDirX),
+
Math.abs((canvas.width - x2) / rayDirX),
+
Math.abs((0 - y2) / rayDirY),
+
Math.abs((canvas.height - y2) / rayDirY),
+
);
+
ctx.lineTo(x2 + rayDirX * t, y2 + rayDirY * t);
+
}
+
+
function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) {
+
// Check if the input values are valid
+
if (radius <= 0) {
+
throw new Error("Invalid input values.");
+
}
+
+
// Calculate the slope of the line from (x1, h) to (x3, y3)
+
const m = (y3 - (centerY - h)) / (x3 - x1);
+
+
// Define the line equation: y = h + m * (x - x1)
+
// Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2
+
// y = h + m * (x - x1)
+
// (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2
+
+
// Coefficients for the quadratic equation
+
const a = 1 + m * m;
+
const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1);
+
const c =
+
centerX * centerX +
+
(centerY - h - centerY - m * x1) *
+
(centerY - h - centerY - m * x1) -
+
radius * radius;
+
+
// Calculate the discriminant
+
const discriminant = b * b - 4 * a * c;
+
+
if (discriminant < 0) {
+
throw new Error("No intersection found.");
+
}
+
+
// Calculate the two possible x values
+
const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a);
+
const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a);
+
+
// Calculate the corresponding y values
+
const yIntersect1 = centerY - h + m * (xIntersect1 - x1);
+
const yIntersect2 = centerY - h + m * (xIntersect2 - x1);
+
+
// Return the intersection points
+
return [
+
{ x: xIntersect1, y: yIntersect1 },
+
{ x: xIntersect2, y: yIntersect2 },
+
];
+
}
+
+
function drawRays(isConcave, R, objDist) {
+
const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value);
+
const F = R / 2;
+
const h = parseFloat(objectHeightInput.value) * scale;
+
const centerX = canvas.width / 2 + R * scale + offsetX;
+
const centerY = canvas.height / 2 + offsetY;
+
const objX =
+
centerX +
+
objDist * scale * (isConcave ? -1 : -1) -
+
R * scale * !isConcave;
+
const objY = centerY;
+
+
drawArrow(objX, objY, h);
+
+
ctx.beginPath();
+
ctx.moveTo(0, centerY);
+
ctx.lineTo(canvas.width, centerY);
+
ctx.stroke();
+
+
ctx.fillStyle = "red";
+
ctx.beginPath();
+
ctx.arc(centerX - F * scale, centerY, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
ctx.fillStyle = "blue";
+
ctx.beginPath();
+
ctx.arc(centerX - R * scale * isConcave, centerY, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
const circleCenterX = isConcave
+
? centerX - R * scale
+
: centerX - R * scale;
+
+
if (isConcave) {
+
// ray that travels from the top of the object towards the mirror and then calculating the bounce angle it goes in that direction
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
let intersectionX =
+
Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX;
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
intersectionX,
+
objY - h,
+
centerX - F * scale,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(0, 128, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
centerX - F * scale,
+
centerY,
+
intersectionX,
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(intersectionX, objY - h, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(centerX - F * scale, centerY);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(0, extendedRay2[0].y);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(128, 0, 128, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(canvas.width, extendedRay2[0].y);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(extendedRay2[0].x, extendedRay2[0].y, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object through the radius of curvature of the mirror
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(circleCenterX, centerY);
+
const extendedRay3 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
circleCenterX,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
centerX - R * scale,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
centerX - R * scale,
+
centerY,
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(extendedRay3[0].x, extendedRay3[0].y, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
} else {
+
// draw a ray that travels from the top of the object horizontally towards the mirror
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
extendRayToCanvasEdge(
+
centerX - F * scale,
+
centerY,
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(0, 128, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
ctx.lineTo(centerX - F * scale, centerY);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
const extendedRay2Y = centerY - (extendedRay2[0].y - centerY);
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
);
+
ctx.lineTo(0, centerY - (extendedRay2[0].y - centerY));
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(128, 0, 128, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
);
+
ctx.lineTo(canvas.width, centerY - (extendedRay2[0].y - centerY));
+
ctx.stroke();
+
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object through the radius of curvature of the mirror
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
// ctx.lineTo(centerX, centerY);
+
const extendedRay3ScaleFactor =
+
(R * scale) / Math.abs(objX - centerX);
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
);
+
extendRayToCanvasEdge(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
objX,
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
);
+
extendRayToCanvasEdge(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
centerX,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay3Y) ** 2,
+
),
+
centerY - (extendedRay3[0].y - centerY),
+
);
+
extendRayToCanvasEdge(
+
centerX - R * scale,
+
centerY,
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay3Y) ** 2,
+
),
+
centerY - (extendedRay3[0].y - centerY),
+
);
+
ctx.stroke();
+
}
+
}
+
+
function update() {
+
canvas.width = canvas.offsetWidth;
+
canvas.height = canvas.offsetHeight;
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
+
ctx.fillStyle = "#f0f0f0";
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
+
const isConcave = mirrorType.value === "concave";
+
const R = parseFloat(radiusInput.value);
+
const objDist = parseFloat(objectDistInput.value);
+
+
drawMirror(isConcave, R);
+
drawRays(isConcave, R, objDist);
+
}
+
+
function resetHeight() {
+
objectHeightInput.value = Math.max(
+
parseFloat(((radiusInput.value * 2) / 3).toFixed(2)),
+
0.1,
+
);
+
}
+
+
mirrorType.addEventListener("change", update);
+
radiusInput.addEventListener("input", () => {
+
resetHeight();
+
update();
+
});
+
objectDistInput.addEventListener("input", update);
+
objectHeightInput.addEventListener("input", update);
+
zoomInput.addEventListener("input", update);
+
window.addEventListener("resize", update);
+
+
resetHeight();
+
update();
+
+
let isCanvasHovered = false;
+
+
canvas.addEventListener("mouseenter", () => {
+
isCanvasHovered = true;
+
});
+
+
canvas.addEventListener("mouseleave", () => {
+
isCanvasHovered = false;
+
});
+
+
document.addEventListener("keydown", (e) => {
+
if (!isCanvasHovered) return;
+
if (e.key === "+" || e.key === "=") {
+
zoomInput.value = Math.min(parseFloat(zoomInput.value) + 0.1, 8);
+
update();
+
}
+
if (e.key === "-" || e.key === "_") {
+
zoomInput.value = Math.max(parseFloat(zoomInput.value) - 0.1, 0.1);
+
update();
+
}
+
+
// translate the canvas
+
if (e.key === "ArrowUp") {
+
offsetY -= 25;
+
update();
+
}
+
if (e.key === "ArrowDown") {
+
offsetY += 25;
+
update();
+
}
+
if (e.key === "ArrowLeft") {
+
offsetX -= 25;
+
update();
+
}
+
if (e.key === "ArrowRight") {
+
offsetX += 25;
+
update();
+
}
+
});
+
</script>
+1 -1
templates/shortcodes/mark.html
···
-
<mark>{{content}}</mark>
+
<mark>{{content}}</mark>
+238
templates/shortcodes/now_status.html
···
+
{% set api_url =
+
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update"
+
%} {% set response = load_data(url=api_url, format="json") %}
+
+
<style>
+
#status-updates-container {
+
display: flex;
+
flex-direction: column;
+
gap: 1.5rem;
+
width: 100%;
+
margin-bottom: 2rem;
+
}
+
+
.bsky-post {
+
border-left: 0.375rem solid var(--accent);
+
padding: 0.7em 1em;
+
font-size: 1rem;
+
background-color: var(--bg-light);
+
border-radius: 0.375rem;
+
}
+
+
.bsky-post-content {
+
margin-bottom: 0.75rem;
+
line-height: 1.4;
+
}
+
+
.bsky-post-footer {
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
color: var(--text-light);
+
}
+
+
.bsky-post-footer cite {
+
display: inline-flex;
+
align-items: center;
+
gap: 0.4rem;
+
}
+
+
.bsky-post-time {
+
font-size: 0.8rem;
+
color: var(--text-light);
+
}
+
</style>
+
+
<div id="status-updates-container">
+
{% if response.records %} {% for record in response.records |
+
sort(attribute="value.createdAt") | reverse %} {% set created_at =
+
record.value.createdAt %} {% set status_text = record.value.text %}
+
<div
+
class="bsky-post"
+
data-cid="{{ record.cid }}"
+
data-created="{{ created_at }}"
+
>
+
<div class="bsky-post-content">Kieran was {{ status_text }}</div>
+
<div class="bsky-post-footer">
+
<cite>
+
<img
+
src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg"
+
alt="Kieran's avatar"
+
class="avatar"
+
/>
+
<a
+
href="https://bsky.app/profile/doing.dunkirk.sh"
+
target="_blank"
+
rel="noopener"
+
>@doing.dunkirk.sh</a
+
>
+
</cite>
+
<span class="bsky-post-time">
+
{{ record.value.createdAt | date(format="%b %d, %Y") }}
+
</span>
+
</div>
+
</div>
+
{% endfor %} {% else %}
+
<div class="bsky-post">
+
<div class="bsky-post-content">No status updates found.</div>
+
</div>
+
{% endif %}
+
</div>
+
+
<script>
+
document.addEventListener("DOMContentLoaded", () => {
+
const container = document.getElementById("status-updates-container");
+
const API_URL =
+
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update";
+
const existingPosts = new Map();
+
+
// Collect existing posts by CID
+
document.querySelectorAll(".bsky-post[data-cid]").forEach((post) => {
+
existingPosts.set(post.dataset.cid, {
+
element: post,
+
created: new Date(post.dataset.created),
+
});
+
});
+
+
// Format time relative to now
+
function formatTimeAgo(date) {
+
const now = new Date();
+
const diffInMs = now - date;
+
const diffInMins = Math.floor(diffInMs / (1000 * 60));
+
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
+
+
if (diffInMins < 1) return "just now";
+
if (diffInMins < 60) return `${Math.round(diffInMins)}m`;
+
if (diffInHours < 24) return `${Math.round(diffInHours)}h`;
+
+
return new Intl.DateTimeFormat("en", {
+
month: "short",
+
day: "numeric",
+
}).format(date);
+
}
+
+
// Update timestamps and verbs on existing posts
+
function updateTimestamps() {
+
existingPosts.forEach((post) => {
+
const timeElement =
+
post.element.querySelector(".bsky-post-time");
+
const contentElement =
+
post.element.querySelector(".bsky-post-content");
+
if (timeElement) {
+
timeElement.textContent = formatTimeAgo(post.created);
+
}
+
+
// Update the is/was verb based on post age
+
const now = new Date();
+
const diffInMs = now - post.created;
+
const diffInMins = diffInMs / (1000 * 60);
+
const verb = diffInMins < 30 ? "is" : "was";
+
+
// Get the status text (everything after "Kieran was/is ")
+
if (contentElement) {
+
const text = contentElement.textContent;
+
const statusText = text.replace(/^Kieran (is|was) /, "");
+
contentElement.textContent = `Kieran ${verb} ${statusText}`;
+
}
+
});
+
}
+
+
// Create a new post element
+
function createPostElement(record) {
+
const createdDate = new Date(record.value.createdAt);
+
const postElement = document.createElement("div");
+
postElement.className = "bsky-post";
+
postElement.dataset.cid = record.cid;
+
postElement.dataset.created = record.value.createdAt;
+
+
// Determine if status is recent (within 30 minutes)
+
const now = new Date();
+
const diffInMs = now - createdDate;
+
const diffInMins = diffInMs / (1000 * 60);
+
const verb = diffInMins < 30 ? "is" : "was";
+
+
postElement.innerHTML = `
+
<div class="bsky-post-content">Kieran ${verb} ${record.value.text}</div>
+
<div class="bsky-post-footer">
+
<cite>
+
<img src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg" alt="Kieran's avatar" class="avatar" />
+
<a href="https://bsky.app/@doing.dunkirk.sh" target="_blank" rel="noopener">@doing.dunkirk.sh</a>
+
</cite>
+
<span class="bsky-post-time">${formatTimeAgo(createdDate)}</span>
+
</div>
+
`;
+
+
return postElement;
+
}
+
+
// Fetch and update posts
+
function fetchAndUpdatePosts() {
+
fetch(API_URL)
+
.then((response) => response.json())
+
.then((data) => {
+
if (!data.records || data.records.length === 0) {
+
if (existingPosts.size === 0) {
+
container.innerHTML =
+
'<div class="bsky-post"><div class="bsky-post-content">No status updates found.</div></div>';
+
}
+
return;
+
}
+
+
// Sort newest first
+
const sortedRecords = data.records.sort((a, b) => {
+
return (
+
new Date(b.value.createdAt) -
+
new Date(a.value.createdAt)
+
);
+
});
+
+
// Track if we need to reorder
+
let needsReordering = false;
+
+
// Add new posts
+
for (const record of sortedRecords) {
+
if (!existingPosts.has(record.cid)) {
+
const newPostElement = createPostElement(record);
+
// Always insert at the beginning for now (we'll reorder if needed)
+
container.insertBefore(
+
newPostElement,
+
container.firstChild,
+
);
+
existingPosts.set(record.cid, {
+
element: newPostElement,
+
created: new Date(record.value.createdAt),
+
});
+
needsReordering = true;
+
}
+
}
+
+
// If we added new posts, reorder everything
+
if (needsReordering) {
+
const sortedElements = [...existingPosts.entries()]
+
.sort((a, b) => b[1].created - a[1].created)
+
.map((entry) => entry[1].element);
+
+
// Reattach in correct order
+
sortedElements.forEach((element) => {
+
container.appendChild(element);
+
});
+
}
+
+
// Update all timestamps
+
updateTimestamps();
+
})
+
.catch((error) => {
+
console.error("Error fetching status updates:", error);
+
});
+
}
+
+
// Initial update
+
fetchAndUpdatePosts();
+
+
// Update timestamps every minute
+
setInterval(updateTimestamps, 60000);
+
+
// Fetch new posts every 5 minutes
+
setInterval(fetchAndUpdatePosts, 300000);
+
});
+
</script>
-4
templates/shortcodes/webring.html
···
-
<span class="webring">
-
<a class="no-style" href={{prev}}><svg class="icons"><use href="{{ get_url(path='icons.svg#chevronLeft', trailing_slash=false) | safe }}"></use></svg></a>
-
<a href={{webring}}>{{webringName}}</a>
-
<a class="no-style" href={{next}}><svg class="icons"><use href="{{ get_url(path='icons.svg#chevronRight', trailing_slash=false) | safe }}"></use></svg></a></span>
+13 -10
templates/shortcodes/youtube.html
···
-
<div class="yt-embed">
-
<iframe
-
src="https://www.youtube-nocookie.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}"
-
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
-
webkitallowfullscreen
-
mozallowfullscreen
-
allowfullscreen
-
>
-
</iframe>
-
</div>
+
<figure class="yt-embed">
+
<iframe
+
src="https://www.youtube-nocookie.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}"
+
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+
webkitallowfullscreen
+
mozallowfullscreen
+
allowfullscreen
+
>
+
</iframe>
+
{% if caption %}
+
<figcaption>{{caption}}</figcaption>
+
{% endif %}
+
</figure>
+5 -7
templates/tags/list.html
···
-
{% extends "base.html" %}
-
-
{% block content %}
+
{% extends "base.html" %} {% block content %}
<h1>{{ taxonomy.name }}</h1>
<p>
-
{% for term in terms %}
-
<a href="{{ term.permalink | safe }}">#{{ term.name }}</a>[{{ term.pages | length }}]
-
{% endfor %}
+
{% for term in terms %}
+
<a href="{{ term.permalink | safe }}">{{ term.name }}</a>[{{ term.pages |
+
length }}] {% endfor %}
</p>
-
{% endblock content %}
+
{% endblock content %}
+20 -16
templates/tags/single.html
···
-
{% extends "base.html" %}
-
-
{% block content %}
+
{% extends "base.html" %} {% block content %}
<h1>{{ term.name }}</h1>
-
{% if paginator %}
-
{% set pages = paginator.pages %}
-
{% else %}
-
{% set pages = term.pages %}
-
{% endif %}
+
{% if paginator %} {% set pages = paginator.pages %} {% else %} {% set pages =
+
term.pages %} {% endif %}
<ul>
-
{% for page in pages %}
-
<li>
-
<a href="{{ page.permalink | safe }}">{% if page.date %}{{ page.date }} - {% endif %}{{ page.title }}</a>
-
</li>
-
{% endfor %}
+
{% for page in pages %}
+
<li>
+
<a href="{{ page.permalink | safe }}"
+
>{% if page.date %}{{ page.date }} - {% endif %}{{ page.title }}</a
+
>
+
</li>
+
{% endfor %}
</ul>
{% if paginator %}
-
<p>{% if paginator.previous %}<a href="{{ paginator.first }}">&lt;&lt; First</a> <a href="{{ paginator.previous }}">&lt; Previous</a>{% endif %} [{{ paginator.current_index }}/{{ paginator.number_pagers }}] {% if paginator.next %}<a href="{{ paginator.next }}">Next &gt;</a> <a href="{{ paginator.last }}">Last &gt;&gt;</a>{% endif %}</p>
-
{% endif %}
-
{% endblock content %}
+
<p>
+
{% if paginator.previous %}<a href="{{ paginator.first }}"
+
>&lt;&lt; First</a
+
>
+
<a href="{{ paginator.previous }}">&lt; Previous</a>{% endif %} [{{
+
paginator.current_index }}/{{ paginator.number_pagers }}] {% if
+
paginator.next %}<a href="{{ paginator.next }}">Next &gt;</a>
+
<a href="{{ paginator.last }}">Last &gt;&gt;</a>{% endif %}
+
</p>
+
{% endif %} {% endblock content %}
tools/bun.lockb

This is a binary file and will not be displayed.

-90
tools/genOG.ts
···
-
import puppeteer from "puppeteer";
-
import { readdir, mkdir } from "node:fs/promises";
-
-
const template = await Bun.file("tools/og.html").text();
-
-
const browser = await puppeteer.launch();
-
-
async function og(
-
postname: string,
-
outputPath: string,
-
width = 1200,
-
height = 630,
-
) {
-
const page = await browser.newPage();
-
-
await page.setViewport({ width, height });
-
-
await page.setContent(template.toString().replace("{{postname}}", postname));
-
-
await page.screenshot({ path: outputPath });
-
}
-
-
async function fileExists(path: string): Promise<boolean> {
-
try {
-
await Bun.file(path);
-
return true;
-
} catch (e) {
-
return false;
-
}
-
}
-
-
try {
-
// check if the public/blog folder exists
-
// if not exit
-
// if it does, get all the folders and then get the title tag from the index.html
-
-
if (!(await fileExists("public/"))) {
-
console.error("public/ does not exist");
-
process.exit(1);
-
}
-
-
// read all the files in the current directory filtering for index.htmls
-
const files = (await readdir("public/", { recursive: true })).filter((file) =>
-
file.endsWith("index.html"),
-
);
-
-
const directories = new Set(
-
files.map((file) => file.replace("index.html", "")),
-
);
-
-
const existing = (await readdir("static/")).filter((file) =>
-
directories.has(file),
-
);
-
-
// create not existing
-
for (const dir of directories) {
-
if (!existing.includes(dir)) {
-
await mkdir(`static/${dir.split("/").slice(0, -1).join("/")}`, {
-
recursive: true,
-
});
-
}
-
}
-
-
console.log("Generating OG images for", files.length, "files");
-
-
// for each file, get the title tag from the index.html
-
for (const file of files) {
-
const index = await Bun.file(`public/${file}`).text();
-
let title: string;
-
if (file.startsWith("tags/")) {
-
const parts = file.split("/");
-
title = `Tag: ${parts[1]}`; // take the next directory as the title
-
} else {
-
const match = index.match(/<title>(.*?)<\/title>/);
-
if (match) {
-
title = match[1];
-
} else {
-
console.error(`No title found for ${file}`);
-
continue;
-
}
-
}
-
-
console.log("Generating OG for", title);
-
await og(title, `static/${file.replace("index.html", "og.png")}`);
-
}
-
} catch (e) {
-
console.error(e);
-
} finally {
-
await browser.close();
-
}
-70
tools/og.html
···
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<style>
-
:root,
-
::backdrop {
-
color-scheme: dark;
-
--bg: #222529;
-
--bg-light: #464949;
-
--text: #d6d6d6;
-
--text-light: #c5c0b7;
-
--accent: #78b6ad;
-
--accent-light: #87c9e5;
-
--accent-text: var(--bg);
-
--border: #dbd5bc;
-
--link: #e2c8a2;
-
}
-
-
body {
-
font-weight: 600;
-
color: #d6d6d6;
-
background-color: #222529;
-
font-family: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono",
-
monospace;
-
display: flex;
-
flex-direction: column;
-
text-align: center;
-
}
-
-
div {
-
margin: 0;
-
display: flex;
-
flex-direction: column;
-
align-items: center;
-
justify-content: center;
-
height: 90vh; /* 90% of viewport height */
-
width: 90vw; /* 90% of viewport width */
-
padding: 5vh 5vw; /* 5% border on all sides */
-
box-sizing: border-box;
-
align-self: center;
-
}
-
-
h1 {
-
font-size: calc(
-
2rem + 2vw
-
); /* Adjust font size based on viewport width */
-
margin: 0.5em 0;
-
line-height: 1.1;
-
}
-
-
h1::before {
-
color: var(--accent);
-
content: "# ";
-
}
-
-
p {
-
margin: 1rem 0;
-
font-size: calc(
-
1rem + 1vw
-
); /* Adjust font size based on viewport width */
-
}
-
</style>
-
</head>
-
<body>
-
<div>
-
<h1>{{postname}}</h1>
-
<p>By Kieran Klukas</p>
-
</div>
-
</body>
-
</html>
-15
tools/package.json
···
-
{
-
"name": "zera",
-
"module": "index.ts",
-
"type": "module",
-
"scripts": {
-
"gen": "bun run tools/genOG.ts"
-
},
-
"devDependencies": {
-
"@types/bun": "latest",
-
"puppeteer": "^23.6.0"
-
},
-
"peerDependencies": {
-
"typescript": "^5.0.0"
-
}
-
}