Compare commits
43 Commits
6b68960e8a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dff1b11e5 | |||
| 7b78695b9e | |||
| 4e72aab2fc | |||
| 72e256fcd3 | |||
| 1fdab41a8d | |||
| 514c95a82d | |||
| 151084a84e | |||
| 7fb3f19ee0 | |||
| 85db1d0342 | |||
| ced9cd11f3 | |||
| f02bd683da | |||
| d3efb33db8 | |||
| 809bb3c081 | |||
| 4ddc2445eb | |||
| 3e546f95c6 | |||
| bd73fcd39b | |||
| cfb55e0e51 | |||
| 4b11adf3c5 | |||
| 8d44a08cf4 | |||
| 5179fd456c | |||
| 625b2d9729 | |||
| 6a0ebf5f45 | |||
| d3f0d2bfe2 | |||
| 37a810bf84 | |||
| 60c4ab4137 | |||
| a47a28e074 | |||
| aae7d716a2 | |||
| 5d099548fc | |||
| 73e56f66e3 | |||
| a679c59b7b | |||
| 2fad7c173b | |||
| 23a44c1e4f | |||
| d4399ed779 | |||
| e13f3f47b5 | |||
| fd1dc78ab0 | |||
| 3d57a9015b | |||
| 6d70da14ca | |||
| 652c3b2326 | |||
| a72883a13f | |||
| 57c931c7cf | |||
| dfb1bf4ee5 | |||
| 723fda29f2 | |||
| 591e4e85ef |
@@ -1,7 +1,145 @@
|
|||||||
personal_ws-1.1 en 0
|
personal_ws-1.1 en 0
|
||||||
|
AFAICT
|
||||||
|
ai
|
||||||
|
AirBnB
|
||||||
|
anon
|
||||||
|
anon's
|
||||||
|
Anthropic
|
||||||
|
ASMR
|
||||||
|
aspell
|
||||||
|
backend
|
||||||
|
bluetooth
|
||||||
|
Bly
|
||||||
|
btw
|
||||||
|
CalyxOS
|
||||||
|
catppuccin
|
||||||
|
Cena
|
||||||
|
ChatGPT
|
||||||
|
cli
|
||||||
|
colemak
|
||||||
|
Colemak
|
||||||
|
config
|
||||||
|
CPU
|
||||||
|
crunchers
|
||||||
|
css
|
||||||
|
Cyano
|
||||||
|
Cyanogen
|
||||||
|
debuffs
|
||||||
|
decrypt
|
||||||
|
dev
|
||||||
|
devs
|
||||||
|
direnv
|
||||||
|
distro
|
||||||
|
DMV
|
||||||
|
drupals
|
||||||
|
dysplasia
|
||||||
|
Fairphone
|
||||||
|
FOMO
|
||||||
|
formatter
|
||||||
|
Foss
|
||||||
|
fosscat
|
||||||
|
gitea
|
||||||
|
Gitea
|
||||||
|
Gokarna
|
||||||
|
Goodreads
|
||||||
|
GrapheneOS
|
||||||
|
Hayao
|
||||||
|
heatmaps
|
||||||
|
hehe
|
||||||
|
hugo
|
||||||
|
idk
|
||||||
|
ish
|
||||||
|
Javascript
|
||||||
|
jpg
|
||||||
|
json
|
||||||
|
KBAQ
|
||||||
|
keymap
|
||||||
|
Kimber
|
||||||
|
kombucha
|
||||||
lastmod
|
lastmod
|
||||||
showTableOfContents
|
LDS
|
||||||
Zig
|
libre
|
||||||
|
linux
|
||||||
|
LLM's
|
||||||
|
lobste
|
||||||
|
localhost
|
||||||
|
lol
|
||||||
|
LSP
|
||||||
|
MacOS
|
||||||
|
Mahjong
|
||||||
|
Makefile
|
||||||
|
markdownlint
|
||||||
|
md
|
||||||
|
Memex
|
||||||
|
minification
|
||||||
|
Miyazaki
|
||||||
|
nate
|
||||||
|
nav
|
||||||
|
Nephi
|
||||||
|
NewPipe
|
||||||
|
nginx
|
||||||
Nim
|
Nim
|
||||||
tils
|
Niri
|
||||||
|
nixos
|
||||||
|
NixOS
|
||||||
|
Noctalia
|
||||||
|
noogies
|
||||||
|
normie
|
||||||
|
npm
|
||||||
|
OnePlus
|
||||||
|
Opencode
|
||||||
|
otto
|
||||||
|
params
|
||||||
|
philips
|
||||||
|
PinePhone
|
||||||
|
pixelated
|
||||||
|
PostMarketOS
|
||||||
|
POV
|
||||||
|
pre
|
||||||
|
Pre
|
||||||
|
Reddit
|
||||||
|
repo
|
||||||
|
repo's
|
||||||
|
rpg
|
||||||
|
RSS
|
||||||
|
sakura
|
||||||
|
sandboxing
|
||||||
|
sciency
|
||||||
|
SDL
|
||||||
|
showTableOfContents
|
||||||
|
Sora
|
||||||
Spotify
|
Spotify
|
||||||
|
spudger
|
||||||
|
statusColor
|
||||||
|
submodule
|
||||||
|
symlinks
|
||||||
|
syscall
|
||||||
|
syscalls
|
||||||
|
TCL
|
||||||
|
Termux
|
||||||
|
tik
|
||||||
|
tiktok
|
||||||
|
tils
|
||||||
|
TILs
|
||||||
|
tok
|
||||||
|
toks
|
||||||
|
treking
|
||||||
|
Uber
|
||||||
|
Uhhh
|
||||||
|
UI
|
||||||
|
Uno
|
||||||
|
upgradable
|
||||||
|
vendored
|
||||||
|
videogame
|
||||||
|
videogames
|
||||||
|
VoLTE
|
||||||
|
Vue
|
||||||
|
webdev
|
||||||
|
webp
|
||||||
|
webpack
|
||||||
|
yapper
|
||||||
|
Yay
|
||||||
|
zig
|
||||||
|
Zig
|
||||||
|
Zillow
|
||||||
|
zoomers
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
if [[ ! -d "/home/nate/source/fosscat-site" ]]; then
|
|
||||||
echo "Cannot find source directory; Did you move it?"
|
|
||||||
echo "(Looking for "/home/nate/source/fosscat-site")"
|
|
||||||
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# rebuild the cache forcefully
|
|
||||||
_nix_direnv_force_reload=1 direnv exec "/home/nate/source/fosscat-site" true
|
|
||||||
|
|
||||||
# Update the mtime for .envrc.
|
|
||||||
# This will cause direnv to reload again - but without re-building.
|
|
||||||
touch "/home/nate/source/fosscat-site/.envrc"
|
|
||||||
|
|
||||||
# Also update the timestamp of whatever profile_rc we have.
|
|
||||||
# This makes sure that we know we are up to date.
|
|
||||||
touch -r "/home/nate/source/fosscat-site/.envrc" "/home/nate/source/fosscat-site/.direnv"/*.rc
|
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
echo "Running pre-commit checks..."
|
echo "Running pre-commit checks..."
|
||||||
|
|
||||||
|
# Auto-update lastmod in content files (non-blocking)
|
||||||
|
# Runs before other checks so lint/spell run on updated files
|
||||||
|
# Resolve symlink to find the actual script directory
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
if [ -x "$SCRIPT_DIR/update-lastmod.sh" ]; then
|
||||||
|
"$SCRIPT_DIR/update-lastmod.sh" || echo "⚠️ lastmod update script encountered an issue"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track overall success/failure
|
||||||
|
OVERALL_RESULT=0
|
||||||
|
|
||||||
# Get list of staged markdown files
|
# Get list of staged markdown files
|
||||||
STAGED_MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)
|
STAGED_MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)
|
||||||
|
|
||||||
@@ -14,55 +26,58 @@ fi
|
|||||||
|
|
||||||
echo "Checking markdown files..."
|
echo "Checking markdown files..."
|
||||||
|
|
||||||
# Check markdown formatting with markdownlint-cli
|
# --- Stage 1: Markdown linting ---
|
||||||
if command -v markdownlint &> /dev/null; then
|
if command -v markdownlint &> /dev/null; then
|
||||||
markdownlint $STAGED_MD_FILES || {
|
if ! markdownlint $STAGED_MD_FILES; then
|
||||||
echo "❌ Markdown linting failed. Fix issues or use 'git commit --no-verify' to skip."
|
echo "❌ Markdown linting failed."
|
||||||
exit 1
|
OVERALL_RESULT=1
|
||||||
}
|
else
|
||||||
|
echo -e "${GREEN}✅ Markdownlint passed!${NC}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "⚠️ markdownlint not found, skipping markdown linting"
|
echo "⚠️ markdownlint not found, skipping markdown linting"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Spell check
|
# --- Stage 2: Interactive spell check ---
|
||||||
if command -v aspell &> /dev/null; then
|
# Runs before tag check so typos in tags get corrected first
|
||||||
# Check if aspell can find dictionaries
|
if [ -x "./scripts/spellcheck-interactive.sh" ]; then
|
||||||
if ! aspell dump dicts 2>/dev/null | grep -q "en"; then
|
if ! ./scripts/spellcheck-interactive.sh $STAGED_MD_FILES; then
|
||||||
echo "⚠️ aspell found but no English dictionaries available"
|
echo "❌ Spell check failed."
|
||||||
echo " Make sure ASPELL_CONF is set correctly in your direnv"
|
OVERALL_RESULT=1
|
||||||
echo " Skipping spell check"
|
|
||||||
else
|
|
||||||
echo "Running spell check..."
|
|
||||||
MISSPELLED=0
|
|
||||||
for file in $STAGED_MD_FILES; do
|
|
||||||
# Get list of misspelled words
|
|
||||||
ERRORS=$(cat "$file" | aspell list --mode=markdown --lang=en --personal=./.aspell.en.pws 2>/dev/null | sort -u)
|
|
||||||
if [ ! -z "$ERRORS" ]; then
|
|
||||||
echo "⚠️ Possible misspellings in $file:"
|
|
||||||
# For each misspelled word, show context
|
|
||||||
while IFS= read -r word; do
|
|
||||||
if [ ! -z "$word" ]; then
|
|
||||||
SUGGESTION=$(echo "$word" | aspell pipe --mode=markdown --lang=en --personal=./.aspell.en.pws 2>/dev/null | grep -E "^&" | cut -d: -f2 | cut -d, -f1 | sed 's/^ //')
|
|
||||||
echo " '$word' → suggestion: $SUGGESTION"
|
|
||||||
# Use grep to find lines containing the word (case-insensitive) with line numbers
|
|
||||||
grep -n -i -w --color=always "$word" "$file" | head -3 | while IFS= read -r line; do
|
|
||||||
echo " $line"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done <<< "$ERRORS"
|
|
||||||
MISSPELLED=1
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ $MISSPELLED -eq 1 ]; then
|
|
||||||
echo "Review spelling errors above. Add correct terms to .aspell.personal"
|
|
||||||
echo "Use 'git commit --no-verify' to skip if needed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "⚠️ aspell not found, skipping spell check"
|
echo "⚠️ Spell check script not found or not executable, skipping spell check"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Stage 3: Tag similarity check ---
|
||||||
|
# Runs after spell check so corrected tags are compared
|
||||||
|
if command -v python3 &> /dev/null && [ -f "./scripts/check-tags.py" ]; then
|
||||||
|
echo "Running tag similarity check..."
|
||||||
|
if ! python3 ./scripts/check-tags.py $STAGED_MD_FILES; then
|
||||||
|
echo "❌ Tag similarity check failed."
|
||||||
|
OVERALL_RESULT=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Tag checker (python3 or scripts/check-tags.py) not found, skipping tag check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Stage 4: Link validation ---
|
||||||
|
if [ -x "./scripts/check-links.sh" ]; then
|
||||||
|
echo "Running link validation..."
|
||||||
|
if ! ./scripts/check-links.sh $STAGED_MD_FILES; then
|
||||||
|
echo "❌ Link validation failed."
|
||||||
|
OVERALL_RESULT=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Link checker script not found or not executable, skipping link validation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final result
|
||||||
|
if [ $OVERALL_RESULT -eq 0 ]; then
|
||||||
echo "✅ Pre-commit checks passed!"
|
echo "✅ Pre-commit checks passed!"
|
||||||
exit 0
|
exit 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "❌ Some pre-commit checks failed. Fix issues or use 'git commit --no-verify' to skip."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Auto-update lastmod field in staged content files
|
||||||
|
# This script should never block a commit - errors are warnings only
|
||||||
|
|
||||||
|
update_lastmod() {
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
# Generate RFC3339 timestamp with timezone (e.g., 2025-11-08T16:19:07-06:00)
|
||||||
|
local tz_offset=$(date +%z)
|
||||||
|
local tz_formatted="${tz_offset:0:3}:${tz_offset:3:2}" # Insert colon: -0600 -> -06:00
|
||||||
|
local now=$(date +%Y-%m-%dT%H:%M:%S)${tz_formatted}
|
||||||
|
|
||||||
|
# Only update if file has lastmod field
|
||||||
|
if grep -q "^lastmod:" "$file"; then
|
||||||
|
# Cross-platform sed -i (macOS vs Linux)
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
sed -i '' "s/^lastmod:.*$/lastmod: $now/" "$file"
|
||||||
|
else
|
||||||
|
sed -i "s/^lastmod:.*$/lastmod: $now/" "$file"
|
||||||
|
fi
|
||||||
|
git add "$file"
|
||||||
|
echo " Updated lastmod in $file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get staged content files
|
||||||
|
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^content/.*\.md$' || true)
|
||||||
|
|
||||||
|
if [ -z "$staged_files" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in $staged_files; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
update_lastmod "$file" || echo "Warning: Failed to update lastmod in $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "themes/gokarna"]
|
|
||||||
path = themes/gokarna
|
|
||||||
url = https://github.com/gokarna-theme/gokarna-hugo.git
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# fosscat.com image optimization manifest — do not edit manually
|
||||||
|
2aeea48d6df9070f9c500ffa0af9dd58ff3fbb4a8200098a6a224d4bdc1be692 static/images/arch-logo.webp
|
||||||
|
49588883b06b4ec5a8e0bc09ca608816c83a0b0d4df158893d00e51fd93d60be static/images/eagle-wizard.webp
|
||||||
|
1db221e53cdab01ddf9847f9c2b56e39e95d8792eaeac775af7426704758ac6f static/images/elephant-and-blind-men.webp
|
||||||
|
a0628903bb9cb0ede12850d31692258db086d5a4ac0850ab91bea92235041345 static/images/ford-f150.webp
|
||||||
|
fa1a695caee36958d0ef33187b2c051ac3d7e3daf1bf3f87fe0410f87ccd4d94 static/images/fosscat_icon.webp
|
||||||
|
b8e279fc1296c255785b8c6938abc167576e68ad16f76c9e482ec07e36f76ac6 static/images/framework-banner.webp
|
||||||
|
3026c3136af7cff1f63f8148af6ad5fc082f41e4622c9cdbe03748ceaabf7fdc static/images/hammock.webp
|
||||||
|
6b778f29cfb98a79bc195ad9313ff90c06b06c3b2c658ab094a8cdb2a46a4e2c static/images/japan-arrival.webp
|
||||||
|
790b7e2f9a2b34333ada91ca37feb1ad219e51ecfd9103d30e50ae402beb19c1 static/images/japan-fuji-town.webp
|
||||||
|
25299e08100976e4b64e0b1e1e62aa68fd65515e24206f241ed16e63e4f40306 static/images/japan-fuji.webp
|
||||||
|
277eb4cea7f007987f437664ded582edd355e650aa3c083b526006a0dee82805 static/images/japan-mcdonalds.webp
|
||||||
|
918ecc79f2ac4a926ee39fe58ab286acbf94a715fab0695eaf2b66cfbee1abc4 static/images/japan-nekomachi.webp
|
||||||
|
a5b3a9a08fbccc48e41af6e77cbd9d5d50ff82482bf70e28fadcbcedf4446ee7 static/images/japan-nippori-graveyard.webp
|
||||||
|
a7f2129eea90da48a53be5115139786b4bb70429b089478f75a21d3a49f9d7c2 static/images/japan-nippori-walk.webp
|
||||||
|
83c301aca3822e953e6c0193a0ac0beb7fc9b3e1dfc48eef2f8fc5bc06b45440 static/images/japan-noodle-cup.webp
|
||||||
|
84754eefc88b71ec92abc008f9dddd21b8891e8a1864335ff94d1cc4a03af161 static/images/japan-shinjuku.webp
|
||||||
|
396851979b183b195ecdfd30e01d67dab6de3891eaaba043607f6eec5954b43e static/images/japan-shinjuzu-garden.webp
|
||||||
|
66b3456d8d6f670efb1609143dd987ee2540fae4d161ec57882ae0524dc6df33 static/images/japan-spirit-tree.webp
|
||||||
|
0b1179ecbc906abc418baa0ae2cb24b7f23128f4c3e748fba3a02d6e3ef55c2a static/images/monochrome-path.webp
|
||||||
|
13196fdb9ae802e1998e1ceeeaff6e774d9962b8a918107d016f63a60b99416f static/images/nginx-mumble.webp
|
||||||
|
bf721881652c94518b2b46dab9c13c9ca05c2211dade76f8cc68ce0c1fb7e481 static/images/ocean-aerial.webp
|
||||||
|
c59b6bfe6695c3d82b13375098d4c4f8ac62859e53e7a028d2dee3680f18d6c9 static/images/otto-1.webp
|
||||||
|
5814fb056057f022d67e144ad9c66fb2c7bfd2bfb7460eb50f8c33597cde33af static/images/otto-on-nature-path-algorithm.webp
|
||||||
|
29604251ddc2c916e470d77d37a88539888adb3dba45b8ce7c238fdd1aa88e95 static/images/rpi-bookshelf.webp
|
||||||
|
160b041274ea70f90484af2bc376f969888d2418dabc51eef55ce05ea306c9b9 static/images/salt-lake-basin-view.webp
|
||||||
|
d2db5eb5fb460359a9017eba824d9e07d80adf206b30953c0d5fbcea4e421e89 static/images/red-path.webp
|
||||||
|
9e9ccdd43cf44fa8f830dd34043a8ddb6b596565c8b56ccc59d596184b1f18b9 static/images/moonlit_landscape_with_bridge_1990.6.1.webp
|
||||||
|
eebed31e76a47681a3aae7809034ecd7366f3680068224488a58f24e8974ae85 static/images/woman_holding_a_balance_1942.9.97.webp
|
||||||
@@ -53,24 +53,7 @@
|
|||||||
"maximum": 1
|
"maximum": 1
|
||||||
},
|
},
|
||||||
// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md013.md
|
// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md013.md
|
||||||
"MD013": {
|
"MD013": false,
|
||||||
// Number of characters
|
|
||||||
"line_length": 120,
|
|
||||||
// Number of characters for headings
|
|
||||||
"heading_line_length": 120,
|
|
||||||
// Number of characters for code blocks
|
|
||||||
"code_block_line_length": 120,
|
|
||||||
// Include code blocks
|
|
||||||
"code_blocks": true,
|
|
||||||
// Include tables
|
|
||||||
"tables": true,
|
|
||||||
// Include headings
|
|
||||||
"headings": true,
|
|
||||||
// Strict length checking
|
|
||||||
"strict": false,
|
|
||||||
// Stern length checking
|
|
||||||
"stern": false
|
|
||||||
},
|
|
||||||
// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md014.md
|
// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md014.md
|
||||||
"MD014": true,
|
"MD014": true,
|
||||||
// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md018.md
|
// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md018.md
|
||||||
@@ -139,10 +122,7 @@
|
|||||||
// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md032.md
|
// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md032.md
|
||||||
"MD032": true,
|
"MD032": true,
|
||||||
// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md033.md
|
// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md033.md
|
||||||
"MD033": {
|
"MD033": false,
|
||||||
// Allowed elements
|
|
||||||
"allowed_elements": []
|
|
||||||
},
|
|
||||||
// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md034.md
|
// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md034.md
|
||||||
"MD034": true,
|
"MD034": true,
|
||||||
// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md035.md
|
// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md035.md
|
||||||
@@ -266,7 +246,7 @@
|
|||||||
// MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md056.md
|
// MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md056.md
|
||||||
"MD056": true,
|
"MD056": true,
|
||||||
// MD058/blanks-around-tables : Tables should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md058.md
|
// MD058/blanks-around-tables : Tables should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md058.md
|
||||||
"MD058": true,
|
"MD058": false,
|
||||||
// MD059/descriptive-link-text : Link text should be descriptive : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md059.md
|
// MD059/descriptive-link-text : Link text should be descriptive : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md059.md
|
||||||
"MD059": {
|
"MD059": {
|
||||||
// Prohibited link texts
|
// Prohibited link texts
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# AGENTS.md — fosscat.com Hugo Site
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
Hugo static site (fosscat.com). Personal blog + projects. Custom vendored Gokarna theme with Kimber base16 color palette and Maple Mono font.
|
||||||
|
|
||||||
|
## Essential Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
hugo server # Dev server with live reload
|
||||||
|
hugo # Build to public/
|
||||||
|
hugo new posts/my-post.md # New blog post
|
||||||
|
hugo new projects/my-project.md # New project
|
||||||
|
```
|
||||||
|
|
||||||
|
No Makefile, no package.json, no CI. Nix flake + direnv auto-loads all tools on directory entry.
|
||||||
|
|
||||||
|
## Verify Changes
|
||||||
|
|
||||||
|
Run `hugo server` and check localhost:1313. Hugo reports template/config errors on build. No test suite — visual verification only.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `config.toml` | Hugo config (theme, nav, params, syntax highlighting) |
|
||||||
|
| `flake.nix` | Nix dev environment (hugo, marksman, prettier, markdownlint, aspell) |
|
||||||
|
| `archetypes/default.md` | Post front matter template |
|
||||||
|
| `archetypes/projects.md` | Project front matter template (includes status color reference) |
|
||||||
|
| `.markdownlint.jsonc` | Markdownlint config |
|
||||||
|
| `.aspell.en.pws` | Personal spell check dictionary |
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
**Posts** (`content/posts/*.md`): Front matter fields: `title`, `date`, `lastmod`, `description`, `tags`, `type: "post"`, `showTableOfContents`, `image`, `image_alt`, `image_caption`, `draft`.
|
||||||
|
|
||||||
|
**Projects** (`content/projects/*.md`): Same as posts plus `status` (string) and `statusColor` (hex). Colors: green `#99c899`, blue `#537c9c`, yellow `#d8b56d`, red `#704f4f`, grey `#c3c3b4`, pink `#c88c8c`.
|
||||||
|
|
||||||
|
**Homepage about**: `content/index-about.md` (loaded by `layouts/index.html` via `readFile`).
|
||||||
|
|
||||||
|
## Layout Overrides (`layouts/`)
|
||||||
|
|
||||||
|
Root `layouts/` overrides take precedence over `themes/gokarna/layouts/`. Current overrides:
|
||||||
|
|
||||||
|
- `index.html` — 2-column home grid: recent posts + project updates card with status badges
|
||||||
|
- `partials/post.html` — featured image support
|
||||||
|
- `partials/page.html` — date/lastmod display logic
|
||||||
|
- `_default/term.html` — custom taxonomy page
|
||||||
|
- `projects/list.html` — project listing with status badges
|
||||||
|
- `projects/single.html` — single project page
|
||||||
|
- `shortcodes/baseurl.html` — outputs site base URL
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
1. **Never manually set `lastmod`** — the pre-commit hook auto-updates it via `scripts/update-lastmod.sh`
|
||||||
|
2. **Theme is vendored, NOT a submodule** — edit `themes/gokarna/` files directly
|
||||||
|
3. **CSS lives in `themes/gokarna/assets/css/`** — `main.css`, `dark.css`, `syntax.css`. Hugo's asset pipeline handles minification and fingerprinting
|
||||||
|
4. **Pre-commit hook** runs on `.md` files: lastmod update, markdownlint, aspell spell check, link validation. See `.githooks/pre-commit`
|
||||||
|
|
||||||
|
## Git Hooks & Scripts
|
||||||
|
|
||||||
|
Pre-commit hook auto-installed by `scripts/install_hooks.sh` (runs on nix shell entry). Scripts in `scripts/`:
|
||||||
|
- `update-lastmod.sh` — updates `lastmod` front matter (RFC3339)
|
||||||
|
- `check-links.sh` — curl-based dead link checker
|
||||||
|
- `install_hooks.sh` — symlinks `.githooks/*` into `.git/hooks/`
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
---
|
---
|
||||||
date: {{ .Date }}
|
date: {{ .Date }}
|
||||||
description: ""
|
description: ""
|
||||||
|
draft: false
|
||||||
lastmod: {{ .Date }}
|
lastmod: {{ .Date }}
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
tags: [ ]
|
tags: [ ]
|
||||||
type: "post"
|
type: "post"
|
||||||
title: "{{ replace .Name "-" " " | title }}"
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
image: ""
|
image: "images/"
|
||||||
|
image_alt: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
date: {{ .Date }}
|
||||||
|
description: ""
|
||||||
|
lastmod: {{ .Date }}
|
||||||
|
showTableOfContents: true
|
||||||
|
type: "projects"
|
||||||
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
status: ""
|
||||||
|
statusColor: "" # green #99c899 | blue - #537c9c | yellow - #d8b56d | red - #704f4f | grey - #c3c3b4 | pink - #c88c8c
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
date: {{ .Date }}
|
|
||||||
description: ""
|
|
||||||
lastmod: {{ .Date }}
|
|
||||||
showTableOfContents: true
|
|
||||||
type: "tils"
|
|
||||||
title: "TIL: {{ replace .Name "-" " " | title }}"
|
|
||||||
image: ""
|
|
||||||
image_credit: ""
|
|
||||||
image_alt: ""
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Context
|
|
||||||
|
|
||||||
# Reflection
|
|
||||||
@@ -2,13 +2,12 @@ baseURL= "https://fosscat.com/"
|
|||||||
defaultContentLanguage = "en-us"
|
defaultContentLanguage = "en-us"
|
||||||
title = "Foss Cat"
|
title = "Foss Cat"
|
||||||
theme = "gokarna"
|
theme = "gokarna"
|
||||||
enableRobotsTXT= true
|
enableRobotsTXT = false
|
||||||
enableEmoji = true
|
enableEmoji = true
|
||||||
# code highlighting style
|
# pygmentsStyle = "catppuccin-frappe"
|
||||||
pygmentsStyle = "catppuccin-frappe"
|
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
accentColor = "#715483"
|
accentColor = "#D8B56D"
|
||||||
socialIcons = [
|
socialIcons = [
|
||||||
{name = "gitea", url = "https://git.fosscat.com/n8r"},
|
{name = "gitea", url = "https://git.fosscat.com/n8r"},
|
||||||
{name = "twitch", url = "https://twitch.tv/fosscat"},
|
{name = "twitch", url = "https://twitch.tv/fosscat"},
|
||||||
@@ -17,19 +16,49 @@ socialIcons = [
|
|||||||
]
|
]
|
||||||
# <link> tags for favicon support
|
# <link> tags for favicon support
|
||||||
customHeadHTML = '''
|
customHeadHTML = '''
|
||||||
|
<meta name="robots" content="noai, noimageai">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<!-- <script>console.log("Custom script")</script> -->
|
<!-- Maple Mono font via fontsource CDN -->
|
||||||
<!-- <script src="/js/custom.js"></script> -->
|
<style>
|
||||||
''' # Import file from `static/`
|
@font-face {
|
||||||
|
font-family: 'Maple Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-normal.woff2) format('woff2'),
|
||||||
|
url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-normal.woff) format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Maple Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-700-normal.woff2) format('woff2'),
|
||||||
|
url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-700-normal.woff) format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Maple Mono';
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-italic.woff2) format('woff2'),
|
||||||
|
url(https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-italic.woff) format('woff');
|
||||||
|
}
|
||||||
|
html, body, code, pre, .post-content, .post-title {
|
||||||
|
font-family: 'Maple Mono', monospace !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
'''
|
||||||
# customFooterHTML = '<p>foot123</p>'
|
# customFooterHTML = '<p>foot123</p>'
|
||||||
togglePreviousAndNextButtons = "true"
|
togglePreviousAndNextButtons = "true"
|
||||||
avatarUrl = "/images/fosscat_icon.png"
|
avatarUrl = "/images/fosscat_icon.webp"
|
||||||
avatarSize = "size-s"
|
avatarSize = "size-s"
|
||||||
numberPostsOnHomePage = 5
|
numberPostsOnHomePage = 5
|
||||||
showPostsOnHomePage = "recent" # "popular"
|
numberProjectsOnHomePage = 3
|
||||||
|
showPostsOnHomePage = "recent"
|
||||||
|
|
||||||
[params.meta]
|
[params.meta]
|
||||||
favicon = true
|
favicon = true
|
||||||
@@ -41,6 +70,8 @@ svg = false
|
|||||||
startLevel = 1
|
startLevel = 1
|
||||||
endLevel = 3
|
endLevel = 3
|
||||||
ordered = false
|
ordered = false
|
||||||
|
[markup.highlight]
|
||||||
|
noClasses = false
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
[[menu.main]]
|
[[menu.main]]
|
||||||
@@ -48,17 +79,12 @@ name = "Home"
|
|||||||
url = "/"
|
url = "/"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[menu.main]]
|
[[menu.main]]
|
||||||
identifier = "tils"
|
name = "Posts"
|
||||||
name = "TIL's"
|
url = "/posts/"
|
||||||
url = "/tils/"
|
|
||||||
weight = 2
|
weight = 2
|
||||||
[[menu.main]]
|
[[menu.main]]
|
||||||
name = "Projects"
|
name = "Projects"
|
||||||
url = "/projects/"
|
url = "/projects/"
|
||||||
weight = 3
|
|
||||||
[[menu.main]]
|
|
||||||
name = "Posts"
|
|
||||||
url = "/posts/"
|
|
||||||
weight = 4
|
weight = 4
|
||||||
[[menu.main]]
|
[[menu.main]]
|
||||||
name = "Tags"
|
name = "Tags"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
## Hello Weary Traveler
|
No tracking or data collection. Just my blog, my thoughts.
|
||||||
|
|
||||||
Took alot of effort to fetch those bytes, hope I have something here worth your while.
|
Took effort to fetch these bytes, hope I have something here worth your while.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ date: 2022-08-31T20:38:09-06:00
|
|||||||
tags: ['self host', 'raspberry pi']
|
tags: ['self host', 'raspberry pi']
|
||||||
description: 'I talk about how the "cloud" works, and show how one can host a site on the internet'
|
description: 'I talk about how the "cloud" works, and show how one can host a site on the internet'
|
||||||
type: "post"
|
type: "post"
|
||||||
image: "/images/ocean-aerial.jpg"
|
image: "/images/ocean-aerial.webp"
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
weight: 1
|
weight: 1
|
||||||
---
|
---
|
||||||
@@ -16,7 +16,7 @@ Back in my senior year of highschool, my buddies and I thought it would be funny
|
|||||||
|
|
||||||
## Internet, I've Heard of That
|
## Internet, I've Heard of That
|
||||||
|
|
||||||
I once heard the internet described as "the cloud", which is good to help people understand you know nothing about it. To give a marginally better explanation, imagine your brain, with all its neurons interconnected and whatnot. Lets call each neuron a "node". Each node holds information, and when it recieves a message it decides what to do with that information, modify it, store it, pass it on, sell it to the highest bidder for ad revenue, the possibilities are endless. In this way, the brain is much like the internet. These "nodes", or nuerons, are actually computers that make up the internet, a big web of interconnected, communicating devices. Our goal is to add a node to the network, and tell it to send specific information to anyone who calls on it.
|
I once heard the internet described as "the cloud", which is good to help people understand you know nothing about it. To give a marginally better explanation, imagine your brain, with all its neurons interconnected and whatnot. Lets call each neuron a "node". Each node holds information, and when it receives a message it decides what to do with that information, modify it, store it, pass it on, sell it to the highest bidder for ad revenue, the possibilities are endless. In this way, the brain is much like the internet. These "nodes", or nuerons, are actually computers that make up the internet, a big web of interconnected, communicating devices. Our goal is to add a node to the network, and tell it to send specific information to anyone who calls on it.
|
||||||
<!--
|
<!--
|
||||||
How did I get Here
|
How did I get Here
|
||||||
---
|
---
|
||||||
@@ -37,10 +37,10 @@ If you want to put your stake on the great world wide web, then you need a few t
|
|||||||
|
|
||||||
I will be walking you through the steps I took to get you here on this web page. There are hundreds of ways to get something on the internet, and my way is certainly one of them. For reference, I am running Arch Linux btw on my main computer.
|
I will be walking you through the steps I took to get you here on this web page. There are hundreds of ways to get something on the internet, and my way is certainly one of them. For reference, I am running Arch Linux btw on my main computer.
|
||||||
|
|
||||||
Since I don't want to pay $5 a month for ease-of-use and stability and scalability, I will be using a rasperry pi zero w2 plugged into a charging brick behind my book shelf to be the node.
|
Since I don't want to pay $5 a month for ease-of-use and stability and scalability, I will be using a raspberry pi zero w2 plugged into a charging brick behind my book shelf to be the node.
|
||||||
|
|
||||||

|

|
||||||
My Rasperry Pi Zero w2 - How it is currently serving up the web
|
My Raspberry Pi Zero w2 - How it is currently serving up the web
|
||||||
|
|
||||||
## Get a Domain
|
## Get a Domain
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ If you don't have a raspberry pi and instead coughed over $$$ to Jeff Bezos, the
|
|||||||
| A | www.<mydomain> | ip address |
|
| A | www.<mydomain> | ip address |
|
||||||
| A | <mydomain> | ip address |
|
| A | <mydomain> | ip address |
|
||||||
|
|
||||||
- If you are self-hosting like I am, you need to port-forward your device to make it visible on the world-wide-web. The steps to do this vary depending on your router. I fortunately and unfortunately have Google Fiber. So I can download Warzone in an hour but my resistance to an all-knowing data-collecting monolith feels futile. To port forward, you want to lock your raspberry pi's IP assigned by the router. This is done through DHCP. Then, open up your router's external port to the cooresponding internal raspberry pi port. For http you want your pi's port `80`. If you use SSL (which you should, its easy to setup), then use port `443`.
|
- If you are self-hosting like I am, you need to port-forward your device to make it visible on the world-wide-web. The steps to do this vary depending on your router. I fortunately and unfortunately have Google Fiber. So I can download Warzone in an hour but my resistance to an all-knowing data-collecting monolith feels futile. To port forward, you want to lock your raspberry pi's IP assigned by the router. This is done through DHCP. Then, open up your router's external port to the corresponding internal raspberry pi port. For http you want your pi's port `80`. If you use SSL (which you should, its easy to setup), then use port `443`.
|
||||||
|
|
||||||
:exclamation: DISCLAIMER :exclamation:
|
:exclamation: DISCLAIMER :exclamation:
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ draft: true
|
|||||||
tags:
|
tags:
|
||||||
summary:
|
summary:
|
||||||
cover:
|
cover:
|
||||||
image: "/images/img.jpg"
|
image: ""
|
||||||
# can also paste direct link from external site
|
# can also paste direct link from external site
|
||||||
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
||||||
alt: ""
|
alt: ""
|
||||||
@@ -5,7 +5,7 @@ draft: true
|
|||||||
tags:
|
tags:
|
||||||
summary:
|
summary:
|
||||||
cover:
|
cover:
|
||||||
image: "/images/img.jpg"
|
image: ""
|
||||||
# can also paste direct link from external site
|
# can also paste direct link from external site
|
||||||
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
||||||
alt: ""
|
alt: ""
|
||||||
@@ -6,7 +6,7 @@ tags:
|
|||||||
summary:
|
summary:
|
||||||
tocOpen: true
|
tocOpen: true
|
||||||
cover:
|
cover:
|
||||||
image: "/images/img.jpg"
|
image: ""
|
||||||
# can also paste direct link from external site
|
# can also paste direct link from external site
|
||||||
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
||||||
alt: ""
|
alt: ""
|
||||||
@@ -4,7 +4,7 @@ date: 2022-11-18T14:31:04-07:00
|
|||||||
tags: ['travel', 'japan']
|
tags: ['travel', 'japan']
|
||||||
description: 'We arrived to Tokyo, experienced some of the city as we found our way to our hotel for the night.'
|
description: 'We arrived to Tokyo, experienced some of the city as we found our way to our hotel for the night.'
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
image: "/images/japan-arrival.JPG"
|
image: "/images/japan-arrival.webp"
|
||||||
weight: 1
|
weight: 1
|
||||||
type: "post"
|
type: "post"
|
||||||
---
|
---
|
||||||
@@ -15,18 +15,18 @@ My wife and I finally get to experience first hand all of the things we watched
|
|||||||
|
|
||||||
## Flight Experience
|
## Flight Experience
|
||||||
|
|
||||||
We had a short flight to Seattle early in the morning, an hour wait, then hopped on the massive 10-people-per-row, serves-you-two-meals passenger plane. I've never flown internationally to this extent, so this was all very new experiece for me. The food was surprisingly delicious, my butt was unsurprisingly sore after a 9.5 hour flight. Things passed honestly quicker than I anticipated. We arrived in Japan at 2PM the next day. Pretty trippy, but sleeping on the plane kinda felt like we didn't just lose a day to the time-zone lords. We were both pretty frazzled after getting off the plane, but there were a ton of Japanese people pointing and holding signs of where to go to get processed through the system. When I was getting my fringerprints scanned, I didn't know it was also taking a picture. My nose was itchy so I did what anyone would do with an itchy nose and occupied hands, I scrunched my face rapidly and desperately. When my fingers were done it showed me my lemon-head motion-blurred image of a fugitive. Hopefully there is no system that flags me. Inside the airport, we purchased a SIM card for one of our phones to have data, and luckily found super friendly english-speaking worker that told us how to get to our hotel. We took a short one hour bus ride from Haneda to Shinjuku where we will be staying for the next three days.
|
We had a short flight to Seattle early in the morning, an hour wait, then hopped on the massive 10-people-per-row, serves-you-two-meals passenger plane. I've never flown internationally to this extent, so this was all very new experience for me. The food was surprisingly delicious, my butt was unsurprisingly sore after a 9.5 hour flight. Things passed honestly quicker than I anticipated. We arrived in Japan at 2PM the next day. Pretty trippy, but sleeping on the plane kinda felt like we didn't just lose a day to the time-zone lords. We were both pretty frazzled after getting off the plane, but there were a ton of Japanese people pointing and holding signs of where to go to get processed through the system. When I was getting my fingerprints scanned, I didn't know it was also taking a picture. My nose was itchy so I did what anyone would do with an itchy nose and occupied hands, I scrunched my face rapidly and desperately. When my fingers were done it showed me my lemon-head motion-blurred image of a fugitive. Hopefully there is no system that flags me. Inside the airport, we purchased a SIM card for one of our phones to have data, and luckily found super friendly english-speaking worker that told us how to get to our hotel. We took a short one hour bus ride from Haneda to Shinjuku where we will be staying for the next three days.
|
||||||
|
|
||||||
## The City
|
## The City
|
||||||
|
|
||||||
Immediately after getting off the bus, we found ourselves in the heart of a cement and steel labrynth. I've been to New York, but otherwise the largest city I've been in is Phoenix. Shinjuku (which to my understanding is inside the "bigger" Tokyo, but I think there is a Tokyo, Tokyo, kinda like NY, NY..?) is huge, with every development towering over the people below. Carrying luggage in Shinjuku was no sweat, wide sidewalks and side streets dedicated to foot traffic makes walking around here super convenient. We didn't realize that the hotel we booked was a massive chain, like a Hilton, but with the compactness of Tokyo, there were APA hotels just across the street from each other. So it took us not a few conversations with APA receptionists to find the right one. My wife and I were told that inside Tokyo we would be fine with English, and so far that is barely true. The language barrier is very apparent, and we struggle to understand pretty frequently the english japanese people can speak. But if anything it adds to the adventure of it all.
|
Immediately after getting off the bus, we found ourselves in the heart of a cement and steel labrynth. I've been to New York, but otherwise the largest city I've been in is Phoenix. Shinjuku (which to my understanding is inside the "bigger" Tokyo, but I think there is a Tokyo, Tokyo, kinda like NY, NY..?) is huge, with every development towering over the people below. Carrying luggage in Shinjuku was no sweat, wide sidewalks and side streets dedicated to foot traffic makes walking around here super convenient. We didn't realize that the hotel we booked was a massive chain, like a Hilton, but with the compactness of Tokyo, there were APA hotels just across the street from each other. So it took us not a few conversations with APA receptionists to find the right one. My wife and I were told that inside Tokyo we would be fine with English, and so far that is barely true. The language barrier is very apparent, and we struggle to understand pretty frequently the english Japanese people can speak. But if anything it adds to the adventure of it all.
|
||||||
|
|
||||||
## Shopping
|
## Shopping
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Of course the first thing we had to do was shop! The city is filled with conbinis (cone-bee-knees, small convience stores, 7/11 brand :open_mouth:), H&M looking shopping stores, sooo many bars, girls in super tall boots holding signs I can't read so I have no idea what they are up to, and people! Its an anxious feeling, in the excited and nervous sense, feeling so small in such a huge city.
|
Of course the first thing we had to do was shop! The city is filled with conbinis (cone-bee-knees, small convenience stores, 7/11 brand :open_mouth:), H&M looking shopping stores, sooo many bars, girls in super tall boots holding signs I can't read so I have no idea what they are up to, and people! Its an anxious feeling, in the excited and nervous sense, feeling so small in such a huge city.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
We saw a 6 story McDonalds on our way to the hotel so we satisfied our cravings there then went to a store called UNIQLO. Everything is so clean here, people have been really kind, and they even laugh when I say "konichiwa" (I won't read into why, feels better to assume my humor is bilingual). After about an hour of shopping we headed back to our room. We hit the sac around 9 and sleep caught us faster than we hoped.
|
We saw a 6 story McDonalds on our way to the hotel so we satisfied our cravings there then went to a store called UNIQLO. Everything is so clean here, people have been really kind, and they even laugh when I say "konichiwa" (I won't read into why, feels better to assume my humor is bilingual). After about an hour of shopping we headed back to our room. We hit the sac around 9 and sleep caught us faster than we hoped.
|
||||||
@@ -5,7 +5,7 @@ tags: ['japan', 'travel', 'cats']
|
|||||||
description: 'Our second day was full of cats, visiting Nippori and walking down the "shotengai", or the main shopping street.'
|
description: 'Our second day was full of cats, visiting Nippori and walking down the "shotengai", or the main shopping street.'
|
||||||
type: "post"
|
type: "post"
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
image: "/images/japan-nippori-walk.JPG"
|
image: "/images/japan-nippori-walk.webp"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Japan is Fond of Their Cats
|
# Japan is Fond of Their Cats
|
||||||
@@ -14,27 +14,27 @@ My wife and I have two cats of our own, so naturally we had to experience the ca
|
|||||||
|
|
||||||
## Morning - Garden Walk
|
## Morning - Garden Walk
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
We had our equivalent of a 'continental' breakfast at our hotel, which was leaps ahead of American hosting hospitality standards. Plus, it was largely fats and protiens, which is a much better way to check off consuming your 'most important meal of the day'. We then headed to Shinjuku National Garden - $5 admission, and enjoyed the h\*ck out of the 65 degree weather. We walked and viewed the grounds, the whole garden is probably a mile across. Before we left we found a little bench in the sun for my wife to paint, she brought a pocket watercolor set. Away to Niporri to meet En and cat city.
|
We had our equivalent of a 'continental' breakfast at our hotel, which was leaps ahead of American hosting hospitality standards. Plus, it was largely fats and proteins, which is a much better way to check off consuming your 'most important meal of the day'. We then headed to Shinjuku National Garden - $5 admission, and enjoyed the h\*ck out of the 65 degree weather. We walked and viewed the grounds, the whole garden is probably a mile across. Before we left we found a little bench in the sun for my wife to paint, she brought a pocket watercolor set. Away to Niporri to meet En and cat city.
|
||||||
|
|
||||||
## Afternoon - Cat City
|
## Afternoon - Cat City
|
||||||
|
|
||||||
En is from Japan, learned English in Scottland of all places (she spoke english with a Japanese accent instead of Scottish, which disappointed me just a little), and was such a kind guide. We walked down the shotengai of Nippori, the main shopping street of a city, exploring all the little shops on each side. Each store was themed it seemed, like bamboo goods, paulownia wood boxes, and of course, cat shops, selling things to show your love for cats, not to care for them. We loved it. There were also a number of local art galleries, often hosted inside historic homes in the neighborhood that had been renovated. It was really interesting to walk through these renovated-homes-converted-art-cafes and see traditional alongside contemorary art by the local residents, while people order teas and coffee at the cafe.
|
En is from Japan, learned English in Scottland of all places (she spoke English with a Japanese accent instead of Scottish, which disappointed me just a little), and was such a kind guide. We walked down the shotengai of Nippori, the main shopping street of a city, exploring all the little shops on each side. Each store was themed it seemed, like bamboo goods, paulownia wood boxes, and of course, cat shops, selling things to show your love for cats, not to care for them. We loved it. There were also a number of local art galleries, often hosted inside historic homes in the neighborhood that had been renovated. It was really interesting to walk through these renovated-homes-converted-art-cafes and see traditional alongside contemporary art by the local residents, while people order teas and coffee at the cafe.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This house called itself the nekomachi, or cat-town of the neighborhood, with a gallery inside of exclusively cat-themed art.
|
This house called itself the nekomachi, or cat-town of the neighborhood, with a gallery inside of exclusively cat-themed art.
|
||||||
|
|
||||||
It was probbably the most 'local' thing we will do on our trip, we loved it. We ended our walk in Niporri passing through the neighborhood's 'graveyard', in the word of En, though I think the western idea of graveyard is tainted with Halloween spooky connotations. En told us that it has been full for probably 30 years, with any new bodies desiring to be buried needing to relocate further from Tokyo. Some of the gravesites had massive stones with characters carved into it's face. En told us they chronicled the person's life and their family, not unlike a westerner's tombstone, just more verbose. I should mention that we stopped and purchased ourselves some taiyaki, a fish pastry filled with sweet red bean paste, and my wife was crazy for it. They are pretty good.
|
It was probably the most 'local' thing we will do on our trip, we loved it. We ended our walk in Niporri passing through the neighborhood's 'graveyard', in the word of En, though I think the western idea of graveyard is tainted with Halloween spooky connotations. En told us that it has been full for probably 30 years, with any new bodies desiring to be buried needing to relocate further from Tokyo. Some of the gravesites had massive stones with characters carved into it's face. En told us they chronicled the person's life and their family, not unlike a westerner's tombstone, just more verbose. I should mention that we stopped and purchased ourselves some taiyaki, a fish pastry filled with sweet red bean paste, and my wife was crazy for it. They are pretty good.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Evening - Cat Cafe and Ramen
|
## Evening - Cat Cafe and Ramen
|
||||||
|
|
||||||
We ended our evening visiting Akihabara. We were hoping to enjoy its renowned anime consumer content, but were disappointed to not find any stickers or shirts, just pins, figurines, charms, manga, flags, posters, chibis, funko pops, and probably every other piece of parafanelia. I guess the Japanese just do it differently. We were pretty hungry so we headed over to the cat cafe. You pay for time and get to use an auto dispensing hot/cold drink machine (coffee, tea, cocoa) and pet any of the 20+ cats in the space. It had a good vibe, and the cats were beyond my comprehension soft. They also were all so aloof, they get so much attention and stimulation they can get anything they want, so they all were very uninterested in you. Felt insulting. We finished the evening by getting some genuine 'pork oil noodle' ramen at a walk-up ramen bar. The taste was pretty awesome, just had to get over the idea of them pouring pork oil on the noodles.
|
We ended our evening visiting Akihabara. We were hoping to enjoy its renowned anime consumer content, but were disappointed to not find any stickers or shirts, just pins, figurines, charms, manga, flags, posters, chibis, funko pops, and probably every other piece of parafanelia. I guess the Japanese just do it differently. We were pretty hungry so we headed over to the cat cafe. You pay for time and get to use an auto dispensing hot/cold drink machine (coffee, tea, cocoa) and pet any of the 20+ cats in the space. It had a good vibe, and the cats were beyond my comprehension soft. They also were all so aloof, they get so much attention and stimulation they can get anything they want, so they all were very uninterested in you. Felt insulting. We finished the evening by getting some genuine 'pork oil noodle' ramen at a walk-up ramen bar. The taste was pretty awesome, just had to get over the idea of them pouring pork oil on the noodles.
|
||||||
|
|
||||||
We had a super fun second day. Felt less wiped at the end of the day, but our feet were aching. I was wearing my minmalist flip-flops because the weather was so nice, but that is probably going to end.
|
We had a super fun second day. Felt less wiped at the end of the day, but our feet were aching. I was wearing my minimalist flip-flops because the weather was so nice, but that is probably going to end.
|
||||||
|
|
||||||
|
|
||||||
## My Japan Travel Tips (JTT)
|
## My Japan Travel Tips (JTT)
|
||||||
@@ -4,7 +4,7 @@ date: 2022-11-21T05:19:38-07:00
|
|||||||
tags: ['travel', 'japan']
|
tags: ['travel', 'japan']
|
||||||
description: 'We packed up from Shinjuku and headed to a small town near the base of Mount Fuji and experienced the generosity and kindness of an old man there.'
|
description: 'We packed up from Shinjuku and headed to a small town near the base of Mount Fuji and experienced the generosity and kindness of an old man there.'
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
image: "/images/japan-fuji.JPG"
|
image: "/images/japan-fuji.webp"
|
||||||
weight: 1
|
weight: 1
|
||||||
type: "post"
|
type: "post"
|
||||||
---
|
---
|
||||||
@@ -18,20 +18,20 @@ Waking up this morning we weren't in a rush, but we felt like we had no idea how
|
|||||||
My wife asked if he knew where any good shrines were in the area, anticipating some directions, and he just said something along the lines of "ah, we go". He took us straight there. He taught us the customs and etiquette of visiting the shrines:
|
My wife asked if he knew where any good shrines were in the area, anticipating some directions, and he just said something along the lines of "ah, we go". He took us straight there. He taught us the customs and etiquette of visiting the shrines:
|
||||||
- When entering through the gate (Torii, the big red curved beam spanning across red columns) you bow twice
|
- When entering through the gate (Torii, the big red curved beam spanning across red columns) you bow twice
|
||||||
- Also when entering you walk on one side of the path, not the middle. He didn't know the word, but my guess is its reserved for either religious or royal persons.
|
- Also when entering you walk on one side of the path, not the middle. He didn't know the word, but my guess is its reserved for either religious or royal persons.
|
||||||
- At this shrine there was a fountain with multiple spigots, he told us to wash our hands, but motioned not to rinse your mouth and spit out the water. I didn't have a problem supressing my urge to rinse out my mouth at the sight of running water, so I considered myself lucky.
|
- At this shrine there was a fountain with multiple spigots, he told us to wash our hands, but motioned not to rinse your mouth and spit out the water. I didn't have a problem suppressing my urge to rinse out my mouth at the sight of running water, so I considered myself lucky.
|
||||||
- At the back of the grounds is the shrine offering. You toss a 5-50 yen coin in it and bow twice, then clap twice, then bow one more time, during which you make a wish and "remember it in your heart".
|
- At the back of the grounds is the shrine offering. You toss a 5-50 yen coin in it and bow twice, then clap twice, then bow one more time, during which you make a wish and "remember it in your heart".
|
||||||
- As you leave, you turn back to shrine and bow once more.
|
- As you leave, you turn back to shrine and bow once more.
|
||||||
|
|
||||||

|

|
||||||
Our host told us that its believed that the god lives on top. Its probably 40+ feet around at the base.
|
Our host told us that its believed that the god lives on top. Its probably 40+ feet around at the base.
|
||||||
|
|
||||||
Not sure how much got lost in translation, but it seemed we did a good job of following his instructions. He then dropped us off by Lake Kawaguchi, which had a stunning view of Mount Fuji with the sun conveniently setting. He left us to sight see some more as the valley was swallowed in Mt. Fuji's shadow. Wow I'm impressed by my literary devices tonight.
|
Not sure how much got lost in translation, but it seemed we did a good job of following his instructions. He then dropped us off by Lake Kawaguchi, which had a stunning view of Mount Fuji with the sun conveniently setting. He left us to sight see some more as the valley was swallowed in Mt. Fuji's shadow. Wow I'm impressed by my literary devices tonight.
|
||||||
|
|
||||||
## Walking the Neighborhood
|
## Walking the Neighborhood
|
||||||
|
|
||||||
It got dark really quickly, and much cooler than Tokyo got in the evenings. We stopped a local walmart equivalent and purchased some gloves and ramen cups to cook once we got home. We wanted to go out for food, but Mondays are like western Sundays, and every shop we tried was closed.
|
It got dark really quickly, and much cooler than Tokyo got in the evenings. We stopped a local Walmart equivalent and purchased some gloves and ramen cups to cook once we got home. We wanted to go out for food, but Mondays are like western Sundays, and every shop we tried was closed.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
There was one open but it has questionable reviews and pictures on google, so we decided better stick with the MSG than food poisoning. A note I've noticed about portions here in Japan: everything is packaged in really sensibly sized portions. Its almost as if the governing body of Japan and food distributors are colluding together to get us to not over eat. It feels really intrusive.
|
There was one open but it has questionable reviews and pictures on google, so we decided better stick with the MSG than food poisoning. A note I've noticed about portions here in Japan: everything is packaged in really sensibly sized portions. Its almost as if the governing body of Japan and food distributors are colluding together to get us to not over eat. It feels really intrusive.
|
||||||
|
|
||||||
@@ -39,6 +39,6 @@ There was one open but it has questionable reviews and pictures on google, so we
|
|||||||
|
|
||||||
Our host was greatly amused at our noodle cup dinner. He heated up some water for us and pulled out wrapped frozen pork he had prepared and told us to heat it up to put in our ramen. It definitely leveled up the quality.
|
Our host was greatly amused at our noodle cup dinner. He heated up some water for us and pulled out wrapped frozen pork he had prepared and told us to heat it up to put in our ramen. It definitely leveled up the quality.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
His house is off-grid with solar panels, with lots of custom wood carpentry, he told us its a hobby of his. He also runs a small cafe out of the house that is all organic, he serves food he grows in his little farm. Its a pretty incredible passion project of his, all in an effort he says to reduce his CO2 emissions, as global warming has increased the size and damage of typhoons to Japan. His eletric car also has a big sticker on it about being in some sort of EV club that I suspect views him as the president. As he showed us the house, he mentioned that we could use his 'onsen' (hot spring / public bath). He shows us into a sauna looking room with a large wooden bath. We happily accepted the offer. We had a wonderfully warm evening after some trekking out in the cold.
|
His house is off-grid with solar panels, with lots of custom wood carpentry, he told us its a hobby of his. He also runs a small cafe out of the house that is all organic, he serves food he grows in his little farm. Its a pretty incredible passion project of his, all in an effort he says to reduce his CO2 emissions, as global warming has increased the size and damage of typhoons to Japan. His electric car also has a big sticker on it about being in some sort of EV club that I suspect views him as the president. As he showed us the house, he mentioned that we could use his 'onsen' (hot spring / public bath). He shows us into a sauna looking room with a large wooden bath. We happily accepted the offer. We had a wonderfully warm evening after some trekking out in the cold.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
title: "My First Real Job"
|
||||||
|
date: 2022-12-27T00:47:29-07:00
|
||||||
|
description: 'I take a trip down memory lane, explaining how I got my first real job as a developer.'
|
||||||
|
tags: ["work", "thoughts"]
|
||||||
|
showTableOfContents: true
|
||||||
|
image: "/images/monochrome-path.webp"
|
||||||
|
weight: 1
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# What's a 'Real' Job?
|
||||||
|
|
||||||
|
Previous to the 'real' job I got, I had worked a handful of other jobs, some were even related to my field of work,
|
||||||
|
computer wizardry. What made this job different from my previous internships and odd-job contract work, was that this
|
||||||
|
job payed a salary, and a pretty good one for taking a "beginner". I thought I would chronicle the experience for other
|
||||||
|
software developers out there curious of what one random anon's experience was like.
|
||||||
|
|
||||||
|
# The Dating Period
|
||||||
|
|
||||||
|
I dropped out of college, just temporarily, to pursue other interests, and also try the job market, to see what I could
|
||||||
|
catch with, what was in my mind, a decent amount of real world experience. I had worked some sort of software developer
|
||||||
|
job for about two years, and had just recently concluded a summer internship. I could have continued at my internship
|
||||||
|
except for the fact that I wasn't offered a full time position, which is a good story for another time. So, with my 2.5
|
||||||
|
year college education and nearly equal work experience, I ventured onto Indeed and other similar shark sites for poor
|
||||||
|
saps like myself with naive hopes to throw their resume into the abyss. A month or so into my college semester break,
|
||||||
|
I received an email from a recruiting agency with an offer for a full time, remote position paying a salaried amount
|
||||||
|
I thought surely wasn't deserving of a college drop out. To sweeten the pot, the job's tech stack mentioned flutter,
|
||||||
|
which I had spent the last year building a handful of apps and had really come to love the framework. I am compelled to
|
||||||
|
add that I really don't like React or Vue, or really Javascript / npm / webpack / css in any form / webdev in general
|
||||||
|
(typescript is the only thing I do like in that space), so building something in Flutter got me really excited. However,
|
||||||
|
it was one of probably 20 emails I got a week spamming my inbox, I really don't know why I was reading this one, and
|
||||||
|
thought surely I wasn't qualified enough for the position. I clicked a link in the email and was brought to a calendar
|
||||||
|
to schedule a zoom call with the recruiter. I wasn't sure this was even legit, and there was a time slot in 15 minutes
|
||||||
|
that a scheduled. I 'showed up' and sure enough, a recruiter on the other side of the internet was waiting for me. We
|
||||||
|
chatted and I walked him through my work experience, no mention of school. He seemed impressed. A few days pass and
|
||||||
|
he connects me with someone from the company, we set up another zoom meeting, more of a 'get to know you'. That call
|
||||||
|
was interesting. I talked with two guys, which are the only two full time devs in the company, and neither of them had
|
||||||
|
really hired anyone before. The questions they asked really weren't technical in nature, but I also couldn't get a read
|
||||||
|
in the slightest what kind of information they were looking for so I could tailor my answers. 15 minutes passed and
|
||||||
|
the lead dev said something like "Alright, well that's all the questions I could think of..." * trails off... Uhhh --
|
||||||
|
I desperately thought of a question to ask them about the company to buy myself some time to assess what was happening.
|
||||||
|
It seemed to me that this interview had gone poorly, they didn't see my true awesomeness (because their questions
|
||||||
|
weren't really introspective or technical) so I thought of something I could do to rescue this sinking ship. I learned
|
||||||
|
more about the product they are building and what their jobs are like. They seemed to like that. After 40 minutes, we
|
||||||
|
ended the call and I felt a bit more satisfied with that ending. I believe that was on a Monday, and on Thursday they
|
||||||
|
reached out to setup a lunch the next day. Though the company is remote, both devs are located close to me, so we met
|
||||||
|
over lunch the next day. I was told by the recruiter that I would have a more technical portion of the interview, a paid
|
||||||
|
day working in the office with the team to see if I'm a good fit. Heading to lunch, I expected them to congratulate me
|
||||||
|
for a god interview and schedule the 'technical' part, which is probably the most dreaded step of the software engineer
|
||||||
|
acquisition ritual. We had lunch and just got to know each other better, I discovered both of them align pretty closely
|
||||||
|
with my conservative values, which was a nice bonus. Made conversation easy as there is lots to talk about with new
|
||||||
|
acquaintances that you discover to have similar passions. At the end of the hour dinner, with no previous mention of the
|
||||||
|
position, the lead dev says "So we both like you, we want you on the team." This came as a bit of a surprise, where was
|
||||||
|
my gauntlet technical obstacle course? But obviously I was thrilled. The dating phase ended as quickly as it began, and
|
||||||
|
with that, I found myself a married to the corporate system.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Incoming!! Analogies to life as an rpg with points assigned to various traits that may be more genetic / permanent
|
||||||
|
than I make it seem. Character composition is a little more that just stats, buffs / debuffs, and inventory. But only
|
||||||
|
a little...
|
||||||
|
|
||||||
|
# Advice
|
||||||
|
|
||||||
|
How much luck was involved in this process? How much was my modestly stacked resume? I can't say for certain. From my
|
||||||
|
experience, there are a few things within your control that you can try to develop to increase your odds in the game.
|
||||||
|
I stress *in your control*. I wager 80% of the time when you aren't hired, but you are qualified, its because of forces
|
||||||
|
completely outside your control, hiring freeze, nepotism, off by one error, lazy hr guy, etc, the list has no end.
|
||||||
|
|
||||||
|
## Emotional intelligence
|
||||||
|
|
||||||
|
During this hiring process, they took a lot of my expertise and experience by my word, and I can only assume they did
|
||||||
|
so because it seemed that my word was one they could trust. I think experience is great; lots of experience gives you
|
||||||
|
the confidence to answer questions about previous problems you have solved, but experience isn't all, or possibly even,
|
||||||
|
the majority of what gets you hired. I think a lot of it comes down to your interpersonal skills, your charisma stat,
|
||||||
|
how chiseled your chin looks. Some of that you can't change easily, I think I was born with an above average emotional
|
||||||
|
intelligence. But, I can't stress enough how important it is that you appear confident but not overbearing, and eager to
|
||||||
|
learn and work. With everything today getting people to get as far away from socializing I R L, not having any grounded
|
||||||
|
confidence in their abilities, I get the sense that a lot of qualified individuals just don't sell themselves enough.
|
||||||
|
Want the job, and convince yourself you would be a great addition to the team.
|
||||||
|
|
||||||
|
## Most Important Trait
|
||||||
|
|
||||||
|
If you could do a character stat reset on yourself and spec everything into one category, I think you should go all
|
||||||
|
in on "Ability / Willingness to Learn". I think the sciency term is brain plasticity. But you don't need to spend 250
|
||||||
|
drupals to activate the stat reset, I think its a habit of thought, and can be a learned behavior. What do you do when
|
||||||
|
you encounter a problem? Do you google it? What if google has no answers? What if the answer is "Read these 20 pages
|
||||||
|
of docs"? Do you read them? Or do you say you are 'blocked' during stand up and let your team lead or that one guy that
|
||||||
|
solves every mystery get to the bottom of it? If you can teach yourself something by gritting your teeth and stumbling
|
||||||
|
along the way, only to realize your solution is magnitudes slower that its supposed to be, I think you are still leaps
|
||||||
|
ahead of the snooty pampered rich mom's son who has kombucha and white granite counter tops at home to retreat to when
|
||||||
|
he gets too 'whelmed with work. A wise woman said to me that grit is the greatest word in the English language, if you
|
||||||
|
got enough of it, there isn't anything you can't do. Wise words I reckon.
|
||||||
@@ -3,7 +3,7 @@ title: "The Migration to Arch"
|
|||||||
date: 2023-08-15T02:04:21-06:00
|
date: 2023-08-15T02:04:21-06:00
|
||||||
description:
|
description:
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
image: "/images/arch-logo.png"
|
image: "/images/arch-logo.webp"
|
||||||
weight: 1
|
weight: 1
|
||||||
type: "post"
|
type: "post"
|
||||||
---
|
---
|
||||||
@@ -18,7 +18,7 @@ As they say, there are two wolves inside each man, one that craves stable, calm
|
|||||||
hood, fat finger `rm -rf /` and other monstrosities that I don't care to joke about because they hurt me too much.
|
hood, fat finger `rm -rf /` and other monstrosities that I don't care to joke about because they hurt me too much.
|
||||||
|
|
||||||
So, I moved off of the Raspberry Pi Zero W2, and on to a much more legitimate pc build. The blog could have run fine on the pi, but it didn't take long for me to
|
So, I moved off of the Raspberry Pi Zero W2, and on to a much more legitimate pc build. The blog could have run fine on the pi, but it didn't take long for me to
|
||||||
feel justified in spending some money on a faster machine. Bought it second hand from a crypto mining rig, swapped out the pentium for a respectable i7 of some
|
feel justified in spending some money on a faster machine. Bought it second hand from a crypto mining rig, swapped out the Pentium for a respectable i7 of some
|
||||||
recent generation, and I had an upcycled machine ready for some data crunching!
|
recent generation, and I had an upcycled machine ready for some data crunching!
|
||||||
|
|
||||||
## The Services
|
## The Services
|
||||||
@@ -42,7 +42,7 @@ Eventually, I envision a whole bunch of services running in my 'home lab', as ho
|
|||||||
|
|
||||||
Arch Linux was not actually the first OS I put on this new (to me) machine. I had drunk from the FSF goblet and got it in my head to try out
|
Arch Linux was not actually the first OS I put on this new (to me) machine. I had drunk from the FSF goblet and got it in my head to try out
|
||||||
[Guix System, or Guix SD,](https://guix.gnu.org/) or whatever its officially called. I recommend checking it out. It was a couple month adventure, but I think my lisp-less mind
|
[Guix System, or Guix SD,](https://guix.gnu.org/) or whatever its officially called. I recommend checking it out. It was a couple month adventure, but I think my lisp-less mind
|
||||||
couldn't handle the parantheses required. It has some really cool concepts similar to NixOS, your whole system (users, installed packages, mount points, etc) is defined in one or
|
couldn't handle the parentheses required. It has some really cool concepts similar to NixOS, your whole system (users, installed packages, mount points, etc) is defined in one or
|
||||||
more files. Despite its great documentation, it is hard to understand what is going on without some good Guile / Scheme fundamentals, which I lack. So, back to what I know: Arch (btw).
|
more files. Despite its great documentation, it is hard to understand what is going on without some good Guile / Scheme fundamentals, which I lack. So, back to what I know: Arch (btw).
|
||||||
|
|
||||||
## What Did I Learn?
|
## What Did I Learn?
|
||||||
@@ -61,7 +61,7 @@ But, for only ~$5.00 a month, I get access to a sftp server with 1TB capacity, w
|
|||||||
to a HDD drive connected to the computer. I have lots more to say about so many of these things, I spent pretty much all day configuring everything and getting it all to work, but I
|
to a HDD drive connected to the computer. I have lots more to say about so many of these things, I spent pretty much all day configuring everything and getting it all to work, but I
|
||||||
told myself to prioritize frequency over comprehensiveness, so I will leave it at that.
|
told myself to prioritize frequency over comprehensiveness, so I will leave it at that.
|
||||||
|
|
||||||
Oh! I almost forgot, if you want to use the gitea instance I am running as an alternative to having your code fed to AI models, please email me! Im no enterprise company with infinite
|
Oh! I almost forgot, if you want to use the gitea instance I am running as an alternative to having your code fed to AI models, please email me! I'm no enterprise company with infinite
|
||||||
backups and resources, but I'd love to share what little I have if it would be useful to you!
|
backups and resources, but I'd love to share what little I have if it would be useful to you!
|
||||||
|
|
||||||
Thanks for stopping in :)
|
Thanks for stopping in :)
|
||||||
@@ -6,7 +6,7 @@ tags:
|
|||||||
summary:
|
summary:
|
||||||
tocOpen: true
|
tocOpen: true
|
||||||
cover:
|
cover:
|
||||||
image: "/images/img.jpg"
|
image: ""
|
||||||
# can also paste direct link from external site
|
# can also paste direct link from external site
|
||||||
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
||||||
alt: ""
|
alt: ""
|
||||||
@@ -4,7 +4,7 @@ date: 2024-01-04T10:04:57-07:00
|
|||||||
description: 'How to host a mumble server on a subdomain behind nginx reverse proxy'
|
description: 'How to host a mumble server on a subdomain behind nginx reverse proxy'
|
||||||
tags: ["nginx"]
|
tags: ["nginx"]
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
image: "/images/nginx-mumble.png"
|
image: "/images/nginx-mumble.webp"
|
||||||
weight: 1
|
weight: 1
|
||||||
type: "post"
|
type: "post"
|
||||||
---
|
---
|
||||||
@@ -13,7 +13,7 @@ type: "post"
|
|||||||
|
|
||||||
Well I couldn't find any actual examples of someone doing what I wanted, namely, hosting
|
Well I couldn't find any actual examples of someone doing what I wanted, namely, hosting
|
||||||
the murmur server on a subdomain on my machine behind an nginx proxy. I only have ports 80
|
the murmur server on a subdomain on my machine behind an nginx proxy. I only have ports 80
|
||||||
and 443 opened on my router, so I chose to recieve the mumble traffic to come in on port 443.
|
and 443 opened on my router, so I chose to receive the mumble traffic to come in on port 443.
|
||||||
Sounds easy enough, but the problem comes when you let nginx decrypt the packets in the process
|
Sounds easy enough, but the problem comes when you let nginx decrypt the packets in the process
|
||||||
of passing them to the murmur server, it raises a TLS/SSL Termination Error. Murmur insists on
|
of passing them to the murmur server, it raises a TLS/SSL Termination Error. Murmur insists on
|
||||||
End to End Encryption (E2EE), which is a good thing.
|
End to End Encryption (E2EE), which is a good thing.
|
||||||
@@ -23,7 +23,7 @@ an Ad riddled page, here is the nginx config that got my setup working, all of t
|
|||||||
on an Arch Linux install, minus the `stream` block. Ports need to be defined for your setup for
|
on an Arch Linux install, minus the `stream` block. Ports need to be defined for your setup for
|
||||||
`INTERNAL_MUMBLE_PORT` (port that murmur is listening on) and `NEW_NGINX_SSL_PORT`. Previously,
|
`INTERNAL_MUMBLE_PORT` (port that murmur is listening on) and `NEW_NGINX_SSL_PORT`. Previously,
|
||||||
`NEW_NGINX_SSL_PORT` was 443, but the stream block now will be using 443, and you can't bind to the same
|
`NEW_NGINX_SSL_PORT` was 443, but the stream block now will be using 443, and you can't bind to the same
|
||||||
port with seperate services. So pick a new port for the other ssl nginx services to listen on,
|
port with separate services. So pick a new port for the other ssl nginx services to listen on,
|
||||||
as well as pass traffic to, internally.
|
as well as pass traffic to, internally.
|
||||||
|
|
||||||
`nginx.conf`
|
`nginx.conf`
|
||||||
@@ -6,7 +6,7 @@ tags:
|
|||||||
summary:
|
summary:
|
||||||
tocOpen: true
|
tocOpen: true
|
||||||
cover:
|
cover:
|
||||||
image: "/images/img.jpg"
|
image: ""
|
||||||
# can also paste direct link from external site
|
# can also paste direct link from external site
|
||||||
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
# ex. https://i.ibb.co/K0HVPBd/paper-mod-profilemode.png
|
||||||
alt: ""
|
alt: ""
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
date: 2025-01-28
|
date: 2025-01-28
|
||||||
description: "I realized that I've been holding myself back because of pride."
|
description: "I realized that I've been holding myself back because of pride."
|
||||||
# image: ""
|
lastmod: 2026-06-03T00:08:31-06:00
|
||||||
lastmod: 2025-01-28
|
showTableOfContents: false
|
||||||
showTableOfContents: true
|
tags: ["ai", "reflection"]
|
||||||
tags: ["AI", "Reflection"]
|
|
||||||
title: "AI Hesitation Turning Around"
|
title: "AI Hesitation Turning Around"
|
||||||
type: "post"
|
type: "post"
|
||||||
---
|
---
|
||||||
@@ -37,16 +36,16 @@ I have seen it as a crutch. I see it as having very muddy licensing
|
|||||||
and potential legal issues in its future. I see it partly destroying the craft of software engineering. I think its
|
and potential legal issues in its future. I see it partly destroying the craft of software engineering. I think its
|
||||||
very over hyped.
|
very over hyped.
|
||||||
|
|
||||||
_BUT_
|
## BUT
|
||||||
|
|
||||||
I think its time I give up fighting against the machine in this respect and see if using AI
|
I think its time I give up fighting against the machine in this respect and see if using AI
|
||||||
as a tool in my tool belt helps me become a better engineer.
|
as a tool in my tool belt helps me become a better engineer.
|
||||||
|
|
||||||
## The Pivot Point
|
# The Pivot Point
|
||||||
|
|
||||||
I read [this blog post](https://blog.nelhage.com/post/personal-software-with-claude/) about building personal software
|
I read [this blog post](https://blog.nelhage.com/post/personal-software-with-claude/) about building personal software
|
||||||
with Claude and I realized that perhaps I could better my life and those around me by taking advantage of this tool. Was
|
with Claude and I realized that perhaps I could better my life and those around me by taking advantage of this tool. Was
|
||||||
it just stubbornness and pride that was preventing me from benefitting? You may think its just FOMO, but I've held off
|
it just stubbornness and pride that was preventing me from benefiting? You may think its just FOMO, but I've held off
|
||||||
for a long time from using AI.[^1] I think it is just a recognition of my hard-headed and misplaced judgement. So, today
|
for a long time from using AI.[^1] I think it is just a recognition of my hard-headed and misplaced judgement. So, today
|
||||||
I am signing up for a Claude account. I'm going to build with AI (I'm not going to write these posts with it though). As
|
I am signing up for a Claude account. I'm going to build with AI (I'm not going to write these posts with it though). As
|
||||||
this new perspective settled in my mind, I had a couple thoughts:
|
this new perspective settled in my mind, I had a couple thoughts:
|
||||||
@@ -75,5 +74,5 @@ Not sure if it's a higher plane of Nirvana I am about to enter or if I'll be get
|
|||||||
waxed wings and plummet to my death, but I figure its never a bad idea to keep my mind open and give it a year.
|
waxed wings and plummet to my death, but I figure its never a bad idea to keep my mind open and give it a year.
|
||||||
|
|
||||||
[^1]: Not entirely. My best guess is I've queried ChatGPT less than 50 times. I have used it over the last two years as a Google
|
[^1]: Not entirely. My best guess is I've queried ChatGPT less than 50 times. I have used it over the last two years as a Google
|
||||||
Search replacement occassionally. When AI was first released I tried it with some coding / reasoning tasks and formed
|
Search replacement occasionally. When AI was first released I tried it with some coding / reasoning tasks and formed
|
||||||
most of my opinion from that experience.
|
most of my opinion from that experience.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
date: 2025-03-03T09:19:07-07:00
|
date: 2025-03-03T09:19:07-07:00
|
||||||
description: "I learned the importance of taking time away from the computer in software development"
|
description: "I learned the importance of taking time away from the computer in software development"
|
||||||
lastmod: 2025-03-03T09:19:07-07:00
|
lastmod: 2026-03-04T01:31:12-07:00
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
type: "tils"
|
type: "post"
|
||||||
title: "TIL: Hammock Driven Development"
|
title: "TIL: Hammock Driven Development"
|
||||||
image: "images/hammock.jpg"
|
image: "/images/hammock.webp"
|
||||||
alt: "hammock with a cat"
|
image_alt: "hammock with a cat"
|
||||||
tags: ["clojure", "practices", "rich hickey"]
|
tags: ["clojure", "practices", "rich hickey", "til"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Context
|
# Context
|
||||||
@@ -3,9 +3,9 @@ date: 2025-03-10T22:35:46-06:00
|
|||||||
description: "Did I discover the middle ground of calisthenics and weight lifting?"
|
description: "Did I discover the middle ground of calisthenics and weight lifting?"
|
||||||
lastmod: 2025-03-10T22:35:46-06:00
|
lastmod: 2025-03-10T22:35:46-06:00
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
type: "tils"
|
type: "post"
|
||||||
title: "TIL: I Like Hybrid Weighted Body Weight Exercises"
|
title: "TIL: I Like Hybrid Weighted Body Weight Exercises"
|
||||||
tags: [ "fitness", "life" ]
|
tags: [ "fitness", "life", "til" ]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Context
|
# Context
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
date: 2025-03-16T22:17:30-06:00
|
date: 2025-03-16T22:17:30-06:00
|
||||||
description: "Wowzas, if things pan out, I'm going to be a Dad!"
|
description: "Wowzas, if things pan out, I'm going to be a Dad!"
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
type: "tils"
|
type: "post"
|
||||||
lastmod: 2025-03-16T22:17:30-06:00
|
lastmod: 2026-03-02T00:40:43-07:00
|
||||||
tags: [ "life", "parenting" ]
|
tags: [ "life", "parenthood", "til" ]
|
||||||
title: "I Learned My Wife Is Pregnant"
|
title: "I Learned My Wife Is Pregnant"
|
||||||
image: ""
|
image: ""
|
||||||
---
|
---
|
||||||
@@ -3,12 +3,12 @@ date: 2025-09-30T23:28:53-06:00
|
|||||||
description: "There's a reason things become mainstream"
|
description: "There's a reason things become mainstream"
|
||||||
lastmod: 2025-09-30T23:28:53-06:00
|
lastmod: 2025-09-30T23:28:53-06:00
|
||||||
showTableOfContents: true
|
showTableOfContents: true
|
||||||
type: "tils"
|
type: "post"
|
||||||
title: "TIL: People Are Actually Right"
|
title: "TIL: People Are Actually Right"
|
||||||
image: ""
|
image: ""
|
||||||
image_credit: ""
|
image_credit: ""
|
||||||
image_alt: ""
|
image_alt: ""
|
||||||
tags: ["philosophy", "lifestyle"]
|
tags: ["philosophy", "life", "til"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Context
|
# Context
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
date: 2025-10-01T22:32:18-06:00
|
||||||
|
description: "Why flushing buffers is necessary"
|
||||||
|
lastmod: 2026-02-24T00:35:36-07:00
|
||||||
|
showTableOfContents: true
|
||||||
|
type: "post"
|
||||||
|
title: "TIL: Why We Cant Forget to Flush"
|
||||||
|
image: ""
|
||||||
|
image_credit: ""
|
||||||
|
image_alt: ""
|
||||||
|
tags: ["zig", "programming", "til"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
I just learned about this book [Systems Programming with Zig](https://www.manning.com/books/systems-programming-with-zig).
|
||||||
|
I'm reading through the free introductory chapters to familiarize myself with things I thought I'd already know. But, as
|
||||||
|
a college drop-out, I think there are a handful of low level concepts I missed, and buffer flushing was one of them!
|
||||||
|
|
||||||
|
# Reflection
|
||||||
|
|
||||||
|
Zig is all about removing implicit behavior. Its number one of the `zig zen` after all:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nate in ~/source/zig-systems on main λ zig zen
|
||||||
|
|
||||||
|
* Communicate intent precisely.
|
||||||
|
* Edge cases matter.
|
||||||
|
* Favor reading code over writing code.
|
||||||
|
* Only one obvious way to do things.
|
||||||
|
* Runtime crashes are better than bugs.
|
||||||
|
* Compile errors are better than runtime crashes.
|
||||||
|
* Incremental improvements.
|
||||||
|
* Avoid local maximums.
|
||||||
|
* Reduce the amount one must remember.
|
||||||
|
* Focus on code rather than style.
|
||||||
|
* Resource allocation may fail; resource deallocation must succeed.
|
||||||
|
* Memory is a resource.
|
||||||
|
* Together we serve the users.
|
||||||
|
```
|
||||||
|
|
||||||
|
When you want to write some text to the console, the program needs to make a syscall to actually send that data to
|
||||||
|
the OS, to then print out those characters. By default, most languages flush buffers for you, because when you call
|
||||||
|
`writeOut("some text")`, you expect it to write it! But, to save on syscalls, zig will buffer those bytes, and any more
|
||||||
|
bytes, until you tell it to `flush()`, which then tells your program to make the syscall so we can see those beautiful
|
||||||
|
bytes.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
date: 2025-10-03T16:19:07-06:00
|
||||||
|
description: "We were made for dogs, and they us."
|
||||||
|
lastmod: 2026-03-04T01:31:12-07:00
|
||||||
|
showTableOfContents: true
|
||||||
|
type: "post"
|
||||||
|
title: "TIL: We Created Dogs and Dogs Created Us"
|
||||||
|
image: "/images/otto-1.webp"
|
||||||
|
image_caption: "Otto, Stalwart"
|
||||||
|
image_alt: "Image of my sweet pup Otto, Irish Setter 7 months"
|
||||||
|
tags: ["life", "dogs", "history", "til"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
I started listening to [_Sapiens: A Brief History of Human
|
||||||
|
kind_](https://en.wikipedia.org/wiki/Sapiens:_A_Brief_History_of_Humankind) and was struck at the significance of dogs
|
||||||
|
in human history. I [realized](/posts/25-09-30-people-are-actually-right) that dogs really are man's best
|
||||||
|
friend, and only animal that has evolved alongside us since the hunter gatherer period. Its incredible!
|
||||||
|
|
||||||
|
# Reflection
|
||||||
|
|
||||||
|
Otto (pictured above) is my first dog. I feel like having a dog isn't for everybody, but having humans is for every dog.
|
||||||
|
I've found such satisfaction in my relationship with him. We really mutually benefit each other. Dogs are so malleable,
|
||||||
|
they really pick up on so much of what you do and how you want them to behave. There is some learning required from
|
||||||
|
you as the human, and moments to stop and think critically about what it is you want from them. But, once you train
|
||||||
|
something only a handful of times, dogs _learn it_, they _understand_ us. What an incredible relationship us monkeys
|
||||||
|
created with these wolves.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
date: 2025-10-05T20:08:25-06:00
|
||||||
|
description: "Turns out our brains capacity hasn't really changed much over the last couple thousand years..."
|
||||||
|
lastmod: 2026-02-24T00:35:36-07:00
|
||||||
|
showTableOfContents: true
|
||||||
|
type: "post"
|
||||||
|
title: "TIL: Hunter Gatherers Were Not Dumb"
|
||||||
|
image: ""
|
||||||
|
image_alt: ""
|
||||||
|
tags: ["history", "til"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
[As I mentioned](/posts/25-10-03-we-created-dogs-and-dogs-created-us), I'm really enjoying the
|
||||||
|
[Sapiens](https://en.wikipedia.org/wiki/Sapiens:_A_Brief_History_of_Humankind) book. I was shocked to discover that
|
||||||
|
hunter gatherer societies were comprised of extremely intelligent humans!
|
||||||
|
|
||||||
|
# Reflection
|
||||||
|
|
||||||
|
Because of my biases (and probably seeing many depictions of foolish looking cave men), I had
|
||||||
|
assumed that what caused the human to "pull themselves out" of the wild animal stage and into
|
||||||
|
the industrial age was them finally reaching a point of intelligence to do so. When, [the
|
||||||
|
reality](https://www.bbc.com/future/article/20240517-the-human-brain-has-been-shrinking-and-no-one-quite-knows-why) is
|
||||||
|
actually the opposite! The hunter gatherer people had to be so intimately familiar with their surroundings to survive.
|
||||||
|
Weather patterns, migration patterns of many different animals, which foods were safe to eat, and how to prepare the
|
||||||
|
foods for consumption. The variety of foods they gathered was also greater than the average person today eating cereal
|
||||||
|
grains and one of four different meats. History is so fascinating. I find time and time again, my assumption of how or
|
||||||
|
what something must have been is frequently proven completely wrong.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
date: 2025-10-14
|
||||||
|
description: "Its not the super intelligence, but the super isolation I'm concerned about."
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2025-10-14
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["ai","culture"]
|
||||||
|
title: "The Ai Singularity No One Saw Coming"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artificial Super Isolation
|
||||||
|
|
||||||
|
My biggest worry with AI is not it gaining sentience. I'm not worried about a _Terminator_ type story line. But I am
|
||||||
|
worried about the power it can exert on us as _singular_ individuals.
|
||||||
|
|
||||||
|
### Hallucinations Welcome
|
||||||
|
|
||||||
|
A super intelligent AI needs to be able to reason. Not the "reasoning" models AI companies are shilling. I mean,
|
||||||
|
actually think abstractly. Large Language Models work on statistics. It doesn't take many prompts to realize it. They
|
||||||
|
cannot even [consider their own output](https://vgel.me/posts/seahorse/) as they generate it.
|
||||||
|
|
||||||
|
I feel that the current AI cycle is starting to wane, like the top end of a hockey-stick-shaped graph. But the current
|
||||||
|
capabilities are more than enough to create a sort of real life matrix.
|
||||||
|
|
||||||
|
### Sora is Terrifying
|
||||||
|
|
||||||
|
I don't think words can quite capture the experience of watching AI generated tik-toks. The humans depicted in these 10
|
||||||
|
second videos feel cold. They have climbed out of the uncanny valley. Gone are the days of six-fingered hands. Yet, I
|
||||||
|
feel no emotional contagion from these probable pixels. I stare emptily. My lack of reaction to a very normal looking
|
||||||
|
human doing a slightly abnormal thing startles me.
|
||||||
|
|
||||||
|
But what got me to write this was the reaction of the creator. Or maybe more aptly, the initiator. Manager?
|
||||||
|
|
||||||
|
A friend sent me some they made. In my estimation, they found them utterly entertaining.
|
||||||
|
|
||||||
|
## Fascination Complication
|
||||||
|
|
||||||
|
I remember running [one of the earliest image generators](https://en.wikipedia.org/wiki/Stable_Diffusion) on my laptop.
|
||||||
|
ChatGPT hadn't yet launched. The market didn't depend on AI hype yet. Each image took at least a minute to generate.
|
||||||
|
There was no fancy web UI. And, the images it made mostly sucked. Yet, I sat at my laptop (characteristically) for hours
|
||||||
|
making loads of garbage images.
|
||||||
|
|
||||||
|
I wasn't convinced at the time any if it was "art". But, its unbelievable when you first experience a computer do sort
|
||||||
|
of "exactly" what you ask. It knows who John Cena is and how Hayao Miyazaki's art looks! Its an eager servant that never
|
||||||
|
sleeps, never eats, [glazes](https://www.merriam-webster.com/slang/glaze) you constantly. Its no wonder why one might
|
||||||
|
find it enthralling!
|
||||||
|
|
||||||
|
In my mind, the natural extension of the current landscape is AI generated social feeds. Sora is literally just an
|
||||||
|
AI-video-only tik-tok. This worries me. I imagine the life of a teenager today: AI (revenge) porn, ChatGPT doing all
|
||||||
|
of your homework, social feeds full of content with plummeting levels of authenticity. Luckily us grown ups know how to
|
||||||
|
handle this technology responsibly and we just gotta worry about the children.
|
||||||
|
|
||||||
|
In the U.S. I feel like commonly held values are eroding. Now, trusting institutions is naive. Citizen researchers with
|
||||||
|
none of the skills to read past the abstract are a dime a dozen. AI feels like a multiplying factor to these issues. In
|
||||||
|
the hands of most, its a tool for goofs. But in the hands of a few, its a tool for fabricating truth.
|
||||||
|
|
||||||
|
## Escape Velocity
|
||||||
|
|
||||||
|
[Previously](/posts/25-01-28-ai-hestiation-turning-around), I stated I'd give AI a fair shake. I committed to using AI
|
||||||
|
for a year. I'm contemplating cutting that short. I think AI has made me dumber. I'm certainly more dependant on a $20
|
||||||
|
subscription to do my job and less sure of myself. Perhaps we can have a healthy intermittent fasting culture with AI.
|
||||||
|
I'm not sure.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-02
|
||||||
|
description: "Flash Fiction: Cyano Pill"
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-02-21T15:59:38-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["flash fiction", "post apocalypse"]
|
||||||
|
title: "Cyano Pill"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
I've been interested in writing fiction, so I figured what better place to start than with [flash fiction](https://en.wikipedia.org/wiki/Flash_fiction)! I'll try to remember to title and tag them all so its easy to read any of my latest fiction bits :D
|
||||||
|
|
||||||
|
## The Cyanogen Pill
|
||||||
|
|
||||||
|
My glow is fading, heart rate rising. Not much time left by the look of it.
|
||||||
|
|
||||||
|
I don't recall which tunnel I took to get out here. I really am a _dimwit_! I should have heeded the painted signs and left my mark. And all this for some rusted disk boxes!
|
||||||
|
|
||||||
|
I double check my dad's hand-me-down canister -- empty. _Dark vein!_
|
||||||
|
|
||||||
|
The low, rocky ceiling opened up to reveal a large chasm. I scanned in the dim light hoping for the familiar to jump out. Instead, voices. Voices!
|
||||||
|
|
||||||
|
I raised my spirits and my pace, racing deftly toward the echoing chatter.
|
||||||
|
|
||||||
|
~~I froze.
|
||||||
|
|
||||||
|
Masks, beams, and a massive parked drill cart. Its the darks.
|
||||||
|
|
||||||
|
"-- those flickers keep whining about the ore, say its getting harder to get. As if its hiding or something?" one of the darks mocked.
|
||||||
|
I flattened against the shaft walls instinctively. I might burn out in the Maze, but I was not going to get snuffed. Or worse.
|
||||||
|
|
||||||
|
"Hax, any luck over there?" shouted one, seemed like the leader of the group. Probably looking for more dig sites.
|
||||||
|
|
||||||
|
But this wasn't new dig. In fact, none of this arm of the maze had seen any Delegate work in years. What were Dark out here doing?
|
||||||
|
|
||||||
|
Suddenly a beam swept over me. My heart skipped a beat.
|
||||||
|
|
||||||
|
"Yup, found her!", only feet away, a Dark steel suit rounded a corner I hadn't noticed to my right, walking toward the group.
|
||||||
|
|
||||||
|
I shivered as the steel stomped past. Somehow they failed to notice me. Guess I thank the Ten for getting covered in dirt earlier.
|
||||||
|
|
||||||
|
The suit's massive claws clutched a struggling form -- crying out, softly -- like a fish out of water. It was hard to see her glow with all of the beams on her. But I could have sworn her veins weren't lit.
|
||||||
|
|
||||||
|
Then, I was seized by the stupidest urge. I watched as a small rock arced toward the suit and plinked off its side.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-11
|
||||||
|
description: "My thoughts on the movie Everything Everywhere All At Once"
|
||||||
|
image: "https://image.tmdb.org/t/p/original/atQ5kACEJrCLqmRrUa4NwR6gbKe.jpg"
|
||||||
|
lastmod: 2026-02-21T15:59:38-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["movie review", "nihilism", "life"]
|
||||||
|
title: "Movie Review Everything Everywhere All at Once"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Media Wednesdays
|
||||||
|
|
||||||
|
My partner and I decided recently to institute a tradition of enjoying some piece of media every wednesday evening.
|
||||||
|
|
||||||
|
This is our second week, and I chose the movie
|
||||||
|
[Everything Everywhere All At Once](https://en.wikipedia.org/wiki/Everything_Everywhere_All_at_Once). I really enjoyed
|
||||||
|
it. Below are some of my thoughts. Its a first pass, and I'm not going to try to capture everything, but I do not want
|
||||||
|
to let the perfect be the enemy of the good here.
|
||||||
|
|
||||||
|
The film made me laugh and cry. It made me want to profess my love for my partner with reckless abandon. It made me
|
||||||
|
feel and think, and at the end of the day, what else could we ask from a piece of media.
|
||||||
|
|
||||||
|
*SPOILERS AHEAD*
|
||||||
|
|
||||||
|
## Younger Generation Embodying Chaos
|
||||||
|
|
||||||
|
The mother, Evelyn, struggles. I was going to add "with ...", but really, with everything. Her alternate-universe
|
||||||
|
husband says, she failed at everything she had tried. But, specifically with her daughter Joy, she fails to acknowledge
|
||||||
|
and truly accept her.
|
||||||
|
|
||||||
|
The villain of the movie is an alternate dimension version of her daughter, shattered across the multiverse,
|
||||||
|
being everywhere at once. This sounds wacky. It was. But it absolutely worked.
|
||||||
|
|
||||||
|
I thought it was apt to compare an unwillingness to understand and meet people where they are by blaming or diverting "the enemy's" intention --
|
||||||
|
|
||||||
|
Nobody understood the motivation of Jobu Tupaki, the name they gave this broken version of Joy. And the resolution
|
||||||
|
of the film involves finally meeting her, hugging her, and hearing her. When we don't understand someone, their actions
|
||||||
|
seem ridiculous, dangerous, or confusing. We fear that which we don't understand. The movie showed the common gap between generations, in meeting each other, seeing each other, really well.
|
||||||
|
|
||||||
|
## A Reframing of Nihilism
|
||||||
|
|
||||||
|
I started reading _The Myth of Sysafus_, a book about suicide and Nihilism (don't worry, I am doing very well right now,
|
||||||
|
I'm just trying to learn philosophy). The book spends thousands of words answering the question of absurdity, why we
|
||||||
|
exist in a universe with no aparent reason (which religion / sprituality attempt to resolve, but the author sees as
|
||||||
|
cop outs). _Everything Everywhere All At Once_ concludes with a beautifully simple reframing of the
|
||||||
|
phrase "Nothing really matters". The daughter has given in, allowing whatever base or benal desire to drive. Crumbling, chaos,
|
||||||
|
catastrophe, none of it mattered, and stepping in to care for anyone or anything was pointless. At the very end of the movie, the dialog
|
||||||
|
between the mother making up and the daughter opening up:
|
||||||
|
|
||||||
|
(loosely)
|
||||||
|
|
||||||
|
*Tons of stuff has just happened and the end of the world was held at bay*
|
||||||
|
|
||||||
|
Joy: "Do you still want to throw your party"
|
||||||
|
|
||||||
|
Evelyn: "We can do whatever we want. Nothing really matters."
|
||||||
|
|
||||||
|
Without changing the words, Evelyn turned the phrase on its head. If nothing really matters, sure it can lead to lack
|
||||||
|
of meaning. But also, it gives us all the room to find the meaning we want in life. Without the context of the movie, it
|
||||||
|
doesn't sound very impactful, and may still sound negative, but I promise, that line was a welcome blow of relief. An emptying of the tension that the first act did such a good job establishing.
|
||||||
|
|
||||||
|
## Positivity for Survival
|
||||||
|
|
||||||
|
The husband Waymond maintains a happy and optimistic attitude throughout, opting for peaceful understanding to break
|
||||||
|
tension at the end of the film. In his various dimensional versions, I recieved a message that the way he survived
|
||||||
|
through life was his positivity.
|
||||||
|
|
||||||
|
I've always thought myself a happy person. Lately, I've identified that I am, in fact, much more optimistic than the
|
||||||
|
average person. I related to Waymond. And I also see that those that don't move through life this way aren't flawed,
|
||||||
|
they just have learned to survive differently than I. It made me stop and reflect, how often we think the way we think
|
||||||
|
or do things is surely the most optimized or correct. In reality, its just _a_ way.
|
||||||
|
|
||||||
|
## Understanding > Resisting
|
||||||
|
|
||||||
|
In line with that thought, at the end, Evelyn has to fight off people to get to her daughter before losing her to the
|
||||||
|
everything bagel hole. Rather than resisting with force (which she had done previously), she sought to understand. And each
|
||||||
|
of these people in the way seemed to have some deeper unmet need that she helped fill, a listening ear, a strappy sexual
|
||||||
|
encounter lol, a perfume that reminded an older man of his deceased wife.
|
||||||
|
|
||||||
|
And really, I think that on a deep level, we all are broken in our own ways. If someone sought to really understand us
|
||||||
|
we wouldn't find any reason to fight each other.
|
||||||
|
|
||||||
|
## All At Once
|
||||||
|
|
||||||
|
What a wonderful film. I learned from the wikipedia page it is the first movie to win more awards than Return of the King! And it deserves it. It feels unfortunate that the political climate may prevent some people from engaging with it and enjoying it. Its lude and breaks lots of "norms". The plot sounds ridiculous. But the message was a real human one. I loved it.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-21T19:52:06-07:00
|
||||||
|
description: "An old parable poorly applied"
|
||||||
|
image: "/images/elephant-and-blind-men.webp"
|
||||||
|
image_alt: "Still of an elephant and blind men from the animated short video Discovering Truth by the LDS YouTube channel."
|
||||||
|
lastmod: 2026-03-04T01:31:12-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["philosophy", "buddhism", "truth"]
|
||||||
|
title: "Seeing the Blind Men and the Elephant"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Which Man is Blind?
|
||||||
|
|
||||||
|
A few days ago I listened to [an interview by Alex O'Connor (YouTube)](https://www.youtube.com/watch?v=wCo7QozGKWA&pp=ygUMYWxleCBvY29ubm9y) with the Hindu monk [Swami Sarvapriyananda (Wikipedia)](https://en.wikipedia.org/wiki/Swami_Sarvapriyananda) (Swami in his name is a title, much like a Father or Bishop in Christian practices).
|
||||||
|
|
||||||
|
In the interview, Swami Sarvapriyananda mentioned an old Buddhist parable, [_the blind men and an elephant_ (Wikipedia)](https://en.wikipedia.org/wiki/Blind_men_and_an_elephant). The poet [John Godfrey Saxe (Wikipedia)](https://en.wikipedia.org/wiki/John_Godfrey_Saxe) most famously introduced the parable to western audiences.
|
||||||
|
|
||||||
|
<!-- Using (non-breaking space) for spaces at the beginning of the line -->
|
||||||
|
<!-- And adding two extra spaces at the end of a line preserves the newline -->
|
||||||
|
> It was six men of Indostan
|
||||||
|
> To learning much inclined,
|
||||||
|
> Who went to see the Elephant
|
||||||
|
> (Though all of them were blind),
|
||||||
|
> That each by observation
|
||||||
|
> Might satisfy his mind
|
||||||
|
>
|
||||||
|
> Moral:
|
||||||
|
> So oft in theologic wars,
|
||||||
|
> The disputants, I ween,
|
||||||
|
> Rail on in utter ignorance
|
||||||
|
> Of what each other mean,
|
||||||
|
> And prate about an Elephant
|
||||||
|
> Not one of them has seen!
|
||||||
|
|
||||||
|
I surprisingly recognized the parable, from a much earlier time in my life, from a very different source. I could even picture the animated elephant with the westerner's stereotypical depiction of "Indians" (eastern kind) grasping at its parts.
|
||||||
|
|
||||||
|
## The Whole Truth
|
||||||
|
|
||||||
|
In [Dieter F. Uchtdorf's talk (speeches.byu.com)](https://speeches.byu.edu/talks/dieter-f-uchtdorf/what-is-truth/), to students of Brigham Young University, an apostle of the Church of Jesus Christ of Latter-Day Saints spoke about the importance of seeing the whole Truth.
|
||||||
|
|
||||||
|
> The thing about truth is that it exists beyond belief.
|
||||||
|
|
||||||
|
The church later made a short animated video, clipping up his talk in a nice portable format that I remember watching many times as a kid. Growing up, Sundays were special days, so YouTube was only kocher on a church affiliated channel. I had watched each of those videos many times over. :grin:
|
||||||
|
|
||||||
|
I reflected today on the possibility that this parable of the elephant may have been misconstrued or appropriated by Uchtdorf for his, or more generally, for the church's, purposes. I wondered how exactly he had presented the parable those years ago, and if it matched its original intended purpose.
|
||||||
|
|
||||||
|
|
||||||
|
I think my gut instinct was to use this as some sort of fodder. Pointing out a mistake on the part of Uchtdorf could really reaffirm my decision to step away from the Mormon faith. On further reflection and researching for this post, I think it makes more sense, and is intellectually more honest, to compare how this story has been applied. As Wikipedia states, the parable "has been used to illustrate a range of truths and fallacies".
|
||||||
|
|
||||||
|
And isn't that how parables, allegories, and friends all work? We mold them to our time. The cultural morays shape the parts of the elephant the blind men see. For the 500 B.C.E. crowd its a plowshare, a mortar, and a pestle, and for us modern folk, a spear, a tree, and a snake; for an elephant's tusk, leg, and tail respectively.
|
||||||
|
|
||||||
|
## Ancient Elephants
|
||||||
|
|
||||||
|
In the earliest version of the parable, found in the [Tittha Sutta 6.4 (accesstoinsight.com)](https://www.accesstoinsight.org/tipitaka/kn/ud/ud.6.04.than.html), the Buddha highlights the flawed position of the sages in his day:
|
||||||
|
|
||||||
|
> In the same way, monks, the wanderers of other sects are blind & eyeless. They don't know what is beneficial and what is harmful. They don't know what is the Dhamma and what is non-Dhamma. [^1]
|
||||||
|
|
||||||
|
Buddha here clearly seems to indicate he sees the elephant for what it is, not these monks of other sects.
|
||||||
|
|
||||||
|
And what about my favorite German pilot?
|
||||||
|
|
||||||
|
## Modern Parables
|
||||||
|
|
||||||
|
Other than taking a strong position against Flat Earthers:
|
||||||
|
|
||||||
|
> For example, in spite of one-time overwhelming consensus, the earth isn’t flat.
|
||||||
|
|
||||||
|
Utchdorf claims there is such a thing as absolute truth. And this truth, like seeing the whole elephant clearly, "is His gospel. It is the gospel of Jesus Christ. Jesus Christ is 'the way, the truth, and the life.'"
|
||||||
|
|
||||||
|
## The Battle for the Elephant
|
||||||
|
|
||||||
|
And so it seems, from my cursory investigation, that this parable has been used by each to advance their own perspective, which is ironic.
|
||||||
|
|
||||||
|
Not sure why the thought crossed my mind, but I'm trying to write everyday, and so I thought it'd supply an interesting lens into my mind these days.
|
||||||
|
|
||||||
|
[^1]: [Buddha in Tittha Sutta 6.4, text snippet on accesstoinsight.com](https://www.accesstoinsight.org/tipitaka/kn/ud/ud.6.04.than.html#:~:text=In%20the%20same%20way%2C%20monks%2C%20the%20wanderers%20of%20other%20sects%20are%20blind%20%26%20eyeless%2E%20They%20don%27t%20know%20what%20is%20beneficial%20and%20what%20is%20harmful%2E%20They%20don%27t%20know%20what%20is%20the%20Dhamma%20and%20what%20is%20non%2DDhamma%2E)
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-22T23:53:05-07:00
|
||||||
|
description: "Wondering what sort of algorithm my dog uses for navigation"
|
||||||
|
image: "/images/otto-on-nature-path-algorithm.webp"
|
||||||
|
lastmod: 2026-03-04T01:31:12-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["dogs", "optimization"]
|
||||||
|
title: "Dog Based Search Path"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
I was walking today on my preferred route.
|
||||||
|
|
||||||
|
I can leave my house, walk through a park behind it, cross a neighborhood road, walk through a "nature park" (where the nature is left to do its thing), cross a church parking lot, then take a canal-like path through tall natural grasses back to my neighborhood. Then its just some sidewalks to get back home.
|
||||||
|
|
||||||
|
Its wonderful! It helps me feel like I'm actually out in nature and escape from suburban sprawl.
|
||||||
|
|
||||||
|
While walking along the path, Otto came running up to check in with me, as he does after having sufficiently strayed off.
|
||||||
|
|
||||||
|
The path seems like it was a part of a pond or lake shore, as there is a steep drop off on one side. Otto happened to be on this side, and as he approached, I could see the "path" through many bushes and up a very steep, albeit short, ridge wall. I thought quickly and efficiently to myself that he would not be able to scale it.
|
||||||
|
|
||||||
|
Despite my extremely based and factual take, Otto charged ahead. And with little more effort than he normally gives while sprinting around, he crested and came to pant next to me with such ease!
|
||||||
|
|
||||||
|
Our drastically different calculus made me wonder. I wondered if dogs have some complex decision hierarchy like we do. I wondered if he just tried the first path he could see, whether or not it was viable.
|
||||||
|
|
||||||
|
I remember also reading _Entangled Life_ by Merlin Sheldrake, and hearing how slime molds perform various search algorithms in mazes to find food. I wonder how many biological processes should be mapped more frequently to the world of computer science!
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-23T23:25:19-07:00
|
||||||
|
description: "How far can our brains be pushed to learn and adapt?"
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-02-24T00:35:36-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["til", "neuroscience", "technology"]
|
||||||
|
title: "TIL: The Limits of Neuroplasticity"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
> My brain is plastic, its fantastic :musical_note:
|
||||||
|
|
||||||
|
I was listening to an interesting interview today on the [Waking Up app (wakingup.com)](https://www.wakingup.com/) between [Sam Harris (Wikipedia)](https://en.wikipedia.org/wiki/Sam_Harris) and [Adam Gazzaley (Wikipedia)](https://en.wikipedia.org/wiki/Adam_Gazzaley). In it, they discuss the cost of multitasking, and Gazzaley's ground-breaking, FDA-approved prescription videogame, [Endeavorrx (product site)](https://www.endeavorrx.com/).
|
||||||
|
|
||||||
|
# Reflection
|
||||||
|
|
||||||
|
I was surprised to hear them distill why they thought meditation is effective as a practice to improve focus. Our brains work incredibly well at improving circuits that fire together.
|
||||||
|
|
||||||
|
> Things that fire together, wire together [^1]
|
||||||
|
|
||||||
|
It's sort of like bushwhacking. Walking through dense forest is tough, then a little path clears, the dirt packs harder, plants make way and mother nature bends around the force. Stop walking the path, and she fills it back in. Our brains do this work in disparate areas of cognition: memory, problem solving, creativity, anxious rumination, flares of rage, etc. These circuits have no value judgements attached in the brain, they aren't labelled "beneficial" or "harmful". If you practice things over and over, your brain does the growth part on its own.
|
||||||
|
|
||||||
|
I found that fascinating!
|
||||||
|
|
||||||
|
The FDA approved videogame is a different story, but also really cool sounding. I wonder in the future it will become common to medicate our problematic brain habits with fun videogames! Or the videogames we play become standardized around certain practices that promote healthy circuits instead of the obsessive and negative ones.
|
||||||
|
|
||||||
|
So many of the things we do each day are things we have been doing for many days previous. Our brains find these grooves and keep practicing the same routes. The conversation really spurred on some introspection, the double-edged nature of our brains. I'm constantly improving my ability to do whatever I'm doing in each moment, whether I choose to or not.
|
||||||
|
|
||||||
|
[^1]: [Hebb's Rule (Wikipedia)](https://en.wikipedia.org/wiki/Hebbian_theory)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-24T23:45:11-07:00
|
||||||
|
description: "I just dropped my phone in my dog's water bowl -- I'm thinking what I want from my phone"
|
||||||
|
image: ""
|
||||||
|
lastmod: 2026-02-25T12:06:38-07:00
|
||||||
|
showTableOfContents: true
|
||||||
|
tags: ["phone", "technology"]
|
||||||
|
title: "What Do You Want in a Phone"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phone Woes
|
||||||
|
|
||||||
|
I only moments ago dropped my Pixel Fold into a freshly cleaned stainless steel bowl of water.
|
||||||
|
|
||||||
|
This phone is not waterproof.
|
||||||
|
|
||||||
|
It is now resting in a bowl of brown rice. My wife commented she hadn't seen a rice bath pulled out for a phone since the dumb phone era.
|
||||||
|
|
||||||
|
I rather liked some aspects of my Fold.
|
||||||
|
|
||||||
|
I've also come to loathe what these devices have largely become.
|
||||||
|
|
||||||
|
What is the phrase? It's only once you've lost the thing that you know how much you really were ready to buy a new one for thousands of dollars that could suck you in deeper to its engaging algorithms... :relieved:
|
||||||
|
|
||||||
|
I was curious to record my journey with different phones, my reasoning / requirements around getting a new phone, and whether I want to have a smart phone at all.
|
||||||
|
|
||||||
|
At the end of the day, I think the amount it robs me of my attention, interrupts my activities and thought processes, and sells my attention to any willing buyer leads me to feel like these devices are actually very hostile.
|
||||||
|
|
||||||
|
I'm not sure what I'll do. If my phone is broken, I may just take a break and see how it feels. Can I navigate in my city okay? Does my wife get worried while I'm away? Do I feel better without one?
|
||||||
|
|
||||||
|
I'm almost hoping my fold is broken just so I can pursue this self experimentation.
|
||||||
|
|
||||||
|
## My Phone Saga
|
||||||
|
|
||||||
|
I have tried quite a few different phones over the last five-ish years.
|
||||||
|
|
||||||
|
### Linux In My Pocket!
|
||||||
|
|
||||||
|
I purchased [the original PinePhone (pine64)](https://pine64.org/devices/pinephone/) when it launched, even got the keyboard case. It was barely usable as a linux computer, let alone a phone. I wanted it to work so badly I persisted despite my wife's pleas to have a way to be contacted consistently. I relented after a month or so.
|
||||||
|
|
||||||
|
### Google Normie
|
||||||
|
|
||||||
|
Then a Pixel 4. It was fine. I ran [CalyxOS](https://calyxos.org/) on it at first and then [GrapheneOS (homepage)](https://grapheneos.org/) I think. Both custom flavors of Android that have a privacy focus, spoofing or sandboxing required Google services to respect your data just a bit more.
|
||||||
|
|
||||||
|
### Linux In My Pocket! Continued...
|
||||||
|
|
||||||
|
I jumped back in the mobile linux pool for another month about a year ago with a OnePlus 6T. It was much more usable, phone calls worked fairly well (provided you weren't doing any bluetooth tomfoolery), and once I compiled a custom VoLTE package I was even able to make calls on 4G networks!
|
||||||
|
|
||||||
|
{{< aside caption="Oh yeah..." >}} I'm remembering, if I wanted to receive calls, I had to be sure to set my phone to 2G networks only, so having the VoLTE setup was super convenient. I desperately wanted mobile linux to work. I dropped many calls, missed group chats, my family for a while thought they couldn't call me :sob: so I'm not a linux-hater, I think the people at [PostMarketOS (homepage)](https://postmarketos.org/) are doing awesome stuff, it just needs more time in the oven, and ALOT more love AKA money / donated development.
|
||||||
|
{{< /aside >}}
|
||||||
|
|
||||||
|
{{< aside >}}
|
||||||
|
Looks like they have made lots of improvements since I last used my OP 6T! (which is not surprising) The [wiki page (postmarket wiki)](https://wiki.postmarketos.org/wiki/OnePlus_6_(oneplus-enchilada)) has a good summary of functional parts
|
||||||
|
{{< /aside >}}
|
||||||
|
|
||||||
|
### Flip Minimalism
|
||||||
|
|
||||||
|
I also tried a flip phone for a minute. I ended up purchasing a [TCL Flip 2 (product page)](https://us.tcl.com/products/flip-2). Incredible how cheap it was, and it worked surprisingly well! I put some custom software on it, unfortunately I didn't document the process, but managed to install a custom cursor tool to set a button to toggle controlling a cursor on its 2.5" screen. This made using some of the apps much easier / possible than using the simple ^ v <- -> controls. It got me detoxed off my phone and showed me just how much time I can spend on it. Then, I switched back, telling myself I could handle the raw power of these internet-connected, eye-catching attention bricks.
|
||||||
|
|
||||||
|
### Flip Maxxing
|
||||||
|
|
||||||
|
Back to my Pixel. Until I dropped it and it wouldn't keep a charge. The phone would only power on while plugged in, essentially unusable as a mobile device.
|
||||||
|
|
||||||
|
I then got my favorite phone to date, the Pixel Fold! Second hand from a chap in my area for $400. Not a bad deal if you ask me.
|
||||||
|
|
||||||
|
Other than the weight, (and the fingerprint scanner stopped working suddenly last week...) I had no complaints! And it was so fun to use!
|
||||||
|
|
||||||
|
I put GrapheneOS on it right away. I have loved reading blogs on the dual internal screens. It has been fun to be able to watch videos on the front screen and just open it slightly, the phone acts as its own stand! Same with pictures. Also, opening two apps side by side on the inside screens comes in handy. I did some mobile software development with my split keyboard plugged in, it worked extremely well! I used Termux and, most recently, the native Linux Terminal app which shipped with Android 16.
|
||||||
|
|
||||||
|
But alas, the Fold took a splash and now I'm worried it won't survive.
|
||||||
|
|
||||||
|
I was listening to a session in the Waking Up app and it kept playing underwater audio after I fished it out, so I may be over dramatic... but I know people have talked about dust getting into the internal screens and rendering them useless. I don't have a lot of faith that water would be more gentle.
|
||||||
|
|
||||||
|
## Do I _Need_ a Phone?
|
||||||
|
|
||||||
|
I considered many times over the last year how nice it would be to my brain to cut smartphones out entirely.
|
||||||
|
|
||||||
|
I think it would make some things extremely difficult in my life.
|
||||||
|
|
||||||
|
Or would it...? Let me write down what I typically do on my phone everyday.
|
||||||
|
|
||||||
|
I have a good sense as I've been tracking my screen time fairly consistently the last few weeks. I was really proud for having multiple days in a row under 2 hours, some barely over 1! I still had some days hit 3 or 4, but those always has a huge spike in the Signal app, so I think it was just video calls with friends and family. I consider this an excellent use of my phone.
|
||||||
|
|
||||||
|
### Phone Requirements
|
||||||
|
|
||||||
|
My most used apps (from memory) are:
|
||||||
|
|
||||||
|
- Browser
|
||||||
|
- This one is a large umbrella, but I think largely it is my social media use: [Mastodon](https://mastodon.social) or [lobste.rs](https://lobste.rs)
|
||||||
|
- Occasionally online shopping or googling if some symptom means I'm going to surely die
|
||||||
|
- Signal
|
||||||
|
- Anthropic app
|
||||||
|
- Waking Up
|
||||||
|
- Ground News
|
||||||
|
- NewPipe (YouTube client, when a channel I follow posts a video)
|
||||||
|
Then at this point its like >10 minutes in various utility apps, work, calculator, etc.
|
||||||
|
|
||||||
|
Honestly, none of these things require an android/smart phone, except Waking Up.
|
||||||
|
|
||||||
|
I think my phone is really just an insane distraction that I've been convinced I need and want in my life.
|
||||||
|
|
||||||
|
Its so apparent when watching other people how locked in they seem on their devices. I wonder how guilty I am of the same thing.
|
||||||
|
|
||||||
|
## Phones I would consider
|
||||||
|
|
||||||
|
[Fairphone (home page)](https://www.fairphone.com/en/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
It doesn't make sense to me to purchase any other phones at this point.
|
||||||
|
|
||||||
|
If you cannot easily repair a phone, it is not worth purchasing.
|
||||||
|
|
||||||
|
I think buying some Google, Samsung, or Apple device is communicating to these companies that us consumers do not want or need basic re-use as things wear.
|
||||||
|
|
||||||
|
Can you imagine buying a car with the reassuring guarantee from the sales rep that you get to buy a new one once its oil needs to be changed? Okay, that's extreme, how about new spark plugs, or tires. It is crazy that we consistently purchase these devices that they manufacture as essentially, disposable.
|
||||||
|
|
||||||
|
Fairphone seems like a really legit company from Europe, and I like their mission. Much like Framework, I would like to spend my dollars toward businesses that respect humans and the world they share with us.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-25T23:08:49-07:00
|
||||||
|
description: "Looks like I will be returning to my little flip phone"
|
||||||
|
draft: false
|
||||||
|
lastmod: 2026-03-06T22:41:50-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["phone", "technology", "lifestyle"]
|
||||||
|
type: "post"
|
||||||
|
title: "To Fold or to Flip"
|
||||||
|
image: ""
|
||||||
|
image_alt: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rice Baths are Fake News
|
||||||
|
|
||||||
|
Apparently, putting your phone in rice doesn't really fix things.
|
||||||
|
|
||||||
|
Shaking excess water off and putting the phone in a well ventilated area is what is actually recommended.
|
||||||
|
|
||||||
|
[Yesterday, my Pixel Fold fell](/posts/26-02-24-what-do-you-want-in-a-phone) into a dog bowl, and I'm using it as an excuse to go back to my TCL Flip 2. The Pixel Fold is a great phone, but I think my life will be more full without it.
|
||||||
|
|
||||||
|
I booted it up and swapped my SIM card into it. It reminded me how much I enjoyed having this little phone on me for those months last year.
|
||||||
|
|
||||||
|
I am curious if this becomes my new normal.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-26T11:21:43-07:00
|
||||||
|
description: "A quick overview of what I think mindfullness is and how my practice looks."
|
||||||
|
image: ""
|
||||||
|
lastmod: 2026-02-27T00:48:58-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["mindfullness", "lifestyle"]
|
||||||
|
title: "What Is Meditation"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Awareness
|
||||||
|
|
||||||
|
I was given a four week paternity leave. It gave me a perfectly timed reprieve to find a new center. I enjoyed an hour long walk with my loyal pooch nearly every day. And on some of those days, I stopped at a rock, to sit and breathe.
|
||||||
|
|
||||||
|
_Years_ ago I would have thought this was pointless. Unless I was praying.
|
||||||
|
|
||||||
|
_A year_ ago I would have thought it theoretically is good for you, but not be very interested in actually doing it.
|
||||||
|
|
||||||
|
Over the last two months, my mind has really been opened to its own vastness.
|
||||||
|
|
||||||
|
Mindfulness is, at bottom, simply becoming aware of what is already there.
|
||||||
|
|
||||||
|
Becoming aware of your thoughts, yes. But also your body. Noticing tension, aversion, attraction.
|
||||||
|
|
||||||
|
I've only begun my journey into my mind, but I've seen and felt glimpses of an emptiness and peace I never thought possible.
|
||||||
|
|
||||||
|
## What Emptiness Is
|
||||||
|
|
||||||
|
There are many schools of thought around mindfulness and meditation. Some believe we are all connected to a greater single consciousness, others say there is only a vast emptiness. I subscribe to a non-dual view, basically that the notion of "I" is a construct. The thinker behind my eyes is not a real "me", it is but a bundle of sensory experience and thought what I have come to identify with. I think I took this route largely because it is what the Waking Up app espouses (sorry this is like 3rd day mentioning it, but I really like it).
|
||||||
|
|
||||||
|
I get the sense that all of these people that disagree about the terms and "doctrines" of the state of our minds in Eastern traditions, largely agree around the importance of meditation. It seems they are more fighting over the pedantry of terms.
|
||||||
|
|
||||||
|
{{< aside caption="So the goal is emptiness, you aren't supposed to feel anything?" >}}
|
||||||
|
The goal is not to eliminate experience. In fact, my understanding is its purpose is to train yourself to notice all of it. To expand your awareness to that which is always there.
|
||||||
|
{{</ aside >}}
|
||||||
|
|
||||||
|
I recently was lying in bed, waiting for sleep to come. Breathing deeply and slowly, attending to the sensation of my breath. I noticed that my jaw seemed tight. I tried opening my mouth a bit, and noticed it did not want to sit anywhere other than closed, at rest. This seemed odd. Sure enough, with time, and a gauge of improvement, in the form of my fist in my teeth, my jaw slowly opened agape. I had never had my mouth opened so wide! I thought previously I was entirely relaxed. In reality, I had become numb to the tension. This caught me so surprised. I was shocked something like chronic lived-with tightness like this could live so long right under my nose (literally)!
|
||||||
|
|
||||||
|
If you are curious to try mindfulness, I highly recommend taking the chance to just attend to your mind and body. Attend to the world around you, because it will never arrange itself the same for you.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-27T21:44:27-07:00
|
||||||
|
description: "Things we do each day compound. I'd like to write each day."
|
||||||
|
image: ""
|
||||||
|
lastmod: 2026-03-02T00:40:43-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["writing", "goals"]
|
||||||
|
title: "Writing: The Daily Duty"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Writing Every Day for Myself
|
||||||
|
|
||||||
|
I have a goal to end each day with an addition to my record. Likely, living here on my blog. But, I would be satisfied with any form.
|
||||||
|
|
||||||
|
A blog is convenient. It's viewable on many devices. It is easy to share, on the occasions I consider the content noteworthy.
|
||||||
|
|
||||||
|
I eventually would like to have some form of review where I look back at what I wrote. Contemplating my growth, or perhaps just, my change. And with enough records, I think I will start to get a clearer picture of who I am.
|
||||||
|
|
||||||
|
## Interesting Links
|
||||||
|
|
||||||
|
These sources motivated me to write. To get over that what-if hump. The "once I'm ready" hump.
|
||||||
|
|
||||||
|
I share them with little context because they may resonate differently. But I think they are all valuable.
|
||||||
|
|
||||||
|
- [Cory Doctorow's Memex Method (pluralistic.net)](https://pluralistic.net/2021/05/09/the-memex-method/)
|
||||||
|
- [Several Short Sentences about Writing (Goodreads)](https://www.goodreads.com/book/show/13155290-several-short-sentences-about-writing)
|
||||||
|
- More to come...
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-28T21:23:17-07:00
|
||||||
|
description: "In praise of baby carriers! Its a win-win!"
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-02T00:40:43-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["life", "parenthood"]
|
||||||
|
title: "Baby Carriers Are Exceedingly Good"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Straps of Life
|
||||||
|
|
||||||
|
As a father, I have really enjoyed getting time to be physically close with my child. Mom has it built-in with breastfeeding. I researched carriers before our baby arrived. I concluded they were safe and would be helpful. Never could I have predicted just how much I love and rely on one!
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
Hip dysplasia and asphyxiation are the main concerns using carriers.
|
||||||
|
|
||||||
|
Both can fairly easily be avoided if you take the time learn how to properly seat baby and adjust everything.
|
||||||
|
|
||||||
|
Be sure to read safety guidelines, sign the terms and conditions and the end user license agreement and comply with state and local laws, etc, etc.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
I have enjoyed most the closeness it gives me to be with baby.
|
||||||
|
|
||||||
|
My arms can only hold baby for so long. With a carrier, I'm able to go much longer. My back tends to take the brunt of the work. Fortunately I'm young, and with extra stretching and strength training, I feel like everything is working out.
|
||||||
|
|
||||||
|
I highly recommend getting a carrier for your little one! We actually ended up with three various kinds, from gifts and our own purchases. I use the [LennyLamb Lite (product page)](https://us.lennylamb.com/shop/by/product_group/baby_carriers/erp_product_type/lenny_light), my partner uses a wrap (just fabric) and a [sakura bloom (product page)](https://sakurabloom.com/) carrier. We like them all for different reasons.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-28
|
||||||
|
description: "A brief retelling of my journey that led me to leave the Mormon faith."
|
||||||
|
image: ""
|
||||||
|
lastmod: 2026-03-02T00:40:43-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["religion", "life"]
|
||||||
|
title: "Why I Let Go of the Rod"
|
||||||
|
type: "post"
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Why I Stepped Away from The Mormon Church
|
||||||
|
|
||||||
|
## First Nephi
|
||||||
|
|
||||||
|
I recognize it's important to establish some things before embarking on this voyage. The audience I could be reaching is extremely broad, but this route is filled with narrow passes and nautical lingo (so to speak). Before I get to telling my tall tale, I want to make my intentions clear, so I don't run the risk of losing any passengers along the way.
|
||||||
|
|
||||||
|
{{<aside caption="It's not Mormon anymore" >}}
|
||||||
|
At the time of writing, it is widely held preference by members of the Church of Jesus Christ of Latter-Day Saints to not say Mormon. One would posit, the name Mormon does not reflect the Christ-centered nature of the church that the full name does. I will use the term Mormon or LDS going forward interchangeably. I don't mean to offend or be rude, I'd instead prefer to prioritize clearly expressing my experience. I also am more comfortable typing what comes to mind and not fretting over always saying the longer full name.
|
||||||
|
{{</ aside >}}
|
||||||
|
|
||||||
|
- I'm not writing this to persuade anyone. This is my story. I am
|
||||||
|
going to put the onus on you, the reader, to draw what conclusions you deem appropriate. If my feelings seem extreme, then I will apologize now. But I also hope you can recognize the sincerity in my writing.
|
||||||
|
- My motivation to write this is to have something more thought out and prepared when people ask why I left. This request
|
||||||
|
typically comes from friends and family still a part of the faith, which I always appreciate. Your brain is left
|
||||||
|
whirling a bit when you leave the Church, so hopefully this can clarify things for both of us.
|
||||||
|
- If you, the reader, don't know much about the (more commonly recognized) Mormon faith, then this post may be hard
|
||||||
|
to follow. I will waste no time explaining concepts and assume the reader is familiar with the doctrines, history,
|
||||||
|
and positions of the church. I will do my best to delineate between commonly held **facts** and my _opinions_ and cite
|
||||||
|
my sources when appropriate.
|
||||||
|
- If you, the reader, take issue with how I've portrayed things, please reach out! I mean it. There's no real way to
|
||||||
|
express my sincerity through high contrast pixels on your device of choice, but I mean all of these words. I feel strongly it is
|
||||||
|
important to always keep these conversations open, and I'm willing to accept I have made mistakes. [Tell me your thoughts](mailto:nate@fosscat.com)!
|
||||||
|
|
||||||
|
_Hopefully_ I haven't lost you, and I want to thank you in advance for any of the time and effort you take to
|
||||||
|
hear my story.
|
||||||
|
|
||||||
|
So, without further ado, lets "begin at the very beginning"
|
||||||
|
|
||||||
|
## "Born of goodly parents"
|
||||||
|
|
||||||
|
I grew up in a loving home with parents that did their best to give us kids a great life. I'm the middle kid, the
|
||||||
|
glue you might say *smirk. My involvement in the church informed much of my life. My friends were typically the boys in
|
||||||
|
my ward's Sunday school class or in my boy scout troop. My views of the world were most entirely shaped by my religion.
|
||||||
|
There was some anxiety as a kid in the calculus of membership of everyone around me. I learned quickly to look for the tells: multiple ear rings, tattoos, swearing, revealing clothes / garment lines under adults clothing, and of course, the lack of brightness in someone's eyes. It felt crucial to determine the activity of someone was a member because it meant Friends houses that weren't
|
||||||
|
members of the church felt unsafe.
|
||||||
|
When I was 8 or 9, I remember feeling immense responsibility, mixed with privilege and dread, thinking that God had
|
||||||
|
restored His One True Church to my neighborhood! Relief washed over me when I learned that there were actually millions
|
||||||
|
of us, not just the two hundred in my ward. lol.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-01T23:19:05-07:00
|
||||||
|
description: "I've realized that I only fully relax in bed with a roll under my neck."
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-02T00:41:46-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["life", "sleep"]
|
||||||
|
title: "Sleeping on a Towel"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# I Rock with the Roll
|
||||||
|
|
||||||
|
I realized over the last few months, that I relax much more deeply with a neck pillow instead of a head pillow. A towel roll, or just a really narrow pillow, placed under the [cervical curve (Wikipedia)](https://en.wikipedia.org/wiki/Cervical_vertebrae), allows those muscles in the neck to release.
|
||||||
|
|
||||||
|
Traditional Western pillows lift the head up substantially. This almost runs counter to releasing the neck muscles and leads to widespread neck tension that I think most people assume is inevitable.
|
||||||
|
|
||||||
|
A towel is not the most comfortable, but it works. You should give it a try! Different bodies and spines will need different shapes or sizes of support. The main idea is to lift the neck slightly, and let the head rest flat on the bed (or your preferred sleeping surface). If you feel your neck with your fingers, gently pressing and massaging, you may notice a significant difference between your regular pillow and this new approach. This also requires you to sleep on your back, which I don't always do, so I rotate around. But, I think this is something valuable to have in case of some nasty neck soreness!
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-02T22:46:28-07:00
|
||||||
|
description: "Its very interesting how different your reality can be on drugs."
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-03T00:25:05-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["life", "drugs", "Marijuana"]
|
||||||
|
title: "Perscription Self Augmentation"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
{{<aside caption="DISCLAIMER" >}}
|
||||||
|
I am not endorsing everyone take drugs. Chemicals affect every mind and body differently.
|
||||||
|
|
||||||
|
I think reporting on your experience is valuable, but it's important to be honest with yourself. I also recognize that honest self reflection is probably the hardest thing to do. A challenge worthy of any person's time.
|
||||||
|
|
||||||
|
Illicit drug use is not advisable.
|
||||||
|
|
||||||
|
There are things everyone should do, and none of us do all of them.
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
# Limitless in Reality
|
||||||
|
|
||||||
|
I remember so vividly watching the movie [_Limitless_ (Wikipedia)](https://en.wikipedia.org/wiki/Limitless_(film)) as kid. Not just because it had pixelated full-frontal. I reckon a teenage boy never forgets those, for some inexplicable reason.
|
||||||
|
|
||||||
|
The movie depicts (to my best recollection from ~10 years ago) the main character gaining the world by way of some mystery pills. The drugs work really well as far as I recall, with ~no real~ some serious side effects (alright I looked it up I misremembered lol).
|
||||||
|
|
||||||
|
Anyway, what seemed so alluring about the premise I think is having such an easy solution.
|
||||||
|
|
||||||
|
Us humans love the easy solution.
|
||||||
|
|
||||||
|
Take Wall-E: believable predicament humans would get themselves into. All too obese to get out of their floating La-Z-Boy chairs.
|
||||||
|
|
||||||
|
Obviously every easy path has downsides. Like, making every want and desire be delivered to you seamlessly. Or a pill that makes you super capable but has terrible withdrawals.
|
||||||
|
|
||||||
|
I think this is generally true.
|
||||||
|
|
||||||
|
But individuals are not generalities.
|
||||||
|
|
||||||
|
## My First Trip
|
||||||
|
|
||||||
|
I tried weed for the first time in my mid-twenties.
|
||||||
|
|
||||||
|
I grew up Mormon and was frankly shielded from all drug culture. I don't think I knew anyone personally that had smoked weed until my Mormon mission in Washington State. Lol. Lots of people smoked weed there. I imagine I was only aware of a small percentage.
|
||||||
|
|
||||||
|
I had a neighbor, and dear friend, show me the way. He also was a formerly active member of the church (of Jesus Christ of Latter-Day Saints (I did it once, I'm trying)). I feel like having a guide made a big difference.
|
||||||
|
|
||||||
|
I think we made affogados, watched _Emporer's New Groove_ together with our partners, and then listened to some music that hits just right.
|
||||||
|
|
||||||
|
Honestly, I think each of those activities blew me away.
|
||||||
|
|
||||||
|
I don't mean to overstate this, but when you have a good experience on marijuana, it can feel very transcendental. That evening opened my mind to altered states of consciousness.
|
||||||
|
|
||||||
|
{{< aside caption="Altered States of Consciousness 'Woo Woo'" >}}
|
||||||
|
I realize that there is a stereotype for stoners to talk in ways that seem utterly unintelligent. I feel sincerely that this is not that. If you are starting to check out or think I'm lost in the sauce, let me raise you this:
|
||||||
|
|
||||||
|
Have you ever focused so hard on something, looked at the time and thought "Gee wiz! It only felt like \<much less time\> passed!"
|
||||||
|
|
||||||
|
Or got wound up so tightly in anxiety and thought things you never normally do. Perhaps irrationally fearing imminent danger.
|
||||||
|
|
||||||
|
Or maybe, you find yourself constantly on guard around a certain coworker, ready to defend yourself.
|
||||||
|
|
||||||
|
I'm trying to convey that the phrase "altered states of consciousness" sounds exclusive. The reality is, we all experience them all the time.
|
||||||
|
|
||||||
|
Growing up, 96.9 KEZ always played the hits. It was predictable, sometimes even, enjoyable (I'm referring to an FM radio station btw >= zoomers). Certain chemicals and substances change the radio station, so to speak.
|
||||||
|
|
||||||
|
Sometimes, you hear the same thing on another station. It may even sound worse on this new station. Lesson learned, don't leave KEZ.
|
||||||
|
|
||||||
|
But other times, instead of turning the volume to "Fire Burning on the Dance Floor", you foolishly tune the radio -- 89.5 KBAQ. Suddenly, instead of singing along with Sean Kingston, you are enveloped with Brahms or Mozart. You never knew notes could be strung together like that... Such dynamic range of volume! A world opens before you, only moments ago, completely unknown. (Maybe you still don't like it hehe)
|
||||||
|
{{</ aside >}}
|
||||||
|
|
||||||
|
## Marijuana in Practice
|
||||||
|
|
||||||
|
I think various states in the U.S. legalizing marijuana has made experience with it much more prevalent. From my own experience, in the right circumstances I believe it can help you sort out some internal baggage. It's a shame there aren't more guidelines on how to use it well. Social morays.
|
||||||
|
|
||||||
|
Much like alcohol.
|
||||||
|
|
||||||
|
There are rules and expectations. You don't drive hammered. You call a cab or Uber. Drinking alone is risky and kind of sad...? (Granted, this is a U.S. perspective from someone who does not drink regularly)
|
||||||
|
|
||||||
|
These loose guardrails are helpful! I think it's a failure of a social system if you have to codify rules. The 'unspoken' social consequences of things can be powerful forces for self-guidance. But I digress.
|
||||||
|
|
||||||
|
I posit: My life and my dearest relationship with my wife has improved with intentional use of marijuana.
|
||||||
|
|
||||||
|
I may have made that progress regardless (I'd be surprised, but its possible), but I think the rate of improvement was dramatically boosted with this plant.
|
||||||
|
|
||||||
|
> Using plants to help a relationship?? Preposterous!
|
||||||
|
|
||||||
|
Or maybe just
|
||||||
|
|
||||||
|
> I don't feel like using or relying on drugs is healthy.
|
||||||
|
|
||||||
|
I think there are layers and layers here that would need to be peeled away for some to see genuinely that its possible these things are true, for me and my life.
|
||||||
|
|
||||||
|
Maybe just a seed:
|
||||||
|
|
||||||
|
### Addiction
|
||||||
|
|
||||||
|
_noun_
|
||||||
|
|
||||||
|
> being abnormally tolerant to and dependent on something that is psychologically or physically habit-forming
|
||||||
|
|
||||||
|
With this definition, I believe its certainly possible to be addicted to weed. I also think there is a wide range of use that would not fall under this classification.
|
||||||
|
|
||||||
|
## Liking Who I Am
|
||||||
|
|
||||||
|
I remember thinking "I really like who I am and how I treat others while under the influence".
|
||||||
|
|
||||||
|
I think these substances can show us a side of ourselves we don't normally show. Perhaps we are afraid to show. Or didn't know existed (what a notion, a part of you hidden from yourself)!
|
||||||
|
|
||||||
|
I don't think the goal is to escape to these sides through drug use. Instead, to incorporate.
|
||||||
|
|
||||||
|
### Self Diagnosis: Anxious
|
||||||
|
|
||||||
|
I find myself self reflecting much more on cannabis. I turn my pattern-matching analytical brain inwards.
|
||||||
|
|
||||||
|
Sometimes it has gotten me panicked. I experienced it spiral. I start to convince myself I'm "losing it". On reflection, I've realized that I have anxiety. It is the force that has unwittingly compelled me to be the people pleaser I described myself to be for years. I thought the pleasing was my choice. I thought it was intentional because I'm a good person, or because I'm just a nice guy. Those may be partially true, but I certainly get a racing heart anticipating social interactions. Anticipating what they might say, or what funny thing I can say to ease tensions and get them to like me. My face flushing when making jokes to my peers (thanks Zoom calls).
|
||||||
|
|
||||||
|
I never would have described myself as anxious, but I think the altered state that marijuana puts me in helped me see it clearly.
|
||||||
|
|
||||||
|
Sensations are accentuated. Time feels a bit slower. Its much easier to get absorbed into engaging things. My internal voice feels more intentional with its words, like its already written the first draft, instead of the usual ramble and point.
|
||||||
|
|
||||||
|
## Limit-full
|
||||||
|
|
||||||
|
As I mentioned earlier, I wish there were better social norms. I wish it wasn't stigmatized, not to mention its illegality in many U.S. states. I think that "Set and Setting" make a big difference. Essentially, intentional use. Obviously at first, you don't know what you are doing. That is why it's good to go with someone who does. But once you know yourself and, more importantly, yourself on these substances, you need to be prudent.
|
||||||
|
|
||||||
|
When you don't overuse and abuse, (speaking from some experience) it makes each experience more potent and enjoyable.
|
||||||
|
|
||||||
|
If I may be so bold, I'd like to propose a few guidelines. For those that don't have anyone, or would like to try to be more intentional with their use, join me! If you have any ideas, please shoot me an email: nate at this_domain.com. I'd be curious to get your thoughts to improve my experiences too!
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
- DO Dial in an amount you find enjoyable, but that doesn't leave you incoherent or unable to communicate and function, be consistent with the amount you take. You can predict the expectations better as can others around you.
|
||||||
|
- DON'T Put yourself in a situation where failing to perform something well would cause dire consequences, e.g. driving in traffic
|
||||||
|
- DO Put yourself around people you love, that know you are high (ideally), and love you
|
||||||
|
- DON'T view it as a crutch or an escape. It is an opportunity to see differently, that can be brought back to regular life to help you cope better with how your issues.
|
||||||
|
- When in doubt, pick an _Excellent Vibes_:tm: activity:
|
||||||
|
- Movement and Exercise
|
||||||
|
- Music & ASMR
|
||||||
|
- Creative Pursuits
|
||||||
|
- Everyday Activities / Chores
|
||||||
|
- Emotional & Sexual Intimacy
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
Again, I don't mean to encourage people to try drugs. I think that is each person's own prerogative. But I do think there is a dearth of information and experience of others out there, and thought I'd share. Let me know your thoughts!
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-03T23:23:32-07:00
|
||||||
|
# description: ""
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-04T01:31:12-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["stoicism", "philosophy"]
|
||||||
|
title: "The Stoic Practice of Negative Visualization"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Negative Visualization
|
||||||
|
|
||||||
|
I experimented today during my walk and meditation. I've taken interest at times to different practices and beliefs of the [Stoic (Wikipedia)](https://en.wikipedia.org/wiki/Stoicism) philosophy. I feel like Meditations by Marcus Aurelius really made the rounds a year or two ago amongst young millennial / gen z guys.
|
||||||
|
|
||||||
|
One of the standout practices to me is "futurorum malorum præmeditatio", or [negative visualization (Wikipedia)](https://en.wikipedia.org/wiki/Negative_visualization). It is believed to help with your resilience and gratitude. You imagine a bad scenario, losing something or someone dear to you. The reality of impermanence is brought forward intentionally in your mind.
|
||||||
|
|
||||||
|
While sitting on the special meditation rock, I tried my darndest to simply take in the view. I thought to myself, "man, I will really miss this place". I know it will be gone eventually. Its either bulldozed to erect a wonderful office building, or I move to a faraway land. I'm not a pessimist. I think this is just a fact of life. Eventually, we all get check-mated. Including the sitting rock.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## A Turn of Perspective
|
||||||
|
|
||||||
|
I caught myself. I wondered at all the times I, and others, use phrases like
|
||||||
|
|
||||||
|
> "I'm so excited for __ ..."
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
> "I'm going to miss __ ..."
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
> "I'm not looking forward to __ ..."
|
||||||
|
|
||||||
|
All living for the future.
|
||||||
|
|
||||||
|
Why don't I state how much I am enjoying the present moment? These are all futures that don't exist. They never will! Have I ever heard of someone pulling some future wish down to the present? The future is merely an idea in my head. Once I get to that future, its never what I thought exactly. Half of the time, I get to the future moment and am looking to the next one, not even enjoying the one I was really banking on enjoying. What a shame!
|
||||||
|
|
||||||
|
So instead, I thought to the sitting rock that I really enjoyed its company. I was grateful for the seat.
|
||||||
|
|
||||||
|
## Love & Loss
|
||||||
|
|
||||||
|
I then had an unexpected experience.
|
||||||
|
|
||||||
|
I sort of intuitively combined the Buddhist practice of Metta with the stoic's negative visualization.
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
I imagined losing my life partner. The love of my life. What that would be like, being there as she left me here. Alone to hold things together. It was vague, the timeline wasn't very clear, would this be from old age or some early life health complication, I wasn't sure. But the feeling of loss I felt got very deep. I breathed in and out, trying to stay with the feeling. I could feel tears pricking at the corners of my eyes. I persisted with the sensation. Then, I cried.
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
I don't recall ever willfully imagining a loss so profound, solely in my mind.
|
||||||
|
|
||||||
|
Breathing with this sensation, I realized that sadness was not exactly what I was feeling.
|
||||||
|
|
||||||
|
The loss was coupled with love. A love as deep as the sorrow. Like the two sides of your hand: one side clenches and the other releases, vice versa.
|
||||||
|
|
||||||
|
## Gratitude
|
||||||
|
|
||||||
|
I'm not claiming to be some wild guru, I don't really know what I'm doing with mindfulness considering my lack of time in the saddle. But this was an experience unlike I have ever had in a "spiritual" sense.
|
||||||
|
|
||||||
|
I don't know how to describe it, but the sensation of love and loss resonated in me. Realizing that they are almost one in the same.
|
||||||
|
|
||||||
|
I came home from that experience light on my feet. Giddy and grateful to get to kiss the love of my life, to see her as she is, and have another day to enjoy being with her.
|
||||||
|
|
||||||
|
I will certainly try to make the time to imagine terrible things more often :wink:
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-04T23:20:24-07:00
|
||||||
|
description: "A hopeless plea to those in power to stop fighting"
|
||||||
|
image: ""
|
||||||
|
lastmod: 2026-03-06T22:41:50-07:00
|
||||||
|
draft: false
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["politics", "war"]
|
||||||
|
title: "Why Do We Keep Fighting Wars?"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# I Don't Get It
|
||||||
|
|
||||||
|
Why did we (the United States of America) bomb Iran?
|
||||||
|
|
||||||
|
What are we hoping to do?
|
||||||
|
|
||||||
|
Do our leaders understand the dynamics of the government there? Do they know how to manage a smooth transition of power once we take out the current leader? Is the U.S. government going to do a good job in Iran? Have they done any of this successfully before?
|
||||||
|
|
||||||
|
I really hope we don't get roped into another 10+ year stalemate in a country we don't belong in, meddling with affairs nobody ever asked us to take part in.
|
||||||
|
|
||||||
|
It's so disheartening to me.
|
||||||
|
|
||||||
|
Why can't our leaders be honest? If it is about nuclear weapons, then why didn't the strike earlier last year take care of it all, [like they said it did (Wikipedia)](https://en.wikipedia.org/wiki/2025_United_States_strikes_on_Iranian_nuclear_sites#:~:text=%22completely%20and%20totally%20obliterated%22)?
|
||||||
|
|
||||||
|
If it is about removing a terrible leader terribly abusing their power, then how will we ensure a new leader doesn't repeat their mistakes? I think many people smarter than me have written [lots of thoughts](https://washdiplomat.com/academics-say-u-s-interventions-to-force-regime-change-often-fail/) [on why this](https://www.nbcnews.com/world/iran/iran-trump-israel-regime-change-tehran-khamenei-iraq-afghanistan-bush-rcna213427) [foreign push for](https://theconversation.com/destruction-is-not-the-same-as-political-success-us-bombing-of-iran-shows-little-evidence-of-endgame-strategy-277201) [regime change doesn't work](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5416614).
|
||||||
|
|
||||||
|
## A Mental Exercise
|
||||||
|
|
||||||
|
Let's take a break from the Iran conflict, and entertain my little analogy for a moment.
|
||||||
|
|
||||||
|
In your quaint town full of little rascals like yourself, everyone plays the game. Pick up sticks, Chinese Checkers, hop scotch, take your pick. Everyone puts a lot of stock in the game, and how well you play the game. You are, however, very bad at it.
|
||||||
|
|
||||||
|
You could pinpoint many reasons why. Born with two left feet, to parents without the means to get you your own chess set, needing to borrow others to practice. So on and so on.
|
||||||
|
|
||||||
|
More importantly, loud-mouthed-professional-yapper Chad excels at the game. Many think he is unbeatable.
|
||||||
|
|
||||||
|
> So what's the rub?
|
||||||
|
|
||||||
|
Every time you lose at the game, which you do every time playing against this bro, Chad tells you in no uncertain terms how bad you are.
|
||||||
|
|
||||||
|
"Yeah, that was a really stupid move you made with your pawn."
|
||||||
|
|
||||||
|
"You know that holding your foot makes it harder to balance on one foot right? Now you know :wink:" (always that annoying wink)
|
||||||
|
|
||||||
|
Chad's aggression toward you and your skill-issues intensifies with each accumulated loss. Noogies enter the picture, which escalates to pushing, shoving, kicking, beating.
|
||||||
|
|
||||||
|
Unfortunately, not playing the game isn't an option. It is just too popular. Your skill at the game determines your social status amongst the neighborhood kiddos. Everyone you know plays, and they all watch when it's your turn to play, and lose.
|
||||||
|
|
||||||
|
So.
|
||||||
|
|
||||||
|
How do you feel about this scenario you find yourself in?
|
||||||
|
|
||||||
|
Are you grateful for Chad and the helpful pointers and bruises he has given you along the way? "He just wants people to improve," his fans protest. "Getting knocked down just helps you grow back stronger". "How could you not like Chad? He's so good at the game!"
|
||||||
|
|
||||||
|
It gets worse when you realize Chad is boosting. His parents pay for private tutors, his parents' parents had the same. His school lets him practice the game for hours. Chad's siblings all give him pointers.
|
||||||
|
|
||||||
|
Why are your mistakes met with force? How do you feel about your shortcomings in one area of life signaling your worth to everyone? No one cares you can make an incredible paper crane. You entered the world with the rules predetermined, the value system established, so play the game.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This is, evidently, childish behavior. The analogy, though perhaps murky, seemed easier to convey with literal children subjects.
|
||||||
|
|
||||||
|
Our country is acting like a child. A fully loaded child with the biggest baddest guns on the block. Yay. We are Chad by the way.
|
||||||
|
|
||||||
|
But imagine the resentment we are stirring up. The needless suffering we are causing. I don't support these other despotic and corrupt leaders. I realize in the analogy, they were not portrayed accurately. They have committed some horrible crimes against humanity. So how then is more destruction the answer?
|
||||||
|
|
||||||
|
I hope things don't escalate from here...
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-05T21:15:30-07:00
|
||||||
|
description: "Taking time to pay attention to your body pays off."
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-12T23:41:32-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["body", "health"]
|
||||||
|
title: "Body Awareness Is No Joke"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Releasing Tension
|
||||||
|
|
||||||
|
I [carry baby in a carrier](/posts/26-02-28-baby-carriers-are-exceedingly-good) pretty often, and find it leaves my back achy.
|
||||||
|
|
||||||
|
I took a moment to really try and feel the texture and shape of this ache. After some online anatomy searching, I pinpointed the vertebrae (T5-T9 area) and that seemed to contain the strain.
|
||||||
|
|
||||||
|
With just patience and breathing, I focused on expanding my posterior rib cage. Waiting for my body to loosen the grip of just the surrounding muscles guarding the tension.
|
||||||
|
|
||||||
|
{{<aside caption="My Big Insight">}}
|
||||||
|
I always thought relaxing was a willed state. A quieting of the mind, of thinking to not think.
|
||||||
|
|
||||||
|
This of course only reinforces the sympathetic nervous system.
|
||||||
|
|
||||||
|
In reality, it's the mind dropping the background.
|
||||||
|
|
||||||
|
I learned I can only create the conditions for the parasympathetic system to take control. Similar to dilating your own pupils, you can't will that, it happens based on the lighting conditions around you.
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
For some "light" reading on all of this if you are curious:
|
||||||
|
|
||||||
|
- [The Stretch Reflex (Wikipedia)](https://en.wikipedia.org/wiki/Stretch_reflex) causes muscle flexion when stretched to a certain point
|
||||||
|
- [Golgi tendon organ (Wikipedia)](https://en.wikipedia.org/wiki/Golgi_tendon_organ) signals to muscles relax when the right conditions are met
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-06T22:13:10-07:00
|
||||||
|
description: "Some thoughts on what makes a good gift, and why its hard to do it well."
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-06T22:41:50-07:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["life", "practices"]
|
||||||
|
title: "Good Gift Giving Is Difficult for Good Reason"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# A Gift is No Simple Thing
|
||||||
|
|
||||||
|
I think the standard low-effort but always appreciated gift is money.
|
||||||
|
|
||||||
|
Money indicates that you value some person, but don't really have the time to invest in finding something meaningful.
|
||||||
|
|
||||||
|
I think every person is different, so having a class of "evergreen" gifts is nonsense.
|
||||||
|
|
||||||
|
To give a truly nice gift requires selflessness.
|
||||||
|
|
||||||
|
You need to take time to start listening.
|
||||||
|
|
||||||
|
I think people signal things they want all the time.
|
||||||
|
|
||||||
|
## The First Kind of Gift
|
||||||
|
|
||||||
|
Lots of people I know are uncomfortable with the question "What do you want for \<special occasion\>?" It's so direct. Worse, its a trap, answering broadcasts how truly un-humble you are, needing trinkets and worldly things. No no, better to say something bland, normal, or nothing at all.
|
||||||
|
|
||||||
|
But unprompted, people will telegraph countless things they wish for. Some are hard to wrap, like a bigger home, or a different career, or more fulfilling relationships.
|
||||||
|
|
||||||
|
But notice, each of those things can be delivered, in some form, as a gift of time and attention: time set aside to scroll Zillow together and dream, a tighter budget to allocate funds to education or trade school.
|
||||||
|
|
||||||
|
This is the first precious gift you can give someone: __noticing__.
|
||||||
|
|
||||||
|
## The Priceless Gift
|
||||||
|
|
||||||
|
Time.
|
||||||
|
|
||||||
|
We come here with only so much.
|
||||||
|
|
||||||
|
Any moment you take to be with someone is precious, final, and non-refundable.
|
||||||
|
|
||||||
|
Giving your time to someone typically looks in our heads like being with them. But it's certainly possible to give your time to someone by yourself. Researching just the right piece of jewelry or keyboard based on the criteria you know they look for may be invaluable.
|
||||||
|
|
||||||
|
I want to stretch my gift giving abilities. Put myself in another's shoes, think from their POV (as best as I can manage) and give a thing I (they) would truly appreciate.
|
||||||
|
|
||||||
|
It's so easy to give the thing we would appreciate; always presented in our minds. A meal, a movie ticket, an obscure tiny Linux computer. Much harder is to give what the person actually wants.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-07T23:30:48-07:00
|
||||||
|
description: "From a legal loophole in the 70's, we now deal with tanks on American roads."
|
||||||
|
image: "/images/ford-f150.webp"
|
||||||
|
image_alt: "An image of a vintage blue Ford F150"
|
||||||
|
lastmod: 2026-03-09T00:32:23-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["cars", "politics"]
|
||||||
|
title: "How Cars Became Trucks"
|
||||||
|
type: "post"
|
||||||
|
---
|
||||||
|
|
||||||
|
# An American Saga
|
||||||
|
|
||||||
|
As every great story in the U.S. of A. goes, what started small and mighty is now big and ugly.
|
||||||
|
|
||||||
|
The American car manufacturers played a big role in American life and in American government. They represented the heart of this country's manufacturing as well as the force that got everyone where they needed to go.
|
||||||
|
|
||||||
|
No other country adopted the car quite so integrally as the United States did.
|
||||||
|
|
||||||
|
But, as the [government cracked down on polluting cars](https://en.wikipedia.org/wiki/Clean_Air_Act_(United_States)#Regulatory_programs) coupled with the [1973 oil embargo](https://en.wikipedia.org/wiki/1973_oil_crisis), consumer sentiments toward these gas guzzlers started to shift. Car manufacturers were obligated to reach a target MPG of 27.5 of the average car sold in their fleet by 1985.
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
I was shocked to learn this was the _average_ MPG of an entire manufacturer's offering. 30 years ago!
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
This new legislative requirements for efficiency and safety applied to cars. However, a little slice was cut out for light trucks, work trucks, non-passenger work trucks. Seems like there are multiple terms for it...
|
||||||
|
|
||||||
|
Which makes some intuitive sense. A farmer's work truck shouldn't be held to the same standard as a commuting vehicle, its very hard to achieve that sort of fuel efficiency while idling half the day outside the barn.
|
||||||
|
|
||||||
|
## The Truck-Shaped Loop-Hole
|
||||||
|
|
||||||
|
However, the American car manufacturers notice something interesting:
|
||||||
|
People will pay more for bigger cars, we can make bigger cars for cheaper. If we classify them as work trucks they aren't beholden to the same safety regulations, and sell them for a larger margin!
|
||||||
|
|
||||||
|
Long story short, massive trucks flood the streets of you average road in the U.S. It's evolved into a collective action problem due to the danger these huge vehicles pose.
|
||||||
|
|
||||||
|
With regular cars, there are tighter regulations on the bumper, namely how high off the ground they sit. But for work trucks? No, we gotta get over some rocks, raise that bumper up!
|
||||||
|
|
||||||
|
A [great video on the topic (YouTube)](https://www.YouTube.com/watch?v=JPm4de6-eTg) was recently published by Climate Town on YouTube, I enjoy their stuff so I recommend giving it a watch!
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-08T23:12:51-06:00
|
||||||
|
description: "As the internet sours past its due date, why blog? And what about?"
|
||||||
|
# image: ""
|
||||||
|
lastmod: 2026-03-10T22:47:30-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["privacy", "internet", "life"]
|
||||||
|
title: "What Is Appropriate for the Internet"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# What should go here?
|
||||||
|
|
||||||
|
As I write each day, I find myself self-censoring some topics.
|
||||||
|
|
||||||
|
Obviously if I had extreme political views or problematic takes on things, I could suffer real world consequences. Cancel culture is alive and well in the United States, on both sides of the aisle. So, regardless of your political leaning, the internet is not safe.
|
||||||
|
|
||||||
|
With AI becoming [more _amazing_ every day (nytimes)](https://www.nytimes.com/2025/11/02/opinion/ai-privacy.html), and [data brokers amassing mounds of data (cnet)](https://www.cnet.com/tech/services-and-software/data-brokers-how-your-personal-data-becomes-business/) on each of us, one might think putting some intimate thoughts online is not the wisest decision.
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
To the uninitiated, this is really only a concern to the privacy conscious. Or perhaps, the privacy paranoid :grin:.
|
||||||
|
|
||||||
|
Social Media aggregates tons of data on its users, which is sold to data brokers and integrated into algorithms. I don't mean to overstate this but everyone I know uses social media. Lots of people I know feel like this whole dilemma is too far gone for anything to be done.
|
||||||
|
|
||||||
|
At least on my website, people have to come and take it. Plus, I have just added some [more explicit guards](/terms) against [getting my data harvested](/robots.txt). So, fingers crossed the line gets crossed then I can sue for billions :smiling_imp:
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
But, I think the with some prudence, putting thoughts online is an excellent use of time.
|
||||||
|
|
||||||
|
## Why It Matters
|
||||||
|
|
||||||
|
AI is melting my brain.
|
||||||
|
|
||||||
|
At my day job as a Software Developer, I cannot code like I used to. Sure, perhaps I've learned how to be an "AI-Whisperer" or something.
|
||||||
|
|
||||||
|
But it feels like I've lost an edge.
|
||||||
|
|
||||||
|
I think about the times I studied hard for a final exam. All the necessary concepts are fresh. Poised to strike, from neuron to graphite. The formulas are at the tip of my tongue ready to be spat out onto the page to prove my know on how to find the area of a circle rotated around an axis in three dimensions offset by some coordinate. Amazing.
|
||||||
|
|
||||||
|
I've lost the edge for the things I ~do~ did every day: write code.
|
||||||
|
|
||||||
|
Now? I, manage. Architect? Oversee? Sometimes just skim?
|
||||||
|
|
||||||
|
Anyway, this is really material for a different post.
|
||||||
|
|
||||||
|
Point is, writing these posts keeps me sharp. Maybe not for coding, but certainly for self reflection.
|
||||||
|
|
||||||
|
I find that invaluable.
|
||||||
|
|
||||||
|
And if some data broker gets to track me down and endanger my family and I because GPT-10 identified me as a terrorist threat, then so be it. These privacy concerns unfortunately feel like a collective action problem. Though an individual can logically conclude it foolish or dangerous, I think it's important to fight for what we would like to see in the world.
|
||||||
|
|
||||||
|
I'd like to see less billboards. Less [trucks](/posts/26-03-07-how-cars-became-trucks/) (which is also a collective action problem). Less AI generated internet content. Hopefully someone lands here from searching on a search engine (did you know Google isn't the only one?). I'd like to be a part of the human-made side. I find writing these to be helpful, and I hope someone out there will find something helpful here too.
|
||||||
|
|
||||||
|
[Let me know](mailto:feeback@fosscat.com) what you think! If you agree of disagree with anything on this site, I'd love to hear from you. Take care.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-09T13:14:02-06:00
|
||||||
|
description: "If we didn't need to distinguish between you and me, we would just be we."
|
||||||
|
# image: "/images/"
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-03-09T17:41:55-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["philosophy", "buddhism"]
|
||||||
|
title: "Discriminating Creates the Other"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Why Do We Discriminate?
|
||||||
|
|
||||||
|
When I use the term _discriminate_ here, I don't mean specifically prejudice towards another race (the purely social construct). I mean any sort of discriminating. The sort of discerning between me and thee. The identification of yourself against the backdrop of what is before you.
|
||||||
|
|
||||||
|
Wishy washy terms aside, lets get real with a fake story:
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
You meet someone new standing in line at the DMV. Busy day. You don't normally talk to strangers around you (as you were taught, stranger danger!). But this stranger had a t-shirt on of your favorite TV show! You compliment their taste. They agree with your seemingly shared excellent taste in "the classics". So, conversation starts up. First around the show, but then you ask questions about their life, their circumstance. You find you agree more and more with this person. Their politics line up with yours. Their food preferences are eerily similar. They even ask for the sauce to be put on the side instead of the bun like you do! By the time your number gets called by Deb behind counter 7, you have fallen fast. You both exchanged phone numbers (theirs was only 1 digit different from yours), made plans to play Mahjong together (they also just got into it!), and you feel like you just met your soul mate / friend! You soul friend! (that should be a term)
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
What started as a stranger ended up being your greatest friend! What prevented you from treating this person as a friend from the start?
|
||||||
|
|
||||||
|
Discussing your lives did not change what they were already doing. It did not change their quirks and preferences to align with yours. They were already just so!
|
||||||
|
|
||||||
|
So what changed?
|
||||||
|
|
||||||
|
## You Don't Trust
|
||||||
|
|
||||||
|
I think its a survival instinct we come installed with. Perhaps its bloatware, but it was certainly necessary at some point in our evolution.
|
||||||
|
|
||||||
|
Through introspection, I believe you can find the uninstall button. But it takes work.
|
||||||
|
|
||||||
|
This stranger in line becomes a wonderful companion because you feel safe with them. You agree with each other after all!
|
||||||
|
|
||||||
|
But disagreeing doesn't represent anything dangerous. I think we often feel that way. If someone disagrees with my worldview, then is my worldview wrong?
|
||||||
|
|
||||||
|
I remember as a Mormon missionary literally feeling my fight or flight response kick in when I was approached by spiritual "enemies". Ex-members, pagans, other Christians (some would say Mormons aren't, if disagreeing with me here helps you feel better, I encourage you to start from the top), etc.
|
||||||
|
|
||||||
|
I wasn't in danger physically. But my body responded nonetheless! Because my brain was so sure if I was proven wrong, my world as I knew it would cease to exist!
|
||||||
|
|
||||||
|
## Soul Friends
|
||||||
|
|
||||||
|
I think we are our own worst enemies, keeping any ordinary person from appearing as a soul friend.
|
||||||
|
|
||||||
|
Sure, not everyone is just like us. In fact, most people aren't like us in the specificity.
|
||||||
|
|
||||||
|
But, every person is also just like us. Deeply flawed, unique, with our own baggage and experience and fishing stories and fears of childhood recesses because of Blake Thorne.
|
||||||
|
|
||||||
|
We all are alive. We all will die. We all get to grapple with the same forces day to day. We all see the sun and feel the rain.
|
||||||
|
|
||||||
|
I don't mean to sound stereotypical here. Really, I challenge you to contemplate this:
|
||||||
|
|
||||||
|
> In a very real way, we are all the same.
|
||||||
|
|
||||||
|
But our brains want us to live. We have to identify threats. Categorize objects. Remember wrongs and being wronged so we don't get burned. Or worse.
|
||||||
|
|
||||||
|
We gossip about the neighbor because in some small way, they pose a threat to us. Our routine.
|
||||||
|
|
||||||
|
I wonder how different things would be if we could get over discriminating.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-10T22:10:22-06:00
|
||||||
|
description: "Communicating is hard. I've realized that we talk to others how we would like to be talked to."
|
||||||
|
# image: "/images/"
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-03-10T22:47:30-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["psycology", "communication", "relationships"]
|
||||||
|
title: "We Talk How We Would Like to Be Talked To"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Value of Contraction
|
||||||
|
|
||||||
|
Today I learned what the Platinum Rule is, the Golden Rule's cool older brother.
|
||||||
|
|
||||||
|
> Treat others how they would like to be treated.
|
||||||
|
|
||||||
|
Nothing wrong with the golden per se. But I think once you have tested out, the platinum is perhaps the next step.
|
||||||
|
|
||||||
|
We often assume how we want to be treated is how everyone wants to be treated.
|
||||||
|
|
||||||
|
Sure, this may be the case generally, but specifically, not everyone likes to be affirmed the way you do. Not everyone likes the gifts you like, or the holding and consoling the way you like.
|
||||||
|
|
||||||
|
[The false consensus effect (Wikipedia)](https://en.wikipedia.org/wiki/False_consensus_effect) posits that people often overestimate how common their beliefs and views are held by others.
|
||||||
|
|
||||||
|
I think our natural state is here. We assume people think like us. This is the cause of a large portion of contention in relationships.
|
||||||
|
|
||||||
|
When we become curious however and turn in, magic happens.
|
||||||
|
|
||||||
|
## Some Curious Questions to Try
|
||||||
|
|
||||||
|
I think everyone at any given moment is doing their best. (I think you could equally say their worst, because I think we just are being).
|
||||||
|
|
||||||
|
If any one of us had the brain and memories of another, we would act just like the other.
|
||||||
|
|
||||||
|
So, let's cut the assumptions. We really don't know how any other would like to be treated specifically.
|
||||||
|
|
||||||
|
Some questions I think you could try asking someone close to you to help you turn in to them:
|
||||||
|
|
||||||
|
> "How best could I communicate to you that I'm listening?"
|
||||||
|
|
||||||
|
> "How can I best help you when I want to give you aid?"
|
||||||
|
|
||||||
|
> "What are some things I could do spontaneously that you would probably always appreciate?"
|
||||||
|
|
||||||
|
If you can create a space big enough and safe enough, you might be surprised what comes out.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-11T23:00:22-06:00
|
||||||
|
description: "My reflection on the Netflix show Love is Blind, it strips people of their humanity."
|
||||||
|
# image: "/images/"
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-03-12T00:32:27-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["media", "rant", "communication", "relationships"]
|
||||||
|
title: "Love Is Blind Causes Blindness"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
My wife and I used to watch Love is Blind pretty consistently. Years ago. Perhaps seasons 2-4.
|
||||||
|
|
||||||
|
Every now and again we dip our toes in. A family member watches and wants to have a tea talk.
|
||||||
|
|
||||||
|
Well, the season 10 final episode, the reunion, aired tonight. I came away feeling so empty.
|
||||||
|
|
||||||
|
## The Villain is in Each of Us
|
||||||
|
|
||||||
|
There was one man (I almost said character, yeesh) who was collectively identified as the season's villain.
|
||||||
|
|
||||||
|
He made an emotional connection, got engaged, and on leaving the pods, seemingly turned on his fiance. Apparently, he was not physically attracted to her. Had weird conversations and speaking poorly of her behind her back. Made advances on another girl who was currently engaged to another guy in the show. There were lots of other details that highlighted his flawed state.
|
||||||
|
|
||||||
|
Well, the final episode airs. They give about a year's time after the actual season was recorded. The people from that season come back together to hash out details, get some last words in, and catch all of us up on our para-social relationships' statuses. It's all very strange, but totally normal and mainstream seemingly.
|
||||||
|
|
||||||
|
Anyway, so many people wanted to call this guy out for his actions.
|
||||||
|
|
||||||
|
My wife commented "I wonder how much they have to pay him to show up to this beat down."
|
||||||
|
|
||||||
|
I'd be curious to know behind the scenes what motivated him to show up. Honestly, it might have been to apologize, because that's basically all he did.
|
||||||
|
|
||||||
|
I respected him for that. And I quickly pitied him.
|
||||||
|
|
||||||
|
I think there is something really compelling to a villain that is humanized. Good villains deserve their punishment. Great villains are understandable and reprehensible at the same time.
|
||||||
|
|
||||||
|
I was so disappointed for the people in the room, those that claim so much to focus on emotional connections and emotional intelligence, cannot see that he needs emotional support.
|
||||||
|
|
||||||
|
It seemed to me that he struggled with some insecurities. I thought that he showed he could not communicate clearly his feelings.
|
||||||
|
|
||||||
|
And to this the mob of emotional intellects want to watch him squirm. I felt compassion for this guy. Sure, he crushed this girl's heart. I'm by no means saying getting his apology was bad. But I think the collective has caused immense damage to a man that may not ever heal, just so we, the completely removed from the situation viewers, can have some sense of justice?
|
||||||
|
|
||||||
|
I was equal parts filled with compassion and disgust. I almost can't believe this is what now constitutes entertainment.
|
||||||
|
|
||||||
|
## Patterns of Blindness
|
||||||
|
|
||||||
|
The patterns of communication that bothered me but I may not exactly know why:
|
||||||
|
|
||||||
|
- Women frequently saying to their male connection they did not marry "I know there is a good guy in there"
|
||||||
|
- The male host kept saying "man to man" when trying to catch some of the guys out, like being honest and a man of your word is just a guy thing? I guess all women don't keep their word? Or like, you aren't a "man" if you lie, you are a boy?
|
||||||
|
- I would absolutely consider myself a feminist (if this seems weird to hear from a guy, I encourage you to look up the definition), but there was a lot of man hate energy. It felt uncomfortable. Not as a man feeling targeted, I think more as giving women a bad look? idk. that might not be quite it...
|
||||||
|
- I felt like so many of the women that were single were working on themselves, which was met with applause. The guys were kind of laughed at for just being around lol sad
|
||||||
|
- I think it was clear to see in the majority of cases the women had much more emotional maturity and intelligence than the men. Women could articulate well their perspective, identify problems better, etc. The men often missed getting the memo. This bothers me. I wish men would step up and take responsibility to learn communication skills.
|
||||||
|
|
||||||
|
All in all, I think I'm filled up on Love is Blind. It was fun and interesting in the beginning. Watching couples genuinely try to grow in love together gave my marriage relationship something to learn from and contrast with. I think we found it helpful and cathartic. Now, I don't know what this Frankenstein has become.
|
||||||
|
|
||||||
|
Also, the end of the show culminated in an ad from Turbo Tax. They gifted a couple a honeymoon trip. Insane product placement. It kind of blew my mind, felt like a Truman Show level ad. I felt like no one saw the Turbo Tax part, they just heard free trip.
|
||||||
|
|
||||||
|
In summation:
|
||||||
|
|
||||||
|
There was no love, only blindness.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-12T23:01:14-06:00
|
||||||
|
description: "At the end of it all, what will it take for us to feel enough?"
|
||||||
|
image: "/images/red-path.webp"
|
||||||
|
image_caption: "Red Path, St. Prex - Courtesy of the National Gallery of Art"
|
||||||
|
image_alt: "This painting is an abstract composition of irregular shapes and vibrant colors. The different sections overlap and intersect in shades of green, blue, red, purple, and pink. A blue section in the top left has black dots, and two purple sections towards the bottom have white dots with yellow centers. In the upper center of the painting, there is a section of crossing black lines in a small grid. The paint has been applied unevenly in thick, scribbled lines, and some patches of the white canvas are visible."
|
||||||
|
lastmod: 2026-03-12T23:41:32-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["life", "reflection"]
|
||||||
|
title: "What Is Enough?"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
> When I reach the end of it all,
|
||||||
|
>
|
||||||
|
> What will help me feel like it was enough?
|
||||||
|
>
|
||||||
|
> With the weight of my duties
|
||||||
|
>
|
||||||
|
> Puffing my chest out to seem extra tough.
|
||||||
|
>
|
||||||
|
> Safety in a full bank account
|
||||||
|
>
|
||||||
|
> And a house full of stuff?
|
||||||
|
>
|
||||||
|
> Or perhaps a love so real
|
||||||
|
>
|
||||||
|
> I'm shown what life is made of.
|
||||||
|
>
|
||||||
|
> Can anyone reach it?
|
||||||
|
>
|
||||||
|
> What would I feel if I got there?
|
||||||
|
>
|
||||||
|
> Does craving cease? Or will I finally understand what is "crave"?
|
||||||
|
>
|
||||||
|
> A rouse, an illusion --
|
||||||
|
>
|
||||||
|
> A reflection of something more --
|
||||||
|
>
|
||||||
|
> A dream waiting to be dispersed with waking --
|
||||||
|
>
|
||||||
|
> Would, that I just can be enough.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-13T23:57:57-06:00
|
||||||
|
description: "My first attempt to do some art analysis myself on a painting I've never seen before."
|
||||||
|
image: "/images/woman_holding_a_balance_1942.9.97.webp"
|
||||||
|
image_caption: "Johannes Vermeer, courtesy of National Gallery of Art"
|
||||||
|
image_alt: "In this vertical painting, a woman stands near the corner of a dimly lit room, facing our left in profile and looking down at a balance she holds suspended in her right hand over a wooden table. She wears a peacock-blue velvet jacket with a white hood and fur lining, and a voluminous, mustard-yellow skirt. A window near the upper left corner is partially covered by a canary-yellow curtain. Light coming in through that window falls on the pale skin of the woman's face and hands, and highlights the white trim of her garment. Her left hand, closer to us, rests on the edge of the table near two open boxes, and a blue cloth is bunched at the back of the table to our left. Gold chains and pearl strands drape over the edge of one box. The woman stands in front of a framed painting. Much of the detail is lost in shadow but at the top center of the painting, a person surrounded by a golden halo floats in the sky with both arms raised, and is flanked by people encased within a bank of clouds. Nude people on the ground below in the painting, seen to either side of the woman, writhe, twist, and point upward."
|
||||||
|
lastmod: 2026-03-20T03:02:02-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["art", "religion", "ai"]
|
||||||
|
title: "Art Analysis: Woman Holding a Balance - Vermeer"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
I don't think I've ever really sat with art. Static art.
|
||||||
|
|
||||||
|
Motion pictures and temporal audio (music) feel like a different class. The still art requires something more from you.
|
||||||
|
|
||||||
|
I wanted to know what that more was, and what better time than the present.
|
||||||
|
|
||||||
|
## My Journey to The Balance
|
||||||
|
|
||||||
|
Not knowing any good resources I asked AI (Claude) the following:
|
||||||
|
|
||||||
|
{{<aside>}}
|
||||||
|
I feel like I'm not sure how to appreciate art. I have done some writing and doodling, not terrible at either, but when I read poetry, or look at art (paintings, photography) I feel like I'm not sure how to appreciate it. Is there a science to it? or just some helpful mindsets to take toward observing art? I feel like I've moved past the "need" for art to speak to me and instead just be open to whatever a piece of art may invoke. What sorts of things make art art? I imagine its in the eye of the beholder? Is art's worth determined by its creator? Can you appreciate art if the artist is a terrible person? Not exactly sure how to feel toward art, but I haven't ever really had any sort of moving experience with art outside of music and motion picture.
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
Aside from the philosophical questions (perhaps they all were), some interesting highlights I pulled out:
|
||||||
|
|
||||||
|
- Visual art and poetry require you to impose a temporal unfolding yourself (unlike video or music that "play")
|
||||||
|
- _First Pass_ With no context, look at a piece of art for a few minutes. Notice what your eye is drawn to. Notice how you feel.
|
||||||
|
- _Context_ Then with some observation, learn more about the art. Its background, the artist's process.
|
||||||
|
- _Synthesis_ Sit with this new information and the art again. See what arises in you.
|
||||||
|
|
||||||
|
With this rough framework here is what I felt and observed about Vermeer's 400 year-old painting:
|
||||||
|
|
||||||
|
{{<aside caption="Analysis">}}
|
||||||
|
|
||||||
|
### On First Pass
|
||||||
|
|
||||||
|
- The balance sits perfectly balanced in the very center of the painting.
|
||||||
|
- There are many other features of the painting that are balanced, a dark blue shade of fabrics on the middle left and right sides, light yellow of the window and dress in the top-left and bottom right.
|
||||||
|
- At the same time, lack of balance in the painting: only one woman on a side. The much larger framed painting in the background on the right compared to the one on the wall on the left, the lighter portion of the painting is the top right, darker bottom left (cut diagonally almost, which feels balanced more than imbalanced I suppose)
|
||||||
|
- The woman's gaze is easy to follow - lands on the balance she is holding
|
||||||
|
- The woman seems perhaps pregnant?
|
||||||
|
- The framed painting seems like a Jesus second coming or judgement day type? perhaps the woman being a Mary symbol?
|
||||||
|
- did the artist put the scale in the middle of the painting at the very beginning? (maybe first the background wall, but then the scale) thought that would be an interesting grounding piece for the artist to work around and place balancing and unbalancing elements around the balance.
|
||||||
|
- I thought that the painting left me feeling maybe a little dissatisfied, like I wanted compositional completeness but something feels missing? I felt I guess... off? only slightly so.
|
||||||
|
- The balance itself is kind of hard to see, or maybe easy to miss? wonder if that speaks to the balance in the painting being easy to miss?
|
||||||
|
|
||||||
|
Then AI gave some context on those thoughts. The highlight for me being about Judgement Day.
|
||||||
|
|
||||||
|
I realized in a moment of reflection the religious trauma I feel I still have.
|
||||||
|
{{</aside>}}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-20T02:09:41-06:00
|
||||||
|
description: "Humans spent thousands of years staring up at the sky. Now I wonder if many of us do at all."
|
||||||
|
image: "/images/moonlit_landscape_with_bridge_1990.6.1.webp"
|
||||||
|
image_caption: "Moonlit Landscape with Bridge: Aert van der Neer"
|
||||||
|
image_alt: "In almost complete darkness, we look across reeds and grasses lining a river spanned by narrow, arched, stone bridge ahead of us in this moonlit, horizontal landscape painting. The horizon comes about a third of the way up the painting, just over the footbridge, and the sky is filled with clouds that glow blush pink, flint gray, and lavender purple. The small, porcelain-white moon casts an opalescent gleam on the water under the arch of the bridge. Barely visible in the gloom, a walking path lined by a fence in the lower right corner leads to a copse of tall trees to our right. Closer inspection reveals a man wearing crimson red and a woman wearing pine green standing together near the gate of a walled enclosure beyond the trees. Moonlight glints on their white collars and cuffs, and on the gold buttons and embroidery on their clothing. Spires and buildings with stepped rooflines along the riverbank are outlined against the illuminated sky, though most of the details of the structures are swallowed in shadow. "
|
||||||
|
lastmod: 2026-03-20T03:02:02-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["technology", "philosophy"]
|
||||||
|
title: "Have We Lost the Stars?"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
There is a park near my home.
|
||||||
|
|
||||||
|
In the middle of this park is a modest hill. If one lays down on the top of the hill, looking to the heavens, your visual field is left stranded in the deep blue.
|
||||||
|
|
||||||
|
I have started going out at night with my dog. He gets to run, and I gaze.
|
||||||
|
|
||||||
|
In our ever-quickening modern world, I wonder how often we look up at the sky. Aside from this recent curiosity, I really only take the opportunity when camping or deep in nature.
|
||||||
|
|
||||||
|
Its a shame really. Its most always there just waiting to be admired.
|
||||||
|
|
||||||
|
The sky is full and (mostly) untouched from human expansion. It remains clear and free from cement sidewalk and suburban sprawl. Until we fill the night sky with satellites, it even twinkles with distant planets and galaxies.
|
||||||
|
|
||||||
|
## Humans Gazed Through History
|
||||||
|
|
||||||
|
I have a hunch, an intuition completely unfounded in any hard science. I think for millennia humans concluded their days staring into crackling fires or expansive night skies.
|
||||||
|
|
||||||
|
Now, many of us end our days staring into a neat little grid of LED's, myself included.
|
||||||
|
|
||||||
|
What, if anything, have we lost?
|
||||||
|
|
||||||
|
I wonder if we wonder less.
|
||||||
|
|
||||||
|
I feel instinctual attraction to an open flame. And when camping, I can't resist gazing up in awe.
|
||||||
|
|
||||||
|
I would wager these infinities are not being tapped into as often.
|
||||||
|
|
||||||
|
Not to sound like a [Luddite (Wikipedia)](https://en.wikipedia.org/wiki/Luddite), but with every new piece of technology, I can't help but think there are always trade-offs. The less obvious the exchange, the more insidious.
|
||||||
|
|
||||||
|
We humans can't stand the tedium. We want everything to be seamless. Processed. Pre-digested ideally. Just give me the AI summary. A tiktok or YouTube short will suffice. We get our information from hyper-processed, cream-of-the-algorithmic-crop content.
|
||||||
|
|
||||||
|
Insights are not discovered, they are simply repeated.
|
||||||
|
|
||||||
|
Contemplation is now just an appearance.
|
||||||
|
|
||||||
|
Why meditate when we have [Marcus Aurelius' (Wikipedia)](https://en.wikipedia.org/wiki/Meditations) cheat sheet?
|
||||||
|
|
||||||
|
I'm not arguing against access to information.
|
||||||
|
|
||||||
|
I do think one of the treasures we traded for infinite knowledge was our considering gaze.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
date: 2026-05-05T01:09:56-06:00
|
||||||
|
description: "Having ritual and ceremony in a society highlights the intimate side in us."
|
||||||
|
# image: "/images/"
|
||||||
|
# image_caption: ""
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-05-05T01:23:09-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["mormon","religion","ritual","ceremony"]
|
||||||
|
title: "The Importance of Ritual"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
I was talking to a friend recently about our shared history with the Mormon faith. They helped me realize something in reminiscing on Mormon temple ceremonies.
|
||||||
|
|
||||||
|
Being in an intimate setting with friends. Dressed in a different garb. Whispering in hushed tones used only in this setting. Quiet interrupted by the water sloshing in the large, tiled baptismal font. A setting to see into the soul of another.
|
||||||
|
|
||||||
|
What do they do when all is made eternal?
|
||||||
|
|
||||||
|
How do they hold themselves when the temple suspends their external identity?
|
||||||
|
|
||||||
|
I noticed that this is, unfortunately, something I have lost in stepping away from the church. I wish there was more ceremony in our culture.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
date: 2026-05-31T22:02:28-06:00
|
||||||
|
description: "A thought I had on what each man and each woman ought to do."
|
||||||
|
# image: "/images/"
|
||||||
|
# image_caption: ""
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-05-31T22:59:04-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["philosophy","masculinity", "femininity"]
|
||||||
|
title: "The Masculine and Feminine Mandates"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
> It is for each man to learn to listen, then to lead with their best judgement.
|
||||||
|
>
|
||||||
|
> It is for each woman to recognize and validate that which they have always known.
|
||||||
|
|
||||||
|
## Mandates Expanded
|
||||||
|
|
||||||
|
I had roughly these thoughts today at some point while stewing on my reading of [_Iron John_ (Wikipedia)](https://en.wikipedia.org/wiki/Iron_John:_A_Book_About_Men) by [Robert Bly (Wikipedia)](https://en.wikipedia.org/wiki/Robert_Bly). Obviously, I don't claim to say that this is an absolute for absolutely every man and woman. But it is what I think is true of myself, and the men and women I know intimately.
|
||||||
|
|
||||||
|
I thought I'd write it down in case it provided any sort of clarity to myself or others down the road.
|
||||||
|
|
||||||
|
## On Men and Me
|
||||||
|
|
||||||
|
I think for myself, as a man, I have learned from my upbringing and my environment an incredible amount of softness and passivity. Deference, kindness, compassion, traits my mother and sisters all taught me.
|
||||||
|
|
||||||
|
None of these are bad traits to have as a man. But I believe I lack many traits you might hope to find in a man.
|
||||||
|
|
||||||
|
One example: Leadership.
|
||||||
|
|
||||||
|
{{<aside caption="my thoughts">}}
|
||||||
|
Well it's rude to tell women what to do, wrong in fact. Especially so in my most intimate relationship. We as men must respect and believe women, because so few do, and so many are pigs. And what do I do when mine and my partner's opinions are at odds? Forcing my way of thinking onto her seems only a stone's throw from forcing my body onto her. So out with what I want.
|
||||||
|
{{</aside>}}
|
||||||
|
|
||||||
|
Effectively, I can never make a decision without weighing my partner's opinion with greater gravity than my own. Which can make me become simply a shadow.
|
||||||
|
|
||||||
|
I must say, my partner would be horrified to know anytime this happens. She is loath to obligate me to anything, even chores around the house. But, alas, this is a self-defeating thought process I have had many times.
|
||||||
|
|
||||||
|
How can a man lead in a world where it is mistaken for coercion? If the leader of our country is anything to go off of, the answer is not to. Don't lead your country, deceive it. Create a fog of lies and a web of phantoms so thick nobody feels the desire to wade through and find the truth. And sit right in the middle of it all. Small, still a boy underneath the pressed suits and cocktails. Limp, ducking under blame and consequence never standing for anything.
|
||||||
|
|
||||||
|
It seems evident to me that the "soft man"[^1] our culture has produced (in me and many of my kin) comes at a cost. We are so extremely tolerant of his despicable behavior. I do not feel that the "wild man"[^2] would put up with such abuse. This is where the soft man is being exploited and taken advantage of in our society.
|
||||||
|
|
||||||
|
I hope to welcome more of my wild man. Taking risks, hunting prey, providing safety and peace for my family.
|
||||||
|
|
||||||
|
I feel that I have much to say about the woman's aspect, and probably have said much to get torched on internet spheres, but I hope to merely express what I am thinking about. If I am flawed, please tell me so. I hope to always open my ears to dissent and hear truth wherever I find it.
|
||||||
|
|
||||||
|
Posting now so there is something rather than nothing :)
|
||||||
|
|
||||||
|
[^1]: Robert Bly covers the idea of the soft man coming to prominence post Industrial Revolution. Men and boys stopped working side-by-side, and instead boys were raised predominantly by their mothers. I find this line of thinking and this shift in boyhood fascinating. I recommend _Iron John_ even though I am only 30 pages in. I'm sure I will have a lot to say here as I continue.
|
||||||
|
|
||||||
|
[^2]: Also a Robert Bly-ism found in Iron John. Essentially the wild man points to the raw, the wet, the untapped core of a man. Not a savage brute, but a shaman, woodsman, or king.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
date: 2026-06-02T23:35:33-06:00
|
||||||
|
description: "In the height of LLM's"
|
||||||
|
# image: "/images/"
|
||||||
|
# image_caption: ""
|
||||||
|
# image_alt: ""
|
||||||
|
lastmod: 2026-06-03T00:08:31-06:00
|
||||||
|
showTableOfContents: false
|
||||||
|
tags: ["ai","aesthetics","philosophy"]
|
||||||
|
title: "The 'Quality' of Ai"
|
||||||
|
type: "post"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
[The blog post that got me to put my words down](https://sinclairtarget.com/blog/2026/06/01/quality-in-the-age-of-slop/).
|
||||||
|
|
||||||
|
[Last year](../25-01-28-ai-hestiation-turning-around) I decided to give AI a shot. Previous to that, I had dabbled with the chat interfaces of Claude and ChatGPT, but
|
||||||
|
had resisted really diving in.
|
||||||
|
|
||||||
|
Just so no one doubts my 'allegiance', or to brand myself into the camp of water-boilers, over that time span I have:
|
||||||
|
|
||||||
|
- Fully vibe-coded a web-rtc baby monitor (flutter app and Go backend)
|
||||||
|
- I've vibe-coded or AI assisted many other projects, but that was the most substantial project that works.
|
||||||
|
- Paid for various subscription services (Claude) for said vibe coding
|
||||||
|
- Used tons of chatting with AI to plan projects, make recipes, research things on my mind
|
||||||
|
- At work, used Opencode to complete many of my work tickets, typically iterating in Plan mode, then executing said plan
|
||||||
|
- No Pr's were ever merged without some minor intervention as far as I recall, there was always a little something here or there that needed tweaking, but it definitely felt very productive.
|
||||||
|
- See-saw'd every week between "this is awesome" and "I'm pretty sure I'm getting dumber and need to stop"
|
||||||
|
|
||||||
|
I feel I have fully submerged myself in the slop swamp. What have I learned?
|
||||||
|
|
||||||
|
## LLM's are Excellent
|
||||||
|
|
||||||
|
Large Language Models are really cool piece of technology. I hope one day there is a fairly and freely (as in libre)
|
||||||
|
trained model to replace search. It is so nice to "describe" what you are searching when you only have the vagaries in
|
||||||
|
your mind. Turning English text prompts into random statistical output is a wildly cool idea.
|
||||||
|
|
||||||
|
_Unfortunately_
|
||||||
|
|
||||||
|
It is so very problematic.
|
||||||
|
|
||||||
|
## LLM's are Evil
|
||||||
|
|
||||||
|
I think my experiment with going all-in with LLM's has taught me just how insidious this technology can be. I believe it
|
||||||
|
has [wreaked havoc in many peoples' lives (Wikipedia)](https://en.wikipedia.org/wiki/Deaths_linked_to_chatbots). I feel
|
||||||
|
that the price a software engineer pays to have a prediction engine produce the code for them is steep. It erodes
|
||||||
|
personal expertise, cheapens the craft and love of the craft, sucks the enjoyment out of work, and steals the ability to make any large maintenance decisions with confidence.
|
||||||
|
|
||||||
|
I feel that my original aversion to these programs was right. The technology inserts itself (by default) in the loop, creating
|
||||||
|
dependence.
|
||||||
|
|
||||||
|
LLM's do not produce Quality, they cannot. So, I think I am done with AI code assistance. Rubber ducking and search still
|
||||||
|
seem very helpful to me, so I'm going to try and move to self-hosted models for those use-cases.
|
||||||
|
|
||||||
|
I want to continue to improve my craft. I want to love this craft. It's why I started down this road nearly ten years
|
||||||
|
ago in college.
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
title: "My First Real Job"
|
|
||||||
date: 2022-12-27T00:47:29-07:00
|
|
||||||
description: 'I take a trip down memory lane, explaining how I got my first real job as a developer.'
|
|
||||||
tags: ["$DAY_JOB", "thoughts"]
|
|
||||||
showTableOfContents: true
|
|
||||||
image: "/images/monochrome-path.jpg"
|
|
||||||
weight: 1
|
|
||||||
type: "post"
|
|
||||||
---
|
|
||||||
|
|
||||||
# What's a 'Real' Job?
|
|
||||||
|
|
||||||
Previous to the 'real' job I got, I had worked a handful of other jobs, some were even related to my field of work, computer wizardry. What made this job different from my previous internships and odd-job contract work, was that this job payed a salary, and a pretty good one for taking a "beginner". I thought I would chronicle the experience for other software developers out there curious of what one random anon's experience was like.
|
|
||||||
|
|
||||||
# The Dating Period
|
|
||||||
|
|
||||||
I dropped out of college, just temporarily, to pursue other interests, and also try the job market, to see what I could catch with, what was in my mind, a decent amount of real world experience. I had worked some sort of software developer job for about two years, and had just recently concluded a summer internship. I could have continued at my internship except for the fact that I wasn't offered a full time position, which is a good story for another time. So, with my 2.5 year college education and nearly equal work experience, I ventured onto Indeed and other similar shark sites for poor saps like myself with naive hopes to throw their resume into the abyss. A month or so into my college semester break, I recieved an email from a recruiting agency with an offer for a full time, remote position paying a salaried amount I thought surely wasn't deserving of a college drop out. To sweeten the pot, the job's tech stack mentioned flutter, which I had spent the last year building a handful of apps and had really come to love the framework. I am compelled to add that I really don't like React or Vue, or really Javascript / npm / webpack / css in any form / webdev in general (typescript is the only thing I do like in that space), so building something in Flutter got me really excited. However, it was one of probably 20 emails I got a week spamming my inbox, I really don't know why I was reading this one, and thought surely I wasn't qualified enough for the position. I clicked a link in the email and was brought to a calendar to schedule a zoom call with the recruiter. I wasn't sure this was even legit, and there was a time slot in 15 minutes that a scheduled. I 'showed up' and sure enough, a recruiter on the other side of the internet was waiting for me. We chatted and I walked him through my work experience, no mention of school. He seemed impressed. A few days pass and he connects me with someone from the company, we set up another zoom meeting, more of a 'get to know you'. That call was interesting. I talked with two guys, which are the only two full time devs in the company, and neither of them had really hired anyone before. The questions they asked really weren't technical in nature, but I also couldn't get a read in the slightest what kind of information they were looking for so I could tailor my answers. 15 minutes passed and the lead dev said something like "Alright, well thats all the questions I could think of..." * trails off... Uhhh -- I desperately thought of a question to ask them about the company to buy myself some time to assess what was happening. It seemed to me that this interview had gone poorly, they didn't see my true awesomeness (because their questions weren't really introspective or technical) so I thought of something I could do to rescue this sinking ship. I learned more about the prooduct they are building and what their jobs are like. They seemed to like that. After 40 minutes, we ended the call and I felt a bit more satisfied with that ending. I believe that was on a Monday, and on Thursday they reached out to setup a lunch the next day. Though the company is remote, both devs are located close to me, so we met over lunch the next day. I was told by the recruiter that I would have a more technical portion of the interview, a paid day working in the office with the team to see if I'm a good fit. Heading to lunch, I expected them to congradulate me for a god interview and schedule the 'technical' part, which is probably the most dreaded step of the software engineer aquisition ritual. We had lunch and just got to know each other better, I discovered both of them align pretty closely with my conservative values, which was a nice bonus. Made conversation easy as there is lots to talk about with new acquantances that you discover to have similar passions. At the end of the hour dinner, with no previous mention of the position, the lead dev says "So we both like you, we want you on the team." This came as a bit of a surprise, where was my gauntlet technical obstacle course? But obviously I was thrilled. The dating phase ended as quickly as it began, and with that, I found myself a married to the corporate system.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Incoming!! Analogies to life as an rpg with points assigned to various traits that may be more genetic / permanent than I make it seem. Character composition is a little more that just stats, buffs / debuffs, and inventory. But only a little...
|
|
||||||
|
|
||||||
# Advice
|
|
||||||
|
|
||||||
How much luck was involved in this process? How much was my modestly stacked resume? I can't say for certain. From my experience, there are a few things within your control that you can try to develop to incerase your odds in the game. I stress *in your control*. I wager 80% of the time when you aren't hired, but you are qualified, its because of forces completely outside your control, hiring freeze, nepotism, off by one error, lazy hr guy, etc, the list has no end.
|
|
||||||
|
|
||||||
## Emotional intelligence
|
|
||||||
|
|
||||||
During this hiring process, they took alot of my expertise and experience by my word, and I can only assume they did so because it seemed that my word was one they could trust. I think experience is great; lots of experience gives you the confidence to answer questions about previous problems you have solved, but experience isn't all, or possibly even, the majority of what gets you hired. I think alot of it comes down to your interpersonal skills, your charisma stat, how chisled your chin looks. Some of that you can't change easily, I think I was born with an above average emotional intelligence. But, I can't stess enough how important it is that you appear confident but not overbearing, and eager to learn and work. With everything today getting people to get as far away from socializing I R L, not having any grounded confidence in their abilities, I get the sense that a lot of qualified individuals just don't sell themselves enough. Want the job, and convince yourself you would be a great addition to the team.
|
|
||||||
|
|
||||||
## Most Important Trait
|
|
||||||
|
|
||||||
If you could do a character stat reset on yourself and spec everything into one category, I think you should go all in on "Ability / Willingness to Learn". I think the sciency term is brain plasticity. But you don't need to spend 250 drupals to activate the stat reset, I think its a habit of thought, and can be a learned behavior. What do you do when you encounter a problem? Do you google it? What if google has no answers? What if the answer is "Read these 20 pages of docs"? Do you read them? Or do you say you are 'blocked' during stand up and let your team lead or that one guy that solves every mystery get to the bottom of it? If you can teach yourself something by gritting your teeth and stumbling along the way, only to realize your solution is magnitudes slower that its supposed to be, I think you are still leaps ahead of the snooty pampered rich mom's son who has kombucha and white granite counter tops at home to retreat to when he gets too 'whelmed with work. A wise woman said to me that grit is the greatest word in the english language, if you got enough of it, there isn't anything you can't do. Wise words I reckon.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Projects"
|
|
||||||
type: "page"
|
|
||||||
---
|
|
||||||
|
|
||||||
Worked on frequently, updated on occassion.
|
|
||||||
|
|
||||||
- ## [Game in Zig & SDL2 ⚡️](/projects/zig-sdl)
|
|
||||||
- ## [Gaining 15 Pounds in 2025 :muscle:]()
|
|
||||||
- ## [Drones :video_game: :helicopter:]()
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Projects"
|
||||||
|
---
|
||||||
|
|
||||||
|
Worked on frequently, updated on occasion.
|
||||||
|
|
||||||
|
As a person with high openness and lower than average follow-thru, this may more just be a catalog of my passing
|
||||||
|
interests and hobbies!
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
date: 2026-02-08T21:49:30-07:00
|
||||||
|
description: "A journey of tweaks to make my super fun Framework 12 Laptop amazing."
|
||||||
|
lastmod: 2026-02-24T00:35:36-07:00
|
||||||
|
showTableOfContents: true
|
||||||
|
type: "projects"
|
||||||
|
title: "Framework12 Tweaks"
|
||||||
|
status: "In Progress"
|
||||||
|
statusColor: "#78b4b4"
|
||||||
|
tags: ["framework", "laptop", "linux", "colemak-dh", "keyboard layout", "nixos", "usability"]
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Credit: Image pulled from Framework's 12" laptop product page
|
||||||
|
|
||||||
|
# Framework 12 Laptop
|
||||||
|
|
||||||
|
I knew I would buy my next laptop from [Framework](https://frame.work/) the first time I saw what they represented -- a radical shift in the consumer market. (I think it was this [Linus Tech Tips video](https://www.youtube.com/watch?v=SYc922ntnKM) a few years ago...)
|
||||||
|
|
||||||
|
It was just a matter of when.
|
||||||
|
|
||||||
|
And then, the [Framework 12](https://frame.work/laptop12) launched.
|
||||||
|
|
||||||
|
I think I have a thing for small computing devices. They are just so cute and quirky and fun. For some reason their small form factor makes me feel like I'll code everywhere. On the train, on the plane, at the park, on a hike. I never actually do, but its the dreaming that makes it exciting probably lol.
|
||||||
|
|
||||||
|
This framework 12 laptop has it all: a smaller (than average) form factor, repairable & upgradable components, durable outer shell, __and__ bold aesthetic (non metal color) colors?! I also kind of saw the, at the time, previous gen (under powered) i3 CPU as a feature. This would be a laptop for *intentional* computing. I couldn't pass it up. I ordered the Sage Framework 12 with the i3 1315U and it has been a blast! After a few months with it, my wife was so allured by its cuteness she demanded the bubblegum colorway :grin:
|
||||||
|
|
||||||
|
## Keyboard Remapping
|
||||||
|
|
||||||
|
This was a bit of journey, but we got there!
|
||||||
|
|
||||||
|
Some people may know that there are some odd balls out there that don't use the default
|
||||||
|
|
||||||
|
### Colemak-DH - My Rant on Keyboard Layouts
|
||||||
|
|
||||||
|
I'm a converted acolyte of the anti-QWERTY alliance.
|
||||||
|
|
||||||
|
I don't hate QWERTY. But it is illogical to persist using the keyboard that was not designed to be used as it is today. In my opinion, it is an inherited sin, and I will not perpetuate its havoc.
|
||||||
|
|
||||||
|
QWERTY was designed for type writers. Keys connected directly to the actuating hammers that would press the inked letters onto paper (or through ink ribbon, etc). Letters that were commonly pressed together were moved apart to prevent jamming. Literally, an anti ergonomic layout.
|
||||||
|
|
||||||
|
I type for my living. The promise made by Colemak (and friends) is reduced strain.
|
||||||
|
|
||||||
|
Look up keyboard layout heatmaps if you are interested. The most commonly pressed keys are put closer to your fingers on home row (who would have thought?). Its like carrying the trash can along as you pick up trash along a path vs going back and treking back and forth between the can and the piece each time (assume trash piece == carrying capacity lol).
|
||||||
|
|
||||||
|
You may think the example is very exaggerated. Yes, its physically a lot more movement. But, I probably type thousands of characters a day. Maybe tens of thousands on the heavier days. So an extra inch stretch * 1000, every day. These add up. And you can feel it.
|
||||||
|
|
||||||
|
I will be honest though, if finger or wrist (especially wrist) pain is a serious problem for you, a layout change provides some benefit, but requires a lot of effort.
|
||||||
|
I had burning wrists after a work day for a while. The thing that fixed it was a split keyboard. Holding both hands close together in front of you to type is akin to being handcuffed. No really! Put your hands out onto your computer keyboard (assuming you have a standard rectangle brick of a board). Your wrists come together, then your hands fan out cock-eyed. The best solution, in my opinion, is to cut your keyboard in half and center them to each hand! Or get the Wave keyboard. They both essentially accomplish the same thing - to keep
|
||||||
|
|
||||||
|
The only real downside to switching is that most of the world hasn't caught on to the genius of alternative layouts.
|
||||||
|
|
||||||
|
- You will need to spend ~2-10 minutes every time you start a new videogame or some piece of software to rebind keys. Sometimes this is kind of painful or impossible.
|
||||||
|
|
||||||
|
- Your keyboard / computer becomes less usable to others.
|
||||||
|
|
||||||
|
- People will expect you to type so much faster now. You may also hope that. Unfortunately, that I think is a separate skill you would have to develop. Proficiency and speed kind of feel like aerobics and resistance training. Yes, one improves the other, but not really directly. You sort of have to lift weights to gain muscle, and to expect to type super fast just because you moved common keys closer to your fingers is missing the point.
|
||||||
|
|
||||||
|
- Most people will think you made a poor life choice and not be very curious why you took time to tank your WPM speed just to confuse your keyboard letters. At least, that's been my experience lol.
|
||||||
|
|
||||||
|
As an aside, my qwerty literacy has not changed. I think I'm slightly slower, but for typing on qwerty keyboards like once a month, if that, you'd think it would atrophy a lot more, but it feels more like riding a bike. The muscle memory kicks in really fast. Its almost spooky lol. But my brain can certainly hold two layouts (or more?) at once.
|
||||||
|
|
||||||
|
### Moving the Keys Physically
|
||||||
|
|
||||||
|
Now, the easy part.
|
||||||
|
|
||||||
|
The Framework 12 shipped with a neat little philips / hex screw and spudger, it even matched the laptop colorway! :heart_eyes: I used the spudger to slide under the north-east corner of the key, and push the tip down into the base whilst lifting the spudger up slowly. This would pop the plastic key off the underlying butterfly switch, and allowed me to rearrange to my heart's content!
|
||||||
|
|
||||||
|
### Reprogramming the Keys
|
||||||
|
|
||||||
|
Now, the busy part.
|
||||||
|
|
||||||
|
I will confess, I happily used AI (Claude Opus 4.5 with Opencode) to figure out how to reprogram my framework's laptop keyboard in the firmware. I don't really like using a keymap setting in the desktop software as it changes external keyboards you plug in as well.
|
||||||
|
|
||||||
|
Turns out, the embedded keyboard controller is programmable, thanks to a [handy ec tool](https://github.com/DHowett/framework-ec)! So, I just had to throw AI at it and give me a big list of key code commands that look like
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ectool raw 0x3E0C d1,d1,b7,b8,w2b # this converts the 'e' key to 'f'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
After an hour, AI could not get it right. Instead, I had AI write a simple script to slowly turn the whole keyboard into semicolons _;_ and I would not the value in the keyboard matrix for each physical key. Apparently no one has documented the framework 12's matrix, and it did not match the 13.
|
||||||
|
|
||||||
|
After doing that and giving AI the correct values, it did it correctly! :roll_eyes:
|
||||||
|
|
||||||
|
It really is painful when AI makes things take way longer... I don't need more reasons to dislike AI, its already a tenuous relationship!
|
||||||
|
|
||||||
|
### Janky Homing Bumps with a Hot Iron
|
||||||
|
|
||||||
|
Finally, the risky part.
|
||||||
|
|
||||||
|
## NixOS - The Greatest OS
|
||||||
|
|
||||||
|
### NNN - The Greatest Desktop Experience
|
||||||
|
|
||||||
|
NixOS + Niri + Noctalia
|
||||||
|
|
||||||
|
These three pieces of software have changed the game for my linux experience.
|
||||||
|
|
||||||
|
Linux, I think, has a usability problem. It can often feel unapproachable and opaque.
|
||||||
|
|
||||||
|
Not that Windows is super user friendly...
|
||||||
|
|
||||||
|
However, that is to me what makes Linux so wonderful. Because you can't as easily assume functionality and get away with it, you actually learn to use your computer.
|
||||||
|
|
||||||
|
Who do you know has ever read their car owners manual substantially? Or at all? But they actually contain really helpful information! Like how to reset the oil change light if you were to change your own oil.
|
||||||
|
|
||||||
|
Anyway, linux is great, and you must learn it. Learning it is half of what makes it great: once you learn how it work, you can tweak it to your heart's content!
|
||||||
|
|
||||||
|
And NixOS makes (some) things so much easier!
|
||||||
|
|
||||||
|
I don't think I've written much on nix, but I've been daily driving it for a while. [My NixOS system repo's](https://git.fosscat.com/n8r/nixos) [first commit](https://git.fosscat.com/n8r/nixos/commit/917ae5f05ec9d62cd40cfdfeca212bd46a06bb43) was 3 years ago! It ended my distro hopping, either from sunk-cost-fallacy or it is just better lol.
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
---
|
---
|
||||||
title: "SDL Game in Zig"
|
title: "SDL Game in Zig"
|
||||||
image: ""
|
image: ""
|
||||||
type: "page"
|
type: "projects"
|
||||||
|
status: "Paused"
|
||||||
|
statusColor: "#ff9800"
|
||||||
|
date: "2025-01-10"
|
||||||
|
lastmod: 2026-02-24T00:35:36-07:00
|
||||||
---
|
---
|
||||||
|
|
||||||
# Zig Game Dev with SDL2
|
# Zig Game Dev with SDL2
|
||||||
|
|
||||||
I'm making a game in Zig!
|
I (was) making a game in Zig! Currently getting back into Zig.
|
||||||
|
|
||||||
Watch me stream it [here](https://twitch.tv/fosscat), or below!
|
Watch me [stream on Twitch](https://twitch.tv/fosscat), or the embed below!
|
||||||
|
|
||||||
Repo for the project is [here](https://git.fosscat.com/n8r/zsdl)
|
Repo for the project is [on my gitea](https://git.fosscat.com/n8r/zsdl)
|
||||||
|
|
||||||
<div id="twitch-embed"></div>
|
<div id="twitch-embed"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: "Terms of Use"
|
||||||
|
date: 2026-03-08
|
||||||
|
draft: false
|
||||||
|
type: "page"
|
||||||
|
showTableOfContents: false
|
||||||
|
---
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
All original content on fosscat.com — including text, images, code samples, and other
|
||||||
|
creative works — is copyright Nate Anderson. All rights reserved unless otherwise noted.
|
||||||
|
|
||||||
|
## Permitted Use
|
||||||
|
|
||||||
|
You are welcome to read, share, and link to any content on this site for personal,
|
||||||
|
non-commercial purposes. Quoting excerpts with proper attribution and a link back to the
|
||||||
|
original is encouraged.
|
||||||
|
|
||||||
|
## Prohibited Use
|
||||||
|
|
||||||
|
The following uses of content from this site are expressly prohibited without prior
|
||||||
|
written consent:
|
||||||
|
|
||||||
|
- **AI and machine learning model training** — using site content as training data,
|
||||||
|
fine-tuning material, or evaluation benchmarks for any AI or ML system
|
||||||
|
- **Data broker resale** — collecting, packaging, or reselling site content as part of a
|
||||||
|
data product or service
|
||||||
|
- **Market research products** — incorporating site content into commercial research
|
||||||
|
reports, databases, or analytics platforms
|
||||||
|
- **Ad-targeting and profiling** — using site content or visitor behavior to build
|
||||||
|
advertising profiles or audience segments
|
||||||
|
- **Any other commercial data exploitation** — automated or manual collection of site
|
||||||
|
content for any for-profit purpose not listed above
|
||||||
|
|
||||||
|
This prohibition applies regardless of the method of collection, whether by web scraper,
|
||||||
|
crawler, API, manual copying, or any other means.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
By accessing and browsing this site, you agree to be bound by these terms. If you do not
|
||||||
|
agree, you should discontinue use of the site immediately.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
If you would like to request an exception to these terms for a specific use case, please
|
||||||
|
contact [legal@fosscat.com](mailto:legal@fosscat.com).
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Today I Learned"
|
|
||||||
type: "til"
|
|
||||||
---
|
|
||||||
|
|
||||||
Anyday I learn something cool, I will write up a brief TIL (Today I Learned) post.
|
|
||||||
|
|
||||||
If they help you, great! But I think having a collection of my learnings will be pretty neat just for myself!
|
|
||||||
|
|
||||||
# My TILs
|
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
People
|
||||||
|
|
||||||
|
| :bowtie: `:bowtie:` | :smile: `:smile:` | :laughing: `:laughing:` |
|
||||||
|
|---|---|---|
|
||||||
|
| :blush: `:blush:` | :smiley: `:smiley:` | :relaxed: `:relaxed:` |
|
||||||
|
| :smirk: `:smirk:` | :heart_eyes: `:heart_eyes:` | :kissing_heart: `:kissing_heart:` |
|
||||||
|
| :kissing_closed_eyes: `:kissing_closed_eyes:` | :flushed: `:flushed:` | :relieved: `:relieved:` |
|
||||||
|
| :satisfied: `:satisfied:` | :grin: `:grin:` | :wink: `:wink:` |
|
||||||
|
| :stuck_out_tongue_winking_eye: `:stuck_out_tongue_winking_eye:` | :stuck_out_tongue_closed_eyes: `:stuck_out_tongue_closed_eyes:` | :grinning: `:grinning:` |
|
||||||
|
| :kissing: `:kissing:` | :kissing_smiling_eyes: `:kissing_smiling_eyes:` | :stuck_out_tongue: `:stuck_out_tongue:` |
|
||||||
|
| :sleeping: `:sleeping:` | :worried: `:worried:` | :frowning: `:frowning:` |
|
||||||
|
| :anguished: `:anguished:` | :open_mouth: `:open_mouth:` | :grimacing: `:grimacing:` |
|
||||||
|
| :confused: `:confused:` | :hushed: `:hushed:` | :expressionless: `:expressionless:` |
|
||||||
|
| :unamused: `:unamused:` | :sweat_smile: `:sweat_smile:` | :sweat: `:sweat:` |
|
||||||
|
| :disappointed_relieved: `:disappointed_relieved:` | :weary: `:weary:` | :pensive: `:pensive:` |
|
||||||
|
| :disappointed: `:disappointed:` | :confounded: `:confounded:` | :fearful: `:fearful:` |
|
||||||
|
| :cold_sweat: `:cold_sweat:` | :persevere: `:persevere:` | :cry: `:cry:` |
|
||||||
|
| :sob: `:sob:` | :joy: `:joy:` | :astonished: `:astonished:` |
|
||||||
|
| :scream: `:scream:` | :neckbeard: `:neckbeard:` | :tired_face: `:tired_face:` |
|
||||||
|
| :angry: `:angry:` | :rage: `:rage:` | :triumph: `:triumph:` |
|
||||||
|
| :sleepy: `:sleepy:` | :yum: `:yum:` | :mask: `:mask:` |
|
||||||
|
| :sunglasses: `:sunglasses:` | :dizzy_face: `:dizzy_face:` | :imp: `:imp:` |
|
||||||
|
| :smiling_imp: `:smiling_imp:` | :neutral_face: `:neutral_face:` | :no_mouth: `:no_mouth:` |
|
||||||
|
| :innocent: `:innocent:` | :alien: `:alien:` | :yellow_heart: `:yellow_heart:` |
|
||||||
|
| :blue_heart: `:blue_heart:` | :purple_heart: `:purple_heart:` | :heart: `:heart:` |
|
||||||
|
| :green_heart: `:green_heart:` | :broken_heart: `:broken_heart:` | :heartbeat: `:heartbeat:` |
|
||||||
|
| :heartpulse: `:heartpulse:` | :two_hearts: `:two_hearts:` | :revolving_hearts: `:revolving_hearts:` |
|
||||||
|
| :cupid: `:cupid:` | :sparkling_heart: `:sparkling_heart:` | :sparkles: `:sparkles:` |
|
||||||
|
| :star: `:star:` | :star2: `:star2:` | :dizzy: `:dizzy:` |
|
||||||
|
| :boom: `:boom:` | :collision: `:collision:` | :anger: `:anger:` |
|
||||||
|
| :exclamation: `:exclamation:` | :question: `:question:` | :grey_exclamation: `:grey_exclamation:` |
|
||||||
|
| :grey_question: `:grey_question:` | :zzz: `:zzz:` | :dash: `:dash:` |
|
||||||
|
| :sweat_drops: `:sweat_drops:` | :notes: `:notes:` | :musical_note: `:musical_note:` |
|
||||||
|
| :fire: `:fire:` | :hankey: `:hankey:` | :poop: `:poop:` |
|
||||||
|
| :shit: `:shit:` | :+1: `:+1:` | :thumbsup: `:thumbsup:` |
|
||||||
|
| :-1: `:-1:` | :thumbsdown: `:thumbsdown:` | :ok_hand: `:ok_hand:` |
|
||||||
|
| :punch: `:punch:` | :facepunch: `:facepunch:` | :fist: `:fist:` |
|
||||||
|
| :v: `:v:` | :wave: `:wave:` | :hand: `:hand:` |
|
||||||
|
| :raised_hand: `:raised_hand:` | :open_hands: `:open_hands:` | :point_up: `:point_up:` |
|
||||||
|
| :point_down: `:point_down:` | :point_left: `:point_left:` | :point_right: `:point_right:` |
|
||||||
|
| :raised_hands: `:raised_hands:` | :pray: `:pray:` | :point_up_2: `:point_up_2:` |
|
||||||
|
| :clap: `:clap:` | :muscle: `:muscle:` | :metal: `:metal:` |
|
||||||
|
| :fu: `:fu:` | :walking: `:walking:` | :runner: `:runner:` |
|
||||||
|
| :running: `:running:` | :couple: `:couple:` | :family: `:family:` |
|
||||||
|
| :two_men_holding_hands: `:two_men_holding_hands:` | :two_women_holding_hands: `:two_women_holding_hands:` | :dancer: `:dancer:` |
|
||||||
|
| :dancers: `:dancers:` | :ok_woman: `:ok_woman:` | :no_good: `:no_good:` |
|
||||||
|
| :information_desk_person: `:information_desk_person:` | :raising_hand: `:raising_hand:` | :bride_with_veil: `:bride_with_veil:` |
|
||||||
|
| :person_with_pouting_face: `:person_with_pouting_face:` | :person_frowning: `:person_frowning:` | :bow: `:bow:` |
|
||||||
|
| :couplekiss: `:couplekiss:` | :couple_with_heart: `:couple_with_heart:` | :massage: `:massage:` |
|
||||||
|
| :haircut: `:haircut:` | :nail_care: `:nail_care:` | :boy: `:boy:` |
|
||||||
|
| :girl: `:girl:` | :woman: `:woman:` | :man: `:man:` |
|
||||||
|
| :baby: `:baby:` | :older_woman: `:older_woman:` | :older_man: `:older_man:` |
|
||||||
|
| :person_with_blond_hair: `:person_with_blond_hair:` | :man_with_gua_pi_mao: `:man_with_gua_pi_mao:` | :man_with_turban: `:man_with_turban:` |
|
||||||
|
| :construction_worker: `:construction_worker:` | :cop: `:cop:` | :angel: `:angel:` |
|
||||||
|
| :princess: `:princess:` | :smiley_cat: `:smiley_cat:` | :smile_cat: `:smile_cat:` |
|
||||||
|
| :heart_eyes_cat: `:heart_eyes_cat:` | :kissing_cat: `:kissing_cat:` | :smirk_cat: `:smirk_cat:` |
|
||||||
|
| :scream_cat: `:scream_cat:` | :crying_cat_face: `:crying_cat_face:` | :joy_cat: `:joy_cat:` |
|
||||||
|
| :pouting_cat: `:pouting_cat:` | :japanese_ogre: `:japanese_ogre:` | :japanese_goblin: `:japanese_goblin:` |
|
||||||
|
| :see_no_evil: `:see_no_evil:` | :hear_no_evil: `:hear_no_evil:` | :speak_no_evil: `:speak_no_evil:` |
|
||||||
|
| :guardsman: `:guardsman:` | :skull: `:skull:` | :feet: `:feet:` |
|
||||||
|
| :lips: `:lips:` | :kiss: `:kiss:` | :droplet: `:droplet:` |
|
||||||
|
| :ear: `:ear:` | :eyes: `:eyes:` | :nose: `:nose:` |
|
||||||
|
| :tongue: `:tongue:` | :love_letter: `:love_letter:` | :bust_in_silhouette: `:bust_in_silhouette:` |
|
||||||
|
| :busts_in_silhouette: `:busts_in_silhouette:` | :speech_balloon: `:speech_balloon:` | :thought_balloon: `:thought_balloon:` |
|
||||||
|
| :feelsgood: `:feelsgood:` | :finnadie: `:finnadie:` | :goberserk: `:goberserk:` |
|
||||||
|
| :godmode: `:godmode:` | :hurtrealbad: `:hurtrealbad:` | :rage1: `:rage1:` |
|
||||||
|
| :rage2: `:rage2:` | :rage3: `:rage3:` | :rage4: `:rage4:` |
|
||||||
|
| :suspect: `:suspect:` | :trollface: `:trollface:` |
|
||||||
|
|
||||||
|
Nature
|
||||||
|
|
||||||
|
| :sunny: `:sunny:` | :umbrella: `:umbrella:` | :cloud: `:cloud:` |
|
||||||
|
|---|---|---|
|
||||||
|
| :snowflake: `:snowflake:` | :snowman: `:snowman:` | :zap: `:zap:` |
|
||||||
|
| :cyclone: `:cyclone:` | :foggy: `:foggy:` | :ocean: `:ocean:` |
|
||||||
|
| :cat: `:cat:` | :dog: `:dog:` | :mouse: `:mouse:` |
|
||||||
|
| :hamster: `:hamster:` | :rabbit: `:rabbit:` | :wolf: `:wolf:` |
|
||||||
|
| :frog: `:frog:` | :tiger: `:tiger:` | :koala: `:koala:` |
|
||||||
|
| :bear: `:bear:` | :pig: `:pig:` | :pig_nose: `:pig_nose:` |
|
||||||
|
| :cow: `:cow:` | :boar: `:boar:` | :monkey_face: `:monkey_face:` |
|
||||||
|
| :monkey: `:monkey:` | :horse: `:horse:` | :racehorse: `:racehorse:` |
|
||||||
|
| :camel: `:camel:` | :sheep: `:sheep:` | :elephant: `:elephant:` |
|
||||||
|
| :panda_face: `:panda_face:` | :snake: `:snake:` | :bird: `:bird:` |
|
||||||
|
| :baby_chick: `:baby_chick:` | :hatched_chick: `:hatched_chick:` | :hatching_chick: `:hatching_chick:` |
|
||||||
|
| :chicken: `:chicken:` | :penguin: `:penguin:` | :turtle: `:turtle:` |
|
||||||
|
| :bug: `:bug:` | :honeybee: `:honeybee:` | :ant: `:ant:` |
|
||||||
|
| :beetle: `:beetle:` | :snail: `:snail:` | :octopus: `:octopus:` |
|
||||||
|
| :tropical_fish: `:tropical_fish:` | :fish: `:fish:` | :whale: `:whale:` |
|
||||||
|
| :whale2: `:whale2:` | :dolphin: `:dolphin:` | :cow2: `:cow2:` |
|
||||||
|
| :ram: `:ram:` | :rat: `:rat:` | :water_buffalo: `:water_buffalo:` |
|
||||||
|
| :tiger2: `:tiger2:` | :rabbit2: `:rabbit2:` | :dragon: `:dragon:` |
|
||||||
|
| :goat: `:goat:` | :rooster: `:rooster:` | :dog2: `:dog2:` |
|
||||||
|
| :pig2: `:pig2:` | :mouse2: `:mouse2:` | :ox: `:ox:` |
|
||||||
|
| :dragon_face: `:dragon_face:` | :blowfish: `:blowfish:` | :crocodile: `:crocodile:` |
|
||||||
|
| :dromedary_camel: `:dromedary_camel:` | :leopard: `:leopard:` | :cat2: `:cat2:` |
|
||||||
|
| :poodle: `:poodle:` | :paw_prints: `:paw_prints:` | :bouquet: `:bouquet:` |
|
||||||
|
| :cherry_blossom: `:cherry_blossom:` | :tulip: `:tulip:` | :four_leaf_clover: `:four_leaf_clover:` |
|
||||||
|
| :rose: `:rose:` | :sunflower: `:sunflower:` | :hibiscus: `:hibiscus:` |
|
||||||
|
| :maple_leaf: `:maple_leaf:` | :leaves: `:leaves:` | :fallen_leaf: `:fallen_leaf:` |
|
||||||
|
| :herb: `:herb:` | :mushroom: `:mushroom:` | :cactus: `:cactus:` |
|
||||||
|
| :palm_tree: `:palm_tree:` | :evergreen_tree: `:evergreen_tree:` | :deciduous_tree: `:deciduous_tree:` |
|
||||||
|
| :chestnut: `:chestnut:` | :seedling: `:seedling:` | :blossom: `:blossom:` |
|
||||||
|
| :ear_of_rice: `:ear_of_rice:` | :shell: `:shell:` | :globe_with_meridians: `:globe_with_meridians:` |
|
||||||
|
| :sun_with_face: `:sun_with_face:` | :full_moon_with_face: `:full_moon_with_face:` | :new_moon_with_face: `:new_moon_with_face:` |
|
||||||
|
| :new_moon: `:new_moon:` | :waxing_crescent_moon: `:waxing_crescent_moon:` | :first_quarter_moon: `:first_quarter_moon:` |
|
||||||
|
| :waxing_gibbous_moon: `:waxing_gibbous_moon:` | :full_moon: `:full_moon:` | :waning_gibbous_moon: `:waning_gibbous_moon:` |
|
||||||
|
| :last_quarter_moon: `:last_quarter_moon:` | :waning_crescent_moon: `:waning_crescent_moon:` | :last_quarter_moon_with_face: `:last_quarter_moon_with_face:` |
|
||||||
|
| :first_quarter_moon_with_face: `:first_quarter_moon_with_face:` | :moon: `:moon:` | :earth_africa: `:earth_africa:` |
|
||||||
|
| :earth_americas: `:earth_americas:` | :earth_asia: `:earth_asia:` | :volcano: `:volcano:` |
|
||||||
|
| :milky_way: `:milky_way:` | :partly_sunny: `:partly_sunny:` | :octocat: `:octocat:` |
|
||||||
|
| :squirrel: `:squirrel:` |
|
||||||
|
|
||||||
|
Objects
|
||||||
|
|
||||||
|
| :bamboo: `:bamboo:` | :gift_heart: `:gift_heart:` | :dolls: `:dolls:` |
|
||||||
|
|---|---|---|
|
||||||
|
| :school_satchel: `:school_satchel:` | :mortar_board: `:mortar_board:` | :flags: `:flags:` |
|
||||||
|
| :fireworks: `:fireworks:` | :sparkler: `:sparkler:` | :wind_chime: `:wind_chime:` |
|
||||||
|
| :rice_scene: `:rice_scene:` | :jack_o_lantern: `:jack_o_lantern:` | :ghost: `:ghost:` |
|
||||||
|
| :santa: `:santa:` | :christmas_tree: `:christmas_tree:` | :gift: `:gift:` |
|
||||||
|
| :bell: `:bell:` | :no_bell: `:no_bell:` | :tanabata_tree: `:tanabata_tree:` |
|
||||||
|
| :tada: `:tada:` | :confetti_ball: `:confetti_ball:` | :balloon: `:balloon:` |
|
||||||
|
| :crystal_ball: `:crystal_ball:` | :cd: `:cd:` | :dvd: `:dvd:` |
|
||||||
|
| :floppy_disk: `:floppy_disk:` | :camera: `:camera:` | :video_camera: `:video_camera:` |
|
||||||
|
| :movie_camera: `:movie_camera:` | :computer: `:computer:` | :tv: `:tv:` |
|
||||||
|
| :iphone: `:iphone:` | :phone: `:phone:` | :telephone: `:telephone:` |
|
||||||
|
| :telephone_receiver: `:telephone_receiver:` | :pager: `:pager:` | :fax: `:fax:` |
|
||||||
|
| :minidisc: `:minidisc:` | :vhs: `:vhs:` | :sound: `:sound:` |
|
||||||
|
| :speaker: `:speaker:` | :mute: `:mute:` | :loudspeaker: `:loudspeaker:` |
|
||||||
|
| :mega: `:mega:` | :hourglass: `:hourglass:` | :hourglass_flowing_sand: `:hourglass_flowing_sand:` |
|
||||||
|
| :alarm_clock: `:alarm_clock:` | :watch: `:watch:` | :radio: `:radio:` |
|
||||||
|
| :satellite: `:satellite:` | :loop: `:loop:` | :mag: `:mag:` |
|
||||||
|
| :mag_right: `:mag_right:` | :unlock: `:unlock:` | :lock: `:lock:` |
|
||||||
|
| :lock_with_ink_pen: `:lock_with_ink_pen:` | :closed_lock_with_key: `:closed_lock_with_key:` | :key: `:key:` |
|
||||||
|
| :bulb: `:bulb:` | :flashlight: `:flashlight:` | :high_brightness: `:high_brightness:` |
|
||||||
|
| :low_brightness: `:low_brightness:` | :electric_plug: `:electric_plug:` | :battery: `:battery:` |
|
||||||
|
| :calling: `:calling:` | :email: `:email:` | :mailbox: `:mailbox:` |
|
||||||
|
| :postbox: `:postbox:` | :bath: `:bath:` | :bathtub: `:bathtub:` |
|
||||||
|
| :shower: `:shower:` | :toilet: `:toilet:` | :wrench: `:wrench:` |
|
||||||
|
| :nut_and_bolt: `:nut_and_bolt:` | :hammer: `:hammer:` | :seat: `:seat:` |
|
||||||
|
| :moneybag: `:moneybag:` | :yen: `:yen:` | :dollar: `:dollar:` |
|
||||||
|
| :pound: `:pound:` | :euro: `:euro:` | :credit_card: `:credit_card:` |
|
||||||
|
| :money_with_wings: `:money_with_wings:` | :e-mail: `:e-mail:` | :inbox_tray: `:inbox_tray:` |
|
||||||
|
| :outbox_tray: `:outbox_tray:` | :envelope: `:envelope:` | :incoming_envelope: `:incoming_envelope:` |
|
||||||
|
| :postal_horn: `:postal_horn:` | :mailbox_closed: `:mailbox_closed:` | :mailbox_with_mail: `:mailbox_with_mail:` |
|
||||||
|
| :mailbox_with_no_mail: `:mailbox_with_no_mail:` | :door: `:door:` | :smoking: `:smoking:` |
|
||||||
|
| :bomb: `:bomb:` | :gun: `:gun:` | :hocho: `:hocho:` |
|
||||||
|
| :pill: `:pill:` | :syringe: `:syringe:` | :page_facing_up: `:page_facing_up:` |
|
||||||
|
| :page_with_curl: `:page_with_curl:` | :bookmark_tabs: `:bookmark_tabs:` | :bar_chart: `:bar_chart:` |
|
||||||
|
| :chart_with_upwards_trend: `:chart_with_upwards_trend:` | :chart_with_downwards_trend: `:chart_with_downwards_trend:` | :scroll: `:scroll:` |
|
||||||
|
| :clipboard: `:clipboard:` | :calendar: `:calendar:` | :date: `:date:` |
|
||||||
|
| :card_index: `:card_index:` | :file_folder: `:file_folder:` | :open_file_folder: `:open_file_folder:` |
|
||||||
|
| :scissors: `:scissors:` | :pushpin: `:pushpin:` | :paperclip: `:paperclip:` |
|
||||||
|
| :black_nib: `:black_nib:` | :pencil2: `:pencil2:` | :straight_ruler: `:straight_ruler:` |
|
||||||
|
| :triangular_ruler: `:triangular_ruler:` | :closed_book: `:closed_book:` | :green_book: `:green_book:` |
|
||||||
|
| :blue_book: `:blue_book:` | :orange_book: `:orange_book:` | :notebook: `:notebook:` |
|
||||||
|
| :notebook_with_decorative_cover: `:notebook_with_decorative_cover:` | :ledger: `:ledger:` | :books: `:books:` |
|
||||||
|
| :bookmark: `:bookmark:` | :name_badge: `:name_badge:` | :microscope: `:microscope:` |
|
||||||
|
| :telescope: `:telescope:` | :newspaper: `:newspaper:` | :football: `:football:` |
|
||||||
|
| :basketball: `:basketball:` | :soccer: `:soccer:` | :baseball: `:baseball:` |
|
||||||
|
| :tennis: `:tennis:` | :8ball: `:8ball:` | :rugby_football: `:rugby_football:` |
|
||||||
|
| :bowling: `:bowling:` | :golf: `:golf:` | :mountain_bicyclist: `:mountain_bicyclist:` |
|
||||||
|
| :bicyclist: `:bicyclist:` | :horse_racing: `:horse_racing:` | :snowboarder: `:snowboarder:` |
|
||||||
|
| :swimmer: `:swimmer:` | :surfer: `:surfer:` | :ski: `:ski:` |
|
||||||
|
| :spades: `:spades:` | :hearts: `:hearts:` | :clubs: `:clubs:` |
|
||||||
|
| :diamonds: `:diamonds:` | :gem: `:gem:` | :ring: `:ring:` |
|
||||||
|
| :trophy: `:trophy:` | :musical_score: `:musical_score:` | :musical_keyboard: `:musical_keyboard:` |
|
||||||
|
| :violin: `:violin:` | :space_invader: `:space_invader:` | :video_game: `:video_game:` |
|
||||||
|
| :black_joker: `:black_joker:` | :flower_playing_cards: `:flower_playing_cards:` | :game_die: `:game_die:` |
|
||||||
|
| :dart: `:dart:` | :mahjong: `:mahjong:` | :clapper: `:clapper:` |
|
||||||
|
| :memo: `:memo:` | :pencil: `:pencil:` | :book: `:book:` |
|
||||||
|
| :art: `:art:` | :microphone: `:microphone:` | :headphones: `:headphones:` |
|
||||||
|
| :trumpet: `:trumpet:` | :saxophone: `:saxophone:` | :guitar: `:guitar:` |
|
||||||
|
| :shoe: `:shoe:` | :sandal: `:sandal:` | :high_heel: `:high_heel:` |
|
||||||
|
| :lipstick: `:lipstick:` | :boot: `:boot:` | :shirt: `:shirt:` |
|
||||||
|
| :tshirt: `:tshirt:` | :necktie: `:necktie:` | :womans_clothes: `:womans_clothes:` |
|
||||||
|
| :dress: `:dress:` | :running_shirt_with_sash: `:running_shirt_with_sash:` | :jeans: `:jeans:` |
|
||||||
|
| :kimono: `:kimono:` | :bikini: `:bikini:` | :ribbon: `:ribbon:` |
|
||||||
|
| :tophat: `:tophat:` | :crown: `:crown:` | :womans_hat: `:womans_hat:` |
|
||||||
|
| :mans_shoe: `:mans_shoe:` | :closed_umbrella: `:closed_umbrella:` | :briefcase: `:briefcase:` |
|
||||||
|
| :handbag: `:handbag:` | :pouch: `:pouch:` | :purse: `:purse:` |
|
||||||
|
| :eyeglasses: `:eyeglasses:` | :fishing_pole_and_fish: `:fishing_pole_and_fish:` | :coffee: `:coffee:` |
|
||||||
|
| :tea: `:tea:` | :sake: `:sake:` | :baby_bottle: `:baby_bottle:` |
|
||||||
|
| :beer: `:beer:` | :beers: `:beers:` | :cocktail: `:cocktail:` |
|
||||||
|
| :tropical_drink: `:tropical_drink:` | :wine_glass: `:wine_glass:` | :fork_and_knife: `:fork_and_knife:` |
|
||||||
|
| :pizza: `:pizza:` | :hamburger: `:hamburger:` | :fries: `:fries:` |
|
||||||
|
| :poultry_leg: `:poultry_leg:` | :meat_on_bone: `:meat_on_bone:` | :spaghetti: `:spaghetti:` |
|
||||||
|
| :curry: `:curry:` | :fried_shrimp: `:fried_shrimp:` | :bento: `:bento:` |
|
||||||
|
| :sushi: `:sushi:` | :fish_cake: `:fish_cake:` | :rice_ball: `:rice_ball:` |
|
||||||
|
| :rice_cracker: `:rice_cracker:` | :rice: `:rice:` | :ramen: `:ramen:` |
|
||||||
|
| :stew: `:stew:` | :oden: `:oden:` | :dango: `:dango:` |
|
||||||
|
| :egg: `:egg:` | :bread: `:bread:` | :doughnut: `:doughnut:` |
|
||||||
|
| :custard: `:custard:` | :icecream: `:icecream:` | :ice_cream: `:ice_cream:` |
|
||||||
|
| :shaved_ice: `:shaved_ice:` | :birthday: `:birthday:` | :cake: `:cake:` |
|
||||||
|
| :cookie: `:cookie:` | :chocolate_bar: `:chocolate_bar:` | :candy: `:candy:` |
|
||||||
|
| :lollipop: `:lollipop:` | :honey_pot: `:honey_pot:` | :apple: `:apple:` |
|
||||||
|
| :green_apple: `:green_apple:` | :tangerine: `:tangerine:` | :lemon: `:lemon:` |
|
||||||
|
| :cherries: `:cherries:` | :grapes: `:grapes:` | :watermelon: `:watermelon:` |
|
||||||
|
| :strawberry: `:strawberry:` | :peach: `:peach:` | :melon: `:melon:` |
|
||||||
|
| :banana: `:banana:` | :pear: `:pear:` | :pineapple: `:pineapple:` |
|
||||||
|
| :sweet_potato: `:sweet_potato:` | :eggplant: `:eggplant:` | :tomato: `:tomato:` |
|
||||||
|
| :corn: `:corn:` |
|
||||||
|
|
||||||
|
Places
|
||||||
|
|
||||||
|
| :house: `:house:` | :house_with_garden: `:house_with_garden:` | :school: `:school:` |
|
||||||
|
|---|---|---|
|
||||||
|
| :office: `:office:` | :post_office: `:post_office:` | :hospital: `:hospital:` |
|
||||||
|
| :bank: `:bank:` | :convenience_store: `:convenience_store:` | :love_hotel: `:love_hotel:` |
|
||||||
|
| :hotel: `:hotel:` | :wedding: `:wedding:` | :church: `:church:` |
|
||||||
|
| :department_store: `:department_store:` | :european_post_office: `:european_post_office:` | :city_sunrise: `:city_sunrise:` |
|
||||||
|
| :city_sunset: `:city_sunset:` | :japanese_castle: `:japanese_castle:` | :european_castle: `:european_castle:` |
|
||||||
|
| :tent: `:tent:` | :factory: `:factory:` | :tokyo_tower: `:tokyo_tower:` |
|
||||||
|
| :japan: `:japan:` | :mount_fuji: `:mount_fuji:` | :sunrise_over_mountains: `:sunrise_over_mountains:` |
|
||||||
|
| :sunrise: `:sunrise:` | :stars: `:stars:` | :statue_of_liberty: `:statue_of_liberty:` |
|
||||||
|
| :bridge_at_night: `:bridge_at_night:` | :carousel_horse: `:carousel_horse:` | :rainbow: `:rainbow:` |
|
||||||
|
| :ferris_wheel: `:ferris_wheel:` | :fountain: `:fountain:` | :roller_coaster: `:roller_coaster:` |
|
||||||
|
| :ship: `:ship:` | :speedboat: `:speedboat:` | :boat: `:boat:` |
|
||||||
|
| :sailboat: `:sailboat:` | :rowboat: `:rowboat:` | :anchor: `:anchor:` |
|
||||||
|
| :rocket: `:rocket:` | :airplane: `:airplane:` | :helicopter: `:helicopter:` |
|
||||||
|
| :steam_locomotive: `:steam_locomotive:` | :tram: `:tram:` | :mountain_railway: `:mountain_railway:` |
|
||||||
|
| :bike: `:bike:` | :aerial_tramway: `:aerial_tramway:` | :suspension_railway: `:suspension_railway:` |
|
||||||
|
| :mountain_cableway: `:mountain_cableway:` | :tractor: `:tractor:` | :blue_car: `:blue_car:` |
|
||||||
|
| :oncoming_automobile: `:oncoming_automobile:` | :car: `:car:` | :red_car: `:red_car:` |
|
||||||
|
| :taxi: `:taxi:` | :oncoming_taxi: `:oncoming_taxi:` | :articulated_lorry: `:articulated_lorry:` |
|
||||||
|
| :bus: `:bus:` | :oncoming_bus: `:oncoming_bus:` | :rotating_light: `:rotating_light:` |
|
||||||
|
| :police_car: `:police_car:` | :oncoming_police_car: `:oncoming_police_car:` | :fire_engine: `:fire_engine:` |
|
||||||
|
| :ambulance: `:ambulance:` | :minibus: `:minibus:` | :truck: `:truck:` |
|
||||||
|
| :train: `:train:` | :station: `:station:` | :train2: `:train2:` |
|
||||||
|
| :bullettrain_front: `:bullettrain_front:` | :bullettrain_side: `:bullettrain_side:` | :light_rail: `:light_rail:` |
|
||||||
|
| :monorail: `:monorail:` | :railway_car: `:railway_car:` | :trolleybus: `:trolleybus:` |
|
||||||
|
| :ticket: `:ticket:` | :fuelpump: `:fuelpump:` | :vertical_traffic_light: `:vertical_traffic_light:` |
|
||||||
|
| :traffic_light: `:traffic_light:` | :warning: `:warning:` | :construction: `:construction:` |
|
||||||
|
| :beginner: `:beginner:` | :atm: `:atm:` | :slot_machine: `:slot_machine:` |
|
||||||
|
| :busstop: `:busstop:` | :barber: `:barber:` | :hotsprings: `:hotsprings:` |
|
||||||
|
| :checkered_flag: `:checkered_flag:` | :crossed_flags: `:crossed_flags:` | :izakaya_lantern: `:izakaya_lantern:` |
|
||||||
|
| :moyai: `:moyai:` | :circus_tent: `:circus_tent:` | :performing_arts: `:performing_arts:` |
|
||||||
|
| :round_pushpin: `:round_pushpin:` | :triangular_flag_on_post: `:triangular_flag_on_post:` | :jp: `:jp:` |
|
||||||
|
| :kr: `:kr:` | :cn: `:cn:` | :us: `:us:` |
|
||||||
|
| :fr: `:fr:` | :es: `:es:` | :it: `:it:` |
|
||||||
|
| :ru: `:ru:` | :gb: `:gb:` | :uk: `:uk:` |
|
||||||
|
| :de: `:de:` |
|
||||||
|
|
||||||
|
Symbols
|
||||||
|
|
||||||
|
| :one: `:one:` | :two: `:two:` | :three: `:three:` |
|
||||||
|
|---|---|---|
|
||||||
|
| :four: `:four:` | :five: `:five:` | :six: `:six:` |
|
||||||
|
| :seven: `:seven:` | :eight: `:eight:` | :nine: `:nine:` |
|
||||||
|
| :keycap_ten: `:keycap_ten:` | :1234: `:1234:` | :zero: `:zero:` |
|
||||||
|
| :hash: `:hash:` | :symbols: `:symbols:` | :arrow_backward: `:arrow_backward:` |
|
||||||
|
| :arrow_down: `:arrow_down:` | :arrow_forward: `:arrow_forward:` | :arrow_left: `:arrow_left:` |
|
||||||
|
| :capital_abcd: `:capital_abcd:` | :abcd: `:abcd:` | :abc: `:abc:` |
|
||||||
|
| :arrow_lower_left: `:arrow_lower_left:` | :arrow_lower_right: `:arrow_lower_right:` | :arrow_right: `:arrow_right:` |
|
||||||
|
| :arrow_up: `:arrow_up:` | :arrow_upper_left: `:arrow_upper_left:` | :arrow_upper_right: `:arrow_upper_right:` |
|
||||||
|
| :arrow_double_down: `:arrow_double_down:` | :arrow_double_up: `:arrow_double_up:` | :arrow_down_small: `:arrow_down_small:` |
|
||||||
|
| :arrow_heading_down: `:arrow_heading_down:` | :arrow_heading_up: `:arrow_heading_up:` | :leftwards_arrow_with_hook: `:leftwards_arrow_with_hook:` |
|
||||||
|
| :arrow_right_hook: `:arrow_right_hook:` | :left_right_arrow: `:left_right_arrow:` | :arrow_up_down: `:arrow_up_down:` |
|
||||||
|
| :arrow_up_small: `:arrow_up_small:` | :arrows_clockwise: `:arrows_clockwise:` | :arrows_counterclockwise: `:arrows_counterclockwise:` |
|
||||||
|
| :rewind: `:rewind:` | :fast_forward: `:fast_forward:` | :information_source: `:information_source:` |
|
||||||
|
| :ok: `:ok:` | :twisted_rightwards_arrows: `:twisted_rightwards_arrows:` | :repeat: `:repeat:` |
|
||||||
|
| :repeat_one: `:repeat_one:` | :new: `:new:` | :top: `:top:` |
|
||||||
|
| :up: `:up:` | :cool: `:cool:` | :free: `:free:` |
|
||||||
|
| :ng: `:ng:` | :cinema: `:cinema:` | :koko: `:koko:` |
|
||||||
|
| :signal_strength: `:signal_strength:` | :u5272: `:u5272:` | :u5408: `:u5408:` |
|
||||||
|
| :u55b6: `:u55b6:` | :u6307: `:u6307:` | :u6708: `:u6708:` |
|
||||||
|
| :u6709: `:u6709:` | :u6e80: `:u6e80:` | :u7121: `:u7121:` |
|
||||||
|
| :u7533: `:u7533:` | :u7a7a: `:u7a7a:` | :u7981: `:u7981:` |
|
||||||
|
| :sa: `:sa:` | :restroom: `:restroom:` | :mens: `:mens:` |
|
||||||
|
| :womens: `:womens:` | :baby_symbol: `:baby_symbol:` | :no_smoking: `:no_smoking:` |
|
||||||
|
| :parking: `:parking:` | :wheelchair: `:wheelchair:` | :metro: `:metro:` |
|
||||||
|
| :baggage_claim: `:baggage_claim:` | :accept: `:accept:` | :wc: `:wc:` |
|
||||||
|
| :potable_water: `:potable_water:` | :put_litter_in_its_place: `:put_litter_in_its_place:` | :secret: `:secret:` |
|
||||||
|
| :congratulations: `:congratulations:` | :m: `:m:` | :passport_control: `:passport_control:` |
|
||||||
|
| :left_luggage: `:left_luggage:` | :customs: `:customs:` | :ideograph_advantage: `:ideograph_advantage:` |
|
||||||
|
| :cl: `:cl:` | :sos: `:sos:` | :id: `:id:` |
|
||||||
|
| :no_entry_sign: `:no_entry_sign:` | :underage: `:underage:` | :no_mobile_phones: `:no_mobile_phones:` |
|
||||||
|
| :do_not_litter: `:do_not_litter:` | :non-potable_water: `:non-potable_water:` | :no_bicycles: `:no_bicycles:` |
|
||||||
|
| :no_pedestrians: `:no_pedestrians:` | :children_crossing: `:children_crossing:` | :no_entry: `:no_entry:` |
|
||||||
|
| :eight_spoked_asterisk: `:eight_spoked_asterisk:` | :eight_pointed_black_star: `:eight_pointed_black_star:` | :heart_decoration: `:heart_decoration:` |
|
||||||
|
| :vs: `:vs:` | :vibration_mode: `:vibration_mode:` | :mobile_phone_off: `:mobile_phone_off:` |
|
||||||
|
| :chart: `:chart:` | :currency_exchange: `:currency_exchange:` | :aries: `:aries:` |
|
||||||
|
| :taurus: `:taurus:` | :gemini: `:gemini:` | :cancer: `:cancer:` |
|
||||||
|
| :leo: `:leo:` | :virgo: `:virgo:` | :libra: `:libra:` |
|
||||||
|
| :scorpius: `:scorpius:` | :sagittarius: `:sagittarius:` | :capricorn: `:capricorn:` |
|
||||||
|
| :aquarius: `:aquarius:` | :pisces: `:pisces:` | :ophiuchus: `:ophiuchus:` |
|
||||||
|
| :six_pointed_star: `:six_pointed_star:` | :negative_squared_cross_mark: `:negative_squared_cross_mark:` | :a: `:a:` |
|
||||||
|
| :b: `:b:` | :ab: `:ab:` | :o2: `:o2:` |
|
||||||
|
| :diamond_shape_with_a_dot_inside: `:diamond_shape_with_a_dot_inside:` | :recycle: `:recycle:` | :end: `:end:` |
|
||||||
|
| :on: `:on:` | :soon: `:soon:` | :clock1: `:clock1:` |
|
||||||
|
| :clock130: `:clock130:` | :clock10: `:clock10:` | :clock1030: `:clock1030:` |
|
||||||
|
| :clock11: `:clock11:` | :clock1130: `:clock1130:` | :clock12: `:clock12:` |
|
||||||
|
| :clock1230: `:clock1230:` | :clock2: `:clock2:` | :clock230: `:clock230:` |
|
||||||
|
| :clock3: `:clock3:` | :clock330: `:clock330:` | :clock4: `:clock4:` |
|
||||||
|
| :clock430: `:clock430:` | :clock5: `:clock5:` | :clock530: `:clock530:` |
|
||||||
|
| :clock6: `:clock6:` | :clock630: `:clock630:` | :clock7: `:clock7:` |
|
||||||
|
| :clock730: `:clock730:` | :clock8: `:clock8:` | :clock830: `:clock830:` |
|
||||||
|
| :clock9: `:clock9:` | :clock930: `:clock930:` | :heavy_dollar_sign: `:heavy_dollar_sign:` |
|
||||||
|
| :copyright: `:copyright:` | :registered: `:registered:` | :tm: `:tm:` |
|
||||||
|
| :x: `:x:` | :heavy_exclamation_mark: `:heavy_exclamation_mark:` | :bangbang: `:bangbang:` |
|
||||||
|
| :interrobang: `:interrobang:` | :o: `:o:` | :heavy_multiplication_x: `:heavy_multiplication_x:` |
|
||||||
|
| :heavy_plus_sign: `:heavy_plus_sign:` | :heavy_minus_sign: `:heavy_minus_sign:` | :heavy_division_sign: `:heavy_division_sign:` |
|
||||||
|
| :white_flower: `:white_flower:` | :100: `:100:` | :heavy_check_mark: `:heavy_check_mark:` |
|
||||||
|
| :ballot_box_with_check: `:ballot_box_with_check:` | :radio_button: `:radio_button:` | :link: `:link:` |
|
||||||
|
| :curly_loop: `:curly_loop:` | :wavy_dash: `:wavy_dash:` | :part_alternation_mark: `:part_alternation_mark:` |
|
||||||
|
| :trident: `:trident:` | :black_square: `:black_square:` | :white_square: `:white_square:` |
|
||||||
|
| :white_check_mark: `:white_check_mark:` | :black_square_button: `:black_square_button:` | :white_square_button: `:white_square_button:` |
|
||||||
|
| :black_circle: `:black_circle:` | :white_circle: `:white_circle:` | :red_circle: `:red_circle:` |
|
||||||
|
| :large_blue_circle: `:large_blue_circle:` | :large_blue_diamond: `:large_blue_diamond:` | :large_orange_diamond: `:large_orange_diamond:` |
|
||||||
|
| :small_blue_diamond: `:small_blue_diamond:` | :small_orange_diamond: `:small_orange_diamond:` | :small_red_triangle: `:small_red_triangle:` |
|
||||||
|
| :small_red_triangle_down: `:small_red_triangle_down:` | :shipit: `:shipit:` |
|
||||||
|
|
||||||
@@ -11,6 +11,12 @@
|
|||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in
|
in
|
||||||
|
let
|
||||||
|
tagCheckPython = pkgs.python313.withPackages (ps: [
|
||||||
|
ps.spacy
|
||||||
|
ps.spacy-models.en_core_web_lg
|
||||||
|
]);
|
||||||
|
in
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
@@ -20,6 +26,13 @@
|
|||||||
markdownlint-cli
|
markdownlint-cli
|
||||||
aspell
|
aspell
|
||||||
aspellDicts.en
|
aspellDicts.en
|
||||||
|
fzf # Interactive spell check and tag selection
|
||||||
|
tagCheckPython # Python + spaCy for semantic tag similarity checker
|
||||||
|
|
||||||
|
# Image optimization tools (used by scripts/optimize-images.sh)
|
||||||
|
perl538Packages.ImageExifTool # EXIF metadata reading/stripping
|
||||||
|
imagemagick # Resize, auto-orient, get dimensions
|
||||||
|
libwebp # cwebp for WebP conversion
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<h3>{{ .Title }}</h3>
|
<h3>{{ .Title }}</h3>
|
||||||
<p>{{ .Date | time.Format ":date_medium" }}</p>
|
<p>{{ .Date | time.Format ":date_medium" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</br>
|
|
||||||
<p>{{ .Summary }}</p>
|
|
||||||
</a>
|
</a>
|
||||||
|
<p>{{ .Params.Description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</br>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end}}
|
{{ end}}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<section class="home-about">
|
||||||
|
<div class="avatar">
|
||||||
|
{{ if isset .Site.Params "avatarurl" }}
|
||||||
|
<img class={{ .Site.Params.AvatarSize | default "size-m" }} src='{{ .Scratch.Get "avatarImgSrc" }}'
|
||||||
|
alt="{{ .Site.Params.AvatarAltText|default " avatar" }}">
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>{{ .Site.Title }}</h1>
|
||||||
|
{{ if isset .Site.Params "description" }}
|
||||||
|
<h2>{{ .Site.Params.Description }}</h2>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="flex-break"></div>
|
||||||
|
|
||||||
|
{{ if isset .Site.Params "socialicons" }}
|
||||||
|
<div class="gk-social-icons">
|
||||||
|
<ul class="gk-social-icons-list">
|
||||||
|
{{ range .Site.Params.SocialIcons }}
|
||||||
|
<li class="gk-social-icon">
|
||||||
|
<a href="{{ .url }}" {{ if .rel }}rel="{{ .rel }}" {{ end }} aria-label="Learn more on {{ .name }}">
|
||||||
|
<img class="svg-inject" src="{{ relURL " svg/icons/" }}{{ .name | lower }}.svg" alt="">
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if os.FileExists "index-about.md" }}
|
||||||
|
<div class="markdown-content">
|
||||||
|
{{ readFile "index-about.md" | markdownify }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if isset .Site.Params "showpostsonhomepage" }}
|
||||||
|
|
||||||
|
<div class="home-grid">
|
||||||
|
<div class="home-posts-column home-posts list-posts">
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
|
||||||
|
{{ $posts := where .Site.Pages "Params.type" "post" }}
|
||||||
|
|
||||||
|
{{ if eq .Site.Params.ShowPostsOnHomePage "popular" }}
|
||||||
|
{{ range $posts.ByWeight | first (or .Site.Params.NumberPostsOnHomePage 4) }}
|
||||||
|
{{- partial "list-posts.html" . -}}
|
||||||
|
{{ end }}
|
||||||
|
{{ else if eq .Site.Params.ShowPostsOnHomePage "recent" }}
|
||||||
|
{{ range $posts.ByDate.Reverse | first (or .Site.Params.NumberPostsOnHomePage 4) }}
|
||||||
|
{{- partial "list-posts.html" . -}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
<a class="view-all" href="/posts/">View all posts →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ $projects := where .Site.RegularPages "Params.type" "projects" }}
|
||||||
|
{{ if gt (len $projects) 0 }}
|
||||||
|
<div class="home-projects-card">
|
||||||
|
<h2>Project Updates</h2>
|
||||||
|
{{ range $projects.ByLastmod.Reverse | first (or $.Site.Params.NumberProjectsOnHomePage 3) }}
|
||||||
|
{{ $project := . }}
|
||||||
|
<div class="home-project-entry">
|
||||||
|
<a href="{{ .Permalink }}">{{ .Title }}</a>
|
||||||
|
<div class="home-project-meta">
|
||||||
|
{{ with .Params.status }}
|
||||||
|
{{ $color := $project.Params.statusColor | default "#9e9e9e" }}
|
||||||
|
<span class="home-project-status"
|
||||||
|
style="color: {{ $color }}; border: 1px solid {{ $color }}; background-color: {{ $color }}20;">{{ . }}</span>
|
||||||
|
·
|
||||||
|
{{ end }}
|
||||||
|
Updated {{ $project.Lastmod.Format "Jan 2, 2006" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<a class="view-all" href="/projects/">View all projects →</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-connect">
|
||||||
|
<p>
|
||||||
|
Subscribe via <a href="{{ "/index.xml" | absURL }}">RSS</a><a href="https://aboutfeeds.com/" class="rss-info" title="What is RSS?" aria-label="What is RSS?">?</a>,
|
||||||
|
send feedback to <a href="mailto:feedback@fosscat.com">feedback@fosscat.com</a>,
|
||||||
|
or check out the <a href="https://git.fosscat.com/n8r/fosscat-site">source code</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="post container">
|
||||||
|
|
||||||
|
<div class="post-header-section">
|
||||||
|
<h1>{{ .Title | markdownify }}</h1>
|
||||||
|
|
||||||
|
{{ if ne .File.Path "projects.md" }}
|
||||||
|
<p class="post-date">
|
||||||
|
{{ if eq .Date .Lastmod }}
|
||||||
|
{{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Date}}
|
||||||
|
{{ end }}
|
||||||
|
{{ if lt .Date .Lastmod }}
|
||||||
|
Updated {{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Lastmod }}
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-content">
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
<!-- Back to top button -->
|
||||||
|
{{ if .Site.Params.ShowBackToTopButton }}
|
||||||
|
{{ partial "back-to-top.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<style>
|
||||||
|
.post-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image img {
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid var(--light-secondary-color);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-caption p {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile - small images fit better */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet - medium size */
|
||||||
|
@media (min-width: 481px) and (max-width: 768px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop - larger but not overwhelming */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 60%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="post container">
|
||||||
|
<div class="post-header-section">
|
||||||
|
<h1>{{ .Title | markdownify }}</h1>
|
||||||
|
|
||||||
|
{{/* Determine whether to display the date & description based on tags */}}
|
||||||
|
|
||||||
|
{{ $displayDate := true }}
|
||||||
|
{{ $displayDescription := true }}
|
||||||
|
{{ $postTags := or .Params.Tags slice }}
|
||||||
|
{{ $hiddenTags := or .Site.Params.Hidden.Tags slice }}
|
||||||
|
{{ $tagsHidePostDate := or .Site.Params.Hidden.TagsPostDate slice }}
|
||||||
|
{{ $tagsHidePostDescription := or .Site.Params.Hidden.TagsPostDescription slice }}
|
||||||
|
|
||||||
|
{{ if gt ($tagsHidePostDate | intersect $postTags | len) 0 }}
|
||||||
|
{{ $displayDate = false }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if gt ($tagsHidePostDescription | intersect $postTags | len) 0 }}
|
||||||
|
{{ $displayDescription = false }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if $displayDescription }}
|
||||||
|
<small role="doc-subtitle">{{ .Description }}</small>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if $displayDate }}
|
||||||
|
<p class="post-date">{{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Date}}
|
||||||
|
{{ if lt .Date .Lastmod }} | Updated {{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Lastmod }}{{ end }}
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<ul class="post-tags">
|
||||||
|
{{ range $tag := $postTags }}
|
||||||
|
{{ if not (in $hiddenTags $tag) }}
|
||||||
|
<li class="post-tag"><a href="{{ "tags/" | absLangURL }}{{ . | urlize }}">{{ . }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Display image if present in front matter */}}
|
||||||
|
{{ if .Params.image }}
|
||||||
|
<div class="post-image">
|
||||||
|
<img src="{{ .Params.image | absURL }}" alt="{{ .Params.image_alt | default .Title }}">
|
||||||
|
</div>
|
||||||
|
{{ with .Params.image_caption }}
|
||||||
|
<div class="image-caption">
|
||||||
|
<p>{{ . }}</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="post-content">
|
||||||
|
{{ .Content }}
|
||||||
|
{{ if .Site.Config.Services.Disqus.Shortname }}
|
||||||
|
<div class="post-comments">
|
||||||
|
{{ template "_internal/disqus.html" . }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prev-next">
|
||||||
|
{{ if eq .Site.Params.TogglePreviousAndNextButtons "true" }}
|
||||||
|
{{ if or .PrevInSection .NextInSection }}
|
||||||
|
{{ partial "prev-next.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Back to top button -->
|
||||||
|
{{ if .Site.Params.ShowBackToTopButton }}
|
||||||
|
{{ partial "back-to-top.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Site.Params.CustomCommentHTML }}
|
||||||
|
<div id="comments">
|
||||||
|
{{ .Site.Params.CustomCommentHTML | safeHTML }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<style>
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.project-date {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-secondary-color);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-title">{{ .Title }}</h1>
|
||||||
|
|
||||||
|
{{ .Content }}
|
||||||
|
|
||||||
|
<div class="posts-list">
|
||||||
|
{{ range .Pages }}
|
||||||
|
<article class="post-entry">
|
||||||
|
<header class="entry-header">
|
||||||
|
<h3 class="entry-title">
|
||||||
|
<a href="{{ .Permalink }}" rel="bookmark">{{ .Title }}</a>
|
||||||
|
{{ $page := . }}
|
||||||
|
{{ with .Params.status }}
|
||||||
|
{{ $color := $page.Params.statusColor | default "#9e9e9e" }}
|
||||||
|
<span class="status-badge" style="color: {{ $color }}; border: 1px solid {{ $color }}; background-color: {{ $color }}20;">{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<span class="project-date">Updated {{ .Lastmod.Format "Jan 2, 2006" }}</span>
|
||||||
|
</h3>
|
||||||
|
</header>
|
||||||
|
{{ with .Description }}
|
||||||
|
<div class="entry-summary">{{ . }}</div>
|
||||||
|
{{ end }}
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if eq (len .Pages) 0 }}
|
||||||
|
<p>No projects yet.</p>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image img {
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid var(--light-secondary-color);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-caption p {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 481px) and (max-width: 768px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.post-image img {
|
||||||
|
width: 60%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="post container">
|
||||||
|
<div class="post-header-section">
|
||||||
|
<h1>
|
||||||
|
{{ .Title | markdownify }}
|
||||||
|
{{ with .Params.status }}
|
||||||
|
{{ $color := $.Params.statusColor | default "#9e9e9e" }}
|
||||||
|
<span class="status-badge" style="color: {{ $color }}; border: 1px solid {{ $color }}; background-color: {{ $color }}20;">{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{{ with .Description }}
|
||||||
|
<small role="doc-subtitle">{{ . }}</small>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<p class="post-date">
|
||||||
|
Started {{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Date }}
|
||||||
|
{{ if lt .Date .Lastmod }} | Updated {{ dateFormat (or .Site.Params.dateFormat "January 2, 2006") .Lastmod }}{{ end }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="post-tags">
|
||||||
|
{{ range .Params.Tags }}
|
||||||
|
<li class="post-tag"><a href="{{ "tags/" | absLangURL }}{{ . | urlize }}">{{ . }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .Params.image }}
|
||||||
|
<div class="post-image">
|
||||||
|
<img src="{{ .Params.image | absURL }}" alt="{{ .Params.image_alt | default .Title }}">
|
||||||
|
</div>
|
||||||
|
<div class="image-caption">
|
||||||
|
<p>{{ .Params.image_caption }}</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="post-content">
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prev-next">
|
||||||
|
{{ if eq .Site.Params.TogglePreviousAndNextButtons "true" }}
|
||||||
|
{{ if or .PrevInSection .NextInSection }}
|
||||||
|
{{ partial "prev-next.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .Site.Params.ShowBackToTopButton }}
|
||||||
|
{{ partial "back-to-top.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- partial "toc.html" . -}}
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{{ define "main" }}
|
|
||||||
<div class="container">
|
|
||||||
<h1>Today I Learned</h1>
|
|
||||||
|
|
||||||
<p>Debug Info:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Total Pages: {{ len .Pages }}</li>
|
|
||||||
<li>Is Section: {{ .IsSection }}</li>
|
|
||||||
<li>Section Name: {{ .Section }}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{{ range .Pages }}
|
|
||||||
<article>
|
|
||||||
<h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
|
|
||||||
<p>Date: {{ .Date.Format "January 2, 2006" }}</p>
|
|
||||||
{{ with .Description }}
|
|
||||||
<p>{{ . }}</p>
|
|
||||||
{{ end }}
|
|
||||||
</article>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if eq (len .Pages) 0 }}
|
|
||||||
<p>No TIL posts found.</p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="aside-block">
|
||||||
|
{{ with .Get "caption" }}
|
||||||
|
<span class="aside-caption">{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<div class="aside-content">
|
||||||
|
{{ .Inner | strings.TrimSpace | .Page.RenderString }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{{ define "main" }}
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="page-title">{{ .Title }}</h1>
|
|
||||||
|
|
||||||
{{ .Content }}
|
|
||||||
|
|
||||||
<div class="posts-list">
|
|
||||||
{{ range .Pages }}
|
|
||||||
<article class="post-entry">
|
|
||||||
<header class="entry-header">
|
|
||||||
<h2 class="entry-title">
|
|
||||||
<a href="{{ .Permalink }}" rel="bookmark">
|
|
||||||
{{ .Date.Format "Jan 2, 2006" }} - {{ .Title }}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{{ with .Description }}
|
|
||||||
<div class="entry-summary">
|
|
||||||
{{ . }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</article>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if eq (len .Pages) 0 }}
|
|
||||||
<p>No TIL posts yet.</p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{{ define "main" }}
|
|
||||||
|
|
||||||
{{- partial "post.html" . -}}
|
|
||||||
{{- partial "toc.html" . -}}
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to check if a URL is reachable
|
||||||
|
check_url() {
|
||||||
|
local url="$1"
|
||||||
|
local file="$2"
|
||||||
|
local line_num="$3"
|
||||||
|
|
||||||
|
# Skip relative URLs and internal anchors
|
||||||
|
if [[ "$url" =~ ^(#|/[^/]|\./) ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip mailto and other non-http protocols
|
||||||
|
if [[ "$url" =~ ^(mailto:|ftp:|file:|javascript:) ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For URLs without protocol, assume https
|
||||||
|
if [[ ! "$url" =~ ^https?:// ]]; then
|
||||||
|
url="https://$url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use curl to check the URL with a reasonable timeout
|
||||||
|
# Follow redirects, but limit to 5 redirects to prevent infinite loops
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
--max-time 10 \
|
||||||
|
--max-redirs 5 \
|
||||||
|
--retry 1 \
|
||||||
|
--user-agent "Mozilla/5.0 (compatible; FossCat Link Checker)" \
|
||||||
|
"$url" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
# Consider 2xx and 3xx status codes as success
|
||||||
|
if [[ "$http_code" =~ ^[23][0-9][0-9]$ ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Dead link in $file:$line_num${NC}"
|
||||||
|
echo -e " URL: $url"
|
||||||
|
echo -e " Status: $http_code"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to extract and check links from a markdown file
|
||||||
|
check_markdown_links() {
|
||||||
|
local file="$1"
|
||||||
|
local failed=0
|
||||||
|
|
||||||
|
echo "Checking links in $file..."
|
||||||
|
|
||||||
|
# Use a simpler approach that works with basic grep
|
||||||
|
# Extract URLs from markdown links [text](url) and images 
|
||||||
|
local urls=$(grep -o '\]([^)]*)' "$file" | sed 's/](\([^)]*\)).*/\1/' | grep -v '^$')
|
||||||
|
|
||||||
|
if [ -n "$urls" ]; then
|
||||||
|
while IFS= read -r url; do
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
# Get line number where this URL appears
|
||||||
|
local line_num=$(grep -n "$url" "$file" | head -1 | cut -d: -f1)
|
||||||
|
if ! check_url "$url" "$file" "$line_num"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$urls"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check for reference-style links [ref]: url
|
||||||
|
local ref_urls=$(grep -o '^\[.*\]: *[^ ]*' "$file" | sed 's/^\[.*\]: *//' | grep -v '^$')
|
||||||
|
|
||||||
|
if [ -n "$ref_urls" ]; then
|
||||||
|
while IFS= read -r url; do
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
local line_num=$(grep -n "$url" "$file" | head -1 | cut -d: -f1)
|
||||||
|
if ! check_url "$url" "$file" "$line_num"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$ref_urls"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $failed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
main() {
|
||||||
|
local files_to_check=()
|
||||||
|
local failed_files=0
|
||||||
|
local total_files=0
|
||||||
|
|
||||||
|
# If arguments provided, check those files, otherwise check all markdown files
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
files_to_check=("$@")
|
||||||
|
else
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
files_to_check+=("$file")
|
||||||
|
done < <(find content -name "*.md" -type f -print0)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#files_to_check[@]} -eq 0 ]; then
|
||||||
|
echo -e "${YELLOW}No markdown files to check.${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}🔗 Checking links in ${#files_to_check[@]} markdown file(s)...${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
for file in "${files_to_check[@]}"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
total_files=$((total_files + 1))
|
||||||
|
if ! check_markdown_links "$file"; then
|
||||||
|
failed_files=$((failed_files + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ File not found: $file${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ $failed_files -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ All links are working! Checked $total_files file(s).${NC}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Found broken links in $failed_files file(s) out of $total_files checked.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if curl is available
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ curl is required but not installed.${NC}"
|
||||||
|
echo "Please install curl to use the link checker."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if basic grep and sed are available
|
||||||
|
if ! command -v sed &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ sed is required but not installed.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
check-tags.py — Semantic tag similarity checker for Hugo content
|
||||||
|
|
||||||
|
Compares tags in staged files against all existing tags in the site.
|
||||||
|
Warns and blocks commit when a new tag is semantically similar to an existing one.
|
||||||
|
|
||||||
|
Uses spaCy word vectors (en_core_web_lg) for cosine similarity — catches
|
||||||
|
conceptual matches like "parenting" ≈ "fatherhood" while ignoring unrelated
|
||||||
|
words that happen to share letters like "dogs" vs "daily".
|
||||||
|
|
||||||
|
Fallback: if spaCy is unavailable, uses conservative edit-distance checks only.
|
||||||
|
|
||||||
|
Skip with: SKIP_TAG_CHECK=1 git commit
|
||||||
|
|
||||||
|
Usage: check-tags.py <file1.md> [file2.md ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# --- Colors ---
|
||||||
|
RED = "\033[0;31m"
|
||||||
|
YELLOW = "\033[0;33m"
|
||||||
|
GREEN = "\033[0;32m"
|
||||||
|
CYAN = "\033[0;36m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
NC = "\033[0m"
|
||||||
|
|
||||||
|
# Cosine similarity threshold for word vectors (0-1).
|
||||||
|
# 0.65 catches morphological variants (parenting/parenthood) and synonyms
|
||||||
|
# (cannabis/marijuana) while avoiding unrelated words. Tuned for short blog tags.
|
||||||
|
SEMANTIC_THRESHOLD = 0.65
|
||||||
|
|
||||||
|
# Edit-distance threshold — only used as a typo catcher alongside semantics.
|
||||||
|
# 0.85 is very conservative: catches "kubernetse" vs "kubernetes" but not
|
||||||
|
# "dogs" vs "daily" (which scores ~0.40).
|
||||||
|
TYPO_THRESHOLD = 0.85
|
||||||
|
|
||||||
|
# Substring match: shorter tag must be at least this many chars
|
||||||
|
# and cover at least this fraction of the longer tag.
|
||||||
|
SUBSTRING_MIN_LEN = 5
|
||||||
|
SUBSTRING_MIN_RATIO = 0.6
|
||||||
|
|
||||||
|
# --- spaCy setup (lazy, with graceful fallback) ---
|
||||||
|
_nlp = None
|
||||||
|
_spacy_available = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_spacy():
|
||||||
|
"""Load spaCy model once. Returns (nlp, True) or (None, False)."""
|
||||||
|
global _nlp, _spacy_available
|
||||||
|
if _spacy_available is not None:
|
||||||
|
return _nlp, _spacy_available
|
||||||
|
try:
|
||||||
|
import spacy
|
||||||
|
|
||||||
|
_nlp = spacy.load("en_core_web_lg")
|
||||||
|
_spacy_available = True
|
||||||
|
except (ImportError, OSError) as e:
|
||||||
|
print(
|
||||||
|
f"{YELLOW}spaCy not available ({e}), "
|
||||||
|
f"falling back to edit-distance only{NC}"
|
||||||
|
)
|
||||||
|
_nlp = None
|
||||||
|
_spacy_available = False
|
||||||
|
return _nlp, _spacy_available
|
||||||
|
|
||||||
|
|
||||||
|
def extract_tags(filepath: Path, *, keep_blanks: bool = False) -> list[str]:
|
||||||
|
"""Extract tags from YAML front matter of a markdown file.
|
||||||
|
|
||||||
|
If keep_blanks is True, empty strings from blank tag entries like
|
||||||
|
tags: ["", "foo"] are preserved so the caller can detect them.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
text = filepath.read_text(encoding="utf-8")
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Match front matter between --- delimiters
|
||||||
|
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
|
||||||
|
if not fm_match:
|
||||||
|
return []
|
||||||
|
|
||||||
|
frontmatter = fm_match.group(1)
|
||||||
|
|
||||||
|
tags: list[str] = []
|
||||||
|
|
||||||
|
# Match inline array: tags: ["foo", "bar"] or tags: ['foo', 'bar'] or tags: [foo, bar]
|
||||||
|
inline = re.search(r"^tags:\s*\[([^\]]*)\]", frontmatter, re.MULTILINE)
|
||||||
|
if inline:
|
||||||
|
raw = inline.group(1).strip()
|
||||||
|
if raw: # non-empty array contents (skip tags: [] and tags: [ ])
|
||||||
|
tags = [t.strip().strip("\"'").lower() for t in raw.split(",")]
|
||||||
|
else:
|
||||||
|
# Match YAML list format:
|
||||||
|
# tags:
|
||||||
|
# - foo
|
||||||
|
# - bar
|
||||||
|
list_match = re.search(
|
||||||
|
r"^tags:\s*\n((?:\s+-\s+.+\n?)+)", frontmatter, re.MULTILINE
|
||||||
|
)
|
||||||
|
if list_match:
|
||||||
|
items = re.findall(r"^\s+-\s+(.*)", list_match.group(1), re.MULTILINE)
|
||||||
|
tags = [t.strip().strip("\"'").lower() for t in items]
|
||||||
|
|
||||||
|
if keep_blanks:
|
||||||
|
return tags
|
||||||
|
return [t for t in tags if t]
|
||||||
|
|
||||||
|
|
||||||
|
def find_similar(
|
||||||
|
new_tag: str,
|
||||||
|
existing_tags: set[str],
|
||||||
|
existing_docs: dict | None = None,
|
||||||
|
) -> list[tuple[str, str]]:
|
||||||
|
"""Find existing tags similar to a new tag.
|
||||||
|
|
||||||
|
Uses semantic similarity (spaCy vectors) as the primary check,
|
||||||
|
with edit-distance as a typo-catching backup.
|
||||||
|
|
||||||
|
If existing_docs is provided, it should be a dict mapping tag strings
|
||||||
|
to their pre-computed spaCy Doc objects (avoids redundant nlp() calls).
|
||||||
|
|
||||||
|
Returns list of (existing_tag, reason) tuples.
|
||||||
|
"""
|
||||||
|
nlp, has_spacy = _load_spacy()
|
||||||
|
similar = []
|
||||||
|
|
||||||
|
for existing in sorted(existing_tags):
|
||||||
|
if existing == new_tag:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Check 1: Substring match (restricted) ---
|
||||||
|
shorter, longer = sorted([new_tag, existing], key=len)
|
||||||
|
if (
|
||||||
|
len(shorter) >= SUBSTRING_MIN_LEN
|
||||||
|
and shorter in longer
|
||||||
|
and len(shorter) / len(longer) >= SUBSTRING_MIN_RATIO
|
||||||
|
):
|
||||||
|
similar.append((existing, "substring match"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Check 2: Semantic similarity (primary) ---
|
||||||
|
if has_spacy:
|
||||||
|
doc_new = nlp(new_tag)
|
||||||
|
doc_ex = existing_docs[existing] if existing_docs else nlp(existing)
|
||||||
|
|
||||||
|
if doc_new.has_vector and doc_ex.has_vector:
|
||||||
|
score = doc_new.similarity(doc_ex)
|
||||||
|
if score >= SEMANTIC_THRESHOLD:
|
||||||
|
similar.append((existing, f"semantic: {score:.0%}"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Check 3: Typo detection via edit distance (conservative) ---
|
||||||
|
ratio = SequenceMatcher(None, new_tag, existing).ratio()
|
||||||
|
if ratio >= TYPO_THRESHOLD:
|
||||||
|
similar.append((existing, f"typo match: {ratio:.0%}"))
|
||||||
|
|
||||||
|
return similar
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if os.environ.get("SKIP_TAG_CHECK") == "1":
|
||||||
|
print(f"{YELLOW}Skipping tag similarity check (SKIP_TAG_CHECK=1){NC}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: check-tags.py <file1.md> [file2.md ...]", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
staged_files = sys.argv[1:]
|
||||||
|
|
||||||
|
# Find repo root
|
||||||
|
repo_root = Path.cwd()
|
||||||
|
content_dir = repo_root / "content"
|
||||||
|
if not content_dir.is_dir():
|
||||||
|
print(f"{YELLOW}No content/ directory found, skipping tag check{NC}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Resolve staged files to absolute paths for exclusion
|
||||||
|
staged_abs = {(repo_root / f).resolve() for f in staged_files}
|
||||||
|
|
||||||
|
# Build tag registry from all content files (excluding staged files)
|
||||||
|
all_tags: set[str] = set()
|
||||||
|
for md_file in content_dir.rglob("*.md"):
|
||||||
|
if md_file.resolve() in staged_abs:
|
||||||
|
continue
|
||||||
|
all_tags.update(extract_tags(md_file))
|
||||||
|
|
||||||
|
if not all_tags:
|
||||||
|
print(f"{GREEN}No existing tags found, nothing to compare against.{NC}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Pre-compute spaCy docs for all existing tags (avoids repeated nlp() calls)
|
||||||
|
nlp, has_spacy = _load_spacy()
|
||||||
|
existing_docs = {tag: nlp(tag) for tag in all_tags} if has_spacy else None
|
||||||
|
|
||||||
|
# Check staged files for similar tags
|
||||||
|
found_issues = False
|
||||||
|
start = time.monotonic()
|
||||||
|
|
||||||
|
for staged_file in staged_files:
|
||||||
|
filepath = repo_root / staged_file
|
||||||
|
if not filepath.is_file():
|
||||||
|
continue
|
||||||
|
# Only check content files
|
||||||
|
if not staged_file.startswith("content/"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_tags_raw = extract_tags(filepath, keep_blanks=True)
|
||||||
|
|
||||||
|
# Check for blank/empty tags
|
||||||
|
blank_count = sum(1 for t in file_tags_raw if not t)
|
||||||
|
if blank_count:
|
||||||
|
found_issues = True
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
f"{YELLOW}{BOLD}Found {blank_count} blank tag(s) "
|
||||||
|
f"in {staged_file}{NC}"
|
||||||
|
)
|
||||||
|
print(f"{YELLOW} Remove empty strings from the tags array{NC}")
|
||||||
|
|
||||||
|
file_tags = [t for t in file_tags_raw if t]
|
||||||
|
|
||||||
|
for tag in file_tags:
|
||||||
|
# If the tag already exists exactly, it's fine
|
||||||
|
if tag in all_tags:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# New tag — check for similarity
|
||||||
|
similar = find_similar(tag, all_tags, existing_docs)
|
||||||
|
|
||||||
|
if similar:
|
||||||
|
found_issues = True
|
||||||
|
print()
|
||||||
|
print(f"{YELLOW}{BOLD}New tag '{tag}' in {staged_file}{NC}")
|
||||||
|
print(f"{YELLOW} Similar existing tags:{NC}")
|
||||||
|
for existing, reason in similar:
|
||||||
|
print(f" {CYAN}\u2192 {existing} ({reason}){NC}")
|
||||||
|
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
|
||||||
|
if found_issues:
|
||||||
|
print()
|
||||||
|
print(f"{RED}{BOLD}Tag similarity check failed.{NC}")
|
||||||
|
print(f"{RED}Consider using an existing tag, or skip with:{NC}")
|
||||||
|
print(f"{RED} SKIP_TAG_CHECK=1 git commit{NC}")
|
||||||
|
print(f"{RED} ({elapsed:.1f}s){NC}")
|
||||||
|
print()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"{GREEN}Tag check passed \u2014 no similar tags found. ({elapsed:.1f}s){NC}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""List all unique tags across Hugo content, sorted alphabetically."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
content_dir = Path(__file__).resolve().parent.parent / "content"
|
||||||
|
tags: set[str] = set()
|
||||||
|
|
||||||
|
for md in content_dir.rglob("*.md"):
|
||||||
|
text = md.read_text(encoding="utf-8")
|
||||||
|
fm = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
|
||||||
|
if not fm:
|
||||||
|
continue
|
||||||
|
inline = re.search(r"^tags:\s*\[([^\]]*)\]", fm.group(1), re.MULTILINE)
|
||||||
|
if inline and inline.group(1).strip():
|
||||||
|
for t in inline.group(1).split(","):
|
||||||
|
t = t.strip().strip("\"'").lower()
|
||||||
|
if t:
|
||||||
|
tags.add(t)
|
||||||
|
else:
|
||||||
|
lm = re.search(
|
||||||
|
r"^tags:\s*\n((?:\s+-\s+.+\n?)+)", fm.group(1), re.MULTILINE
|
||||||
|
)
|
||||||
|
if lm:
|
||||||
|
for t in re.findall(r"^\s+-\s+(.*)", lm.group(1), re.MULTILINE):
|
||||||
|
t = t.strip().strip("\"'").lower()
|
||||||
|
if t:
|
||||||
|
tags.add(t)
|
||||||
|
|
||||||
|
for t in sorted(tags):
|
||||||
|
print(t)
|
||||||
@@ -0,0 +1,886 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# optimize-images.sh — Image auditor, metadata stripper, and WebP optimizer for fosscat.com
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/optimize-images.sh # Interactive mode
|
||||||
|
# ./scripts/optimize-images.sh --dry-run # Show what would happen without changing anything
|
||||||
|
# ./scripts/optimize-images.sh --yes # Skip all confirmation prompts
|
||||||
|
# ./scripts/optimize-images.sh --audit-only # Only run the audit phase (no changes)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
IMAGES_DIR="static/images"
|
||||||
|
CONTENT_DIR="content"
|
||||||
|
CONFIG_FILE="config.toml"
|
||||||
|
MAX_WIDTH=2000
|
||||||
|
MAX_HEIGHT=2000
|
||||||
|
WEBP_QUALITY=82
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI flags
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DRY_RUN=false
|
||||||
|
AUTO_YES=false
|
||||||
|
AUDIT_ONLY=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--yes|-y) AUTO_YES=true ;;
|
||||||
|
--audit-only) AUDIT_ONLY=true ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: $0 [--dry-run] [--yes] [--audit-only]"
|
||||||
|
echo ""
|
||||||
|
echo " --dry-run Show what would happen without making changes"
|
||||||
|
echo " --yes, -y Skip confirmation prompts"
|
||||||
|
echo " --audit-only Only run the audit (no modifications)"
|
||||||
|
echo " --help, -h Show this help"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $arg"
|
||||||
|
echo "Run $0 --help for usage"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Colors and formatting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||||
|
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||||
|
header() { echo -e "\n${BOLD}${CYAN}═══ $* ═══${NC}\n"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dependency checks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_deps() {
|
||||||
|
local missing=()
|
||||||
|
for cmd in exiftool convert identify cwebp; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
missing+=("$cmd")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||||
|
error "Missing required tools: ${missing[*]}"
|
||||||
|
echo " These are provided by the Nix dev shell. Run:"
|
||||||
|
echo " nix develop # or let direnv load the flake"
|
||||||
|
echo ""
|
||||||
|
echo " Required nix packages:"
|
||||||
|
echo " perl538Packages.ImageExifTool (exiftool)"
|
||||||
|
echo " imagemagick (convert, identify)"
|
||||||
|
echo " libwebp (cwebp)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ensure we're in the project root
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]] || [[ ! -d "$IMAGES_DIR" ]]; then
|
||||||
|
error "Must be run from the project root (where $CONFIG_FILE and $IMAGES_DIR exist)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_deps
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Utility: human-readable file size
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
human_size() {
|
||||||
|
local bytes=$1
|
||||||
|
if (( bytes >= 1048576 )); then
|
||||||
|
local mb_whole=$(( bytes / 1048576 ))
|
||||||
|
local mb_frac=$(( (bytes % 1048576) * 10 / 1048576 ))
|
||||||
|
echo "${mb_whole}.${mb_frac} MB"
|
||||||
|
elif (( bytes >= 1024 )); then
|
||||||
|
echo "$(( bytes / 1024 )) KB"
|
||||||
|
else
|
||||||
|
echo "${bytes} B"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Manifest: track which images have been optimized (skip re-processing)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
MANIFEST_FILE=".image-manifest"
|
||||||
|
|
||||||
|
manifest_hash() {
|
||||||
|
sha256sum "$1" | cut -d' ' -f1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Look up a file's stored hash; prints it or empty string if not found
|
||||||
|
manifest_lookup() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ -f "$MANIFEST_FILE" ]]; then
|
||||||
|
grep -F " $file" "$MANIFEST_FILE" 2>/dev/null | head -1 | awk '{print $1}' || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update or add a file's hash in the manifest
|
||||||
|
manifest_update() {
|
||||||
|
local file="$1"
|
||||||
|
local hash
|
||||||
|
hash=$(manifest_hash "$file")
|
||||||
|
|
||||||
|
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
||||||
|
echo "# fosscat.com image optimization manifest — do not edit manually" > "$MANIFEST_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove existing entry for this file (if any)
|
||||||
|
if grep -qF " $file" "$MANIFEST_FILE" 2>/dev/null; then
|
||||||
|
grep -vF " $file" "$MANIFEST_FILE" > "${MANIFEST_FILE}.tmp"
|
||||||
|
mv "${MANIFEST_FILE}.tmp" "$MANIFEST_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Append new entry
|
||||||
|
echo "$hash $file" >> "$MANIFEST_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove a file's entry from the manifest
|
||||||
|
manifest_remove() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ -f "$MANIFEST_FILE" ]] && grep -qF " $file" "$MANIFEST_FILE" 2>/dev/null; then
|
||||||
|
grep -vF " $file" "$MANIFEST_FILE" > "${MANIFEST_FILE}.tmp"
|
||||||
|
mv "${MANIFEST_FILE}.tmp" "$MANIFEST_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Utility: confirm prompt (respects --yes and --dry-run)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
confirm() {
|
||||||
|
local prompt="$1"
|
||||||
|
if $AUTO_YES; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${DIM}(dry-run: would ask) $prompt${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -en " $prompt ${BOLD}[y/N]${NC} "
|
||||||
|
read -r answer
|
||||||
|
[[ "$answer" =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PHASE 1: AUDIT
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
phase_audit() {
|
||||||
|
header "PHASE 1: IMAGE AUDIT"
|
||||||
|
|
||||||
|
# Collect all image files
|
||||||
|
local -a image_files=()
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
image_files+=("$f")
|
||||||
|
done < <(find "$IMAGES_DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.gif' \) -print0 | sort -z)
|
||||||
|
|
||||||
|
if [[ ${#image_files[@]} -eq 0 ]]; then
|
||||||
|
warn "No images found in $IMAGES_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Image inventory table ---
|
||||||
|
echo -e "${BOLD}Image Inventory${NC}"
|
||||||
|
printf " %-40s %-6s %-12s %s\n" "FILENAME" "FORMAT" "DIMENSIONS" "SIZE"
|
||||||
|
printf " %-40s %-6s %-12s %s\n" "--------" "------" "----------" "----"
|
||||||
|
|
||||||
|
local total_size=0
|
||||||
|
for img in "${image_files[@]}"; do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$img")
|
||||||
|
local ext="${fname##*.}"
|
||||||
|
local fsize
|
||||||
|
fsize=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img" 2>/dev/null)
|
||||||
|
total_size=$((total_size + fsize))
|
||||||
|
local dims
|
||||||
|
dims=$(identify -format "%wx%h" "$img" 2>/dev/null || echo "unknown")
|
||||||
|
printf " %-40s %-6s %-12s %s\n" "$fname" "$ext" "$dims" "$(human_size "$fsize")"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Total: ${#image_files[@]} images, $(human_size $total_size)"
|
||||||
|
|
||||||
|
# --- EXIF / Metadata scan ---
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Metadata / Privacy Scan${NC}"
|
||||||
|
|
||||||
|
local privacy_issues=0
|
||||||
|
# Sensitive tag names to check (extracted in a single exiftool call per image)
|
||||||
|
local sensitive_tag_args=(
|
||||||
|
-GPSLatitude -GPSLongitude -GPSPosition
|
||||||
|
-SerialNumber -CameraSerialNumber -BodySerialNumber -LensSerialNumber
|
||||||
|
-OwnerName -Artist -Copyright -Creator -Rights
|
||||||
|
-By-line -Contact
|
||||||
|
-Make -Model -LensModel -Software
|
||||||
|
-DateTime -DateTimeOriginal -CreateDate
|
||||||
|
-CreatorTool -ImageDescription -UserComment
|
||||||
|
)
|
||||||
|
|
||||||
|
for img in "${image_files[@]}"; do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$img")
|
||||||
|
local has_metadata=false
|
||||||
|
local metadata_lines=()
|
||||||
|
|
||||||
|
# Single exiftool call to extract all sensitive tags at once
|
||||||
|
local exif_output
|
||||||
|
exif_output=$(exiftool -s -f "${sensitive_tag_args[@]}" "$img" 2>/dev/null || true)
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
# exiftool -s output format: "TagName : value"
|
||||||
|
local tagname value
|
||||||
|
tagname=$(echo "$line" | sed 's/\s*:.*//' | xargs)
|
||||||
|
value=$(echo "$line" | sed 's/^[^:]*:\s*//')
|
||||||
|
|
||||||
|
# Skip tags with no value (exiftool -f shows "-" for missing tags)
|
||||||
|
[[ "$value" == "-" ]] && continue
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
|
|
||||||
|
has_metadata=true
|
||||||
|
# Highlight GPS data in red
|
||||||
|
if [[ "$tagname" == *GPS* ]] || [[ "$tagname" == *Latitude* ]] || [[ "$tagname" == *Longitude* ]]; then
|
||||||
|
metadata_lines+=("${RED}!!${NC} $tagname: $value")
|
||||||
|
elif [[ "$tagname" == *Serial* ]] || [[ "$tagname" == *Owner* ]] || [[ "$tagname" == *Artist* ]] || [[ "$tagname" == *Creator* ]]; then
|
||||||
|
metadata_lines+=("${YELLOW}!${NC} $tagname: $value")
|
||||||
|
else
|
||||||
|
metadata_lines+=("${DIM}-${NC} $tagname: $value")
|
||||||
|
fi
|
||||||
|
done <<< "$exif_output"
|
||||||
|
|
||||||
|
if $has_metadata; then
|
||||||
|
privacy_issues=$((privacy_issues + 1))
|
||||||
|
echo -e " ${YELLOW}$fname${NC} — metadata found:"
|
||||||
|
for line in "${metadata_lines[@]}"; do
|
||||||
|
echo -e " $line"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}$fname${NC} — clean"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ $privacy_issues -gt 0 ]]; then
|
||||||
|
warn "$privacy_issues image(s) contain metadata that should be stripped"
|
||||||
|
else
|
||||||
|
success "All images are clean of sensitive metadata"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Cross-reference with content ---
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Content Reference Check${NC}"
|
||||||
|
|
||||||
|
# Collect all image references from content files
|
||||||
|
local -a referenced_images=()
|
||||||
|
local -a broken_refs=()
|
||||||
|
local -a inconsistent_paths=()
|
||||||
|
|
||||||
|
while IFS= read -r -d '' mdfile; do
|
||||||
|
# Front matter image field (handles both `image: "..."` and ` image: "..."` under cover:)
|
||||||
|
while IFS= read -r fm_image; do
|
||||||
|
[[ -z "$fm_image" ]] && continue
|
||||||
|
# Clean up: remove surrounding quotes and whitespace
|
||||||
|
fm_image=$(echo "$fm_image" | sed 's/^[[:space:]]*image:[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']\s*$//')
|
||||||
|
|
||||||
|
if [[ -n "$fm_image" ]] && [[ "$fm_image" != '""' ]] && [[ "$fm_image" != http* ]]; then
|
||||||
|
# Normalize: Hugo serves /images/... from static/images/...
|
||||||
|
local fs_path="static/${fm_image#/}"
|
||||||
|
|
||||||
|
# Check if it's a broken reference
|
||||||
|
if [[ ! -f "$fs_path" ]]; then
|
||||||
|
broken_refs+=("$mdfile|$fm_image")
|
||||||
|
else
|
||||||
|
referenced_images+=("$fs_path")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for inconsistent path (missing leading /)
|
||||||
|
if [[ "$fm_image" != /* ]]; then
|
||||||
|
inconsistent_paths+=("$mdfile|$fm_image")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^\s*image:\s' "$mdfile" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Inline markdown images: 
|
||||||
|
while IFS= read -r inline_ref; do
|
||||||
|
[[ -z "$inline_ref" ]] && continue
|
||||||
|
# Strip #fragment
|
||||||
|
local clean_ref="${inline_ref%%#*}"
|
||||||
|
local fs_ref="static/${clean_ref#/}"
|
||||||
|
|
||||||
|
if [[ ! -f "$fs_ref" ]] && [[ "$clean_ref" != http* ]]; then
|
||||||
|
broken_refs+=("$mdfile|$inline_ref")
|
||||||
|
else
|
||||||
|
referenced_images+=("$fs_ref")
|
||||||
|
fi
|
||||||
|
done < <(grep -oP '!\[[^\]]*\]\(\K[^)]+' "$mdfile" 2>/dev/null || true)
|
||||||
|
|
||||||
|
done < <(find "$CONTENT_DIR" -name '*.md' -print0)
|
||||||
|
|
||||||
|
# Also check config.toml for avatarUrl
|
||||||
|
local avatar_path
|
||||||
|
avatar_path=$(grep 'avatarUrl' "$CONFIG_FILE" | sed 's/.*=\s*["'\'']\(.*\)["'\'']/\1/' || true)
|
||||||
|
if [[ -n "$avatar_path" ]]; then
|
||||||
|
referenced_images+=("static/${avatar_path#/}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find unreferenced images (compare using static/images/... paths)
|
||||||
|
local -a unreferenced=()
|
||||||
|
for img in "${image_files[@]}"; do
|
||||||
|
local found=false
|
||||||
|
for ref in "${referenced_images[@]}"; do
|
||||||
|
if [[ "$ref" == "$img" ]]; then
|
||||||
|
found=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if ! $found; then
|
||||||
|
unreferenced+=("$img")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Report broken references
|
||||||
|
if [[ ${#broken_refs[@]} -gt 0 ]]; then
|
||||||
|
warn "${#broken_refs[@]} broken image reference(s):"
|
||||||
|
for entry in "${broken_refs[@]}"; do
|
||||||
|
local file="${entry%%|*}"
|
||||||
|
local ref="${entry##*|}"
|
||||||
|
echo -e " ${RED}$ref${NC} in ${DIM}$file${NC}"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
success "No broken image references"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report unreferenced images
|
||||||
|
echo ""
|
||||||
|
if [[ ${#unreferenced[@]} -gt 0 ]]; then
|
||||||
|
warn "${#unreferenced[@]} unreferenced image(s) (not used in any content):"
|
||||||
|
for img in "${unreferenced[@]}"; do
|
||||||
|
local fsize
|
||||||
|
fsize=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img" 2>/dev/null)
|
||||||
|
echo -e " ${YELLOW}$(basename "$img")${NC} ($(human_size "$fsize"))"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
success "All images are referenced in content"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report inconsistent paths
|
||||||
|
if [[ ${#inconsistent_paths[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
warn "${#inconsistent_paths[@]} image path(s) missing leading '/':"
|
||||||
|
for entry in "${inconsistent_paths[@]}"; do
|
||||||
|
local file="${entry%%|*}"
|
||||||
|
local ref="${entry##*|}"
|
||||||
|
echo -e " ${YELLOW}$ref${NC} in ${DIM}$file${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export arrays for later phases (bash 4+ trick: print to temp files)
|
||||||
|
printf '%s\n' "${image_files[@]}" > /tmp/optimg_files.txt
|
||||||
|
printf '%s\n' "${unreferenced[@]+"${unreferenced[@]}"}" > /tmp/optimg_unreferenced.txt
|
||||||
|
printf '%s\n' "${broken_refs[@]+"${broken_refs[@]}"}" > /tmp/optimg_broken.txt
|
||||||
|
echo "$total_size" > /tmp/optimg_total_size.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PHASE 2: METADATA STRIPPING
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
phase_strip_metadata() {
|
||||||
|
header "PHASE 2: METADATA STRIPPING"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
info "(dry-run) Would strip all EXIF/IPTC/XMP metadata from images"
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a image_files=()
|
||||||
|
mapfile -t image_files < /tmp/optimg_files.txt
|
||||||
|
|
||||||
|
local stripped=0
|
||||||
|
for img in "${image_files[@]}"; do
|
||||||
|
[[ -z "$img" ]] && continue
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$img")
|
||||||
|
|
||||||
|
# Check if image has strippable EXIF/XMP/IPTC metadata (not just file properties)
|
||||||
|
# Use -EXIF:All -XMP:All -IPTC:All to only check real metadata groups
|
||||||
|
local meta_check
|
||||||
|
meta_check=$(exiftool -s -s -s -EXIF:All -XMP:All -IPTC:All "$img" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$meta_check" ]]; then
|
||||||
|
echo -e " ${DIM}$fname — already clean, skipping${NC}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-orient JPEG/PNG before stripping (applies EXIF rotation to pixels)
|
||||||
|
local ext="${fname##*.}"
|
||||||
|
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if [[ "$ext" == "jpg" ]] || [[ "$ext" == "jpeg" ]] || [[ "$ext" == "png" ]]; then
|
||||||
|
magick "$img" -auto-orient "$img" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip all metadata
|
||||||
|
exiftool -all= -overwrite_original "$img" 2>/dev/null
|
||||||
|
stripped=$((stripped + 1))
|
||||||
|
echo -e " ${GREEN}$fname${NC} — metadata stripped"
|
||||||
|
|
||||||
|
# Update manifest since file content changed
|
||||||
|
manifest_update "$img"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
success "Stripped metadata from $stripped image(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PHASE 3: CONVERT & COMPRESS
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
phase_convert() {
|
||||||
|
header "PHASE 3: CONVERT TO WEBP & COMPRESS"
|
||||||
|
|
||||||
|
local -a image_files=()
|
||||||
|
mapfile -t image_files < /tmp/optimg_files.txt
|
||||||
|
|
||||||
|
# Delete unreferenced images first
|
||||||
|
local -a unreferenced=()
|
||||||
|
mapfile -t unreferenced < /tmp/optimg_unreferenced.txt
|
||||||
|
|
||||||
|
if [[ ${#unreferenced[@]} -gt 0 ]] && [[ -n "${unreferenced[0]}" ]]; then
|
||||||
|
echo -e "${BOLD}Removing unreferenced images${NC}"
|
||||||
|
for img in "${unreferenced[@]}"; do
|
||||||
|
[[ -z "$img" ]] && continue
|
||||||
|
local fsize
|
||||||
|
fsize=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img" 2>/dev/null)
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${DIM}(dry-run) Would delete: $(basename "$img") ($(human_size "$fsize"))${NC}"
|
||||||
|
else
|
||||||
|
rm -f "$img"
|
||||||
|
manifest_remove "$img"
|
||||||
|
echo -e " ${RED}Deleted:${NC} $(basename "$img") ($(human_size "$fsize"))"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BOLD}Converting images to WebP (quality $WEBP_QUALITY, max ${MAX_WIDTH}x${MAX_HEIGHT})${NC}"
|
||||||
|
printf " %-40s %-12s %-12s %s\n" "FILENAME" "BEFORE" "AFTER" "STATUS"
|
||||||
|
printf " %-40s %-12s %-12s %s\n" "--------" "------" "-----" "------"
|
||||||
|
|
||||||
|
local total_before=0
|
||||||
|
local total_after=0
|
||||||
|
local converted=0
|
||||||
|
local skipped=0
|
||||||
|
local resized_only=0
|
||||||
|
|
||||||
|
for img in "${image_files[@]}"; do
|
||||||
|
[[ -z "$img" ]] && continue
|
||||||
|
# Skip if this was an unreferenced file we just deleted
|
||||||
|
[[ ! -f "$img" ]] && continue
|
||||||
|
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$img")
|
||||||
|
local ext="${fname##*.}"
|
||||||
|
local base="${fname%.*}"
|
||||||
|
local ext_lower
|
||||||
|
ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||||
|
local webp_path="$IMAGES_DIR/${base}.webp"
|
||||||
|
|
||||||
|
local before_size
|
||||||
|
before_size=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img" 2>/dev/null)
|
||||||
|
total_before=$((total_before + before_size))
|
||||||
|
|
||||||
|
# --- Manifest-based skip logic ---
|
||||||
|
local current_hash stored_hash
|
||||||
|
current_hash=$(manifest_hash "$img")
|
||||||
|
stored_hash=$(manifest_lookup "$img")
|
||||||
|
|
||||||
|
if [[ "$ext_lower" == "webp" ]] && [[ "$current_hash" == "$stored_hash" ]] && [[ -n "$stored_hash" ]]; then
|
||||||
|
# Already optimized, hash unchanged — skip entirely
|
||||||
|
total_after=$((total_after + before_size))
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
printf " %-40s %-12s %-12s ${DIM}%s${NC}\n" \
|
||||||
|
"$fname" "$(human_size "$before_size")" "—" "already optimized"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
if [[ "$ext_lower" == "webp" ]] && [[ -n "$stored_hash" ]] && [[ "$current_hash" != "$stored_hash" ]]; then
|
||||||
|
echo -e " ${DIM}(dry-run) $fname — edited, would check dimensions only${NC}"
|
||||||
|
elif [[ "$ext_lower" == "webp" ]] && [[ -z "$stored_hash" ]]; then
|
||||||
|
echo -e " ${DIM}(dry-run) $fname — new webp, would record in manifest${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}(dry-run) Would convert: $fname -> ${base}.webp${NC}"
|
||||||
|
fi
|
||||||
|
local est_after=$before_size
|
||||||
|
case "$ext_lower" in
|
||||||
|
jpg|jpeg) est_after=$((before_size / 5)) ;;
|
||||||
|
png) est_after=$((before_size / 3)) ;;
|
||||||
|
webp) est_after=$before_size ;;
|
||||||
|
esac
|
||||||
|
total_after=$((total_after + est_after))
|
||||||
|
converted=$((converted + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current dimensions
|
||||||
|
local cur_width cur_height
|
||||||
|
read -r cur_width cur_height < <(identify -format "%w %h\n" "$img" 2>/dev/null || echo "0 0")
|
||||||
|
|
||||||
|
local needs_resize=false
|
||||||
|
if (( cur_width > MAX_WIDTH )) || (( cur_height > MAX_HEIGHT )); then
|
||||||
|
needs_resize=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Already WebP (edited or new): skip lossy re-compression ---
|
||||||
|
if [[ "$ext_lower" == "webp" ]]; then
|
||||||
|
if $needs_resize; then
|
||||||
|
# Resize oversized WebP via ImageMagick (lossless resize, no re-encoding)
|
||||||
|
local tmp_resized
|
||||||
|
tmp_resized=$(mktemp /tmp/optimg_XXXXXX.webp)
|
||||||
|
magick "$img" -resize "${MAX_WIDTH}x${MAX_HEIGHT}>" "$tmp_resized"
|
||||||
|
local new_dims
|
||||||
|
new_dims=$(magick identify -format '%wx%h' "$tmp_resized")
|
||||||
|
mv "$tmp_resized" "$img"
|
||||||
|
info " Resized $fname: ${cur_width}x${cur_height} -> $new_dims"
|
||||||
|
resized_only=$((resized_only + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Record in manifest (whether resized or just newly tracked)
|
||||||
|
manifest_update "$img"
|
||||||
|
|
||||||
|
local after_size
|
||||||
|
after_size=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img" 2>/dev/null)
|
||||||
|
total_after=$((total_after + after_size))
|
||||||
|
|
||||||
|
if $needs_resize; then
|
||||||
|
local savings=0
|
||||||
|
if (( before_size > 0 )); then
|
||||||
|
savings=$(( (before_size - after_size) * 100 / before_size ))
|
||||||
|
fi
|
||||||
|
printf " %-40s %-12s %-12s ${CYAN}%s${NC}\n" \
|
||||||
|
"$fname" "$(human_size "$before_size")" "$(human_size "$after_size")" "resized (${savings}%)"
|
||||||
|
elif [[ -z "$stored_hash" ]]; then
|
||||||
|
printf " %-40s %-12s %-12s ${GREEN}%s${NC}\n" \
|
||||||
|
"$fname" "$(human_size "$before_size")" "—" "recorded"
|
||||||
|
else
|
||||||
|
printf " %-40s %-12s %-12s ${CYAN}%s${NC}\n" \
|
||||||
|
"$fname" "$(human_size "$before_size")" "—" "updated (edited)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
converted=$((converted + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Non-WebP image: full convert + compress pipeline ---
|
||||||
|
local cwebp_input="$img"
|
||||||
|
local tmp_resized=""
|
||||||
|
|
||||||
|
if $needs_resize; then
|
||||||
|
# Resize via ImageMagick, output to temp PNG for cwebp
|
||||||
|
tmp_resized=$(mktemp /tmp/optimg_XXXXXX.png)
|
||||||
|
magick "$img" -resize "${MAX_WIDTH}x${MAX_HEIGHT}>" -quality 100 "$tmp_resized"
|
||||||
|
info " Resized $fname: ${cur_width}x${cur_height} -> $(magick identify -format '%wx%h' "$tmp_resized")"
|
||||||
|
cwebp_input="$tmp_resized"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to WebP via cwebp
|
||||||
|
cwebp -q "$WEBP_QUALITY" "$cwebp_input" -o "$webp_path" 2>/dev/null
|
||||||
|
|
||||||
|
# Cleanup temp file if we resized
|
||||||
|
[[ -n "$tmp_resized" ]] && rm -f "$tmp_resized"
|
||||||
|
|
||||||
|
# Delete original non-WebP source
|
||||||
|
rm -f "$img"
|
||||||
|
|
||||||
|
# Record the new WebP in the manifest
|
||||||
|
manifest_update "$webp_path"
|
||||||
|
|
||||||
|
local after_size
|
||||||
|
after_size=$(stat -c%s "$webp_path" 2>/dev/null || stat -f%z "$webp_path" 2>/dev/null)
|
||||||
|
total_after=$((total_after + after_size))
|
||||||
|
|
||||||
|
local savings=0
|
||||||
|
if (( before_size > 0 )); then
|
||||||
|
savings=$(( (before_size - after_size) * 100 / before_size ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
local savings_color="$GREEN"
|
||||||
|
if (( savings < 10 )); then
|
||||||
|
savings_color="$YELLOW"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %-40s %-12s %-12s ${savings_color}%s%%${NC}\n" \
|
||||||
|
"${base}.webp" "$(human_size "$before_size")" "$(human_size "$after_size")" "$savings"
|
||||||
|
|
||||||
|
converted=$((converted + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
local total_savings=0
|
||||||
|
if (( total_before > 0 )) && (( total_after > 0 )); then
|
||||||
|
total_savings=$(( (total_before - total_after) * 100 / total_before ))
|
||||||
|
fi
|
||||||
|
if (( skipped > 0 )); then
|
||||||
|
info "Skipped $skipped already-optimized image(s)"
|
||||||
|
fi
|
||||||
|
if (( resized_only > 0 )); then
|
||||||
|
info "Resized $resized_only edited image(s) (no re-compression)"
|
||||||
|
fi
|
||||||
|
info "Processed $converted image(s)"
|
||||||
|
if (( total_before > total_after )); then
|
||||||
|
info "Total: $(human_size $total_before) -> $(human_size $total_after) (${total_savings}% reduction)"
|
||||||
|
else
|
||||||
|
info "Total size: $(human_size $total_after)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save totals for summary
|
||||||
|
echo "$total_before" > /tmp/optimg_total_before.txt
|
||||||
|
echo "$total_after" > /tmp/optimg_total_after.txt
|
||||||
|
echo "$converted" > /tmp/optimg_converted.txt
|
||||||
|
echo "$skipped" > /tmp/optimg_skipped.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PHASE 4: UPDATE CONTENT REFERENCES
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
phase_update_refs() {
|
||||||
|
header "PHASE 4: UPDATE CONTENT REFERENCES"
|
||||||
|
|
||||||
|
local updated_files=0
|
||||||
|
|
||||||
|
# --- Step 1: Update image extensions in content files ---
|
||||||
|
# This must happen BEFORE broken ref clearing, since .jpg/.png files are now .webp
|
||||||
|
echo -e "${BOLD}Updating image references (.jpg/.jpeg/.png -> .webp)${NC}"
|
||||||
|
|
||||||
|
while IFS= read -r -d '' mdfile; do
|
||||||
|
local changed=false
|
||||||
|
|
||||||
|
# Normalize front matter paths first: change image: "images/... to image: "/images/...
|
||||||
|
if grep -qE '^\s*image:\s*"images/' "$mdfile" 2>/dev/null; then
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
sed -i -E 's@^(\s*image:\s*)"images/@\1"/images/@' "$mdfile"
|
||||||
|
fi
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update front matter image field (only local paths, not http URLs)
|
||||||
|
# Handles both `image: "/images/..."` and ` image: "/images/..."` (indented under cover:)
|
||||||
|
if grep -qE '^\s*image:\s*"/images/.*\.(jpg|jpeg|JPG|JPEG|png|PNG)"' "$mdfile" 2>/dev/null; then
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
sed -i -E 's@^(\s*image:\s*"/images/[^"]*)\.(jpg|jpeg|JPG|JPEG|png|PNG)"@\1.webp"@' "$mdfile"
|
||||||
|
fi
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update inline markdown images: 
|
||||||
|
# Only match local /images/ paths, not external URLs
|
||||||
|
if grep -qP '!\[[^\]]*\]\(/images/[^)]*\.(jpg|jpeg|JPG|JPEG|png|PNG)(#[^)]*)?\)' "$mdfile" 2>/dev/null; then
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
sed -i -E 's@(!\[[^]]*\]\(/images/[^.)]*)\.(jpg|jpeg|JPG|JPEG|png|PNG)([#][^)]*)?(\))@\1.webp\3\4@g' "$mdfile"
|
||||||
|
fi
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $changed; then
|
||||||
|
local relpath="${mdfile}"
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${DIM}(dry-run) Would update refs in: $relpath${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}Updated${NC} $relpath"
|
||||||
|
fi
|
||||||
|
updated_files=$((updated_files + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$CONTENT_DIR" -name '*.md' -print0)
|
||||||
|
|
||||||
|
# --- Step 2: Update config.toml avatar ---
|
||||||
|
if grep -q 'avatarUrl.*\.png' "$CONFIG_FILE" 2>/dev/null; then
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${DIM}(dry-run) Would update avatarUrl in $CONFIG_FILE${NC}"
|
||||||
|
else
|
||||||
|
sed -i 's@avatarUrl = "/images/fosscat_icon\.png"@avatarUrl = "/images/fosscat_icon.webp"@' "$CONFIG_FILE"
|
||||||
|
echo -e " ${GREEN}Updated${NC} avatarUrl in $CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
updated_files=$((updated_files + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 3: Clear genuinely broken image references ---
|
||||||
|
# Only clear refs that still don't resolve after extension updates
|
||||||
|
# (e.g., placeholder /images/img.jpg that was never a real image)
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Checking for remaining broken image references${NC}"
|
||||||
|
|
||||||
|
local cleared=0
|
||||||
|
while IFS= read -r -d '' mdfile; do
|
||||||
|
# Check front matter image fields
|
||||||
|
while IFS= read -r fm_line; do
|
||||||
|
[[ -z "$fm_line" ]] && continue
|
||||||
|
local fm_image
|
||||||
|
fm_image=$(echo "$fm_line" | sed 's/^[[:space:]]*image:[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']\s*$//')
|
||||||
|
|
||||||
|
[[ -z "$fm_image" ]] && continue
|
||||||
|
[[ "$fm_image" == '""' ]] && continue
|
||||||
|
[[ "$fm_image" == http* ]] && continue
|
||||||
|
|
||||||
|
local fs_path="static/${fm_image#/}"
|
||||||
|
if [[ ! -f "$fs_path" ]]; then
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${DIM}(dry-run) Would clear broken ref in: $mdfile (was: $fm_image)${NC}"
|
||||||
|
else
|
||||||
|
local escaped_image
|
||||||
|
escaped_image=$(echo "$fm_image" | sed 's/[.[\/*^$]/\\&/g')
|
||||||
|
sed -i -E "s@^(\s*image:\s*).*${escaped_image}.*@\1\"\"@" "$mdfile"
|
||||||
|
echo -e " ${GREEN}Cleared${NC} broken ref ${DIM}$fm_image${NC} in ${DIM}$mdfile${NC}"
|
||||||
|
cleared=$((cleared + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^\s*image:\s' "$mdfile" 2>/dev/null || true)
|
||||||
|
done < <(find "$CONTENT_DIR" -name '*.md' -print0)
|
||||||
|
|
||||||
|
if [[ $cleared -eq 0 ]] && ! $DRY_RUN; then
|
||||||
|
success "No broken image references remaining"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Updated $updated_files file(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PHASE 5: SUMMARY
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
phase_summary() {
|
||||||
|
header "PHASE 5: SUMMARY"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e "${BOLD}${YELLOW}DRY RUN — no changes were made${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
local total_before total_after converted skipped
|
||||||
|
total_before=$(cat /tmp/optimg_total_before.txt 2>/dev/null || cat /tmp/optimg_total_size.txt 2>/dev/null || echo 0)
|
||||||
|
total_after=$(cat /tmp/optimg_total_after.txt 2>/dev/null || echo 0)
|
||||||
|
converted=$(cat /tmp/optimg_converted.txt 2>/dev/null || echo 0)
|
||||||
|
skipped=$(cat /tmp/optimg_skipped.txt 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
local savings=0
|
||||||
|
if (( total_before > 0 )) && (( total_after > 0 )); then
|
||||||
|
savings=$(( (total_before - total_after) * 100 / total_before ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " Images processed: ${BOLD}$converted${NC}"
|
||||||
|
if (( skipped > 0 )); then
|
||||||
|
echo -e " Images skipped: ${BOLD}$skipped${NC} (already optimized)"
|
||||||
|
fi
|
||||||
|
if (( total_after > 0 )) && (( total_before > total_after )); then
|
||||||
|
echo -e " Size before: ${BOLD}$(human_size "$total_before")${NC}"
|
||||||
|
echo -e " Size after: ${BOLD}$(human_size "$total_after")${NC}"
|
||||||
|
echo -e " Total reduction: ${BOLD}${GREEN}${savings}%${NC}"
|
||||||
|
elif (( total_after > 0 )); then
|
||||||
|
echo -e " Total size: ${BOLD}$(human_size "$total_after")${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up stale manifest entries (files that no longer exist)
|
||||||
|
if [[ -f "$MANIFEST_FILE" ]] && ! $DRY_RUN; then
|
||||||
|
local stale=0
|
||||||
|
local manifest_tmp
|
||||||
|
manifest_tmp=$(mktemp /tmp/optimg_manifest_XXXXXX.txt)
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Keep comment lines
|
||||||
|
if [[ "$line" == \#* ]]; then
|
||||||
|
echo "$line" >> "$manifest_tmp"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Extract filename (second field, after hash + two spaces)
|
||||||
|
local entry_file
|
||||||
|
entry_file=$(echo "$line" | awk '{print $2}')
|
||||||
|
if [[ -z "$entry_file" ]] || [[ -f "$entry_file" ]]; then
|
||||||
|
echo "$line" >> "$manifest_tmp"
|
||||||
|
else
|
||||||
|
stale=$((stale + 1))
|
||||||
|
fi
|
||||||
|
done < "$MANIFEST_FILE"
|
||||||
|
mv "$manifest_tmp" "$MANIFEST_FILE"
|
||||||
|
if (( stale > 0 )); then
|
||||||
|
info "Removed $stale stale manifest entry/entries"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}Next steps:${NC}"
|
||||||
|
echo -e " 1. Run ${CYAN}hugo server${NC} and verify images look correct"
|
||||||
|
echo -e " 2. Check the browser dev tools Network tab for proper WebP delivery"
|
||||||
|
echo -e " 3. Commit when satisfied: ${CYAN}git add -A && git commit -m \"optimize: convert images to webp, strip metadata\"${NC}"
|
||||||
|
|
||||||
|
# Cleanup temp files
|
||||||
|
rm -f /tmp/optimg_*.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MAIN
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
echo -e "${BOLD}${CYAN}"
|
||||||
|
echo " ┌─────────────────────────────────────────┐"
|
||||||
|
echo " │ fosscat.com Image Optimizer │"
|
||||||
|
echo " │ Strip metadata · Convert to WebP │"
|
||||||
|
echo " │ Resize · Audit references │"
|
||||||
|
echo " └─────────────────────────────────────────┘"
|
||||||
|
echo -e "${NC}"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${YELLOW}Running in DRY RUN mode — no files will be modified${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 1: Audit (always runs)
|
||||||
|
phase_audit
|
||||||
|
|
||||||
|
if $AUDIT_ONLY; then
|
||||||
|
echo ""
|
||||||
|
info "Audit complete. Run without --audit-only to process images."
|
||||||
|
rm -f /tmp/optimg_*.txt
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm before proceeding
|
||||||
|
echo ""
|
||||||
|
if ! $AUTO_YES && ! $DRY_RUN; then
|
||||||
|
echo -en " ${BOLD}Proceed with optimization? [y/N]${NC} "
|
||||||
|
read -r answer
|
||||||
|
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
||||||
|
info "Aborted."
|
||||||
|
rm -f /tmp/optimg_*.txt
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 2: Strip metadata
|
||||||
|
phase_strip_metadata
|
||||||
|
|
||||||
|
# Phase 3: Convert & compress
|
||||||
|
phase_convert
|
||||||
|
|
||||||
|
# Phase 4: Update references
|
||||||
|
phase_update_refs
|
||||||
|
|
||||||
|
# Phase 5: Summary
|
||||||
|
phase_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# spellcheck-interactive.sh — Interactive fzf-based spell checker for markdown
|
||||||
|
#
|
||||||
|
# Checks markdown files with aspell, then presents each misspelled word
|
||||||
|
# interactively with fzf, showing context and offering actions:
|
||||||
|
# - Skip (ignore this word)
|
||||||
|
# - Add to dictionary (.aspell.en.pws)
|
||||||
|
# - Replace with a suggestion
|
||||||
|
# - Type a custom replacement
|
||||||
|
#
|
||||||
|
# If no TTY is available (non-interactive), falls back to batch output.
|
||||||
|
#
|
||||||
|
# Usage: spellcheck-interactive.sh <file1.md> [file2.md ...]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: spellcheck-interactive.sh <file1.md> [file2.md ...]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILES=("$@")
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
DICT_FILE="$REPO_ROOT/.aspell.en.pws"
|
||||||
|
|
||||||
|
# Check aspell availability
|
||||||
|
if ! command -v aspell &> /dev/null; then
|
||||||
|
echo -e "${YELLOW}aspell not found, skipping spell check${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! aspell dump dicts 2>/dev/null | grep -q "en"; then
|
||||||
|
echo -e "${YELLOW}aspell found but no English dictionaries available${NC}"
|
||||||
|
echo " Make sure ASPELL_CONF is set correctly in your direnv"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect interactive mode
|
||||||
|
INTERACTIVE=0
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
INTERACTIVE=1
|
||||||
|
elif ( exec 0</dev/tty ) 2>/dev/null; then
|
||||||
|
# Pre-commit hook: stdin is redirected, but /dev/tty is available
|
||||||
|
INTERACTIVE=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$INTERACTIVE" -eq 1 ] && ! command -v fzf &> /dev/null; then
|
||||||
|
echo -e "${YELLOW}fzf not found, falling back to non-interactive mode${NC}"
|
||||||
|
INTERACTIVE=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Strip front matter and code blocks from markdown ---
|
||||||
|
# Returns cleaned text suitable for spell checking.
|
||||||
|
# We keep line numbers aligned by replacing stripped content with blank lines,
|
||||||
|
# so aspell line numbers match the original file.
|
||||||
|
strip_for_spellcheck() {
|
||||||
|
local file="$1"
|
||||||
|
local in_frontmatter=0
|
||||||
|
local frontmatter_count=0
|
||||||
|
local in_codeblock=0
|
||||||
|
local linenum=0
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
linenum=$((linenum + 1))
|
||||||
|
|
||||||
|
# Track YAML front matter (between --- delimiters)
|
||||||
|
if [[ "$line" =~ ^---[[:space:]]*$ ]]; then
|
||||||
|
frontmatter_count=$((frontmatter_count + 1))
|
||||||
|
if [ "$frontmatter_count" -eq 1 ]; then
|
||||||
|
in_frontmatter=1
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
elif [ "$frontmatter_count" -eq 2 ]; then
|
||||||
|
in_frontmatter=0
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$in_frontmatter" -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track fenced code blocks
|
||||||
|
if [[ "$line" =~ ^\`\`\` ]]; then
|
||||||
|
if [ "$in_codeblock" -eq 0 ]; then
|
||||||
|
in_codeblock=1
|
||||||
|
else
|
||||||
|
in_codeblock=0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$in_codeblock" -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip inline code (`...`) to avoid checking code snippets
|
||||||
|
echo "$line" | sed 's/`[^`]*`//g'
|
||||||
|
done < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Get aspell suggestions for a word ---
|
||||||
|
get_suggestions() {
|
||||||
|
local word="$1"
|
||||||
|
# aspell pipe mode: & means misspelled with suggestions, # means no suggestions
|
||||||
|
local result
|
||||||
|
result=$(echo "$word" | aspell pipe --mode=markdown --lang=en --personal="$DICT_FILE" 2>/dev/null | tail -n +2)
|
||||||
|
|
||||||
|
if [[ "$result" =~ ^'&' ]]; then
|
||||||
|
# Format: & word count offset: suggestion1, suggestion2, ...
|
||||||
|
echo "$result" | sed 's/^& [^ ]* [0-9]* [0-9]*: //' | tr ',' '\n' | sed 's/^[[:space:]]*//' | head -8
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Get context lines around a word occurrence in a file ---
|
||||||
|
get_context() {
|
||||||
|
local file="$1"
|
||||||
|
local word="$2"
|
||||||
|
local max_contexts="${3:-3}"
|
||||||
|
|
||||||
|
# Find line numbers containing the word (case-insensitive, word boundary)
|
||||||
|
local lines
|
||||||
|
lines=$(grep -n -i -w "$word" "$file" 2>/dev/null | head -"$max_contexts") || true
|
||||||
|
echo "$lines"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Add a word to the personal dictionary ---
|
||||||
|
add_to_dictionary() {
|
||||||
|
local word="$1"
|
||||||
|
echo "$word" >> "$DICT_FILE"
|
||||||
|
# Sort and deduplicate (preserving the header line)
|
||||||
|
local header
|
||||||
|
header=$(head -1 "$DICT_FILE")
|
||||||
|
local body
|
||||||
|
body=$(tail -n +2 "$DICT_FILE" | sort -u)
|
||||||
|
printf '%s\n%s\n' "$header" "$body" > "$DICT_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Replace a word in a file ---
|
||||||
|
replace_word_in_file() {
|
||||||
|
local file="$1"
|
||||||
|
local old_word="$2"
|
||||||
|
local new_word="$3"
|
||||||
|
|
||||||
|
# Use word-boundary-aware sed replacement (case-sensitive, first occurrence per line)
|
||||||
|
# We use perl for proper word boundary support
|
||||||
|
if command -v perl &> /dev/null; then
|
||||||
|
perl -pi -e "s/\\b\Q${old_word}\E\\b/${new_word}/g" "$file"
|
||||||
|
else
|
||||||
|
sed -i "s/\b${old_word}\b/${new_word}/g" "$file" 2>/dev/null || \
|
||||||
|
sed -i '' "s/[[:<:]]${old_word}[[:>:]]/${new_word}/g" "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Non-interactive fallback (matches old pre-commit behavior) ---
|
||||||
|
run_batch_check() {
|
||||||
|
local overall_fail=0
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then continue; fi
|
||||||
|
|
||||||
|
local errors
|
||||||
|
errors=$(strip_for_spellcheck "$file" | aspell list --mode=markdown --lang=en --personal="$DICT_FILE" 2>/dev/null | sort -u)
|
||||||
|
|
||||||
|
if [ -n "$errors" ]; then
|
||||||
|
echo -e "${YELLOW}Possible misspellings in ${BOLD}$file${NC}${YELLOW}:${NC}"
|
||||||
|
while IFS= read -r word; do
|
||||||
|
if [ -z "$word" ]; then continue; fi
|
||||||
|
local suggestion
|
||||||
|
suggestion=$(get_suggestions "$word" | head -1)
|
||||||
|
echo -e " ${RED}'$word'${NC} ${DIM}→${NC} ${GREEN}$suggestion${NC}"
|
||||||
|
get_context "$file" "$word" 2 | while IFS= read -r ctx; do
|
||||||
|
echo -e " ${DIM}$ctx${NC}"
|
||||||
|
done
|
||||||
|
done <<< "$errors"
|
||||||
|
echo ""
|
||||||
|
overall_fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$overall_fail" -eq 1 ]; then
|
||||||
|
echo -e "${RED}${BOLD}Spell check failed.${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Interactive spell check with fzf ---
|
||||||
|
run_interactive_check() {
|
||||||
|
# Grab the terminal for input (needed in pre-commit hook context)
|
||||||
|
exec < /dev/tty
|
||||||
|
|
||||||
|
local files_modified=0
|
||||||
|
local overall_skipped=0
|
||||||
|
local words_added=0
|
||||||
|
local words_replaced=0
|
||||||
|
local words_skipped=0
|
||||||
|
local user_quit=0
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then continue; fi
|
||||||
|
|
||||||
|
# Get misspelled words from stripped content
|
||||||
|
local errors
|
||||||
|
errors=$(strip_for_spellcheck "$file" | aspell list --mode=markdown --lang=en --personal="$DICT_FILE" 2>/dev/null | sort -u)
|
||||||
|
|
||||||
|
if [ -z "$errors" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local word_count
|
||||||
|
word_count=$(echo "$errors" | wc -l | tr -d ' ')
|
||||||
|
local current=0
|
||||||
|
|
||||||
|
echo -e "\n${BOLD}${CYAN}Spell checking: $file${NC} ${DIM}($word_count issues)${NC}"
|
||||||
|
|
||||||
|
while IFS= read -r word; do
|
||||||
|
if [ -z "$word" ]; then continue; fi
|
||||||
|
if [ "$user_quit" -eq 1 ]; then
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
current=$((current + 1))
|
||||||
|
|
||||||
|
# Check if this word was already added to dictionary in this session
|
||||||
|
if grep -qx "$word" "$DICT_FILE" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get suggestions
|
||||||
|
local suggestions
|
||||||
|
suggestions=$(get_suggestions "$word")
|
||||||
|
|
||||||
|
# Get context
|
||||||
|
local context
|
||||||
|
context=$(get_context "$file" "$word" 3)
|
||||||
|
|
||||||
|
# Build the context header for fzf
|
||||||
|
local header=""
|
||||||
|
header+="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||||||
|
header+=" Misspelled: '$word' [$current/$word_count] in $file"$'\n'
|
||||||
|
header+="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||||||
|
if [ -n "$context" ]; then
|
||||||
|
header+=$'\n'" Context:"$'\n'
|
||||||
|
while IFS= read -r ctx_line; do
|
||||||
|
header+=" $ctx_line"$'\n'
|
||||||
|
done <<< "$context"
|
||||||
|
fi
|
||||||
|
header+=$'\n'" Choose an action:"
|
||||||
|
|
||||||
|
# Build fzf options
|
||||||
|
local options=""
|
||||||
|
options+="skip (ignore this word)"$'\n'
|
||||||
|
options+="add (add '$word' to dictionary)"$'\n'
|
||||||
|
|
||||||
|
if [ -n "$suggestions" ]; then
|
||||||
|
while IFS= read -r sug; do
|
||||||
|
if [ -n "$sug" ]; then
|
||||||
|
options+="use '$sug'"$'\n'
|
||||||
|
fi
|
||||||
|
done <<< "$suggestions"
|
||||||
|
fi
|
||||||
|
|
||||||
|
options+="type (enter custom replacement)"$'\n'
|
||||||
|
options+="quit (skip remaining words)"
|
||||||
|
|
||||||
|
# Run fzf
|
||||||
|
local choice
|
||||||
|
choice=$(echo -e "$options" | fzf \
|
||||||
|
--header="$header" \
|
||||||
|
--prompt="Action: " \
|
||||||
|
--no-sort \
|
||||||
|
--no-multi \
|
||||||
|
--height=~30 \
|
||||||
|
--reverse \
|
||||||
|
--no-info \
|
||||||
|
--pointer="▶" \
|
||||||
|
--color="header:cyan,pointer:yellow,prompt:yellow" \
|
||||||
|
2>/dev/tty) || {
|
||||||
|
# fzf returns 130 on Ctrl-C/Esc
|
||||||
|
echo -e "${YELLOW}Spell check cancelled.${NC}"
|
||||||
|
user_quit=1
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse the action
|
||||||
|
local action
|
||||||
|
action=$(echo "$choice" | awk '{print $1}')
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
skip)
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
;;
|
||||||
|
add)
|
||||||
|
add_to_dictionary "$word"
|
||||||
|
words_added=$((words_added + 1))
|
||||||
|
echo -e " ${GREEN}+ Added '$word' to dictionary${NC}"
|
||||||
|
;;
|
||||||
|
use)
|
||||||
|
local replacement
|
||||||
|
replacement=$(echo "$choice" | sed "s/^use[[:space:]]*'//;s/'$//" )
|
||||||
|
replace_word_in_file "$file" "$word" "$replacement"
|
||||||
|
files_modified=1
|
||||||
|
words_replaced=$((words_replaced + 1))
|
||||||
|
echo -e " ${GREEN}~ Replaced '$word' → '$replacement'${NC}"
|
||||||
|
;;
|
||||||
|
type)
|
||||||
|
echo -n " Enter replacement for '$word': "
|
||||||
|
local custom
|
||||||
|
read -r custom < /dev/tty
|
||||||
|
if [ -n "$custom" ]; then
|
||||||
|
replace_word_in_file "$file" "$word" "$custom"
|
||||||
|
files_modified=1
|
||||||
|
words_replaced=$((words_replaced + 1))
|
||||||
|
echo -e " ${GREEN}~ Replaced '$word' → '$custom'${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}(empty input, skipping)${NC}"
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
quit)
|
||||||
|
user_quit=1
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
words_skipped=$((words_skipped + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<< "$errors"
|
||||||
|
|
||||||
|
# Re-stage the file if we modified it
|
||||||
|
if [ "$files_modified" -eq 1 ]; then
|
||||||
|
git add "$file" 2>/dev/null || true
|
||||||
|
files_modified=0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Re-stage dictionary if words were added
|
||||||
|
if [ "$words_added" -gt 0 ]; then
|
||||||
|
git add "$DICT_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Spell check summary:${NC}"
|
||||||
|
if [ "$words_replaced" -gt 0 ]; then echo -e " ${GREEN}Replaced: $words_replaced${NC}"; fi
|
||||||
|
if [ "$words_added" -gt 0 ]; then echo -e " ${GREEN}Added to dictionary: $words_added${NC}"; fi
|
||||||
|
if [ "$words_skipped" -gt 0 ]; then echo -e " ${YELLOW}Skipped: $words_skipped${NC}"; fi
|
||||||
|
|
||||||
|
# If any words were skipped (not fixed), that's still a pass —
|
||||||
|
# the user explicitly chose to skip them. Only fail if user quit early.
|
||||||
|
if [ "$user_quit" -eq 1 ] && [ "$words_skipped" -gt 0 ]; then
|
||||||
|
echo -e "\n${YELLOW}Some words were skipped due to early quit.${NC}"
|
||||||
|
echo -e "${YELLOW}Commit will proceed — re-run to address remaining words.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}${BOLD}Spell check complete.${NC}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
echo "Running spell check..."
|
||||||
|
|
||||||
|
if [ "$INTERACTIVE" -eq 1 ]; then
|
||||||
|
run_interactive_check
|
||||||
|
exit $?
|
||||||
|
else
|
||||||
|
run_batch_check
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
Before Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 104 KiB |