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

Compare changes

Choose any two refs to compare.

Changed files
+5478 -1858
.github
images
content
hooks
sass
scripts
static
badges
blog
adding-a-copy-button
analyzing-implications-of-online-safety-legislation
atuin
degraded-zpool-proxmox
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
ssd-removal-mbp-2017
tangled-sync
favicon
js
now
pfp
pfps
tags
accessibility
archival
atproto
biography
cool-stuff
essays
fancy
hilton
homelab
meta
mildrant
nix
physics
project
reverse-engineering
shell
teardown
tool
tutorial
yap-fest
verify
syntaxes
templates
tools
.github/images/preview.webp

This is a binary file and will not be displayed.

+4 -1
.gitignore
···
public
node_modules
-
.env
···
public
+
.zola-build
node_modules
+
.env
+
.crush
+
.DS_Store
+5
.imgbotconfig
···
···
+
{
+
"schedule": "daily",
+
"aggressiveCompression": "true"
+
}
+
+1 -1
README.md
···
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`.
<p align="center">
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
···
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` (and then removed again lol).
<p align="center">
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+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=="],
+
}
+
}
+2 -3
config.toml
···
highlight_code = true
highlight_theme = "css"
[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 = "/root" },
{ url = "/verify", name = "/verify" },
{ url = "/blog", name = "/blog" },
]
···
highlight_code = true
highlight_theme = "css"
+
extra_syntaxes_and_themes = ["syntaxes"]
[slugify]
paths = "on"
···
author = "Kieran Klukas"
display_author = true
default_theme = "light"
list_pages = false
twitter_card = true
···
header_nav = [
{ url = "/", name = "/root" },
{ url = "/verify", name = "/verify" },
+
{ url = "/now", name = "/now" },
{ url = "/blog", name = "/blog" },
]
+27 -14
content/_index.md
···
+++
+++
-
<div style="display: flex; justify-content: center; margin: 2rem;">
-
<img src="/pfps/starry.webp" alt="kieran with a white and gray spotted kitten with a grainy background and star dust" width="512" height="512" class="u-photo"/>
</div>
# About me
-
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 :)
-
> init.ts
-
```ts
-
const kieran = {
-
name: "kieran klukas",
-
age: {{ age(length=2 comma=true) }}
-
education: ["Homeschooled", "Dual Enrollment"],
-
favFoods: ["lo mein", "bacon fried rice", "pretty much any meat"]
}
```
-
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!
# Want to talk to me?
-
Do you want to hire me for a project? If you just have a question or want to talk I'll still answer (admittedly more slowly ^-^).
-
- Email: [me@dunkirk.sh](mailto:me@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/))
-
- If you just want to know when I make a new post then you can subscribe to the [rss](rss.xml) feed ^-^
···
+++
+++
+
<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
+
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 :)
+
+
> flake.nix
+
+
```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;
+
};
}
```
+
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?
+
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: [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/))
+
- If you just want to know when I make a new post then you can subscribe to the [:rss:](rss.xml) feed
+17 -20
content/blog/2023-07-10_install-truenas-core-proxmox.md
···
[taxonomies]
tags = ["tutorial", "archival"]
-
-
[extra]
-
has_toc = true
+++
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 -->
-
{{ 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") }}
## 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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
I chose not to save the configuration file when prompted, proceeded to install the updates, and rebooted.
···
[taxonomies]
tags = ["tutorial", "archival"]
+++
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.
+
![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).
+
![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.
+
![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.
+
![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:
+
![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.
+
![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.
+
![destination media screen](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c53f8a58f746d0ae045882c9563d83219ab9f29f_xD5QxmFtHxw10p624FgwM.png)
Hit enter one last time and enter your password.
+
![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.
+
![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.
+
![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.
+
![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.
+
![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.
+
![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.
+
![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.
+9 -12
content/blog/2023-08-04_garmin-vivoactive-homeassistant.md
···
[taxonomies]
tags = ["essays", "archival"]
-
-
[extra]
-
has_toc = true
+++
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 -->
-
{{ 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 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") }}
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") }}
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") }}
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") }}
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 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") }}
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).
···
[taxonomies]
tags = ["essays", "archival"]
+++
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.
+
![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.
+
![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.
+
![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.
+
![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.
+
![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).
+
![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).
+1 -4
content/blog/2023-11-01_censorship-or-protection.md
···
[taxonomies]
tags = ["essays", "archival"]
-
-
[extra]
-
has_toc = true
+++
<!-- more -->
-
{{ 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?") }}
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?
···
[taxonomies]
tags = ["essays", "archival"]
+++
<!-- 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?
+2 -5
content/blog/2023-11-10_monaspace-vs-code-install.md
···
[taxonomies]
tags = ["tutorial", "archival"]
-
-
[extra]
-
has_toc = true
+++
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 -->
-
{{ 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.") }}
## 1. Download and install the Monaspace font:
···
- 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:
···
[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:
···
- 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:
+8 -11
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 ^_^
<!-- more -->
-
{{ 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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
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") }}
## Postlog and notes
···
[taxonomies]
tags = ["tutorial", "teardown", "archival"]
+++
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 ^_^
<!-- more -->
+
![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.
+
![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)
+
![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.
+
![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
+
![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
+
![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)
+
![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!
+
![the SSD out of the MacBook](https://hc-cdn.hel1.your-objectstorage.com/s/v3/85632b1b9443498a770aae1687b6ff594aad79ff_9img_1846.jpg)
## Postlog and notes
+69 -84
content/blog/2024-10-11_example_post.md
···
+++
-
title = "Test Post"
date = 2024-10-11
-
slug = "test-post"
-
description = "Testing out styling and features."
[taxonomies]
tags = ["meta"]
···
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.
### 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.
## 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. โ”‚
-
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
-
```
### Syntax Highlighting
If you want syntax coloring, you put the name of the programming language immediately after the ticks.
So writing this:
-
~~~
```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."
-
-
[taxonomies]
-
tags = ["meta"]
```
### 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
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>
-
-
But there is no way to do this in markdown so you have to use the `<figure>` tag like so:
-
-
```html
-
<figure>
-
<img src="/path/to/image.png" alt="Alt text goes here.">
-
<figcaption>Caption text goes here.</figcaption>
-
</figure>
```
-
### Videos
-
To embed a video, you use the `youtube` shortcode e.g.
-
> post.md
-
```md
-
{{/* youtube(id="kiWvNwuBbEE") */}}
```
-
You can also add the `autoplay=true` flag to make the video autoplay.
{{ youtube(id="NodwjZF7uZw") }}
-
The shortcode is processed into an iframe which looks like this:
-
> post.html
-
```html
-
{{ youtube(id="kiWvNwuBbEE") }}
-
```
## Miscellaneous
···
---
But these should be used sparingly, if at all.
···
+++
+
title = "The *Mega* test case"
date = 2024-10-11
+
slug = "mega"
+
description = "How I write / leme check if that broke anything page"
[taxonomies]
tags = ["meta"]
···
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`.
+
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
```
+
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
+
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/index.ts`, I can produce this:
+
> 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 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!"
### 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>
## Embedding Media
···
You can also add captions to images:
+
```terra
+
{{/* img(id="https://url.com/image.png" alt="alt text" caption="this can be ommited if you want or added! It's optional :)") */}}
```
+
![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"}
+
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(id="", autoplay?=bool)` shortcode e.g.
{{ youtube(id="NodwjZF7uZw") }}
+
### Bluesky posts
+
This is handled by a shortcode `bluesky(post="")` and takes the post url as a parameter. These will automatically attach images and videos.
+
+
{{ 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 %}
+5 -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 ^_^)
···
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") }}
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") }}
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 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!") }}
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.") }}
···
[taxonomies]
tags = ["reverse engineering", "hilton"]
+++
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 ^_^)
···
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.
+
![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
+
![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://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://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 :)
+
![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."}
+2 -5
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.
<!-- more -->
-
{{ 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") }}
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") }}
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.
···
[taxonomies]
tags = ["reverse engineering", "hilton"]
+++
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.
<!-- 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://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 ([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.
+1 -3
content/blog/2024-12-16_airbuds.md
···
date = 2024-12-16
slug = "airbuds"
description = "Trying to break their api."
[taxonomies]
tags = ["reverse engineering", "graphql"]
-
-
[extra]
-
has_toc = true
+++
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.
···
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.
+3 -5
content/blog/2025-01-01_spotify-to-apple-music.md
···
date = 2025-01-01
slug = "spotify-to-apple-music"
description = "Homegrown solution rather than paying for it ^-^"
[taxonomies]
tags = ["apple", "music"]
-
-
[extra]
-
has_toc = true
+++
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://cloud-r47l8h2er-hack-club-bot.vercel.app/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://cloud-pbd6jl8ws-hack-club-bot.vercel.app/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!
···
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!
+4 -7
content/blog/2025-01-31_my-life-story-with-tech.md
···
[taxonomies]
tags = ["yap fest", "biography"]
-
-
[extra]
-
has_toc = true
+++
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!
···
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!
-
{{ img(id="https://cloud-bwc8bo8f2-hack-club-bot.vercel.app/0image_from_ios.jpg" alt="2 boxes of electronics sitting on a closet shelf" 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.
-
{{ img(id="https://cloud-p1nlhyynq-hack-club-bot.vercel.app/0contributions-graph.gif" alt="gif of my github contributions graph 2021-2025" 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!
-
{{ img(id="https://github.com/thepurplebubble/brand/blob/main/public/logo.png?raw=true" alt="purple bubble logo" 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)
-
{{ img(id="https://cloud-qaa4875b8-hack-club-bot.vercel.app/0image.png" alt="the cockroach charges in hcb" 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!
···
[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!
···
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!
+4 -7
content/blog/2025-02-02_degraded-zpool-proxmox.md
···
[taxonomies]
tags = ["homelab", "tutorial"]
-
-
[extra]
-
has_toc = true
+++
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 -->
-
{{ img(id="https://cloud-n6m4bt2xl-hack-club-bot.vercel.app/2image.png" alt="the zpool reporting a downed disk" 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://cloud-pi335w1l0-hack-club-bot.vercel.app/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.
···
ata-ST3750640NS_3QD0BN6V
```
-
{{ img(id="https://cloud-d0bjeue06-hack-club-bot.vercel.app/0image_from_ios.jpg" alt="chick-fil-a macaroni and cheese with 2 nuggets and some ketchup" 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:
···
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 :)
-
{{ img(id="https://cloud-n6m4bt2xl-hack-club-bot.vercel.app/0image.png" alt="the zpool reporting a downed disk" caption="It's slow but faster then I expected for HDDs") }}
···
[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.
···
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:
···
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"}
+1 -4
content/blog/2025-02-15_remove-exif-git-hook.md
···
[taxonomies]
tags = ["mildrant", "tutorial"]
-
-
[extra]
-
has_toc = true
+++
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.
···
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!
-
{{ img(id="https://cdn.hackclubber.dev/slackcdn/9049d20038cc3058acee1bbe58c5ac3f.png" alt="the commit hook finally working!" 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!
···
[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.
···
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!
-3
content/blog/2025-02-26_spherical-ray-diagrams.md
···
[taxonomies]
tags = ["tool", "fancy", "physics"]
-
-
[extra]
-
has_toc = true
+++
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!
···
[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!
+16 -23
content/blog/2025-03-14_my-animations.md
···
[taxonomies]
tags = ["tool", "fancy", "physics"]
-
-
[extra]
-
has_toc = true
+++
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 ๐ŸŽ‰
···
{{ youtube(id="O7SYcdUM8mI", caption="2021.01.27 first jelly jar") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/2f6bd410317b341df20d8349771bb716.jpg" alt="tesla in a showroom with fire jets" caption="2021.02.10 tesla showroom") }}
{{ youtube(id="7Ozt7WcVwt0", caption="2021.09.27 Chalet a la Tagia minecraft animation") }}
-
{{ img(id="https://cdn.hackclubber.dev/slackcdn/90335a1f835749fe219a677a24aedd02.png" alt="cube diorama" caption="2021.12.15 cube diorama") }}
{{ youtube(id="O5iHoFwKQuE", caption="2021.12.17 creature walk cycle test") }}
···
{{ youtube(id="Gy0K-Gi95Jg", caption="2022.01.22 lost music visualization") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/c1759999d78c2a99312b5b34562c6f72.png" alt="ice sphere" caption="2022.01.24 ice icosphere") }}
-
{{ img(id="https://cdn.hackclubber.dev/slackcdn/c9587628d15c605e99a3a72769bad60a.png" alt="glass jar with marbles" caption="2022.01.27 marble jar") }}
{{ youtube(id="ue-hy7w1-JE", caption="2022.02.08 firefly particle sim") }}
···
{{ youtube(id="BGJbmXqCD5M", caption="2022.03.16 molecular plexus") }}
-
{{ img(id="https://cdn.fluff.pw/slackcdn/57287dfc2f5eb7d6effe43f7ebbde030.png" alt="twisted torus with flattened sphere in the center" 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") }}
-
{{ img(id="https://cdn.fluff.pw/slackcdn/b47952a40fd7695b9667357fb0c8386b.png" alt="airship far bottom" caption="2022.03.31 airship") }}
-
{{ img(id="https://cdn.hackclubber.dev/slackcdn/52c9ccd4378fdac717a5a9f1b924000d.png" alt="airship far side") }}
-
{{ img(id="https://cdn.fluff.pw/slackcdn/4138d497e2deb1f9c7369762e08e5a7c.png" alt="airship front top") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/bac28fe85728b6d009c4e520ccff3140.png" alt="airship front cab") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/963198d64f001f55d703cdc65a257e98.png" alt="airship front side") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/d3c7021428757b3bd607ffdbcc9daa7f.png" alt="minecraft village front door with villager" caption="2022.04.06 viking village") }}
-
{{ img(id="https://cdn.hackclubber.dev/slackcdn/55aff1a73336f1262e625a667ca4d7f8.png" alt="minecraft village from across a small lake with a skeleton and witch" 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") }}
-
{{ img(id="https://cdn.hack.ngo/slackcdn/ab7015d03697d62ebedad011161b40db.gif" alt="clay animation of cubes within cubes looping endlessly" caption="2022.05.02 infinite cubes") }}
-
{{ youtube(id="zqyv7GBTLGA", caption="2022.05.30 fire handwriting") }}
-
{{ img(id="https://cdn.fluff.pw/slackcdn/42e90de7ab09d863c735a6ca74069fdc.png" alt="a cylinder with a bunch of bumps on it" caption="2022.06.24 the cylinder") }}
-
-
{{ img(id="https://cdn.hack.ngo/slackcdn/dc691e3fe9382467a01821aa966e253b.jpg" alt="a castle made out of mud with a mud giant and ufo" caption="2022.07.15 mud castle") }}
{{ youtube(id="XVyMUROofZ8", caption="2022.07.21 the iconic donut") }}
···
{{ youtube(id="zRlgWbW1Qcw", caption="2022.08.01 mirror physics") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/b5876d1491b397c1a80fb8d7411fd627.png" alt="the earth from space" caption="2022.08.30 the earth") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/bb000a699c4aa41413a282a31cfb0f59.png" alt="11 glowing pendulums swinging in a flowing curve" caption="2022.08.31 glowing pendulums") }}
-
{{ img(id="https://cdn.hack.pet/slackcdn/f1cdd1a422c0a3280541f3bbab4f662e.png" alt="an orange flower in a flower pot with skyline dirt" caption="2022.10.22 orange flower") }}
## 2023
···
[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 ๐ŸŽ‰
···
{{ 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="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="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="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
+3 -1
content/blog/2025-04-24_atuin.md
···
Here's the basic scaffolding you'll need for your Nix configuration:
> configuration.nix
```nix
{ config, pkgs, ... }:
···
and then the home-manager bit
> shell.nix in home manager
```nix
{ config, pkgs, ... }:
···
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.sh/@dunkirk.sh/serif) ๐Ÿ‘€)
```
> atuin stats
···
Here's the basic scaffolding you'll need for your Nix configuration:
> configuration.nix
+
```nix
{ config, pkgs, ... }:
···
and then the home-manager bit
> shell.nix in home manager
+
```nix
{ config, pkgs, ... }:
···
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
+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.
+9
content/now.md
···
···
+
+++
+
title = "Now"
+
description = "My status updates"
+
template = "page.html"
+
+++
+
+
## Status Updates
+
+
{{ now_status() }}
+15 -1
content/pfp.md
···
# 2025
-
February 27th to Present
![kieran with an orange cast in a polaroid over a pinkish background](/pfps/instsqc-rat-pfp.webp)
···
# 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)
+13 -14
content/verify.md
···
# 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.
-
If you want to contact me, via email to `me@dunkirk.sh` or `me@kieranklukas.com` are both good places to find me. 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`
-
* Phone #: *Do you really think i'm going to publicly share that?*
# keys
-
* SSH
-
```ssh
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
```
-
```
···
# 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
```
+31 -1
hooks/pre-commit
···
# 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)
···
# 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)
+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"
+
}
+
}
+48 -3
sass/css/_copy-button.scss
···
--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");
-
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;
···
pre {
margin: 0;
box-shadow: none;
-
border-radius: 0 0 0.2em 0.2em;
}
}
···
--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;
···
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;
+
}
+3
sass/css/_view-transitions.scss
···
···
+
@view-transition {
+
navigation: auto;
+
}
+3 -1
sass/css/main.scss
···
@use "mods";
@use "copy-button";
-
@use "theme-toggle";
···
@use "mods";
@use "copy-button";
+
@use "view-transitions";
+
@use "emoji-inline";
+
@use "lightbox";
+274 -121
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;
}
#footer-container {
-
display: flex;
-
flex-direction: column;
-
justify-content: center;
-
align-items: center;
-
text-align: center;
-
padding-bottom: 0.5rem;
}
#footer-container p {
-
margin: 0;
}
.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;
}
.title-list li {
-
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;
}
.icons:hover {
-
background-color: transparent;
-
color: var(--accent);
}
/* footnotes */
.footnote-definition {
-
margin: 0 0 0 0.125rem;
}
.footnote-definition-label {
-
color: var(--accent);
}
.footnote-definition p {
-
display: inline;
-
margin: 0.625rem 0 0 0.625rem;
}
/* general classes */
.no-style {
-
padding: 0;
-
margin: 0;
-
border: none;
-
border-radius: 0;
}
.no-style:hover {
-
background-color: transparent;
-
color: var(--accent);
}
.center {
-
text-align: center;
}
-
.center img {
-
display: block;
-
margin: 1rem auto;
}
.center figcaption {
-
text-align: center;
}
.float-right {
-
float: right;
}
.float-left {
-
float: left;
}
-
div > code,
li code,
p code {
-
padding: 0.1em 0.3em 0.1em 0.3em;
-
color: var(--text-dark);
-
background-color: var(--bg-light);
}
pre {
-
border-top-left-radius: 0;
}
blockquote {
-
padding-top: 0.2rem;
-
padding-bottom: 0.2rem;
}
blockquote:has(+ pre) {
-
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);
}
blockquote:has(+ pre) p::selection {
-
background: var(--pink-puree);
}
.yt-embed {
-
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;
-
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;
-
gap: 0.5rem;
}
cite a {
-
display: inline-flex;
-
align-items: center;
}
cite a img.avatar {
-
margin-right: 5px;
}
-
:root {
-
--nightshade-violet: oklch(0.27 0.0242 287.67);
-
--purple-night: oklch(27.58% 0.0203 289.13);
-
--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(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);
}
···
#nav-bar {
+
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-direction: column;
+
justify-content: center;
+
align-items: center;
+
text-align: center;
+
padding-bottom: 0.5rem;
}
#footer-container p {
+
margin: 0;
}
.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;
}
.title-list li {
+
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;
}
.icons:hover {
+
background-color: transparent;
+
color: var(--accent);
}
/* footnotes */
.footnote-definition {
+
margin: 0 0 0 0.125rem;
}
.footnote-definition-label {
+
color: var(--accent);
}
.footnote-definition p {
+
display: inline;
+
margin: 0.625rem 0 0 0.625rem;
}
/* general classes */
.no-style {
+
padding: 0;
+
margin: 0;
+
border: none;
+
border-radius: 0;
}
.no-style:hover {
+
background-color: transparent;
+
color: var(--accent);
}
.center {
+
text-align: center;
}
+
.center .img-container {
+
margin: 1rem auto;
}
.center figcaption {
+
text-align: center;
}
.float-right {
+
float: right;
}
.float-left {
+
float: left;
}
+
div>code,
li code,
p code {
+
padding: 0.1em 0.3em 0.1em 0.3em;
+
color: var(--text-dark);
+
background-color: var(--bg-light);
}
pre {
+
border-top-left-radius: 0;
}
blockquote {
+
padding-top: 0.2rem;
+
padding-bottom: 0.2rem;
}
blockquote:has(+ pre) {
+
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);
}
blockquote:has(+ pre) p::selection {
+
background: var(--pink-puree);
}
.yt-embed {
+
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;
+
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);
+
}
+415 -375
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;
-
--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: var(--earth-yellow);
-
--noise-strength: 0.3;
-
--bg-light: #cbcdcd;
-
--text: #41474e;
-
--text-light: #686764;
-
--text-dark: #20252b;
-
--accent: oklch(35.55% 0.0754 60.09);
-
--accent-dark: #56412bc5;
-
--accent-text: #dfd1bc;
-
--border: #646868;
-
--link: var(--accent);
-
--selection: color-mix(in oklab, var(--accent), var(--earth-yellow) 50%);
-
}
-
-
/* theme media queries */
-
@media (prefers-color-scheme: dark) {
-
:root,
-
::backdrop {
-
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);
-
--border: var(--pink-puree);
-
--selection: color-mix(
-
in oklab,
-
var(--accent),
-
var(--purple-night) 50%
-
);
-
}
-
}
-
-
[data-theme="dark"] {
-
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);
-
--border: var(--pink-puree);
-
--selection: color-mix(in oklab, var(--accent), var(--purple-night) 50%);
-
}
-
-
@media (prefers-color-scheme: light) {
-
:root,
-
::backdrop {
-
color-scheme: light;
-
--bg: var(--earth-yellow);
-
--noise-strength: 0.15;
-
--bg-light: #cbcdcd;
-
--text: #41474e;
-
--text-light: #686764;
-
--accent: #58310ac5;
-
--accent-dark: #e08f67;
-
--accent-text: #dfd1bc;
-
--border: #646868;
-
--link: var(--accent);
-
--selection: color-mix(
-
in oklab,
-
var(--accent),
-
var(--earth-yellow) 50%
-
);
-
}
-
}
-
[data-theme="light"] {
-
/* default (light) theme */
-
color-scheme: light;
-
--bg: var(--earth-yellow);
-
--noise-strength: 0.22;
-
--bg-light: var(--reseda-green);
-
--text: #41474e;
-
--text-light: #686764;
-
--text-dark: #20252b;
-
--accent: #58310ac5;
-
--accent-dark: #56412bc5;
-
--accent-text: #dfd1bc;
-
--border: #646868;
-
--link: var(--accent);
-
--selection: color-mix(in oklab, var(--accent), var(--earth-yellow) 50%);
}
::selection,
::-moz-selection {
-
color: var(--bg);
-
background: var(--selection);
}
/* chromium scrollbars */
::-webkit-scrollbar {
-
width: 8px;
-
height: 8px;
-
overflow: visible;
}
::-webkit-scrollbar-thumb {
-
background: var(--accent);
-
width: 12px;
}
::-webkit-scrollbar-track {
-
background: var(--bg-light);
}
/* firefox scrollbars */
* {
-
scrollbar-color: var(--accent) var(--bg-light);
-
scrollbar-width: auto;
}
html {
-
color-scheme: light dark;
-
font-family: var(--mono-font);
-
scroll-behavior: smooth;
}
body {
-
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 > footer {
-
color: var(--text-light);
-
font-size: 0.875;
}
/* Format headers */
h1 {
-
font-size: 2rem;
}
h2 {
-
font-size: 1.75rem;
}
h3 {
-
font-size: 1.5rem;
}
h4 {
-
font-size: 1.25rem;
}
h5 {
-
font-size: 1rem;
}
h6 {
-
font-size: 0.75rem;
}
h1,
···
h4,
h5,
h6 {
-
margin: 0.5em 0 0.5em 0;
-
padding: 0.22em 0.4em 0.22em 0.4em;
-
border-radius: 0.1em;
-
background-color: var(--accent);
-
color: var(--accent-text);
-
width: fit-content;
}
/* Fix line height when title wraps */
h1,
h2,
h3 {
-
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;
-
}
}
p {
-
margin: 1rem 0;
}
/* format links */
-
a,
a:visited {
-
text-decoration: none;
-
font-weight: bold;
-
border-radius: 0.125rem;
-
color: var(--accent-dark);
}
-
a {
-
color: var(--link);
}
-
a:hover {
-
text-decoration: underline wavy;
}
/* format lists */
ul {
-
list-style: none;
-
margin-top: 0.25rem;
-
margin-bottom: 0.25rem;
}
ol {
-
list-style-type: decimal;
-
margin-top: 0.25rem;
-
margin-bottom: 0.25rem;
}
li {
-
margin-bottom: 0.125rem;
}
ul li::marker {
-
content: "* ";
-
color: var(--accent);
-
font-size: 1.1rem;
}
ol li::marker {
-
color: var(--accent);
}
ol li:hover::marker {
-
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;
}
/* List items are inline elements, make them behave more like blocks */
-
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);
}
aside {
-
font-size: 1rem;
-
width: 35%;
-
padding: 0 10px;
-
margin-inline-start: 10px;
-
float: right;
}
*[dir="rtl"] aside {
-
float: left;
}
/* make aside full-width on mobile */
@media only screen and (max-width: 720px) {
-
aside {
-
width: 100%;
-
float: none;
-
margin-inline-start: 0;
-
}
}
details {
-
padding: 0.5rem;
}
summary {
-
cursor: pointer;
-
font-weight: bold;
-
word-break: break-all;
}
-
details[open] > summary + * {
-
margin-top: 0;
}
-
details[open] > summary {
-
margin-bottom: 0.5rem;
}
-
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;
}
td,
th {
-
border: 1px solid var(--border);
-
text-align: start;
-
padding: 0.5rem;
}
th {
-
background-color: var(--bg-light);
-
font-weight: bold;
}
tr:nth-child(even) {
-
background-color: var(--bg-light);
}
table caption {
-
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);
}
-
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;
}
textarea,
select,
input {
-
color: var(--text);
-
background-color: var(--bg);
-
border: 1px dashed var(--border);
}
label {
-
display: block;
}
fieldset label {
-
margin: 0 0 0.3rem 0;
}
textarea {
-
max-width: 43.5rem;
-
resize: both;
}
textarea:not([cols]) {
-
width: 100%;
}
@media only screen and (max-width: 720px) {
-
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;
}
.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);
}
input[type="range"] {
-
padding: 0;
-
color: var(--accent);
}
abbr[title] {
-
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-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;
}
/* checkbox and radio button style */
input[type="checkbox"],
input[type="radio"] {
-
vertical-align: middle;
-
position: relative;
-
width: min-content;
-
width: 14px;
-
height: 14px;
}
-
input[type="checkbox"] + label,
-
input[type="radio"] + label {
-
display: inline-block;
}
input[type="radio"] {
-
border-radius: 100%;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
-
background-color: var(--accent);
}
@media only screen and (max-width: 720px) {
-
textarea,
-
select,
-
input {
-
width: 100%;
-
}
}
input[type="color"] {
-
height: 2.5rem;
-
padding: 0.2rem;
}
input[type="file"] {
-
border: 0;
}
/* misc body elements */
hr {
-
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);
}
mark a {
-
color: var(--link);
}
img,
video,
iframe[src^="https://www.youtube-nocookie.com"],
-
iframe[src^="https://www.youtube.com"]
-
{
-
max-width: 90%;
-
margin: 1rem;
-
height: auto;
-
border: dashed 2px var(--accent);
-
border-radius: 15px;
-
opacity: 0.95;
}
figure {
-
margin: 0;
-
display: block;
-
overflow-x: auto;
}
figcaption {
-
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;
}
p:has(cite) {
-
text-align: right;
-
font-size: 0.875rem;
-
color: var(--text-light);
-
font-weight: 600;
}
cite::before {
-
content: "โ€” ";
}
dt {
-
color: var(--text-light);
}
code,
···
pre span,
kbd,
samp {
-
font-family: var(--mono-font);
}
pre {
-
border: 1px solid var(--accent);
-
max-height: 30rem;
-
padding: 0.625rem;
-
overflow-x: auto;
-
font-style: monospace;
}
p code,
li code,
div code {
-
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;
}
iframe {
-
max-width: 90%;
}
/* progress bars */
progress {
-
width: 100%;
}
progress:indeterminate {
-
background-color: var(--bg-light);
}
progress::-webkit-progress-bar {
-
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);
}
progress::-moz-progress-bar {
-
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);
}
dialog {
-
max-width: 40rem;
-
margin: auto;
}
dialog::backdrop {
-
background-color: var(--bg);
-
opacity: 0.8;
}
@media only screen and (max-width: 720px) {
-
dialog {
-
max-width: 100%;
-
margin: auto 1em;
-
}
}
/* superscript & subscript */
/* prevent scripts from affecting line-height. */
sup,
sub {
-
vertical-align: baseline;
-
position: relative;
}
sup {
-
top: -0.4em;
}
sub {
-
top: 0.3em;
-
}
···
: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;
+
--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 */
+
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(--selection);
}
/* chromium scrollbars */
::-webkit-scrollbar {
+
width: 8px;
+
height: 8px;
+
overflow: visible;
}
+
::-webkit-scrollbar-thumb {
+
background: var(--accent);
+
width: 12px;
}
+
::-webkit-scrollbar-track {
+
background: var(--bg-light);
}
/* firefox scrollbars */
* {
+
scrollbar-color: var(--accent) var(--bg-light);
+
scrollbar-width: auto;
}
html {
+
color-scheme: light dark;
+
font-family: var(--mono-font);
+
scroll-behavior: smooth;
}
body {
+
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>footer {
+
color: var(--text-light);
+
font-size: 0.875;
}
/* Format headers */
h1 {
+
font-size: 2rem;
}
+
h2 {
+
font-size: 1.75rem;
}
+
h3 {
+
font-size: 1.5rem;
}
+
h4 {
+
font-size: 1.25rem;
}
+
h5 {
+
font-size: 1rem;
}
+
h6 {
+
font-size: 0.75rem;
}
h1,
···
h4,
h5,
h6 {
+
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;
}
@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;
+
}
}
p {
+
margin: 1rem 0;
}
/* format links */
+
a {
+
color: var(--link);
+
text-decoration: none;
+
font-weight: 600;
+
transition: color 120ms ease;
+
}
+
a:visited {
+
color: var(--link-visited);
}
+
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;
}
ol {
+
list-style-type: decimal;
+
margin-top: 0.25rem;
+
margin-bottom: 0.25rem;
}
li {
+
margin-bottom: 0.125rem;
}
ul li::marker {
+
content: "* ";
+
color: var(--accent);
+
font-size: 1.1rem;
}
ol li::marker {
+
color: var(--accent);
}
ol li:hover::marker {
+
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;
}
/* List items are inline elements, make them behave more like blocks */
+
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);
}
aside {
+
font-size: 1rem;
+
width: 35%;
+
padding: 0 10px;
+
margin-inline-start: 10px;
+
float: right;
}
+
*[dir="rtl"] aside {
+
float: left;
}
/* make aside full-width on mobile */
@media only screen and (max-width: 720px) {
+
aside {
+
width: 100%;
+
float: none;
+
margin-inline-start: 0;
+
}
}
details {
+
padding: 0.5rem;
}
summary {
+
cursor: pointer;
+
font-weight: bold;
+
word-break: break-all;
}
+
details[open]>summary+* {
+
margin-top: 0;
}
+
details[open]>summary {
+
margin-bottom: 0.5rem;
}
+
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;
}
td,
th {
+
border: 1px solid var(--border);
+
text-align: start;
+
padding: 0.5rem;
}
th {
+
background-color: var(--bg-light);
+
font-weight: bold;
}
tr:nth-child(even) {
+
background-color: var(--bg-light);
}
table caption {
+
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);
}
+
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;
}
textarea,
select,
input {
+
color: var(--text);
+
background-color: var(--bg);
+
border: 1px dashed var(--border);
}
label {
+
display: block;
}
fieldset label {
+
margin: 0 0 0.3rem 0;
}
textarea {
+
max-width: 43.5rem;
+
resize: both;
}
textarea:not([cols]) {
+
width: 100%;
}
@media only screen and (max-width: 720px) {
+
+
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;
}
.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);
}
input[type="range"] {
+
padding: 0;
+
color: var(--accent);
}
abbr[title] {
+
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-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;
}
/* checkbox and radio button style */
input[type="checkbox"],
input[type="radio"] {
+
vertical-align: middle;
+
position: relative;
+
width: 14px;
+
height: 14px;
}
+
input[type="checkbox"]+label,
+
input[type="radio"]+label {
+
display: inline-block;
}
input[type="radio"] {
+
border-radius: 100%;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
+
background-color: var(--accent);
}
@media only screen and (max-width: 720px) {
+
+
textarea,
+
select,
+
input {
+
width: 100%;
+
}
}
input[type="color"] {
+
height: 2.5rem;
+
padding: 0.2rem;
}
input[type="file"] {
+
border: 0;
}
/* misc body elements */
hr {
+
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);
}
mark a {
+
color: var(--link);
}
img,
video,
iframe[src^="https://www.youtube-nocookie.com"],
+
iframe[src^="https://www.youtube.com"] {
+
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;
}
figcaption {
+
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;
+
}
+
+
/* 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;
}
cite::before {
+
content: "โ€” ";
}
dt {
+
color: var(--text-light);
}
code,
···
pre span,
kbd,
samp {
+
font-family: var(--mono-font);
}
pre {
+
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);
}
pre code {
+
padding: 0;
+
border-radius: 0;
+
color: inherit;
+
background-color: inherit;
}
iframe {
+
max-width: 90%;
}
/* progress bars */
progress {
+
width: 100%;
}
progress:indeterminate {
+
background-color: var(--bg-light);
}
progress::-webkit-progress-bar {
+
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);
}
progress::-moz-progress-bar {
+
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);
}
dialog {
+
max-width: 40rem;
+
margin: auto;
}
dialog::backdrop {
+
background-color: var(--bg);
+
opacity: 0.8;
}
@media only screen and (max-width: 720px) {
+
dialog {
+
max-width: 100%;
+
margin: auto 1em;
+
}
}
/* superscript & subscript */
/* prevent scripts from affecting line-height. */
sup,
sub {
+
vertical-align: baseline;
+
position: relative;
}
sup {
+
top: -0.4em;
}
sub {
+
top: 0.3em;
+
}
+87 -366
sass/css/syntax-theme.css
···
* theme "Catppuccin" generated by syntect
*/
-
html[data-theme="light"] .z-code {
-
color: #4c4f69;
-
background-color: var(--sunset);
-
}
-
-
html[data-theme="light"] .z-comment {
-
color: #9ca0b0;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-string {
-
color: #40a02b;
-
}
-
html[data-theme="light"] .z-string.z-regexp {
-
color: #fe640b;
-
}
-
html[data-theme="light"] .z-constant.z-numeric {
-
color: #fe640b;
-
}
-
html[data-theme="light"] .z-constant.z-language.z-boolean {
-
color: #fe640b;
-
font-weight: bold;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-constant.z-language {
-
color: #7287fd;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-support.z-function.z-builtin {
-
color: #fe640b;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-other.z-constant {
-
color: #fe640b;
-
}
-
html[data-theme="light"] .z-keyword {
-
color: #d20f39;
-
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;
-
}
-
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;
-
}
-
html[data-theme="light"] .z-support.z-type.z-exception {
-
color: #fe640b;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-keyword.z-operator,
-
html[data-theme="light"] .z-punctuation.z-accessor {
-
color: #04a5e5;
-
font-weight: bold;
-
}
-
html[data-theme="light"] .z-punctuation.z-separator {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-punctuation.z-terminator {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-punctuation.z-section {
-
color: #7c7f93;
-
}
-
html[data-theme="light"] .z-keyword.z-control.z-import.z-include {
-
color: #179299;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-storage {
-
color: #d20f39;
-
}
-
html[data-theme="light"] .z-storage.z-type {
-
color: #df8e1d;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-storage.z-modifier {
-
color: #d20f39;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-namespace,
-
html[data-theme="light"] .z-meta.z-path {
-
color: #dc8a78;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-storage.z-type.z-class {
-
color: #dc8a78;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-label {
-
color: #1e66f5;
-
}
-
html[data-theme="light"] .z-keyword.z-declaration.z-class {
-
color: #d20f39;
-
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;
-
}
-
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;
-
}
-
html[data-theme="light"] .z-keyword.z-declaration.z-function {
-
color: #e64553;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-support.z-function {
-
color: #04a5e5;
-
}
-
html[data-theme="light"] .z-support.z-constant {
-
color: #1e66f5;
-
}
-
html[data-theme="light"] .z-support.z-type,
-
html[data-theme="light"] .z-support.z-class {
-
color: #1e66f5;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-function {
-
color: #1e66f5;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-parameter {
-
color: #dc8a78;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-other {
-
color: #4c4f69;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-variable.z-other.z-member {
-
color: #dc8a78;
-
}
-
html[data-theme="light"] .z-variable.z-language {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-tag {
-
color: #fe640b;
-
}
-
html[data-theme="light"] .z-entity.z-other.z-attribute-name {
-
color: #8839ef;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-punctuation.z-definition.z-tag {
-
color: #e64553;
-
}
-
html[data-theme="light"] .z-markup.z-underline.z-link.z-markdown {
-
color: #dc8a78;
-
text-decoration: underline;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-meta.z-link.z-inline.z-description {
-
color: #7287fd;
-
font-weight: bold;
-
}
-
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;
-
}
-
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;
-
}
-
html[data-theme="light"] .z-markup.z-italic {
-
color: #e64553;
-
font-style: italic;
-
}
-
html[data-theme="light"] .z-markup.z-bold {
-
color: #e64553;
-
font-weight: bold;
-
}
-
html[data-theme="light"] .z-constant.z-character.z-escape {
-
color: #ea76cb;
-
}
-
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;
-
}
-
html[data-theme="light"] .z-variable.z-language.z-shell {
-
color: #d20f39;
-
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-value.z-lua
-
.z-meta.z-mapping.z-key.z-lua
-
.z-string.z-unquoted.z-key.z-lua {
-
color: #7287fd;
-
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;
-
}
-
html[data-theme="light"] .z-entity.z-name.z-constant.z-java {
-
color: #179299;
-
}
-
html[data-theme="light"] .z-support.z-type.z-property-name.z-css {
-
color: #dd7878;
-
font-style: italic;
-
}
-
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;
-
}
-
/* dark */
-
html[data-theme="dark"] .z-code {
color: #cad3f5;
background-color: var(--nightshade-violet);
}
-
html[data-theme="dark"] .z-comment {
color: #6e738d;
font-style: italic;
}
-
html[data-theme="dark"] .z-string {
color: #a6da95;
}
-
html[data-theme="dark"] .z-string.z-regexp {
color: #f5a97f;
}
-
html[data-theme="dark"] .z-constant.z-numeric {
color: #f5a97f;
}
-
html[data-theme="dark"] .z-constant.z-language.z-boolean {
color: #f5a97f;
font-weight: bold;
font-style: italic;
}
-
html[data-theme="dark"] .z-constant.z-language {
color: #b7bdf8;
font-style: italic;
}
-
html[data-theme="dark"] .z-support.z-function.z-builtin {
color: #f5a97f;
font-style: italic;
}
-
html[data-theme="dark"] .z-variable.z-other.z-constant {
color: #f5a97f;
}
-
html[data-theme="dark"] .z-keyword {
color: #ed8796;
font-style: italic;
}
-
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;
}
-
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;
}
-
html[data-theme="dark"] .z-support.z-type.z-exception {
color: #f5a97f;
font-style: italic;
}
-
html[data-theme="dark"] .z-keyword.z-operator,
-
html[data-theme="dark"] .z-punctuation.z-accessor {
color: #91d7e3;
font-weight: bold;
}
-
html[data-theme="dark"] .z-punctuation.z-separator {
color: #8bd5ca;
}
-
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-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
···
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;
}
···
* theme "Catppuccin" generated by syntect
*/
/* dark */
+
.z-code {
color: #cad3f5;
background-color: var(--nightshade-violet);
}
+
.z-comment {
color: #6e738d;
font-style: italic;
}
+
.z-string {
color: #a6da95;
}
+
.z-string.z-regexp {
color: #f5a97f;
}
+
.z-constant.z-numeric {
color: #f5a97f;
}
+
.z-constant.z-language.z-boolean {
color: #f5a97f;
font-weight: bold;
font-style: italic;
}
+
.z-constant.z-language {
color: #b7bdf8;
font-style: italic;
}
+
.z-support.z-function.z-builtin {
color: #f5a97f;
font-style: italic;
}
+
.z-variable.z-other.z-constant {
color: #f5a97f;
}
+
.z-keyword {
color: #ed8796;
font-style: italic;
}
+
.z-keyword.z-control.z-loop,
+
.z-keyword.z-control.z-conditional,
+
.z-keyword.z-control.z-c\+\+ {
color: #c6a0f6;
font-weight: bold;
}
+
.z-keyword.z-control.z-return,
+
.z-keyword.z-control.z-flow.z-return {
color: #f5bde6;
font-weight: bold;
}
+
.z-support.z-type.z-exception {
color: #f5a97f;
font-style: italic;
}
+
.z-keyword.z-operator,
+
.z-punctuation.z-accessor {
color: #91d7e3;
font-weight: bold;
}
+
.z-punctuation.z-separator {
color: #8bd5ca;
}
+
.z-punctuation.z-terminator {
color: #8bd5ca;
}
+
.z-punctuation.z-section {
color: #939ab7;
}
+
.z-keyword.z-control.z-import.z-include {
color: #8bd5ca;
font-style: italic;
}
+
.z-storage {
color: #ed8796;
}
+
.z-storage.z-type {
color: #eed49f;
font-style: italic;
}
+
.z-storage.z-modifier {
color: #ed8796;
}
+
.z-entity.z-name.z-namespace,
+
.z-meta.z-path {
color: #f4dbd6;
font-style: italic;
}
+
.z-storage.z-type.z-class {
color: #f4dbd6;
font-style: italic;
}
+
.z-entity.z-name.z-label {
color: #8aadf4;
}
+
.z-keyword.z-declaration.z-class {
color: #ed8796;
font-style: italic;
}
+
.z-entity.z-name.z-class,
+
.z-meta.z-toc-list.z-full-identifier {
color: #91d7e3;
}
+
.z-entity.z-other.z-inherited-class {
color: #91d7e3;
font-style: italic;
}
+
.z-entity.z-name.z-function,
+
.z-variable.z-function {
color: #8aadf4;
font-style: italic;
}
+
.z-entity.z-name.z-function.z-preprocessor {
color: #ed8796;
}
+
.z-keyword.z-control.z-import {
color: #ed8796;
}
+
.z-entity.z-name.z-function.z-constructor,
+
.z-entity.z-name.z-function.z-destructor {
color: #b7bdf8;
}
+
.z-variable.z-parameter.z-function {
color: #f4dbd6;
font-style: italic;
}
+
.z-keyword.z-declaration.z-function {
color: #ee99a0;
font-style: italic;
}
+
.z-support.z-function {
color: #91d7e3;
}
+
.z-support.z-constant {
color: #8aadf4;
}
+
.z-support.z-type,
+
.z-support.z-class {
color: #8aadf4;
font-style: italic;
}
+
.z-variable.z-function {
color: #8aadf4;
font-style: italic;
}
+
.z-variable.z-parameter {
color: #f4dbd6;
font-style: italic;
}
+
.z-variable.z-other {
color: #cad3f5;
font-style: italic;
}
+
.z-variable.z-other.z-member {
color: #f4dbd6;
}
+
.z-variable.z-language {
color: #8bd5ca;
}
+
.z-entity.z-name.z-tag {
color: #f5a97f;
}
+
.z-entity.z-other.z-attribute-name {
color: #c6a0f6;
font-style: italic;
}
+
.z-punctuation.z-definition.z-tag {
color: #ee99a0;
}
+
.z-markup.z-underline.z-link.z-markdown {
color: #f4dbd6;
text-decoration: underline;
font-style: italic;
}
+
.z-meta.z-link.z-inline.z-description {
color: #b7bdf8;
font-weight: bold;
}
+
.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;
}
+
.z-punctuation.z-definition.z-heading,
+
.z-entity.z-name.z-section {
color: #8aadf4;
font-weight: bold;
}
+
.z-markup.z-italic {
color: #ee99a0;
font-style: italic;
}
+
.z-markup.z-bold {
color: #ee99a0;
font-weight: bold;
}
+
.z-constant.z-character.z-escape {
color: #f5bde6;
}
html[data-theme="dark"]
···
.z-meta.z-function-call.z-identifier.z-shell {
color: #f5bde6;
}
+
.z-variable.z-language.z-shell {
color: #ed8796;
font-style: italic;
}
+
+
.z-source.z-lua
.z-meta.z-function.z-lua
.z-meta.z-block.z-lua
.z-meta.z-mapping.z-value.z-lua
···
color: #b7bdf8;
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;
}
+
.z-entity.z-name.z-constant.z-java {
color: #8bd5ca;
}
+
.z-support.z-type.z-property-name.z-css {
color: #f0c6c6;
font-style: italic;
}
+
.z-support.z-constant.z-property-value.z-css {
color: #cad3f5;
}
+
.z-constant.z-numeric.z-suffix.z-css,
+
.z-keyword.z-other.z-unit.z-css {
color: #8bd5ca;
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;
}
+
.z-entity.z-name.z-tag.z-css {
color: #b7bdf8;
}
+
.z-variable.z-other.z-sass {
color: #8bd5ca;
}
+
.z-invalid {
color: #cad3f5;
background-color: #ed8796;
}
+
.z-invalid.z-deprecated {
color: #cad3f5;
background-color: #c6a0f6;
}
+
.z-meta.z-diff,
+
.z-meta.z-diff.z-header {
color: #6e738d;
}
+
.z-markup.z-deleted {
color: #ed8796;
}
+
.z-markup.z-inserted {
color: #a6da95;
}
+
.z-markup.z-changed {
color: #eed49f;
}
+
.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/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/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/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/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.

+65 -53
static/js/copy-button.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");
-
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"; // 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", "crt", "scanlines");
-
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 },
-
);
-
}
-
});
···
// 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);
+
});
+
});
-49
static/js/theme-toggle.js
···
-
const toggleButton = document.getElementById("theme-toggle");
-
const themeIcon = document
-
.getElementById("theme-toggle-label")
-
.querySelector("i");
-
const themeSound = document.getElementById("theme-sound");
-
-
// Function to update the theme icon based on the current theme
-
const updateThemeIcon = (isDarkMode) => {
-
themeIcon.style.setProperty(
-
"--icon-toggle",
-
isDarkMode ? "var(--icon-dark)" : "var(--icon-light)",
-
);
-
};
-
-
// 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.currentTime = 0;
-
themeSound.play();
-
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
-
};
-
-
// 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/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.

-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/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/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/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
+5 -16
templates/blog-page.html
···
{% 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 %}
<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 %}
</ul>
-
{% endif %}
</li>
{% endfor %}
</ul>
···
{% endif %}
<h1>{{ page.title }}</h1>
+
{% if page.toc and page.extra["has_toc"] %}
<h2>Table of contents</h2>
<ul>
+
{% 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>
+51 -55
templates/footer.html
···
<hr />
<div id="footer-container">
-
<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">
-
Member of the
-
<a href="https://w.elr.sh">elr</a>
-
webring -
-
<span class='webring-links'>
-
<a href='javascript:void(0)' onclick='randomSite()'>random</a> |
-
<a href='#' id='prev-link'>previous</a> |
-
<a href='#' id='next-link'>next</a>
-
</span>
-
</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://tangled.sh/@dunkirk.sh/zera/blob/main/LICENSE.md"
-
>AGPL 3.0</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>
···
<hr />
<div id="footer-container">
+
<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>
+33 -12
templates/head.html
···
<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="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",
···
type="text/css"
href="{{ get_url(path='css/main.css?' ~ cssHash, trailing_slash=false) | safe }}"
/>
-
{% endblock css %} {% set jsHash = get_hash(path="js/theme-toggle.js", sha_type=256,
base64=true) %}
<script
-
src="{{ get_url(path='js/theme-toggle.js?' ~ jsHash, trailing_slash=false) | safe }}"
defer
></script>
-
{% 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>
<script>
function cb(res) {
const fmt = new Intl.NumberFormat('en', { notation: 'compact' });
···
<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" />
<meta name="fediverse:creator" content="@taciturnaxoltol@social.dino.icu" />
<div class="h-card" style="display:none">
···
<div class="p-adr h-adr">
<span class="p-country-name">United States of America</span>
</div>
-
<img class="u-photo" src="/pfps/instsqc-rat-pfp.webp" alt="kieran with an orange cast in a polaroid over a pinkish background" />
</div>
···
<meta property="twitter:image" content="{{ image }}" />
{% endif %} {% endif %}
<link rel="canonical" href="{{ url | safe }}" />
+
<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",
···
type="text/css"
href="{{ get_url(path='css/main.css?' ~ cssHash, trailing_slash=false) | safe }}"
/>
+
{% endblock css %}
+
+
{% 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>
+
+
{% 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
></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' });
···
<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">
···
<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>
+17 -23
templates/header.html
···
-
{% if config.extra.header_nav %} {% if not current_url %} {% set current_url =
-
"" %} {% 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 %} text-glow"
-
>
-
{{ nav_item.name }}
-
</a>
-
{% endfor %}
-
<div>
-
<input type="checkbox" id="theme-toggle" style="display: none" />
-
<label for="theme-toggle" id="theme-toggle-label"
-
><i class="icon"></i
-
></label>
-
<audio id="theme-sound">
-
<source
-
src="{{ get_url(path='click.ogg', trailing_slash=false) | safe }}"
-
type="audio/ogg"
-
/>
-
</audio>
-
</div>
</nav>
-
{% 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 == active_path or (nav_item.url == '/' and active_path == '') %}active{% endif %}">
+
{{ nav_item.name }}
+
</a>
+
{% endfor %}
</nav>
+
{% endif %}
+32 -13
templates/shortcodes/bluesky.html
···
set content = post_data.record.text %} {% set has_embed = post_data.embed is
defined and post_data.embed %}
<blockquote>
-
{{ content }} {% if has_embed and post_data.embed["$type"] ==
"app.bsky.embed.video#view" %}
<video controls poster="{{ post_data.embed.thumbnail }}">
<source
···
type="application/x-mpegURL"
/>
</video>
-
{% endif %}
-
<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>
</blockquote>
{% else %}
<blockquote>
<div class="bsky-post">
···
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
···
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">
+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>
+4 -2
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 %}
</figure>
···
<figure {% if class %}class="{{class}}" {% else %}class="center" {% 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>
+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>
-233
tools/bun.lock
···
-
{
-
"lockfileVersion": 1,
-
"workspaces": {
-
"": {
-
"name": "zera",
-
"dependencies": {
-
"dotenv": "^16.4.7",
-
},
-
"devDependencies": {
-
"@types/bun": "latest",
-
"puppeteer": "^23.6.0",
-
},
-
"peerDependencies": {
-
"typescript": "^5.0.0",
-
},
-
},
-
},
-
"packages": {
-
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
-
-
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
-
-
"@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.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
-
-
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
-
-
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
-
-
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
-
-
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
-
-
"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.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
-
-
"bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
-
-
"bare-fs": ["bare-fs@4.0.1", "", { "dependencies": { "bare-events": "^2.0.0", "bare-path": "^3.0.0", "bare-stream": "^2.0.0" } }, "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg=="],
-
-
"bare-os": ["bare-os@3.4.0", "", {}, "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA=="],
-
-
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
-
-
"bare-stream": ["bare-stream@2.6.5", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA=="],
-
-
"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.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
-
-
"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.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
-
-
"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.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
-
-
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-
"end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
-
-
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
-
-
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
-
-
"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=="],
-
-
"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.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="],
-
-
"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@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
-
-
"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.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
-
-
"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
-
-
"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@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
-
-
"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.1.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-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw=="],
-
-
"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=="],
-
-
"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.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="],
-
-
"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.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
-
-
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
-
-
"socks": ["socks@2.8.4", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ=="],
-
-
"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=="],
-
-
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
-
-
"streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
-
-
"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.0.8", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg=="],
-
-
"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=="],
-
-
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
-
-
"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@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
-
-
"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.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
-
-
"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=="],
-
}
-
}
···
-122
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({
-
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
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;
-
--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>
···
-18
tools/package.json
···
-
{
-
"name": "zera",
-
"module": "index.ts",
-
"type": "module",
-
"scripts": {
-
"gen": "bun genOG.ts"
-
},
-
"devDependencies": {
-
"@types/bun": "latest",
-
"puppeteer": "^23.6.0"
-
},
-
"peerDependencies": {
-
"typescript": "^5.0.0"
-
},
-
"dependencies": {
-
"dotenv": "^16.4.7"
-
}
-
}
···