diff --git a/.gitignore b/.gitignore index f1199783c..a76e14184 100644 --- a/.gitignore +++ b/.gitignore @@ -19,13 +19,3 @@ plaintext/ /result .wrangler/ -# cliamp's runtime files. Since 2026-09-13 ~/.config/cliamp is a real directory -# with only config.toml and themes/ linked in (home/home.nix), so these no -# longer land here; the lines stay so a stale checkout or a revert cannot -# commit them again. resume.json carries a Navidrome stream URL with a Subsonic -# token — never track it. -home/dot_config/cliamp/cliamp.log -home/dot_config/cliamp/cliamp.sock -home/dot_config/cliamp/cliamp.sock.pid -home/dot_config/cliamp/history.toml -home/dot_config/cliamp/resume.json diff --git a/AGENTS.md b/AGENTS.md index 3686dc7fe..4f14a3b2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,16 @@ +**Claude seats (2026-09-22).** Two Claude Code logins live on the coordinator +and nowhere else: `cc` is `~/.claude` (the personal Claude Max login, config file +`~/.claude.json`), `cc2` is `~/.claude-work` (the leger.run Claude Max login, +config file `~/.claude-work/.claude.json`), selected only by `CLAUDE_CONFIG_DIR` +in the fish launchers (`home/dot_config/fish/config.fish`). Both share the same +skills and settings links; login, history, sessions and trust are per seat, so a +session id resumes only on the seat and from the cwd that created it +(`claude-sessions` fans out over the seats). Claude never saves workspace trust +for `$HOME`, so the launchers move into `$CLAUDE_ENVELOPE` (default `~/today`) +when typed from `~`; never start a seat in the home directory. Logins are hand +`/login`s, never a delivered secret (the old claude-credentials seed was removed +this day). The seat meters are `~/.local/state/tally-rewrite/meters/.json`. + **Physical seats (2026-09-16, supersedes older headless/client-only wording below).** Tom is returning the coordinator to primary-desktop duty with two upright LG 5K displays side by side at scale 2. Both coordinator and client have diff --git a/docs/theme-switcher-2026-09-17.md b/docs/theme-switcher-2026-09-17.md new file mode 100644 index 000000000..63db2c7c8 --- /dev/null +++ b/docs/theme-switcher-2026-09-17.md @@ -0,0 +1,127 @@ +# Theme switcher: noir, claude-dark, claude-light (2026-09-17) + +Three hand-curated themes, one palette source of truth in Nix, one symlink flipped +at runtime. No palette generation from wallpapers (matugen/pywal), no Home Manager +specialisations — see "Why not" below. Research report with the full option +analysis and the light-theme ANSI derivation: `~/colors/theme-switcher-design.md` +(not in this repo; ~/colors holds the colour investigation). + +## The three themes + +| theme | ground (terminal) | desktop | accents | source | +|---|---|---|---|---| +| `noir` | `#000000` | `#000000` | Claude Dark's 12-role syntax palette | Tom's catppuccin-noir; accents matched claude.ai's bundle bit for bit | +| `claude-dark` | `#1A1A1A` (code-block ground) | `#20201F` (`--bg-000`) | same as noir | `~/colors/waves/capture/claude-code-theme/claude-tokens-dark.json` | +| `claude-light` | `#F9F9F7` (`--bg-100`) | `#F0EFEC` (`--bg-300`) | Claude Light's 12-role palette | `claude-themes-SOURCE.json`, `claude-tokens-light.json` | + +noir and claude-dark differ only in grounds (`home/themes/claude-dark.nix` is +`noir // { ground; muted; … }`). Claude ships no 16-slot ANSI palette, so +claude-light's is designed: roles fill slots 1–6, `--warning-100` fills the yellow +Claude has no role for, and `color0/7/15` are dark (punctuation / text-secondary / +fg) because herdr's `terminal` theme uses `White`/`Gray` as foregrounds. Every slot +passes WCAG AA on white. It is a proposal to tune in `~/colors/colorlab`. + +## How it is wired + +``` +home/themes/{noir,claude-dark,claude-light}.nix palettes, keyed by ROLE +home/themes/default.nix render: palette -> per-app fragments +home/theme.nix HM: ~/.config/themes//{kitty.conf,niri.kdl,colors.fish,theme.lua,meta} + + activation: bootstrap/heal ~/.config/theme +~/.config/theme -> ~/.config/themes/ the pointer; the ONLY runtime state +home/dot_local/bin/theme flip the pointer, fire reloads +``` + +Every RAW config keeps its body raw and joins its fragment through the app's own +include of `~/.config/theme/` — the same move as `kitty-scrollback-nix.conf` +and `niri-local.kdl` (generated files at a neutral path, included from inside a +whole-dir RAW symlink). Home Manager owns `themes/` (plural); the switcher owns +`theme` (singular). Switching needs no rebuild; adding a theme or changing a +palette value does. + +| consumer | joins via | live reload on `theme ` | +|---|---|---| +| kitty | `include ${HOME}/.config/theme/kitty.conf` (kitty.conf) | `kitten @ --to unix:@kitty- load-config` per instance (sockets from `/proc/net/unix`) | +| ghostty (cmux Browser panes) | `config-file = ?/home/tom/.config/theme/ghostty` LAST in ghostty/config.ghostty (a config-file loads after its parent, so the fragment wins; absolute because Ghostty resolves relative paths against the symlinked config's dir) | nothing — libghostty reads the config when cmux opens a surface; new panes follow | +| niri | `include optional=true "~/.config/theme/niri.kdl"` LAST in config.kdl; sections merge, later wins | `niri msg action load-config-file` | +| herdr | `[theme] name = "terminal"`: every token is an ANSI slot | nothing — kitty reports the bg change via DEC 2031 (`CSI ?997;n`), herdr re-queries OSC 10/11/4 and repaints chrome + every pane | +| nvim | `~/.local/bin/nvim-lua/theme.lua` dofile()s `theme.lua`; catppuccin/bufferline/lualine read it | `nvim --server --remote-expr` → `require('theme').reload()` per instance | +| fish | `conf.d/colors.fish` sources the fragment; re-sources at the next prompt when the pointer moved | automatic (no universal variables: `fish_variables` is tracked) | +| starship | already ANSI-named; the one `#F47B85` became `red` | follows kitty | +| Claude Code | `theme` key in `~/.claude.json` ← `meta` `claude_code=` | written tmp+rename; takes effect on next start | +| GTK3 | `gsettings gtk-theme` (`theme apply`) → MacTahoe-Dark-grey / MacTahoe-Claude-{Dark,Light}-orange | live: GTK3's Wayland backend reads org.gnome.desktop.interface from dconf and follows "changed". Needs the schema on XDG_DATA_DIRS (modules/common.nix) and no `GTK_THEME` env — both fixed in this PR | +| GTK4 / libadwaita | `~/.config/gtk-4.0/{gtk.css,gtk-dark.css,assets}` → `~/.config/theme/gtk-4.0/` (home/theme.nix); `color-scheme` via gsettings | color-scheme live (portal); gtk.css on next app start | +| icons / folder colour | `gsettings icon-theme MacTahoe[-]-{dark,light}` (`theme icons`) | live; accent from the wallpaper, polarity from the theme | +| Chrome | follows `color-scheme` through the portal | live | +| qt6ct | not yet | — | + +**GTK themes.** `pkgs/mactahoe-gtk-theme.nix` builds MacTahoe from source per +`variant`: `oled` (noir, the existing OLED-black substitutions) and `claude` (both +the light and the dark branch of `src/sass/_colors.scss` recoloured to claude.ai's +tokens — bg-000/100/200, text-000/200/400, links = accent-100 — with the `orange` +accent slot set to Anthropic clay `#D97757`). Theme dirs: `MacTahoe-Claude-{Dark,Light}[-solid]-orange[-(x)hdpi]`. +`GTK_THEME` is gone from `environment.sessionVariables`: it pinned one theme for +the whole session and could not change live. It had been load-bearing only +because `gsettings-desktop-schemas` was never on XDG_DATA_DIRS, so GTK fell back +to `settings.ini`; verified with `gtk-query-settings` before and after. + +**Wallpaper accents and folder colours.** `wallpaper ` sets one of the +seven claude.ai/imagine grounds (oat olive cactus sky fig heather coral, all +rendered at 5120x2880 from the recovered SVG), remembers it in +`~/.local/state/wallpaper/accent`, and calls `theme icons`. +`pkgs/mactahoe-icon-theme.nix` prebuilds `MacTahoe-{,-light,-dark}` for all +seven — folders in the accent's darker ground (`--bg-primary-dark`) — so every +combination is on the system already; everything but the folder SVGs dedupes. + +`F2` opens an fzf picker for the theme, `Shift+F2` one for the accent (wallpaper + +folder colour), both in the F1/F9/F10 prompt style; `Mod+Shift+T` cycles. `theme` prints the current name; `theme list`, +`theme apply` (re-fire hooks, also run at login from startup.kdl), `theme toggle`, +`theme icons`. + +## Testing before a switch + +`home/themes/default.nix` is pure (`{ lib }`), so a preview renders without a +generation: + +```sh +nix eval --json --impure --expr 'let lib = (builtins.getFlake (toString ./.)).inputs.nixpkgs.lib; in (import ./home/themes { inherit lib; }).fragments' \ + | python3 -c 'import json,sys,os; [ (os.makedirs(os.path.dirname(p:=os.path.expanduser("~/.cache/theme-preview/"+k.split("/",1)[1])),exist_ok=True), open(p,"w").write(v)) for k,v in json.load(sys.stdin).items() ]' +ln -sfn ~/.cache/theme-preview/noir ~/.config/theme +THEMES_DIR=~/.cache/theme-preview theme claude-dark +``` + +`home.activation.themePointer` re-targets a pointer left outside `~/.config/themes/` +to the live theme of the same name on the next switch. + +## Why not + +- **Home Manager specialisations**: HM runs as a NixOS module here + (`flake.nix`, `home-manager.nixosModules.home-manager`), so the base generation + re-activates at every boot and a specialisation does not survive a reboot; and + out-of-store symlinks are theme-invariant by construction, so every themed file + would have to leave the RAW doctrine. +- **matugen / pywal / wallust / stylix**: generators for palettes derived from a + wallpaper; three fixed palettes need only their *template + include + reload* + mechanics, which is what `default.nix` + `theme` are. +- **darkman**: two states, no "noir vs claude-dark". Could later *schedule* + `theme claude-light` / `theme ` by time of day. + +## Hazards + +- A symlink retarget fires no watcher event: the explicit kitty/niri/nvim reloads + in `theme` are load-bearing. +- kitty's `dynamic_background_opacity` stays in the RAW kitty.conf: it cannot be + changed by reload and must be present at startup. +- herdr's in-app settings overlay upserts `[theme] name` and `auto_switch = false` + into the tracked `config.toml`. Do not use it; `terminal` is the one setting. +- `~/.claude.json` is rewritten by Claude Code itself: the switcher writes tmp+rename + and accepts a lost update against a running instance. Running agents keep their + theme until restarted. +- Client laptop: `git pull` ahead of `nixos-rebuild switch` leaves kitty/niri/fish/nvim + including a pointer that does not exist yet. niri's include is `optional`, nvim's + loader falls back to noir, fish and kitty fall back to their defaults — degraded, + not broken — until the switch renders `~/.config/themes/`. +- Kitty windows reload in place; agents' TUIs and GTK4 apps restart. +- Fonts (Anthropic Sans/Serif/Mono from ~/colors) are NOT part of this: another + session owns that spec; they are proprietary brand faces and must follow the + NAS `requireFile` pattern of pkgs/sf-pro.nix, never land in git. diff --git a/flake.nix b/flake.nix index fcae9c295..bc9518f46 100644 --- a/flake.nix +++ b/flake.nix @@ -289,7 +289,7 @@ # piri — niri IPC extension daemon (github.com/Asthestarsfalll/piri): one # Rust daemon that tails niri's event stream and layers plugins on top — # scratchpads, marks, window/workspace rules. We use it for the "music" - # auto-scratchpad (Mod+M toggles a right-side SoundCloud/cliamp pane). + # auto-scratchpad (Mod+M toggles a right-side SoundCloud pane). # Third-party but consumed exactly like tally: flake input pinned in # flake.lock, follows nixpkgs so the Rust build resolves against our one pin. # piri ships packages.default + a NixOS module, but NOT a home-manager @@ -660,6 +660,7 @@ # requiring the binaries to be installed on the caller's PATH. local-models-prune mactahoe-gtk-theme + mactahoe-claude-gtk-theme mactahoe-icon-theme music-acquire sfmono-liga diff --git a/home/dot_config/cliamp/config.toml b/home/dot_config/cliamp/config.toml deleted file mode 100644 index 36ce053a5..000000000 --- a/home/dot_config/cliamp/config.toml +++ /dev/null @@ -1,55 +0,0 @@ -# Requires cliamp >= 1.10 (we ship 1.63.2, pkgs/cliamp.nix). The v1.9.0 this -# repo pinned until 2026-08-18 parsed config.toml with a flat key=value -# scanner over ten hardcoded keys: it skipped [section] headers, matched -# nothing below them, and did no ${VAR} expansion — so every section in this -# file was silently inert on that build. -# -# GAIN IN dB, NOT PERCENT — 0 is the "100%" setting: unity gain, samples pass -# through untouched, and the system mixer alone decides how loud things are. -# Negative attenuates, positive boosts. The ceiling is always +6; the floor is -# volume_min (default -50, itself clamped to [-90, 0]), and with no volume_min -# set here the effective floor is -50. The -30 dB figure cliamp's own `--vol` -# help text quotes belongs to other code paths entirely — the CLI flag, the Lua -# plugin API and the MPRIS volume mapping — not to this key. -# This read 70 until 2026-08-18, which clamped to +6 — a 2x boost applied -# before the system volume ever saw the signal, with clipping on loud masters. -volume = 0 -repeat = "off" -shuffle = false -theme = "" -provider = "navidrome" - -eq_preset = "Custom" -eq = [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0] -speed = 1.00 -[navidrome] -# Tailnet MagicDNS name, not localhost: this config is shared fleet-wide and -# only the coordinator could reach localhost. It had a second reader, the -# zenbook-duo, until that host left the fleet on 2026-08-30. -url = "http://coordinator.tail8dd1.ts.net:4533" -user = "${NAVIDROME_USER}" -password = "${NAVIDROME_PASSWORD}" -browse_sort = "alphabeticalByName" -scrobble = true - -# SoundCloud: off by default upstream. yt-dlp-backed (no client_id/secret model — -# SoundCloud closed OAuth signups in 2014). `user` swaps the curated genre browse -# list for lemurisback's own Tracks/Likes/Reposts. -[soundcloud] -enabled = true -user = "lemurisback" -# Go+ restored 2026-08-03 (dotfiles#70). cliamp shells to -# `yt-dlp --cookies-from-browser chrome`, so Chrome must be signed into -# SoundCloud as lemurisback on whichever box runs cliamp (the coordinator). -# Note: SoundCloud now serves major-label/monetized catalog -# over DRM-encrypted HLS that yt-dlp cannot decrypt (won't-fix upstream, -# yt-dlp#17335) -- this unlocks the non-DRM Go+ subset (most indie/DJ -# uploads), not the whole catalog. -cookies_from = "chrome" - -# YouTube / YouTube Music: no config needed — works out of the box with cliamp's -# built-in fallback OAuth client. Just select it in the provider browser (Esc/b). -# -# Bandcamp: no dedicated provider/config section exists in cliamp. It plays only -# via direct yt-dlp URL handling — paste a bandcamp.com URL with `u` in the TUI, -# or `cliamp https://artist.bandcamp.com/album/x` from the shell. diff --git a/home/dot_config/cliamp/themes/catppuccin-noir.toml b/home/dot_config/cliamp/themes/catppuccin-noir.toml deleted file mode 100644 index 4491d14c0..000000000 --- a/home/dot_config/cliamp/themes/catppuccin-noir.toml +++ /dev/null @@ -1,11 +0,0 @@ -# catppuccin-noir — catppuccin mocha on pure OLED black (#000000), -# with Claude Code accent colors layered on top. -# Mirrors the exact overrides in dot_config/nvim/lua/plugins.lua -# and the kitty palette in dot_config/kitty/kitty.conf. - -accent = "#70B8FF" # blue — titles, seek bar, selection, interactive -bright_fg = "#EAECF0" # text — primary text, time display -fg = "#818898" # overlay1 — muted labels, help bar, inactive -green = "#9BE963" # green — playing indicator, volume bar, spectrum low -yellow = "#f9e2af" # yellow — spectrum middle -red = "#F47B85" # red — spectrum high, errors diff --git a/home/dot_config/fish/conf.d/colors.fish b/home/dot_config/fish/conf.d/colors.fish index 947522117..a7af207a7 100644 --- a/home/dot_config/fish/conf.d/colors.fish +++ b/home/dot_config/fish/conf.d/colors.fish @@ -1,63 +1,28 @@ -# Color Palette (Claude-inspired, mapped from catppuccin mocha nvim overrides) -set -l normal eaecf0 -set -l command 70b8ff -set -l param 9be963 -set -l keyword f47b85 -set -l quote 9be963 -#set -l redirection f5c2e7 # catppuccin pink — no nvim override -set -l end fbad60 -set -l comment 818898 -set -l error f47b85 -#set -l gray 6c7086 # catppuccin overlay0 — no nvim override -#set -l selection 313244 # catppuccin surface0 — no nvim override -#set -l search_match 313244 # catppuccin surface0 — no nvim override -set -l option 9be963 -#set -l operator f5c2e7 # catppuccin pink — no nvim override -#set -l escape eba0ac # catppuccin maroon — no nvim override -#set -l autosuggestion 6c7086 # catppuccin overlay0 — no nvim override -set -l cancel f47b85 -set -l cwd f9e2af -set -l user 5eeded -set -l host 70b8ff -set -l host_remote 9be963 -#set -l status f38ba8 -#set -l pager_progress 6c7086 # catppuccin overlay0 — no nvim override -#set -l pager_prefix f5c2e7 # catppuccin pink — no nvim override -set -l pager_completion eaecf0 -#set -l pager_description 6c7086 # catppuccin overlay0 — no nvim override - -# Syntax Highlighting Colors -set -g fish_color_normal $normal -set -g fish_color_command $command -set -g fish_color_param $param -set -g fish_color_keyword $keyword -set -g fish_color_quote $quote -#set -g fish_color_redirection $redirection # no nvim override -set -g fish_color_end $end -set -g fish_color_comment $comment -set -g fish_color_error $error -#set -g fish_color_gray $gray # no nvim override -#set -g fish_color_selection --background=$selection # no nvim override -#set -g fish_color_search_match --background=$search_match # no nvim override -set -g fish_color_option $option -#set -g fish_color_operator $operator # no nvim override -#set -g fish_color_escape $escape # no nvim override -#set -g fish_color_autosuggestion $autosuggestion # no nvim override -set -g fish_color_cancel $cancel -set -g fish_color_cwd $cwd -set -g fish_color_user $user -set -g fish_color_host $host -set -g fish_color_host_remote $host_remote +# Colours come from the theme fragment ~/.config/theme/colors.fish, rendered by +# home/theme.nix from home/themes/.nix for whichever theme ~/.config/theme +# points at (~/.local/bin/theme retargets it). This file only loads it — at +# startup, and again at the next prompt after the pointer has moved, so running +# shells follow a `theme` switch too. No universal variables: `set -U` would +# write fish_variables, which is tracked in this repo. +# +# The theme-independent bits stay here. set -g fish_color_cwd_root red set -g fish_color_history_current --bold set -g fish_color_match --background=brblue set -g fish_color_valid_path --underline +set -g fish_pager_color_selected_background -r -set -g fish_color_status f47b85 +function __theme_load --description 'source ~/.config/theme/colors.fish if the pointer moved' + set -l target (readlink ~/.config/theme 2>/dev/null) + if test -n "$target" -a "$target" != "$__theme_loaded" + set -g __theme_loaded $target + if test -r ~/.config/theme/colors.fish + source ~/.config/theme/colors.fish + end + end +end +__theme_load -# Completion Pager Colors -#set -g fish_pager_color_progress $pager_progress # no nvim override -#set -g fish_pager_color_prefix $pager_prefix # no nvim override -set -g fish_pager_color_completion $pager_completion -#set -g fish_pager_color_description $pager_description # no nvim override -set -g fish_pager_color_selected_background -r +function __theme_on_prompt --on-event fish_prompt + __theme_load +end diff --git a/home/dot_config/fish/config.fish b/home/dot_config/fish/config.fish index 86fcedec8..5fa17cde4 100644 --- a/home/dot_config/fish/config.fish +++ b/home/dot_config/fish/config.fish @@ -42,36 +42,51 @@ alias l.="eza -a | grep -E \"^\.\"" alias cd='z' # zi command -# Claude Code -alias cc='claude --dangerously-skip-permissions' -alias cac='claude --continue --dangerously-skip-permissions' -# Second Claude account (work). CLAUDE_CONFIG_DIR isolates login, history and -# state in ~/.claude-work; skills/settings are the same dotfiles links (home.nix). -alias cc2='env CLAUDE_CONFIG_DIR=$HOME/.claude-work claude --dangerously-skip-permissions' -alias cac2='env CLAUDE_CONFIG_DIR=$HOME/.claude-work claude --continue --dangerously-skip-permissions' +# Claude Code: two seats, one launcher shape (2026-09-22). +# cc / cac -> ~/.claude the personal Claude Max login (gmail org) +# cc2 / cac2 -> ~/.claude-work the leger.run Claude Max login +# CLAUDE_CONFIG_DIR isolates login, history and state per seat; skills and +# settings are the same dotfiles links for both (home.nix). TALLY_SEAT names the +# seat to hooks and receipts. Claude Code never saves workspace trust for $HOME +# (docs, security: "trust acceptance is held for the current session only and +# is not written to disk"), so a seat started in ~ shows "Accessing workspace" +# on every launch. Both launchers therefore move into the desk envelope when +# typed from ~; the shell stays there afterwards, which is where the desk +# session's files belong (~/today/CLAUDE.md section 4). +set -q CLAUDE_ENVELOPE; or set -gx CLAUDE_ENVELOPE $HOME/today +function __claude_seat --description 'start one Claude seat outside $HOME' + set -l seat $argv[1] + set -l config_dir $argv[2] + set -l args $argv[3..] + if test (pwd) = $HOME + if not test -d $CLAUDE_ENVELOPE + echo "$seat: \$CLAUDE_ENVELOPE ($CLAUDE_ENVELOPE) does not exist; cd into a project first" >&2 + return 1 + end + builtin cd $CLAUDE_ENVELOPE + echo "($seat: started in $CLAUDE_ENVELOPE, not in ~)" >&2 + end + # A terminal opened from inside a cc2 session already carries + # CLAUDE_CONFIG_DIR, so `cc` must clear it explicitly or it silently starts + # cc2 (measured 2026-09-22). Each seat sets its own value or none. + if test -n "$config_dir" + env CLAUDE_CONFIG_DIR=$config_dir TALLY_SEAT=$seat claude --dangerously-skip-permissions $args + else + env -u CLAUDE_CONFIG_DIR TALLY_SEAT=$seat claude --dangerously-skip-permissions $args + end +end +function cc; __claude_seat cc "" $argv; end +function cac; __claude_seat cc "" --continue $argv; end +function cc2; __claude_seat cc2 $HOME/.claude-work $argv; end +function cac2; __claude_seat cc2 $HOME/.claude-work --continue $argv; end # Third Claude account (2026-09-05), same rotation: state in ~/.claude-3. alias cc3='env CLAUDE_CONFIG_DIR=$HOME/.claude-3 claude --dangerously-skip-permissions' alias cac3='env CLAUDE_CONFIG_DIR=$HOME/.claude-3 claude --continue --dangerously-skip-permissions' # ── Music ──────────────────────────────────────────────────────────────── -# The library lives on the NAS and is served by Navidrome; cliamp is the client. -# `cliamp` here is the wrapper function in functions/cliamp.fish, which injects -# the server and credentials from agenix — so bare cliamp opens straight into -# the Navidrome browser with no argument needed. Inside the TUI: Shift+N opens -# Browse (By Album / By Artist / By Artist-Album), Ctrl+F searches, Q quits. -alias m='cliamp' # open the library browser -alias music='cliamp' # same, spelled out -alias mshuffle='cliamp --shuffle --auto-play' # open it with shuffle already on - -# Transport control from outside the TUI. cliamp registers MPRIS as -# org.mpris.MediaPlayer2.cliamp, so -p cliamp addresses it specifically rather -# than whatever player playerctl happens to enumerate first (Chrome also -# registers, and would otherwise swallow these). -alias mp='playerctl -p cliamp play-pause' # play/pause (F3 does this too) -alias mn='playerctl -p cliamp next' # next track -alias mb='playerctl -p cliamp previous' # back a track -alias mnow='playerctl -p cliamp metadata --format "{{artist}} — {{title}}"' # what's playing - +# The library lives on the NAS and is served by Navidrome (hosts/nas/media.nix). +# The cliamp TUI client and its m/music/mp/mn/mb/mnow aliases were removed +# 2026-09-17 (unused). alias mscan='navidrome-scan' # reindex after a beets pass (--full for tag-only edits) type -q atuin || exit diff --git a/home/dot_config/fish/functions/cliamp.fish b/home/dot_config/fish/functions/cliamp.fish deleted file mode 100644 index 73e988589..000000000 --- a/home/dot_config/fish/functions/cliamp.fish +++ /dev/null @@ -1,40 +0,0 @@ -function cliamp --wraps cliamp --description "cliamp wired to the NAS Navidrome (credentials from agenix)" - # Every variable here is declared at function scope BEFORE the if, then - # assigned inside it. fish scopes `set -l` to the enclosing block, so - # exporting from within the `if` would drop the values before the final - # `command cliamp` ever runs — the provider would silently vanish and cliamp - # would fall back to the radio browser. - set -l url $NAVIDROME_URL - set -l user "" - set -l pass "" - - # URL is the coordinator's tailnet name, never localhost or `nas`: Navidrome - # runs on the NAS (hosts/nas/media.nix), reached from the coordinator over - # the LAN (the 10.77.0.0/30 tether is retired, #264), so the coordinator's - # navidrome-relay socket (hosts/coordinator/nas-client.nix) is the sole - # front door for every box. - test -n "$url"; or set url http://coordinator.tail8dd1.ts.net:4533 - - set -l creds /run/agenix/navidrome-credentials - if test -r $creds - set user (grep '^NAVIDROME_USER=' $creds | string replace 'NAVIDROME_USER=' '') - set pass (grep '^NAVIDROME_PASSWORD=' $creds | string replace 'NAVIDROME_PASSWORD=' '') - else - # Only the coordinator gets the secret (secrets.nix). Anywhere else, - # say so instead of opening a silently Navidrome-less browser. - echo "cliamp: $creds unreadable — Navidrome disabled, local files only" >&2 - end - - # config.toml's [navidrome] block interpolates ${NAVIDROME_USER} and - # ${NAVIDROME_PASSWORD}, so those two names must match its placeholders - # exactly. NAVIDROME_URL/USER/PASS feed navidrome.NewFromEnv(), the - # config-less fallback main.go tries second — it needs all three non-empty - # or it returns nil. Exporting both sets means cliamp connects whether or - # not config.toml is present, and both paths name the same server. - set -lx NAVIDROME_URL $url - set -lx NAVIDROME_USER $user - set -lx NAVIDROME_PASSWORD $pass - set -lx NAVIDROME_PASS $pass - - command cliamp $argv -end diff --git a/home/dot_config/fish/functions/navidrome-scan.fish b/home/dot_config/fish/functions/navidrome-scan.fish index 5ffe7cadc..ae2b5b5d5 100644 --- a/home/dot_config/fish/functions/navidrome-scan.fish +++ b/home/dot_config/fish/functions/navidrome-scan.fish @@ -23,7 +23,7 @@ function navidrome-scan --description "Trigger a Navidrome library rescan and wa set -l url $NAVIDROME_URL test -n "$url"; or set url http://coordinator.tail8dd1.ts.net:4533 - set -l auth "u=$user&p=$pass&v=1.16.1&c=cliamp&f=json" + set -l auth "u=$user&p=$pass&v=1.16.1&c=navidrome-scan&f=json" # First contact wakes the relay and the sleeping NAS service; the HDD may # also have to spin up, so allow a generous timeout here. diff --git a/home/dot_config/ghostty/config.ghostty b/home/dot_config/ghostty/config.ghostty new file mode 100644 index 000000000..bbca8b32f --- /dev/null +++ b/home/dot_config/ghostty/config.ghostty @@ -0,0 +1,83 @@ +# ghostty — kitty.conf's twin (2026-09-17). +# +# The only Ghostty on the fleet today is libghostty inside cmux Browser's +# terminal panes (cmux-tui; ~/.local/opt/cmux-browser-nightly, not packaged yet). +# Its settings page's "Use Ghostty config" reads THIS file, +# $XDG_CONFIG_HOME/ghostty/config.ghostty. Every value below mirrors kitty.conf +# key for key, so a cmux pane looks like a kitty window — a kitty change +# belongs here too. Where Ghostty has no equivalent, the kitty key is named +# and the gap stated. The colours are NOT here: they join through the theme +# fragment at the bottom, like kitty's `include ${HOME}/.config/theme/kitty.conf`. + +# shell fish — kitty.conf says why: the login shell is bash fleet-wide (#236), +# fish stays the terminal UI. +command = fish +shell-integration = fish + +# Font — kitty's EXPLICIT families (2026-08-21): Liga SFMono Nerd Font, bold is +# SemiBold on purpose (the weight the fontconfig fallback actually served). +# Ghostty takes family + style, where kitty takes the full face name. +font-family = Liga SFMono Nerd Font +font-family-bold = Liga SFMono Nerd Font +font-style-bold = SemiBold +font-family-italic = Liga SFMono Nerd Font +font-style-italic = Italic +font-family-bold-italic = Liga SFMono Nerd Font +font-style-bold-italic = SemiBold Italic +# font_size 16.0 (2026-09-16): 14 + one legacy ctrl+= press. +font-size = 16 +# disable_ligatures never: ligatures are Ghostty's default, nothing to set. + +# Cursor: cursor_blink_interval 0. kitty's cursor_trail has no Ghostty +# equivalent (cursor_trail_color in the fragment is kitty-only). +cursor-style = block +cursor-style-blink = false + +# window_padding_width 10 +window-padding-x = 10 +window-padding-y = 10 + +# detect_urls yes / open_url_with default (xdg-open here too). +# underline_hyperlinks always: Ghostty underlines a link on hover only. +link-url = true +# confirm_os_window_close 0 +confirm-close-surface = false +# scrollback_lines 100000 — Ghostty caps scrollback in BYTES per surface +# (lazily allocated, compressed when idle): 100 MB ≈ 100k lines of 1 KB. +scrollback-limit = 100000000 +# enable_audio_bell no — Ghostty's default rings the system bell; cmux may +# still flag the surface (attention/title), never sound. +bell-features = no-system,no-audio +# copy_on_select clipboard +copy-on-select = clipboard +# THE THIN-CLIENT CLIPBOARD CONTRACT (kitty.conf, 2026-09-11), the OSC 52 +# half: a remote program may WRITE this seat's clipboard (write-clipboard) and +# a remote READ prompts (read-clipboard-ask). `clipboard-read = allow` would +# let anything the coordinator runs exfiltrate the clipboard silently, so it +# must never be set. +clipboard-write = allow +clipboard-read = ask + +# map ctrl+equal / ctrl+minus — kitty's legacy increase_font_size step is a +# fixed +2.0pt (0.48.0 boss.py); Ghostty's default step is 1, so pass the amount. +keybind = ctrl+equal=increase_font_size:2 +keybind = ctrl+minus=decrease_font_size:2 +# kitty_mod+h / kitty_mod+g (kitty-scrollback.nvim) and enabled_layouts splits +# have no Ghostty side: cmux owns splits, tabs and the scrollback viewer. + +# Frosted terminal (2026-09-16): background_opacity 0.96. The compositor blur +# rides on the kitty app-ids in niri's window-rules.kdl; cmux is a different +# app-id, so its panes are translucent without the frost until a rule lands. +background-opacity = 0.96 +# kitty does not dim inactive splits; Ghostty's default is 0.7. +unfocused-split-opacity = 1 + +# Colours: the theme fragment home/theme.nix renders from home/themes/*.nix +# under ~/.config/themes//ghostty, joined through the ~/.config/theme +# pointer that ~/.local/bin/theme retargets. Ghostty resolves a relative +# config-file against the directory of THIS file — which is a symlink into the +# checkout — so the path is absolute; $HOME is /home/tom on every host +# (home.homeDirectory). `?` = no error while the fragment is not rendered yet. +# A config-file loads AFTER the file that names it, so the fragment wins. +# No live reload: cmux reads the config when it opens a surface. +config-file = ?/home/tom/.config/theme/ghostty diff --git a/home/dot_config/herdr/config.toml b/home/dot_config/herdr/config.toml index d5a97ec7e..1c589c1c9 100644 --- a/home/dot_config/herdr/config.toml +++ b/home/dot_config/herdr/config.toml @@ -25,6 +25,20 @@ onboarding = false # default prefix can't silently move every binding under it. prefix = "ctrl+b" +[theme] +# "terminal" (2026-09-17): every token is an ANSI slot or Reset +# (src/app/state.rs Palette::terminal), so kitty paints herdr — chrome AND every +# pane — in whatever theme ~/.local/bin/theme has selected, and re-paints it +# live: herdr enables DEC mode 2031 on the host (src/terminal_theme.rs), kitty +# reports a changed default background as CSI ?997;n on reload, and herdr +# answers with a full OSC 10/11/4 re-query. Nothing here ever changes again for +# a theme switch. Before this there was no [theme] section at all, i.e. herdr's +# default catppuccin — not noir. Do NOT pick a theme from herdr's in-app +# settings overlay: it upserts `name` and `auto_switch = false` into THIS +# tracked file (src/config/write.rs). auto_switch stays off — it only chooses +# between two built-in names and ignores `name` while on. +name = "terminal" + [terminal] # fish is the terminal UI here; bash is only the LOGIN shell (#236). herdr's # default for this key is empty, which means "$SHELL, then /bin/sh" — and the diff --git a/home/dot_config/kitty/kitty.conf b/home/dot_config/kitty/kitty.conf index bf6a52741..5ec24c5f7 100644 --- a/home/dot_config/kitty/kitty.conf +++ b/home/dot_config/kitty/kitty.conf @@ -49,7 +49,7 @@ cursor_blink_interval 0 cursor_trail 1 cursor_trail_decay 0.15 0.4 cursor_trail_start_threshold 3 -cursor_trail_color #70B8FF +# cursor_trail_color comes from the theme fragment (include below). # Padding around the text area window_padding_width 10 @@ -197,74 +197,20 @@ mouse_map ctrl+shift+right press ungrabbed combine : mouse_select_command_output #map ctrl+shift+backspace restore_font_size #map ctrl+shift+f11 toggle_fullscreen # -# Claude palette on OLED black -# The basic colors -background #000000 -foreground #EAECF0 -# Selection blue matches Claude Code's internal renderer selection highlight -# (sampled #264F78 from its TUI — also VS Code's default editor.selectionBackground). -selection_background #264F78 -selection_foreground #EAECF0 -cursor_text_color #000000 -url_color #70B8FF - -# black -color0 #000000 -color8 #818898 - -# red -color1 #F47B85 -color9 #F47B85 - -# green -color2 #9BE963 -color10 #9BE963 - -# yellow -color3 #f9e2af -color11 #f9e2af - -# blue -color4 #70B8FF -color12 #70B8FF - -# magenta -color5 #CC7BF4 -color13 #CC7BF4 - -# cyan -color6 #5EEDED -color14 #5EEDED - -# white -color7 #D3D7DE -color15 #EAECF0 - -# cursor -cursor #EAECF0 - -# tabs -active_tab_background #000000 -active_tab_foreground #EAECF0 -inactive_tab_background #000000 -inactive_tab_foreground #818898 -tab_bar_background #000000 +# Colours (2026-09-17): every colour key lives in the theme fragment +# ~/.config/theme/kitty.conf — the symlink ~/.local/bin/theme retargets between +# the themes home/theme.nix renders under ~/.config/themes/ (palettes in +# home/themes/*.nix: noir = the Claude palette on OLED black, claude-dark, +# claude-light). kitty expands env vars in include paths; the symlink flip +# fires no watcher event, so `theme` also calls `kitten @ load-config` on every +# instance. The tab bar edge stays here: it is layout, not colour. +include ${HOME}/.config/theme/kitty.conf tab_bar_edge top -# marks -mark1_foreground #000000 -mark1_background #CC7BF4 - -# splits/windows -# Split divider: match niri's INACTIVE window border (#373735, layout.kdl:30) -# instead of the pink focus accent, so a kitty split reads as a quiet -# niri-native divider rather than a loud highlight (Tom 2026-07-15). -active_border_color #373735 -inactive_border_color #000000 - -# Opaque, always (Tom 2026-09-11): the seat is the Zenbook's OLED, and a -# true-black terminal on a true-black backdrop is the point. No blur rule in -# niri, no wallpaper behind it, and dynamic_ stays off so nothing can dial the -# opacity back at runtime. -background_opacity 1.0 -dynamic_background_opacity no +# Frosted terminal (Tom 2026-09-16): a slightly translucent background pairs +# with the niri blur rule for the kitty app-ids (window-rules.kdl), so the +# compositor frosts the Imagine olive wallpaper behind it — frosted glass, not +# transparency; text stays opaque. dynamic_ allows live +# `kitty @ set-background-opacity`. Was 1.0 / no from 2026-09-11 (OLED black). +background_opacity 0.96 +dynamic_background_opacity yes diff --git a/home/dot_config/niri/binds.kdl b/home/dot_config/niri/binds.kdl index 954584c2d..329ace992 100644 --- a/home/dot_config/niri/binds.kdl +++ b/home/dot_config/niri/binds.kdl @@ -69,6 +69,11 @@ binds { // scroll: bindsym $mod+E exec thunar Mod+E hotkey-overlay-title="File Manager" { spawn "nautilus"; } + // Cycle noir -> claude-dark -> claude-light (~/.local/bin/theme, 2026-09-17): + // one symlink flip plus live reloads of kitty, niri and nvim; herdr and + // fish follow on their own. Mod+T is still free. + Mod+Shift+T hotkey-overlay-title="Theme (cycle)" { spawn-sh "~/.local/bin/theme toggle"; } + // scroll: bindsym Ctrl+N exec flatpak run com.google.Chrome // scroll: bindsym Ctrl+Shift+N exec flatpak run com.google.Chrome --incognito // Chrome is installed from nixpkgs; invoke its stable binary directly. @@ -131,6 +136,11 @@ binds { // F1 opens the Alexa wake listener menu. F1/F2 no // longer carry brightness; the Duo keyboard's XF86MonBrightness keys do. F1 repeat=false hotkey-overlay-title="Alexa Wake On/Off" { spawn-sh "kitty --class wake-prompt -e ~/.local/bin/wake-toggle menu"; } + // F2 (2026-09-17): pick the theme — noir / claude-dark / claude-light. + // Shift+F2: pick the colour accent — one of the seven claude.ai/imagine + // grounds, as wallpaper AND matching folder colour (bin/wallpaper). + F2 repeat=false hotkey-overlay-title="Theme" { spawn-sh "kitty --class theme-prompt -e ~/.local/bin/theme-prompt theme"; } + Shift+F2 repeat=false hotkey-overlay-title="Accent (wallpaper + folders)" { spawn-sh "kitty --class theme-prompt -e ~/.local/bin/theme-prompt accent"; } F3 hotkey-overlay-title="Play/Pause" { spawn-sh "~/.local/bin/media play"; } F4 hotkey-overlay-title="Previous" { spawn-sh "~/.local/bin/media prev"; } F5 hotkey-overlay-title="Next" { spawn-sh "~/.local/bin/media next"; } @@ -159,9 +169,8 @@ binds { // [scratchpads.music]; daemon: home/piri.nix). Mod+M toggles it: first press // (nothing captured) spawns the SoundCloud PWA pinned to the right, later // presses show/hide. Mod+Shift+M captures whatever window is focused INTO the - // music pane — the "mark this as my music window" move, for the coming - // cliamp-in-kitty setup (focus the cliamp window, Mod+Shift+M, then Mod+M - // toggles it thereafter). + // music pane — the "mark this as my music window" move (focus it, + // Mod+Shift+M, then Mod+M toggles it thereafter). Mod+M hotkey-overlay-title="Music (toggle)" { spawn "piri" "scratchpads" "music" "toggle"; } Mod+Shift+M hotkey-overlay-title="Music (capture focused)" { spawn "piri" "scratchpads" "music" "add" "fromRight"; } // SUPERSEDED by the music scratchpad above (Mod+M was YouTube-Music-focus). diff --git a/home/dot_config/niri/config.kdl b/home/dot_config/niri/config.kdl index d10a22af4..44ab7a7ee 100644 --- a/home/dot_config/niri/config.kdl +++ b/home/dot_config/niri/config.kdl @@ -35,15 +35,26 @@ include "window-rules.kdl" include "misc.kdl" include "startup.kdl" -// Shared local overrides. Keep this include mandatory: the pinned niri 26.04 does -// not know `optional=true`, and local.kdl is always deployed with this directory. +// Shared local overrides. local.kdl is always deployed with this directory, so +// the include stays mandatory. (Earlier comments here claimed the pinned niri +// 26.04 knows neither `optional=true` nor `~` in include paths; both work — +// verified with `niri validate` 2026-09-17. The "error parsing KDL" people hit +// is what a MISSING non-optional include reports.) include "local.kdl" // Per-HOST niri config, generated by home.nix (xdg.configFile."niri-local.kdl"). // Absolute path: this file lives OUTSIDE the whole-dir out-of-store symlink so it -// can differ per host; niri include does NOT expand ~ or $HOME, and `optional` is -// unavailable on the pinned niri, so home.nix writes it on EVERY host. It is -// inert on every host but the client (2026-09-11), whose touch mapping and -// chord overrides live there; see home/home.nix. The slot stays because it is -// the only real per-host niri hook. See dotfiles#67. +// can differ per host; home.nix writes it on EVERY host. It is inert on every +// host but the client (2026-09-11), whose touch mapping and chord overrides +// live there; see home/home.nix. The slot stays because it is the only real +// per-host niri hook. See dotfiles#67. include "/home/tom/.config/niri-local.kdl" + +// Theme colours, LAST so they merge over layout.kdl / misc.kdl (niri merges +// sections across includes, later wins per property). ~/.config/theme is the +// symlink ~/.local/bin/theme retargets between the themes home/theme.nix +// renders (home/themes/*.nix). `optional`: a checkout pulled ahead of its +// switch must not take niri down — without the fragment niri falls back to +// its own default colours, nothing worse. A symlink retarget fires no watcher +// event, so `theme` also runs `niri msg action load-config-file`. +include optional=true "~/.config/theme/niri.kdl" diff --git a/home/dot_config/niri/layout.kdl b/home/dot_config/niri/layout.kdl index 59ac88439..3561c97b0 100644 --- a/home/dot_config/niri/layout.kdl +++ b/home/dot_config/niri/layout.kdl @@ -21,12 +21,11 @@ layout { } // scroll: default_border pixel 1 + // Colours (active/inactive/urgent) come from the theme fragment included + // last in config.kdl (~/.config/theme/niri.kdl); geometry lives here. border { on width 1 - active-color "#555451" - inactive-color "#373735" - urgent-color "#F47B85" } center-focused-column "on-overflow" @@ -54,17 +53,7 @@ layout { // scroll: layout_default_height 1.0 // ^ NO NIRI EQUIVALENT - tab-indicator { - active-color "#CC7BF4" - inactive-color "#818898" - urgent-color "#F47B85" - } - - insert-hint { - color "#CC7BF480" - } - - background-color "#000000" + // tab-indicator, insert-hint and background-color: theme fragment. struts {} diff --git a/home/dot_config/niri/misc.kdl b/home/dot_config/niri/misc.kdl index da6a57640..a685d408f 100644 --- a/home/dot_config/niri/misc.kdl +++ b/home/dot_config/niri/misc.kdl @@ -90,9 +90,7 @@ cursor { // scroll: exec hash dbus-update-activation-environment 2>/dev/null && \ // scroll: dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP=sway // ^ DELETE: niri-session handles env export and D-Bus activation natively -overview { - backdrop-color "#000000" -} +// overview { backdrop-color } is set by the theme fragment (see config.kdl). gestures { hot-corners { diff --git a/home/dot_config/niri/piri.toml b/home/dot_config/niri/piri.toml index 0b1ae750e..18aa70859 100644 --- a/home/dot_config/niri/piri.toml +++ b/home/dot_config/niri/piri.toml @@ -25,10 +25,9 @@ default_margin = 0 # "music" — the right-side music pane (Mod+M toggles it; see binds.kdl). # First toggle with no captured window spawns `command` and pins it to the # right; subsequent toggles show/hide it. Today that's the SoundCloud PWA -# (app_id confirmed live: chrome-soundcloud.com__-Default). Later this becomes a -# cliamp-in-kitty pane — swap `command`/`app_id` here (hot-reloads), or capture -# any focused window into it at runtime with `piri scratchpads music add -# fromRight` (bound to Mod+Shift+M). +# (app_id confirmed live: chrome-soundcloud.com__-Default). Swap `command`/`app_id` +# here (hot-reloads), or capture any focused window into it at runtime with +# `piri scratchpads music add fromRight` (bound to Mod+Shift+M). [scratchpads.music] direction = "fromRight" command = "google-chrome-stable --app=https://soundcloud.com" diff --git a/home/dot_config/niri/startup.kdl b/home/dot_config/niri/startup.kdl index 2c27724de..51088297c 100644 --- a/home/dot_config/niri/startup.kdl +++ b/home/dot_config/niri/startup.kdl @@ -16,32 +16,30 @@ spawn-at-startup "sh" "-c" "sleep 0.5 && kanshi" // boot. Mod+D is ~/.local/bin/launcher now — an fzf script in a floating kitty, // with no daemon to start. -// No wallpaper daemon (Tom 2026-09-11): the backdrop is layout.kdl's -// background-color #000000, which the Zenbook's OLED renders as panel-off -// black. ~/.local/bin/wallpaper still exists for a one-off swaybg, and nothing -// restarts one at login. +// Wallpaper (Tom 2026-09-16): the claude.ai/imagine "olive" ribbons, rendered +// at 5120x2880 for the PA27JCV from the recovered source SVG (~/colors/waves). +// ~/.local/bin/wallpaper starts swaybg with that file as its default; run it +// with a path to swap the image at runtime. layout.kdl's background-color +// #000000 stays as the fallback under it. +spawn-at-startup "wallpaper" // ── gsettings ──────────────────────────────────────────────── // scroll: exec_always { // scroll: gsettings set $gnome-schema cursor-theme 'Bibata-Modern-Classic' spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "cursor-theme" "Bibata-Modern-Classic" -// scroll: gsettings set $gnome-schema color-scheme 'prefer-dark' -spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "color-scheme" "prefer-dark" // scroll: gsettings set $gnome-schema cursor-size '48' spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "cursor-size" "48" // scroll: gsettings set $gnome-schema text-scaling-factor '1.0' spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "text-scaling-factor" "1.0" -// scroll: gsettings set $gnome-schema gtk-theme 'Catppuccin-Noir-Standard-Green-Dark' -// ^ scroll-era catppuccin dropped 2026-07-04 — MacTahoe OLED end-to-end, matching home.nix gtk.theme -spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "gtk-theme" "MacTahoe-Dark-grey" -// scroll: gsettings set org.gnome.desktop.interface.wm.preferences theme 'Catppuccin-Noir-Standard-Green-Dark' -// ^ corrected schema: org.gnome.desktop.wm.preferences (not .interface.wm.preferences) -spawn-at-startup "gsettings" "set" "org.gnome.desktop.wm.preferences" "theme" "MacTahoe-Dark-grey" // scroll: gsettings set org.gnome.desktop.wm.preferences button-layout "" spawn-at-startup "gsettings" "set" "org.gnome.desktop.wm.preferences" "button-layout" "" -// scroll: #gsettings set $gnome-schema icon-theme 'Your icon theme' -spawn-at-startup "gsettings" "set" "org.gnome.desktop.interface" "icon-theme" "MacTahoe-dark" +// gtk-theme, wm theme, icon-theme and color-scheme (2026-09-17): set by +// `theme apply` from the selected theme + wallpaper accent, idempotently, so a +// login re-asserts whatever ~/.config/theme points at. (The four gsettings +// lines that were here had been failing silently — no gsettings on the session +// PATH; GTK_THEME did the theming. Both fixed in modules/common.nix.) +spawn-at-startup "theme" "apply" // scroll: } // ^ note: exec_always re-ran on reload. spawn-at-startup runs once. gsettings persist, so fine. diff --git a/home/dot_config/niri/window-rules.kdl b/home/dot_config/niri/window-rules.kdl index 2d88522de..874bd4ff6 100644 --- a/home/dot_config/niri/window-rules.kdl +++ b/home/dot_config/niri/window-rules.kdl @@ -40,6 +40,20 @@ window-rule { } } +// F2 theme picker / Shift+F2 accent picker: the same compact floating prompt. +window-rule { + match app-id="theme-prompt" + open-floating true + open-focused true + default-column-width { fixed 640; } + default-window-height { fixed 300; } + border { + width 2 + active-color "#DA7756" + inactive-color "#DA7756" + } +} + // F1 Alexa wake picker: the same compact floating prompt as F9/F10. window-rule { match app-id="wake-prompt" @@ -157,6 +171,22 @@ window-rule { default-column-width {} } +// Frosted kitty (Tom 2026-09-16, restored for the Imagine olive wallpaper): +// kitty's background_opacity (0.96, kitty.conf) makes only the terminal bg +// translucent; this niri-side blur frosts the wallpaper behind it so the +// window reads as frosted glass, not see-through, and text stays crisp. +// One rule for every kitty-derived app-id: the plain terminal, the herdr +// projector (`kitty --class herdr-projector`), and the fzf *-prompt popups — +// kitty.conf's opacity applies to all of them, so all of them get the blur. +// Deleted 2026-09-11 for the Zenbook's true-black OLED; back now that the +// coordinator has a wallpaper again. +window-rule { + match app-id=r#"^(kitty|herdr-projector|[a-z-]+-prompt)$"# + background-effect { + blur true + } +} + // Frosted Nautilus: MacTahoe's "normal" (non -solid) gtk-4.0 variant already // ships semi-transparent headerbar/sidebar surfaces meant for compositor blur // behind them (home.nix links MacTahoe-Dark-grey's gtk-4.0 CSS) — this rule diff --git a/home/dot_config/nvim/lua/plugins.lua b/home/dot_config/nvim/lua/plugins.lua deleted file mode 100644 index d4e7f78de..000000000 --- a/home/dot_config/nvim/lua/plugins.lua +++ /dev/null @@ -1,398 +0,0 @@ -local fn = vim.fn -local opt = vim.opt -local g = vim.g - -local lazypath = fn.stdpath("data") .. "/lazy/lazy.nvim" -if not vim.uv.fs_stat(lazypath) then - fn.system({ - "git", - "clone", - "--filter=blob:none", - "https://github.com/folke/lazy.nvim.git", - "--branch=stable", -- latest stable release - lazypath, - }) -end - -opt.rtp:prepend(lazypath) - -g.mapleader = " " - -require("lazy").setup({ - { - 'mikesmithgh/kitty-scrollback.nvim', - enabled = true, - lazy = true, - cmd = { 'KittyScrollbackGenerateKittens', 'KittyScrollbackCheckHealth' }, - event = { 'User KittyScrollbackLaunch' }, - -- version = '*', -- latest stable version, may have breaking changes if major version changed - -- version = '^5.0.0', -- pin major version, include fixes and features that do not have breaking changes - config = function() - require('kitty-scrollback').setup() - end, - }, - { - 'tpope/vim-repeat', -- Required for leap.nvim dot-repeats - }, - { - url = 'https://codeberg.org/andyg/leap.nvim', - config = function() - vim.keymap.set({'n', 'x', 'o'}, 's', '(leap-forward)') - vim.keymap.set({'n', 'x', 'o'}, 'S', '(leap-backward)') - vim.keymap.set({'n', 'x', 'o'}, 'gs', '(leap-from-window)') - end - }, - { - 'catgoose/nvim-colorizer.lua', - config = function() - require('colorizer').setup() - end - }, - { - "lukas-reineke/indent-blankline.nvim", - main = "ibl", - opts = {}, - config = function() - require('plugins.indent_blankline') - end - }, - { - 'nvim-treesitter/nvim-treesitter', - lazy = false, - build = ':TSUpdate', - config = function() - require('nvim-treesitter').install({ 'markdown', 'markdown_inline' }) - vim.api.nvim_create_autocmd('FileType', { - pattern = { 'markdown_inline' }, - callback = function() vim.treesitter.start() end, - }) - end - }, - { - 'nvim-tree/nvim-tree.lua', - config = function() - require('plugins.nvim-tree') - end - }, - { - 'lewis6991/gitsigns.nvim', - dependencies = { 'nvim-lua/plenary.nvim' }, - config = function() - require('plugins.gitsigns') - end - }, - { - 'nvim-telescope/telescope.nvim', - dependencies = { 'nvim-lua/plenary.nvim' }, - config = function() - require('plugins.telescope') - end - }, - { - 'akinsho/bufferline.nvim', - config = function() - require('bufferline').setup({ - options = { - hover = { - enabled = true, - delay = 200, - reveal = {'close'} - } - }, - highlights = { - fill = { - bg = "#000000" - }, - background = { - bg = "#000000" - } - } - }) - end - }, - { - 'catppuccin/nvim', - name = 'catppuccin', - config = function() - require('catppuccin').setup({ - flavour = "mocha", - transparent_background = true, - color_overrides = { - mocha = { - base = "#000000", - mantle = "#000000", - crust = "#000000", - text = "#EAECF0", - subtext1 = "#D3D7DE", - overlay1 = "#818898", - red = "#F47B85", - green = "#9BE963", - blue = "#70B8FF", - mauve = "#CC7BF4", - peach = "#FBAD60", - teal = "#5EEDED", - }, - }, - custom_highlights = function(colors) - local U = require("catppuccin.utils.colors") - local bg_amount = 0.095 - local rainbow = { - colors.blue, -- rainbow1 (H1) - colors.peach, -- rainbow2 (H2) - colors.green, -- rainbow3 (H3) - colors.teal, -- rainbow4 (H4) - colors.yellow, -- rainbow5 (H5) - colors.mauve, -- rainbow6 (H6) - } - local highlights = {} - for i, color in ipairs(rainbow) do - highlights["rainbow" .. i] = { fg = color } - highlights["RenderMarkdownH" .. i] = { fg = color, bold = true } - highlights["RenderMarkdownH" .. i .. "Bg"] = { bg = U.darken(color, bg_amount, colors.base) } - end - highlights["RenderMarkdownCode"] = { bg = U.darken(colors.text, 0.05, colors.base) } - highlights["RenderMarkdownCodeInline"] = { fg = colors.peach } - highlights["RenderMarkdownBullet"] = { fg = colors.blue } - highlights["RenderMarkdownDash"] = { fg = colors.overlay1 } - highlights["RenderMarkdownQuote"] = { fg = colors.overlay1 } - highlights["RenderMarkdownLink"] = { fg = colors.blue } - highlights["RenderMarkdownChecked"] = { fg = colors.green } - highlights["RenderMarkdownUnchecked"] = { fg = colors.overlay1 } - highlights["RenderMarkdownTableHead"] = { fg = colors.blue } - highlights["RenderMarkdownTableRow"] = { fg = colors.subtext1 } - return highlights - end, - integrations = { - render_markdown = true, - }, - }) - vim.cmd.colorscheme "catppuccin" - end - }, - { - 'windwp/nvim-autopairs', - config = function() - require('nvim-autopairs').setup() - end - }, - { - "folke/twilight.nvim", - config = function() - require("twilight").setup() - end - }, - { - "folke/zen-mode.nvim", - config = function() - require("zen-mode").setup({ - window = { - width = 90, - options = { - signcolumn = "no", - number = false, - relativenumber = false, - cursorline = false, - }, - }, - plugins = { - twilight = { enabled = true }, -- auto-enable twilight in zen - kitty = { - enabled = true, - font = "+2", -- bump kitty font by 2pt in zen mode - }, - gitsigns = { enabled = false }, -- hide git signs in zen - }, - }) - end - }, - { - 'MeanderingProgrammer/render-markdown.nvim', - dependencies = { - 'nvim-treesitter/nvim-treesitter', - 'nvim-tree/nvim-web-devicons' - }, - ft = {'markdown'}, - opts = { - file_types = { 'markdown' }, - } - }, - -- Git integration stack (ordered by dependency) - { - "sindrets/diffview.nvim", - dependencies = { - "nvim-lua/plenary.nvim", - "nvim-tree/nvim-web-devicons", - }, - cmd = { "DiffviewOpen", "DiffviewClose", "DiffviewToggleFiles", "DiffviewFocusFiles" }, - config = function() - require("diffview").setup({ - default_args = { - DiffviewOpen = { '--imply-local' }, - }, - keymaps = { - view = { - { 'n', 'q', 'DiffviewClose', { desc = 'Close diffview' } }, - }, - file_panel = { - { 'n', 'q', 'DiffviewClose', { desc = 'Close diffview' } }, - }, - file_history_panel = { - { 'n', 'q', 'DiffviewClose', { desc = 'Close diffview' } }, - }, - }, - }) - vim.opt.fillchars:append { diff = "╱" } - vim.api.nvim_create_autocmd('User', { - pattern = 'DiffviewViewLeave', - callback = function() - vim.cmd ':DiffviewClose' - end, - }) - end - }, - { - "NeogitOrg/neogit", - dependencies = { - "nvim-lua/plenary.nvim", - "sindrets/diffview.nvim", - "nvim-telescope/telescope.nvim", - }, - cmd = "Neogit", - config = function() - require('plugins.neogit') - end - }, - { - "pwntester/octo.nvim", - dependencies = { - "nvim-lua/plenary.nvim", - "nvim-telescope/telescope.nvim", - "nvim-tree/nvim-web-devicons", - }, - cmd = "Octo", - config = function() - require("octo").setup() - vim.treesitter.language.register('markdown', 'octo') - end - }, - { - "HakonHarnes/img-clip.nvim", - event = "BufEnter", - opts = { - default = { - -- Save images relative to the current markdown file - relative_to_current_file = true, - -- Create an 'images' directory next to the markdown file - dir_path = function() - -- Gets the directory of the current file - local file_dir = vim.fn.expand('%:p:h') - local images_dir = file_dir .. '/images' - -- Create the images directory if it doesn't exist - vim.fn.mkdir(images_dir, 'p') - return 'images' -- Return relative path - end, - extension = "png", - prompt_for_file_name = true, - }, - filetypes = { - markdown = { - url_encode_path = true, - template = "![$CURSOR]($FILE_PATH)", - download_images = true, -- Enable downloading images from URLs - }, - }, - } - }, - { - "3rd/image.nvim", - build = false, - opts = { - backend = "kitty", - processor = "magick_cli", - integrations = { - markdown = { - enabled = true, - clear_in_insert_mode = false, - download_remote_images = true, - only_render_image_at_cursor = false, - } - }, - -- max_height_window_percentage = 80, -- uncomment to set max height to 80% of window - editor_only_render_when_focused = false, - window_overlap_clear_enabled = false, - hijack_file_patterns = { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.avif" }, - }, - }, - { - "karb94/neoscroll.nvim", - event = "BufRead", - config = function() - require('plugins.neoscroll') - end - }, - { - 'nvim-lualine/lualine.nvim', - dependencies = { 'nvim-tree/nvim-web-devicons', 'catppuccin/nvim' }, - config = function() - require('plugins.lualine').setup() - end, - event = 'VeryLazy', - }, - - -- ===== NEW PLUGINS ADDED BELOW ===== - - -- IMPROVED: pipeline.nvim - CI/CD pipeline viewer (using yq instead of make) - { - "topaxi/pipeline.nvim", - keys = { - { "ci", "Pipeline", desc = "Open pipeline.nvim" }, - }, - -- Removed build step since yq is already installed - opts = { - -- You can customize pipeline.nvim options here if needed - -- For example: - -- refresh_interval = 30, - -- browser = "firefox", - }, - }, - - -- NEW: diagram.nvim - Live diagram rendering (mermaid, plantuml, d2, gnuplot) - { - "3rd/diagram.nvim", - dependencies = { "3rd/image.nvim" }, -- Already have image.nvim above - opts = { - events = { - render_buffer = { "InsertLeave", "BufWinEnter", "TextChanged" }, - clear_buffer = { "BufLeave" }, - }, - renderer_options = { - mermaid = { theme = "default", scale = 1 }, - plantuml = { charset = "utf-8" }, - d2 = {}, - gnuplot = {}, - }, - }, - }, - - -- NEW: Mason + marksman LSP - { - "williamboman/mason.nvim", - dependencies = { - "williamboman/mason-lspconfig.nvim", - "neovim/nvim-lspconfig", - }, - ft = { "markdown", "md" }, - config = function() - require("mason").setup() - require("mason-lspconfig").setup({ - ensure_installed = { "marksman" }, - handlers = { - function(server_name) - require("lspconfig")[server_name].setup({}) - end, - }, - }) - end, - }, - -}) diff --git a/home/dot_config/nvim/lua/plugins/bufferline.lua b/home/dot_config/nvim/lua/plugins/bufferline.lua deleted file mode 100644 index 23b7c961e..000000000 --- a/home/dot_config/nvim/lua/plugins/bufferline.lua +++ /dev/null @@ -1,72 +0,0 @@ -local ok, bufferline = pcall(require, "bufferline") -if not ok then - return -end - -bufferline.setup { - options = { - offsets = { { filetype = "NvimTree", text = "Explorer", highlight = "Normal" } }, - separator_style = { "", ""}, - show_tab_indicators = false, - }, - highlights = { - fill = { - fg = "#000000", - bg = "#000000", - }, - background = { - fg = "#45475a", - bg = "#000000", - }, - - -- buffers - buffer_selected = { - fg = "#EAECF0", - bg = "#000000", - italic = false, - }, - buffer_visible = { - fg = "#45475a", - bg = "#000000", - }, - - -- close buttons - close_button = { - fg = "#45475a", - bg = "#000000", - }, - close_button_visible = { - fg = "#45475a", - bg = "#000000", - }, - close_button_selected = { - fg = "#F47B85", - bg = "#000000", - }, - - indicator_selected = { - fg = "#000000", - bg = "#000000", - }, - - -- modified - modified = { - fg = "#45475a", - bg = "#000000", - }, - modified_visible = { - fg = "#000000", - bg = "#000000", - }, - modified_selected = { - fg = "#9BE963", - bg = "#000000", - }, - - -- tabs - tab_close = { - fg = "#000000", - bg = "#000000", - }, - }, -} diff --git a/home/dot_config/nvim/lua/plugins/lualine.lua b/home/dot_config/nvim/lua/plugins/lualine.lua index 9082cb329..c8b2bf9e5 100644 --- a/home/dot_config/nvim/lua/plugins/lualine.lua +++ b/home/dot_config/nvim/lua/plugins/lualine.lua @@ -81,7 +81,7 @@ components.filename = { -- External changes indicator components.external_changes = { require('external-changes').lualine_component, - color = { fg = '#9BE963', gui = 'bold' }, + color = { gui = 'bold' }, -- fg set in setup() from the theme palette } -- Search count @@ -103,7 +103,7 @@ components.recording = { end return ' REC @' .. reg end, - color = { fg = '#F47B85', gui = 'bold' }, + color = { gui = 'bold' }, -- fg set in setup() from the theme palette cond = function() return vim.fn.reg_recording() ~= '' end, @@ -139,6 +139,14 @@ components.location = { -- Setup function function M.setup() + -- Palette colours are resolved here, not at module load, so theme.reload() + -- can call setup() again after a `theme` switch. + -- pcall: a missing theme module must never cost the status line (noir + -- fallback values, the same as theme.lua's own). + local ok, theme = pcall(require, 'theme') + local T = ok and theme.palette() or { green = '#9BE963', red = '#F47B85' } + components.external_changes.color.fg = T.green + components.recording.color.fg = T.red require('lualine').setup({ options = { theme = 'auto', diff --git a/home/dot_config/starship/starship.toml b/home/dot_config/starship/starship.toml index debc8b743..ad6ef8420 100644 --- a/home/dot_config/starship/starship.toml +++ b/home/dot_config/starship/starship.toml @@ -31,7 +31,7 @@ format = "[$branch]($style)" style = "bright-black" [git_status] -format = "[[(*$conflicted$untracked$modified$staged$renamed$deleted)](#F47B85) ($ahead_behind$stashed)]($style)" +format = "[[(*$conflicted$untracked$modified$staged$renamed$deleted)](red) ($ahead_behind$stashed)]($style)" style = "cyan" conflicted = "​" untracked = "​" diff --git a/home/dot_config/zathura/zathurarc b/home/dot_config/zathura/zathurarc deleted file mode 100644 index dd6b81918..000000000 --- a/home/dot_config/zathura/zathurarc +++ /dev/null @@ -1,39 +0,0 @@ -# vim: set filetype=conf : - -set default-fg "#CDD6F4" -set default-bg "#000000" - -set completion-bg "#313244" -set completion-fg "#CDD6F4" -set completion-highlight-bg "#575268" -set completion-highlight-fg "#CDD6F4" -set completion-group-bg "#313244" -set completion-group-fg "#89B4FA" - -set statusbar-fg "#CDD6F4" -set statusbar-bg "#313244" - -set notification-bg "#313244" -set notification-fg "#CDD6F4" -set notification-error-bg "#313244" -set notification-error-fg "#F38BA8" -set notification-warning-bg "#313244" -set notification-warning-fg "#FAE3B0" - -set inputbar-fg "#CDD6F4" -set inputbar-bg "#313244" - -set recolor-lightcolor "#000000" -set recolor-darkcolor "#CDD6F4" - -set index-fg "#CDD6F4" -set index-bg "#000000" -set index-active-fg "#CDD6F4" -set index-active-bg "#313244" - -set render-loading-bg "#000000" -set render-loading-fg "#CDD6F4" - -set highlight-color "#575268" -set highlight-fg "#F5C2E7" -set highlight-active-color "#F5C2E7" diff --git a/home/dot_local/bin/media b/home/dot_local/bin/media index ab7c3020e..7935b48dd 100755 --- a/home/dot_local/bin/media +++ b/home/dot_local/bin/media @@ -1,19 +1,11 @@ #!/usr/bin/env bash -# media -- MPRIS playback control (bound to F1-F10 in niri/binds.kdl) +# media -- MPRIS playback control (bound to F3-F5 in niri/binds.kdl) # -# Prefer cliamp when it is running. Bare `playerctl` acts on the first player it -# enumerates, and Chrome registers an MPRIS player too — so with a browser open -# the media keys would drive a background tab instead of the music player. -# Falls back to playerctl's default pick when cliamp is not running. -# ^cliamp, not an exact match: MPRIS names may carry an .instanceNNN suffix. -player=() -if playerctl -l 2>/dev/null | grep -q '^cliamp'; then - player=(-p cliamp) -fi - +# Acts on playerctl's default player. (Until 2026-09-17 this preferred cliamp +# when it was running; cliamp was removed, unused.) case "$1" in - play|toggle) playerctl "${player[@]}" play-pause ;; - next) playerctl "${player[@]}" next ;; - prev) playerctl "${player[@]}" previous ;; + play|toggle) playerctl play-pause ;; + next) playerctl next ;; + prev) playerctl previous ;; *) echo "Usage: $0 {play|next|prev}"; exit 1 ;; esac diff --git a/home/dot_local/bin/nvim-lua/theme.lua b/home/dot_local/bin/nvim-lua/theme.lua new file mode 100644 index 000000000..44acfa176 --- /dev/null +++ b/home/dot_local/bin/nvim-lua/theme.lua @@ -0,0 +1,154 @@ +-- theme.lua — the palette every colour in the nvim config comes from. +-- +-- Home Manager renders ~/.config/themes//theme.lua for each theme +-- (home/themes/*.nix); ~/.config/theme is the symlink ~/.local/bin/theme +-- retargets. This module dofile()s the current fragment and applies it to +-- catppuccin, bufferline and lualine — at startup from plugins.lua.in, and +-- again on `theme `, which calls reload() over --remote-expr on every +-- running nvim. +-- +-- Lives in ~/.local/bin/nvim-lua/, not ~/.config/nvim/lua/ (2026-09-17): that +-- directory is ONE out-of-store link into the checkout (home.nix +-- `home.file.".local/bin"`) and init.lua adds it to package.path before +-- plugins load, so require('theme') resolves the moment the checkout has this +-- file. Under nvim/lua/ it needed its own per-file link from a rebuild, and +-- lualine.lua (already linked) failed with "module 'theme' not found" on any +-- machine whose checkout was ahead of its switch. +local M = {} + +local FRAGMENT = vim.fn.expand('~/.config/theme/theme.lua') + +-- noir, verbatim, so nvim still looks right when the pointer is missing +-- (a checkout ahead of its switch on the client laptop). +local fallback = { + name = 'noir', polarity = 'dark', catppuccinFlavour = 'mocha', + ground = { base = '#000000', dim = '#000000', raised = '#000000', desktop = '#000000' }, + fg = '#EAECF0', fgDim = '#D3D7DE', comment = '#818898', muted = '#45475a', + red = '#F47B85', green = '#9BE963', yellow = '#f9e2af', blue = '#70B8FF', + magenta = '#CC7BF4', cyan = '#5EEDED', orange = '#FBAD60', + accent = '#70B8FF', brand = '#D97757', + selection = { bg = '#264F78', fg = '#EAECF0' }, + border = { active = '#555451', inactive = '#373735', urgent = '#F47B85' }, +} + +function M.palette() + local ok, t = pcall(dofile, FRAGMENT) + if ok and type(t) == 'table' and t.fg then + return t + end + return fallback +end + +-- catppuccin: flavour + colour overrides from the palette. transparent_background +-- keeps kitty's ground as the editor ground, so noir -> claude-dark needs no +-- editor change at all; only claude-light flips the flavour to latte. +function M.catppuccin_opts(T) + T = T or M.palette() + return { + flavour = T.catppuccinFlavour, + transparent_background = true, + color_overrides = { + [T.catppuccinFlavour] = { + base = T.ground.base, + mantle = T.ground.dim, + crust = T.ground.dim, + text = T.fg, + subtext1 = T.fgDim, + overlay1 = T.comment, + red = T.red, + green = T.green, + yellow = T.yellow, + blue = T.blue, + mauve = T.magenta, + peach = T.orange, + teal = T.cyan, + }, + }, + custom_highlights = function(colors) + local U = require('catppuccin.utils.colors') + local bg_amount = 0.095 + local rainbow = { + colors.blue, -- rainbow1 (H1) + colors.peach, -- rainbow2 (H2) + colors.green, -- rainbow3 (H3) + colors.teal, -- rainbow4 (H4) + colors.yellow, -- rainbow5 (H5) + colors.mauve, -- rainbow6 (H6) + } + local highlights = {} + for i, color in ipairs(rainbow) do + highlights['rainbow' .. i] = { fg = color } + highlights['RenderMarkdownH' .. i] = { fg = color, bold = true } + highlights['RenderMarkdownH' .. i .. 'Bg'] = { bg = U.darken(color, bg_amount, colors.base) } + end + highlights['RenderMarkdownCode'] = { bg = U.darken(colors.text, 0.05, colors.base) } + highlights['RenderMarkdownCodeInline'] = { fg = colors.peach } + highlights['RenderMarkdownBullet'] = { fg = colors.blue } + highlights['RenderMarkdownDash'] = { fg = colors.overlay1 } + highlights['RenderMarkdownQuote'] = { fg = colors.overlay1 } + highlights['RenderMarkdownLink'] = { fg = colors.blue } + highlights['RenderMarkdownChecked'] = { fg = colors.green } + highlights['RenderMarkdownUnchecked'] = { fg = colors.overlay1 } + highlights['RenderMarkdownTableHead'] = { fg = colors.blue } + highlights['RenderMarkdownTableRow'] = { fg = colors.subtext1 } + -- catppuccin's default Underlined has no fg (just the underline style), + -- so man pages, :help tags, etc. render in plain text color instead of + -- the Claude-blue used everywhere else for links/URLs. + highlights['Underlined'] = { fg = colors.blue, style = { 'underline' } } + return highlights + end, + integrations = { + render_markdown = true, + }, + } +end + +function M.apply_catppuccin(T) + require('catppuccin').setup(M.catppuccin_opts(T)) + vim.cmd.colorscheme('catppuccin') +end + +-- bufferline: the fill and the inactive-buffer strip take the theme ground. +function M.bufferline_opts(T) + T = T or M.palette() + return { + options = { + hover = { + enabled = true, + delay = 200, + reveal = { 'close' }, + }, + }, + highlights = { + fill = { bg = T.ground.base }, + background = { bg = T.ground.base }, + }, + } +end + +-- Re-read the fragment and re-apply everything. catppuccin's setup() does not +-- purge its own modules (that is what :CatppuccinCompile is for), so purge +-- first, then setup, recompile, and re-run the two plugins that consumed +-- palette values at their own setup time. +function M.reload() + local T = M.palette() + for name, _ in pairs(package.loaded) do + if name == 'catppuccin' or name:match('^catppuccin%.') then + package.loaded[name] = nil + end + end + require('catppuccin').setup(M.catppuccin_opts(T)) + pcall(vim.cmd, 'CatppuccinCompile') + vim.cmd.colorscheme('catppuccin') + local ok_b, bufferline = pcall(require, 'bufferline') + if ok_b then + pcall(bufferline.setup, M.bufferline_opts(T)) + end + local ok_l, lualine = pcall(require, 'plugins.lualine') + if ok_l and type(lualine) == 'table' and lualine.setup then + pcall(lualine.setup) + end + vim.notify('theme: ' .. T.name, vim.log.levels.INFO) +end + +return M diff --git a/home/dot_local/bin/theme b/home/dot_local/bin/theme new file mode 100755 index 000000000..e469b4250 --- /dev/null +++ b/home/dot_local/bin/theme @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# theme -- switch the dotfiles-wide colour theme at runtime, no rebuild. +# +# theme print the current theme +# theme list list the themes this generation rendered +# theme switch to (noir | claude-dark | claude-light) +# theme toggle cycle to the next theme in `theme list` order +# theme apply re-fire the reload hooks for the current theme +# theme icons re-pick the icon theme (polarity × wallpaper accent) +# +# Design: docs/theme-switcher-2026-09-17.md. Home Manager renders every theme's +# colour fragments under ~/.config/themes// (home/theme.nix, palettes in +# home/themes/*.nix). The RAW config files include ~/.config/theme/, +# where ~/.config/theme is a plain symlink this script retargets. A symlink +# retarget fires no file-watcher event, so every reload below is load-bearing. +set -euo pipefail + +cfg="${XDG_CONFIG_HOME:-$HOME/.config}" +link="$cfg/theme" +themes_dir="${THEMES_DIR:-$cfg/themes}" # THEMES_DIR: preview a render before its switch + +die() { printf 'theme: %s\n' "$*" >&2; exit 1; } + +# one switch at a time (F2 mashing, a login `apply` racing a manual switch) +exec 9> "${XDG_RUNTIME_DIR:-/tmp}/theme.lock" +flock -w 5 9 || die "another theme switch is still running" + +current() { + [[ -L "$link" ]] || return 1 + basename "$(readlink "$link")" +} + +list() { + [[ -d "$themes_dir" ]] || die "no rendered themes at $themes_dir (switch a generation with home/theme.nix first)" + find "$themes_dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort +} + +# meta is key=value, one per line, written by home/themes/default.nix. Read, +# never sourced. +meta() { + local key="$1" k v + while IFS='=' read -r k v; do + [[ "$k" == "$key" ]] && { printf '%s\n' "$v"; return 0; } + done < "$link/meta" + return 1 +} + +# --- reload hooks ----------------------------------------------------------- + +reload_kitty() { + # One abstract socket per instance: kitty.conf's `listen_on unix:@kitty-{kitty_pid}`. + # `load-config` with no file re-reads the running config, includes and all. + local sock n=0 + while read -r sock; do + kitten @ --to "unix:$sock" load-config >/dev/null 2>&1 && n=$((n + 1)) || true + done < <(awk '$NF ~ /^@kitty-[0-9]+$/ { print $NF }' /proc/net/unix | sort -u) + # a kitty without our listen_on (other config, mid-pull on the client) still + # reloads on SIGUSR1 — the Omarchy way; the socket path is just more precise. + if (( n == 0 )) && pkill -x -USR1 .kitty-wrapped 2>/dev/null; then + echo ' kitty: reloaded via SIGUSR1' + else + printf ' kitty: %d instance(s) reloaded\n' "$n" + fi +} + +reload_niri() { + if [[ -n "${NIRI_SOCKET:-}" ]] || compgen -G "${XDG_RUNTIME_DIR:-/run/user/$UID}/niri.*.sock" >/dev/null; then + if niri msg action load-config-file >/dev/null 2>&1; then + echo ' niri: config reloaded' + else + echo ' niri: reload failed (is a session running?)' >&2 + fi + fi +} + +reload_nvim() { + # nvim >= 0.9 listens on $XDG_RUNTIME_DIR/nvim..0. lua/theme.lua's + # reload() re-applies catppuccin, bufferline and lualine from the new fragment. + local sock n=0 + for sock in "${XDG_RUNTIME_DIR:-/run/user/$UID}"/nvim.*.0; do + [[ -S "$sock" ]] || continue + nvim --server "$sock" --remote-expr 'luaeval("require(\"theme\").reload() or \"ok\"")' >/dev/null 2>&1 && n=$((n + 1)) || true + done + printf ' nvim: %d instance(s) reloaded\n' "$n" +} + +# herdr: nothing to do. Its [theme] name = "terminal" paints every token from +# kitty's ANSI palette, and kitty reports the background change to it through +# DEC mode 2031 (CSI ?997;n) on reload, whereupon herdr re-queries OSC 10/11/4 +# and re-themes its chrome and every pane. Fish: conf.d/colors.fish re-sources +# the fragment at the next prompt when the pointer has moved. + +set_claude_code() { + # Claude Code reads ~/.claude.json at start, and rewrites it itself, so: + # tmp + rename, and a lost update against a running instance is accepted. + local want="$1" f="$HOME/.claude.json" tmp + [[ -f "$f" ]] || return 0 + command -v jq >/dev/null || return 0 + tmp="$(mktemp "$f.XXXXXX")" + if jq --arg t "$want" '.theme = $t' "$f" > "$tmp"; then + mv -T "$tmp" "$f" + printf ' claude code: theme=%s (takes effect on next start)\n' "$want" + else + rm -f "$tmp" + fi +} + +# GTK. GTK3 follows org.gnome.desktop.interface live (its Wayland backend reads +# gsettings/dconf — the schema is on XDG_DATA_DIRS since modules/common.nix +# 2026-09-17). libadwaita apps take color-scheme live and their gtk.css on +# next start (~/.config/gtk-4.0 points through ~/.config/theme, home/theme.nix). +# Chrome follows color-scheme through the portal. +gset() { gsettings set "$@" 2>/dev/null || printf ' gsettings: set %s failed\n' "$*" >&2; } + +apply_gtk() { + command -v gsettings >/dev/null || { echo ' gtk: no gsettings on PATH (pre-switch?)' >&2; return 0; } + local gtk_theme scheme + gtk_theme="$(meta gtk_theme || true)"; scheme="$(meta color_scheme || true)" + [[ -n "$gtk_theme" ]] || return 0 + gset org.gnome.desktop.interface gtk-theme "$gtk_theme" + gset org.gnome.desktop.wm.preferences theme "$gtk_theme" + gset org.gnome.desktop.interface color-scheme "$scheme" + printf ' gtk: %s, %s\n' "$gtk_theme" "$scheme" + apply_icons +} + +# MacTahoe[-]-{dark,light}: polarity from the theme, accent from the +# wallpaper (~/.local/state/wallpaper/accent, written by ~/.local/bin/wallpaper). +# Falls back to the stock blue folders when the accent variant is not installed. +apply_icons() { + command -v gsettings >/dev/null || return 0 + local suffix accent="" icons + suffix="$(meta icon_suffix || echo dark)" + [[ -r "${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper/accent" ]] \ + && read -r accent < "${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper/accent" + icons="MacTahoe-$suffix" + if [[ -n "$accent" ]]; then + local d + for d in "${XDG_DATA_HOME:-$HOME/.local/share}" ${XDG_DATA_DIRS//:/ }; do + [[ -d "$d/icons/MacTahoe-$accent-$suffix" ]] && { icons="MacTahoe-$accent-$suffix"; break; } + done + fi + gset org.gnome.desktop.interface icon-theme "$icons" + printf ' icons: %s\n' "$icons" +} + +apply() { + local name; name="$(current)" || die "no theme selected ($link missing)" + echo "theme: $name" + reload_kitty + reload_niri + reload_nvim + apply_gtk + set_claude_code "$(meta claude_code || echo dark)" +} + +switch_to() { + local name="$1" + [[ -d "$themes_dir/$name" && -f "$themes_dir/$name/meta" ]] \ + || die "unknown theme '$name' — try: $(list | tr '\n' ' ')" + # atomic retarget: build the new link beside the old one, then rename over it + ln -sfn "$themes_dir/$name" "$link.tmp" + mv -T "$link.tmp" "$link" + apply +} + +case "${1:-}" in + "") current || die "no theme selected" ;; + list) list ;; + apply) apply ;; + icons) [[ -L "$link" ]] || die "no theme selected"; apply_icons ;; + toggle) + mapfile -t names < <(list) + cur="$(current || true)"; next="${names[0]}" + for i in "${!names[@]}"; do + if [[ "${names[$i]}" == "$cur" ]]; then next="${names[$(( (i + 1) % ${#names[@]} ))]}"; fi + done + switch_to "$next" ;; + -h|--help) sed -n '2,11p' "$0" ;; + *) switch_to "$1" ;; +esac diff --git a/home/dot_local/bin/theme-prompt b/home/dot_local/bin/theme-prompt new file mode 100755 index 000000000..1265ad639 --- /dev/null +++ b/home/dot_local/bin/theme-prompt @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# F2 / Shift+F2 -> the theme picker and the accent picker: compact floating fzf +# prompts in the F1 (wake) / F9 (audio route) / F10 (sleep monitors) style. +# niri/binds.kdl runs `kitty --class theme-prompt -e ~/.local/bin/theme-prompt +# `, window-rules.kdl floats it. +# +# theme-prompt theme noir | claude-dark | claude-light -> ~/.local/bin/theme +# theme-prompt accent oat olive cactus sky fig heather coral +# -> ~/.local/bin/wallpaper (wallpaper + folder colour) +# +# Esc is a clean cancel. Everything a pick triggers is live (kitty, niri, +# herdr, fish, GTK3, folder colours); GTK4 apps and agent TUIs re-theme on +# their next start. +set -euo pipefail + +mode="${1:-theme}" + +mark() { [[ "$1" == "$2" ]] && echo '●' || echo ' '; } + +case "$mode" in + theme) + cur="$(theme 2>/dev/null || echo '-')" + rows() { + local t + for t in $(theme list 2>/dev/null || printf 'noir\nclaude-dark\nclaude-light\n'); do + printf '%-13s %s\n' "$t" "$(mark "$t" "$cur")" + done + } + prompt='theme ❯ ' ;; + accent) + cur="olive" + [[ -r "${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper/accent" ]] \ + && read -r cur < "${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper/accent" + rows() { + local a + for a in $(wallpaper list); do + printf '%-13s %s\n' "$a" "$(mark "$a" "$cur")" + done + } + prompt='accent ❯ ' ;; + *) printf 'theme-prompt: theme | accent\n' >&2; exit 1 ;; +esac + +pick="$(rows | fzf --prompt="$prompt" --header='● = current Esc cancels' --no-info --reverse --no-sort || true)" +[[ -n "$pick" ]] || exit 0 +name="$(awk '{print $1}' <<< "$pick")" + +case "$mode" in + theme) theme "$name" ;; + accent) wallpaper "$name" ;; +esac diff --git a/home/dot_local/bin/wallpaper b/home/dot_local/bin/wallpaper index c23ec33b5..aa4425494 100755 --- a/home/dot_local/bin/wallpaper +++ b/home/dot_local/bin/wallpaper @@ -1,14 +1,52 @@ #!/usr/bin/env bash -# wallpaper -- set desktop wallpaper -# Uses swaybg. Home Manager deploys the default repository wallpaper to -# ~/.local/share/wallpapers; this script re-points it at runtime. -DEFAULT="$HOME/.local/share/wallpapers/wallpaper.jpg" -path="${1:-$DEFAULT}" - -if [[ ! -f "$path" ]]; then - echo "File not found: $path" >&2 - exit 1 -fi - -pkill -x swaybg -setsid -f swaybg -i "$path" -m fill +# wallpaper -- set the desktop wallpaper (swaybg) and remember it. +# +# wallpaper restore the remembered wallpaper (login; default: olive) +# wallpaper one of the claude.ai/imagine themes rendered at 5120x2880: +# oat olive cactus sky fig heather coral +# wallpaper any image file, one-off (not an accent) +# wallpaper list +# +# The seven accents are the exact recovered claude.ai/imagine artwork +# (~/colors/waves/capture/imagine-background.svg) rasterised under Anthropic's +# own CSS recipe on each theme's ground (~/colors/waves/STATE.md). Home Manager +# deploys them to ~/.local/share/wallpapers. The accent is remembered in +# ~/.local/state/wallpaper/accent and also drives the folder colour: `theme +# icons` picks the MacTahoe--{dark,light} icon variant whose folders +# are that wallpaper's darker ground (pkgs/mactahoe-icon-theme.nix). +set -euo pipefail + +dir="$HOME/.local/share/wallpapers" +state="${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper" +accents=(oat olive cactus sky fig heather coral) +default_accent="olive" + +die() { printf 'wallpaper: %s\n' "$*" >&2; exit 1; } +is_accent() { local a; for a in "${accents[@]}"; do [[ "$a" == "$1" ]] && return 0; done; return 1; } +accent_path() { printf '%s/imagine-%s-5120x2880.png\n' "$dir" "$1"; } + +show() { + local path="$1" + [[ -f "$path" ]] || die "file not found: $path" + pkill -x swaybg || true + setsid -f swaybg -i "$path" -m fill +} + +case "${1:-}" in + list) printf '%s\n' "${accents[@]}" ;; + "") + accent="$default_accent" + [[ -r "$state/accent" ]] && read -r accent < "$state/accent" + is_accent "$accent" || accent="$default_accent" + show "$(accent_path "$accent")" ;; + *) + if is_accent "$1"; then + show "$(accent_path "$1")" + mkdir -p "$state" + printf '%s\n' "$1" > "$state/accent.tmp" && mv -T "$state/accent.tmp" "$state/accent" + # folders follow the wallpaper (no-op until the switch that ships `theme`) + command -v theme >/dev/null && theme icons || true + else + show "$1" + fi ;; +esac diff --git a/home/dot_local/share/wallpapers/imagine-cactus-5120x2880.png b/home/dot_local/share/wallpapers/imagine-cactus-5120x2880.png new file mode 100644 index 000000000..1bff4a8a9 Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-cactus-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-coral-5120x2880.png b/home/dot_local/share/wallpapers/imagine-coral-5120x2880.png new file mode 100644 index 000000000..6507b87ef Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-coral-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-fig-5120x2880.png b/home/dot_local/share/wallpapers/imagine-fig-5120x2880.png new file mode 100644 index 000000000..fbd7987f0 Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-fig-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-heather-5120x2880.png b/home/dot_local/share/wallpapers/imagine-heather-5120x2880.png new file mode 100644 index 000000000..1b79c799c Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-heather-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-oat-5120x2880.png b/home/dot_local/share/wallpapers/imagine-oat-5120x2880.png new file mode 100644 index 000000000..40edab649 Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-oat-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-olive-5120x2880.png b/home/dot_local/share/wallpapers/imagine-olive-5120x2880.png new file mode 100644 index 000000000..fc441e7c0 Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-olive-5120x2880.png differ diff --git a/home/dot_local/share/wallpapers/imagine-sky-5120x2880.png b/home/dot_local/share/wallpapers/imagine-sky-5120x2880.png new file mode 100644 index 000000000..64f7ce92c Binary files /dev/null and b/home/dot_local/share/wallpapers/imagine-sky-5120x2880.png differ diff --git a/home/home.nix b/home/home.nix index 9d1c0988b..023f07d84 100644 --- a/home/home.nix +++ b/home/home.nix @@ -69,17 +69,12 @@ let configDirs = [ "niri" "kitty" + "ghostty" # kitty.conf's twin, read by libghostty in cmux Browser's panes "fish" "starship" - "zathura" "yt-dlp" "kanshi" "qt6ct" - # NOT cliamp: it writes its control socket, pidfile, log, play history and - # resume state beside its config, so a whole-dir link put all of that into - # the working tree (resume.json, carrying a Navidrome stream URL with its - # Subsonic token, was even committed). ~/.config/cliamp is a real directory - # now and only its config is linked, file by file, below. (2026-09-13) ]; # Python interpreter backing the niri helper bin/ scripts (wifi-menu, fzf-nmcli, …). @@ -133,6 +128,7 @@ in ./seat-feeder.nix ./ssh.nix ./tally.nix + ./theme.nix ./tally-filler.nix ./tally-pump.nix ./tally-uplink.nix @@ -160,44 +156,11 @@ in # --------------------------------------------------------------------------- # RAW configs (whole-dir per ~/.config/). # --------------------------------------------------------------------------- - # cliamp's one-time move from a whole-dir link to a real directory - # (2026-09-14). Home Manager cannot make that move by itself: its cleanup - # keeps the old ~/.config/cliamp link (the new generation still has a - # .config/cliamp path), and its link step then backs up config.toml and - # themes/ THROUGH that link, i.e. inside the repo checkout, and writes - # store symlinks there that resolve back to themselves. Reproduced with this - # Home Manager's own check-link-targets/cleanup/link scripts in a scratch - # HOME. So before linkGeneration, drop the old link when it points into a - # Home Manager generation, create the real directory, and carry cliamp's - # history.toml and resume.json across so nothing is lost. Idempotent: once - # ~/.config/cliamp is a real directory the test is false. Removable once - # every host has switched past it. - home.activation.cliampRealDir = lib.hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] '' - cliamp_dir="$HOME/.config/cliamp" - if [[ -L "$cliamp_dir" && "$(readlink "$cliamp_dir")" == $(readlink -e /nix/store)/*-home-manager-files/* ]]; then - cliamp_old="$(readlink -e "$cliamp_dir" || true)" - run rm $VERBOSE_ARG "$cliamp_dir" - run mkdir -p $VERBOSE_ARG "$cliamp_dir" - if [[ -n "$cliamp_old" ]]; then - for f in history.toml resume.json; do - if [[ -f "$cliamp_old/$f" && ! -e "$cliamp_dir/$f" ]]; then - run cp -p $VERBOSE_ARG "$cliamp_old/$f" "$cliamp_dir/$f" - fi - done - fi - fi - ''; - xdg.configFile = lib.genAttrs configDirs (d: { source = link "dot_config/${d}"; }) // { - # cliamp: config and themes only, so the directory itself stays writable - # for the runtime files (see the note in configDirs above). - "cliamp/config.toml".source = link "dot_config/cliamp/config.toml"; - "cliamp/themes".source = link "dot_config/cliamp/themes"; - # kitty/ is a whole-dir out-of-store symlink, so the store-path fragment can't # nest inside it — emit at a neutral path; kitty.conf includes it by absolute # (env-expanded) path. @@ -271,26 +234,18 @@ in '' // GENERATED per-host (home.nix). No host-specific niri config on ${hostName}. ''; - } - // ( - # GTK4 / libadwaita apps (Nautilus) ignore gtk-theme-name; the only override - # they honor is user CSS at ~/.config/gtk-4.0/. Link MacTahoe's gtk-4.0 assets - # there so Nautilus renders the theme from first boot — home-manager's gtk - # module does not do this, which is why nwg-look was needed before. - let - theme4 = "${pkgs.mactahoe-gtk-theme}/share/themes/MacTahoe-Dark-grey/gtk-4.0"; - in - { - "gtk-4.0/gtk.css".source = "${theme4}/gtk.css"; - "gtk-4.0/gtk-dark.css".source = "${theme4}/gtk-dark.css"; - "gtk-4.0/assets".source = "${theme4}/assets"; - } - ); - - # Belt-and-suspenders for any gsettings-aware app (agrees with GTK_THEME env). + }; + # (The gtk-4.0/{gtk.css,gtk-dark.css,assets} links that used to sit here — a + # store symlink of MacTahoe-Dark-grey's gtk-4.0 — moved to home/theme.nix, + # where they point through the ~/.config/theme pointer at whichever theme's + # MacTahoe variant is selected. 2026-09-17.) + + # gtk-theme / icon-theme / color-scheme are deliberately NOT pinned here any + # more (2026-09-17): the theme switcher owns them at runtime (`theme apply` + # → gsettings, docs/theme-switcher-2026-09-17.md), and a pinned value would + # snap a light session back to Dark on every switch. dconf keeps whatever + # `theme` last wrote. Fonts stay. dconf.settings."org/gnome/desktop/interface" = { - gtk-theme = "MacTahoe-Dark-grey"; - color-scheme = "prefer-dark"; # Interface fonts for Nautilus and every other GTK app that reads # font-name. sf-pro ships system-wide via modules/common.nix fonts.packages # (the one Apple family kept in the 2026-08-21 sweep — "too good to @@ -553,7 +508,6 @@ in mpv imv vlc - zathura ffmpeg-full ffmpegthumbnailer @@ -591,7 +545,6 @@ in pkgs.crm # vendored personal CRM CLI; data stays at its built-in notes path pkgs.dcal # vendored calendar CLI; data lives under XDG, nothing in git music-acquire # evidence-gated SoundCloud → YouTube → capture acquisition - cliamp # terminal music player → navidrome. overlay pkg, see pkgs/cliamp.nix uv # Astral Python pkg/project manager. "hot" overlay pkg — rides nixpkgs-fresh HEAD (flake.nix), so it stays latest independent of the main pin. # artifact system (md-artifact / presentation-beta / publish-artifact skills; diff --git a/home/nvim.nix b/home/nvim.nix index 6a45206b0..7d2b15649 100644 --- a/home/nvim.nix +++ b/home/nvim.nix @@ -179,8 +179,10 @@ let "lua/plugins/neoscroll.lua" "lua/plugins/nvim-tree.lua" "lua/plugins/telescope.lua" - "lua/plugins/bufferline.lua" "lua/plugins/lualine.lua" + # NOT lua/theme.lua: the theme switcher's palette loader lives in + # ~/.local/bin/nvim-lua/ (a whole-dir RAW link on init.lua's package.path), + # so a checkout pulled ahead of its switch still finds it. See its header. ]; # every binary the nvim config shells out to (replaces mason; covers all plugin CLIs) diff --git a/home/nvim/plugins.lua.in b/home/nvim/plugins.lua.in index fc22f4eed..21e656dde 100644 --- a/home/nvim/plugins.lua.in +++ b/home/nvim/plugins.lua.in @@ -121,23 +121,8 @@ require("lazy").setup({ 'akinsho/bufferline.nvim', dir = dir('bufferline.nvim'), config = function() - require('bufferline').setup({ - options = { - hover = { - enabled = true, - delay = 200, - reveal = {'close'} - } - }, - highlights = { - fill = { - bg = "#000000" - }, - background = { - bg = "#000000" - } - } - }) + -- options + theme-ground highlights from lua/theme.lua (see catppuccin above) + require('bufferline').setup(require('theme').bufferline_opts()) end }, { @@ -145,63 +130,10 @@ require("lazy").setup({ name = 'catppuccin', dir = dir('catppuccin'), config = function() - require('catppuccin').setup({ - flavour = "mocha", - transparent_background = true, - color_overrides = { - mocha = { - base = "#000000", - mantle = "#000000", - crust = "#000000", - text = "#EAECF0", - subtext1 = "#D3D7DE", - overlay1 = "#818898", - red = "#F47B85", - green = "#9BE963", - blue = "#70B8FF", - mauve = "#CC7BF4", - peach = "#FBAD60", - teal = "#5EEDED", - }, - }, - custom_highlights = function(colors) - local U = require("catppuccin.utils.colors") - local bg_amount = 0.095 - local rainbow = { - colors.blue, -- rainbow1 (H1) - colors.peach, -- rainbow2 (H2) - colors.green, -- rainbow3 (H3) - colors.teal, -- rainbow4 (H4) - colors.yellow, -- rainbow5 (H5) - colors.mauve, -- rainbow6 (H6) - } - local highlights = {} - for i, color in ipairs(rainbow) do - highlights["rainbow" .. i] = { fg = color } - highlights["RenderMarkdownH" .. i] = { fg = color, bold = true } - highlights["RenderMarkdownH" .. i .. "Bg"] = { bg = U.darken(color, bg_amount, colors.base) } - end - highlights["RenderMarkdownCode"] = { bg = U.darken(colors.text, 0.05, colors.base) } - highlights["RenderMarkdownCodeInline"] = { fg = colors.peach } - highlights["RenderMarkdownBullet"] = { fg = colors.blue } - highlights["RenderMarkdownDash"] = { fg = colors.overlay1 } - highlights["RenderMarkdownQuote"] = { fg = colors.overlay1 } - highlights["RenderMarkdownLink"] = { fg = colors.blue } - highlights["RenderMarkdownChecked"] = { fg = colors.green } - highlights["RenderMarkdownUnchecked"] = { fg = colors.overlay1 } - highlights["RenderMarkdownTableHead"] = { fg = colors.blue } - highlights["RenderMarkdownTableRow"] = { fg = colors.subtext1 } - -- catppuccin's default Underlined has no fg (just the underline style), - -- so man pages, :help tags, etc. render in plain text color instead of - -- the Claude-blue used everywhere else for links/URLs. - highlights["Underlined"] = { fg = colors.blue, style = { "underline" } } - return highlights - end, - integrations = { - render_markdown = true, - }, - }) - vim.cmd.colorscheme "catppuccin" + -- flavour, colour overrides and custom highlights all come from the RAW + -- lua/theme.lua, which reads ~/.config/theme/theme.lua (the theme + -- switcher's fragment) and can re-apply it live via reload(). + require('theme').apply_catppuccin() end }, { diff --git a/home/theme.nix b/home/theme.nix new file mode 100644 index 000000000..6b985b431 --- /dev/null +++ b/home/theme.nix @@ -0,0 +1,77 @@ +{ + config, + lib, + pkgs, + ... +}: +# Theme switcher — the Nix half (2026-09-17, docs/theme-switcher-2026-09-17.md). +# +# Every theme's color fragments are written under ~/.config/themes// in +# one generation (store-managed, re-emitted on switch). The RAW config files +# include ~/.config/theme/ — `theme`, singular, a plain symlink into +# `themes/` that ~/.local/bin/theme retargets at runtime and Home Manager never +# writes. Disjoint namespaces: HM owns the plural, the switcher owns the +# singular, so HM's cleanup can never eat the pointer and the pointer can never +# confuse HM's link step. Switching a theme therefore needs no rebuild; only +# adding a theme or changing a palette value does. +# +# The two precedents this copies: kitty-scrollback-nix.conf and niri-local.kdl +# (home.nix), generated files at a neutral ~/.config path pulled in by an +# absolute include from inside a whole-dir RAW symlink. +let + themes = import ./themes { inherit lib; }; + cfgHome = config.xdg.configHome; + # GTK4 / libadwaita apps ignore gtk-theme-name; the only override they honour + # is user CSS at ~/.config/gtk-4.0/. Each theme dir carries its MacTahoe + # variant's gtk-4.0/ (a store path), and ~/.config/gtk-4.0/{gtk.css, + # gtk-dark.css,assets} point THROUGH the ~/.config/theme pointer at it, so + # the one symlink flip re-themes GTK4 too (on the app's next start; GTK4 + # reads gtk.css once). GTK3 follows `gsettings gtk-theme` live instead. + gtk4Dirs = lib.mapAttrs' ( + name: t: + lib.nameValuePair "themes/${name}/gtk-4.0" { + source = "${pkgs.${t.gtk.package}}/share/themes/${t.gtk.theme}/gtk-4.0"; + } + ) themes.checked; + viaPointer = f: { + source = config.lib.file.mkOutOfStoreSymlink "${cfgHome}/theme/gtk-4.0/${f}"; + }; +in +{ + xdg.configFile = + lib.mapAttrs (_: text: { inherit text; }) themes.fragments + // gtk4Dirs + // { + "gtk-4.0/gtk.css" = viaPointer "gtk.css"; + "gtk-4.0/gtk-dark.css" = viaPointer "gtk-dark.css"; + "gtk-4.0/assets" = viaPointer "assets"; + }; + + # Every theme's GTK package must be on the profile so GTK finds + # share/themes/ through XDG_DATA_DIRS (home.nix's gtk.theme.package + # covers noir's; the claude variants ride along here). + # (unique on the attr NAMES: lib.unique on derivations compares attrsets and + # recurses forever.) + home.packages = map (n: pkgs.${n}) (lib.unique (lib.mapAttrsToList (_: t: t.gtk.package) themes.checked)); + + # Bootstrap and self-heal the pointer, never override a choice: + # - missing or dangling → noir + # - pointing outside ~/.config/themes/ (a pre-switch preview render) → the + # theme of the same name if it exists, else noir + # - already a live theme → untouched + home.activation.themePointer = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + theme_link="${cfgHome}/theme" + themes_dir="${cfgHome}/themes" + want="" + if [[ ! -e "$theme_link" ]]; then + want="noir" + elif [[ "$(readlink -f "$theme_link")" != "$themes_dir"/* ]]; then + want="$(basename "$(readlink "$theme_link")")" + [[ -d "$themes_dir/$want" ]] || want="noir" + fi + if [[ -n "$want" ]]; then + run ln -sfn $VERBOSE_ARG "$themes_dir/$want" "$theme_link.tmp" + run mv -T $VERBOSE_ARG "$theme_link.tmp" "$theme_link" + fi + ''; +} diff --git a/home/themes/claude-dark.nix b/home/themes/claude-dark.nix new file mode 100644 index 000000000..c83fb56ce --- /dev/null +++ b/home/themes/claude-dark.nix @@ -0,0 +1,34 @@ +# claude-dark — Claude's own dark mode, as claude.ai ships it. +# +# Same 12-role accent palette as noir (they are the same palette — Claude Dark +# was recovered from the app bundle and Tom's hand-sampled noir accents matched +# it bit for bit, ~/colors/waves/capture/claude-code-theme/README-THEMES.md). +# What changes is the ground: claude.ai's code blocks sit on #1A1A1A (--bg-000 +# at 50% over the page) and the page itself on #20201F (--bg-000 dark), with +# #151515 (--bg-100) one step below. Sources: claude-tokens-dark.json. +let + noir = import ./noir.nix; +in +noir +// { + name = "claude-dark"; + ground = { + base = "#1A1A1A"; # code-block ground: the closest analogue to a terminal + dim = "#151515"; # --bg-100 + raised = "#20201F"; # --bg-000 + desktop = "#20201F"; # the desktop reads one step above the terminal + }; + muted = "#52514E"; # --_gray-600 + cursorText = "#1A1A1A"; + ansi = noir.ansi // { + black = "#1A1A1A"; + }; + + # GTK: MacTahoe recoloured to Claude's dark tokens, clay accent + # (pkgs/mactahoe-gtk-theme.nix, variant claude). + gtk = { + package = "mactahoe-claude-gtk-theme"; + theme = "MacTahoe-Claude-Dark-orange"; + colorScheme = "prefer-dark"; + }; +} diff --git a/home/themes/claude-light.nix b/home/themes/claude-light.nix new file mode 100644 index 000000000..b627fa929 --- /dev/null +++ b/home/themes/claude-light.nix @@ -0,0 +1,75 @@ +# claude-light — Claude's own light mode, as claude.ai ships it. +# +# Accents are the Claude Light 12-role syntax palette from the app bundle +# (~/colors/waves/capture/claude-code-theme/claude-themes-SOURCE.json); grounds +# and chrome are the light-mode CSS tokens (claude-tokens-light.json). Claude's +# theme defines NO terminal.ansi* colors, so the 16-slot mapping below is +# designed, not copied — every slot passes WCAG AA for normal text on #FFFFFF +# (~/colors/theme-switcher-design.md §5). Tune in ~/colors/colorlab. +# +# color0/color7/color15 follow the noir convention (dark text slots, since herdr's +# `terminal` theme uses White/Gray as FOREGROUNDS) rather than Solarized's +# light-grey convention. The price, inherent to every light terminal theme: +# anything that paints bright-white-on-black gets #131313 on #2B303B. +{ + name = "claude-light"; + polarity = "light"; + catppuccinFlavour = "latte"; + claudeCode = "light"; + + ground = { + base = "#F9F9F7"; # --bg-100, the app's main surface + dim = "#F3F3F0"; # --bg-200 + raised = "#FFFFFF"; # --bg-000 + desktop = "#F0EFEC"; # --bg-300 + }; + + fg = "#131313"; # --text-000 (Claude Light syntax fg is #14181F, one step away) + fgDim = "#52514E"; # --cds-text-secondary light + comment = "#6E7687"; # Claude Light `comment` + muted = "#A5A49A"; # --_gray-300 + + red = "#B80A18"; # property + green = "#008000"; # string + yellow = "#A66A00"; # --warning-100 — the palette has no yellow role + blue = "#0051C2"; # function + magenta = "#8100C2"; # keyword + cyan = "#007F80"; # number + orange = "#B24A00"; # variable + + accent = "#0051C2"; + brand = "#D97757"; + + ansi = { + black = "#2B303B"; # punctuation + white = "#52514E"; # --cds-text-secondary + brightBlack = "#6E7687"; # comment + brightWhite = "#131313"; # fg + }; + + selection = { + bg = "#CDE2FB"; # --accent-900 light: Claude's own selection tint + fg = "#131313"; + }; + cursor = "#131313"; + cursorText = "#F9F9F7"; + + border = { + active = "#A5A49A"; # --_gray-300 + inactive = "#D2D1C7"; # --_gray-150 + urgent = "#B80A18"; + }; + tabIndicator = { + active = "#8100C2"; + inactive = "#97958D"; # --text-400 light + }; + insertHint = "#8100C280"; + + # GTK: MacTahoe recoloured to Claude's light tokens, clay accent + # (pkgs/mactahoe-gtk-theme.nix, variant claude). + gtk = { + package = "mactahoe-claude-gtk-theme"; + theme = "MacTahoe-Claude-Light-orange"; + colorScheme = "prefer-light"; + }; +} diff --git a/home/themes/default.nix b/home/themes/default.nix new file mode 100644 index 000000000..9d87628bb --- /dev/null +++ b/home/themes/default.nix @@ -0,0 +1,223 @@ +# The palette source of truth and the per-consumer renderers. +# +# Three hand-curated themes, no generation from wallpapers: each *.nix here is a +# plain attrset keyed by ROLE (fg, comment, red, ground.base, …), never by +# consumer. `render` turns one palette into the color FRAGMENT each app +# includes; home/theme.nix writes every fragment for every theme under +# ~/.config/themes// and the RAW config files join theirs through the +# app's own include of ~/.config/theme/, a symlink ~/.local/bin/theme +# flips at runtime. Design: docs/theme-switcher-2026-09-17.md. +# +# Pure: `{ lib }` only, so a preview can be rendered with a bare `nix eval` +# before any switch (see the doc's "Testing before a switch"). +{ lib }: +let + header = t: mark: "${mark} GENERATED by home/theme.nix from home/themes/${t.name}.nix — edit the palette, not this file."; + + kitty = t: '' + ${header t "#"} + # Joined by kitty.conf's `include ''${HOME}/.config/theme/kitty.conf`. + background ${t.ground.base} + foreground ${t.fg} + selection_background ${t.selection.bg} + selection_foreground ${t.selection.fg} + cursor ${t.cursor} + cursor_text_color ${t.cursorText} + cursor_trail_color ${t.accent} + url_color ${t.accent} + + # black + color0 ${t.ansi.black} + color8 ${t.ansi.brightBlack} + # red + color1 ${t.red} + color9 ${t.red} + # green + color2 ${t.green} + color10 ${t.green} + # yellow + color3 ${t.yellow} + color11 ${t.yellow} + # blue + color4 ${t.blue} + color12 ${t.blue} + # magenta + color5 ${t.magenta} + color13 ${t.magenta} + # cyan + color6 ${t.cyan} + color14 ${t.cyan} + # white + color7 ${t.ansi.white} + color15 ${t.ansi.brightWhite} + + # tabs + active_tab_background ${t.ground.base} + active_tab_foreground ${t.fg} + inactive_tab_background ${t.ground.dim} + inactive_tab_foreground ${t.comment} + tab_bar_background ${t.ground.dim} + + # marks + mark1_foreground ${t.ground.base} + mark1_background ${t.magenta} + + # splits: the divider matches niri's INACTIVE window border, so a kitty + # split reads as a quiet niri-native divider (Tom 2026-07-15). + active_border_color ${t.border.inactive} + inactive_border_color ${t.ground.base} + ''; + + # Ghostty (libghostty inside cmux Browser's terminal panes): the kitty + # fragment, key for key, in Ghostty's `key = value` grammar. + ghostty = t: '' + ${header t "#"} + # Joined by ghostty/config.ghostty's `config-file = ?/home/tom/.config/theme/ghostty`. + background = ${t.ground.base} + foreground = ${t.fg} + selection-background = ${t.selection.bg} + selection-foreground = ${t.selection.fg} + cursor-color = ${t.cursor} + cursor-text = ${t.cursorText} + + # black + palette = 0=${t.ansi.black} + palette = 8=${t.ansi.brightBlack} + # red + palette = 1=${t.red} + palette = 9=${t.red} + # green + palette = 2=${t.green} + palette = 10=${t.green} + # yellow + palette = 3=${t.yellow} + palette = 11=${t.yellow} + # blue + palette = 4=${t.blue} + palette = 12=${t.blue} + # magenta + palette = 5=${t.magenta} + palette = 13=${t.magenta} + # cyan + palette = 6=${t.cyan} + palette = 14=${t.cyan} + # white + palette = 7=${t.ansi.white} + palette = 15=${t.ansi.brightWhite} + + # splits: as kitty, the divider is niri's INACTIVE window border. + split-divider-color = ${t.border.inactive} + unfocused-split-fill = ${t.ground.base} + ''; + + niri = t: '' + ${header t "//"} + // Joined by niri/config.kdl's `include optional=true "~/.config/theme/niri.kdl"`, + // LAST, so these merge over layout.kdl / misc.kdl (niri merges sections + // across includes; later wins per property — wiki, Configuration: Include). + layout { + border { + on + active-color "${t.border.active}" + inactive-color "${t.border.inactive}" + urgent-color "${t.border.urgent}" + } + tab-indicator { + active-color "${t.tabIndicator.active}" + inactive-color "${t.tabIndicator.inactive}" + urgent-color "${t.border.urgent}" + } + insert-hint { + color "${t.insertHint}" + } + background-color "${t.ground.desktop}" + } + overview { + backdrop-color "${t.ground.desktop}" + } + ''; + + # fish wants bare hex, no '#'. + bare = c: lib.removePrefix "#" c; + fish = t: '' + ${header t "#"} + # Sourced by fish/conf.d/colors.fish (which also re-sources it when the + # ~/.config/theme symlink moves). Roles follow the nvim/kitty mapping. + set -g fish_color_normal ${bare t.fg} + set -g fish_color_command ${bare t.blue} + set -g fish_color_param ${bare t.green} + set -g fish_color_keyword ${bare t.red} + set -g fish_color_quote ${bare t.green} + set -g fish_color_end ${bare t.orange} + set -g fish_color_comment ${bare t.comment} + set -g fish_color_error ${bare t.red} + set -g fish_color_option ${bare t.green} + set -g fish_color_cancel ${bare t.red} + set -g fish_color_cwd ${bare t.yellow} + set -g fish_color_user ${bare t.cyan} + set -g fish_color_host ${bare t.blue} + set -g fish_color_host_remote ${bare t.green} + set -g fish_color_status ${bare t.red} + set -g fish_pager_color_completion ${bare t.fg} + ''; + + q = s: ''"${s}"''; + lua = t: '' + ${header t "--"} + -- dofile()'d by nvim/lua/theme.lua; returns the palette as a Lua table. + return { + name = ${q t.name}, + polarity = ${q t.polarity}, + catppuccinFlavour = ${q t.catppuccinFlavour}, + ground = { base = ${q t.ground.base}, dim = ${q t.ground.dim}, raised = ${q t.ground.raised}, desktop = ${q t.ground.desktop} }, + fg = ${q t.fg}, fgDim = ${q t.fgDim}, comment = ${q t.comment}, muted = ${q t.muted}, + red = ${q t.red}, green = ${q t.green}, yellow = ${q t.yellow}, blue = ${q t.blue}, + magenta = ${q t.magenta}, cyan = ${q t.cyan}, orange = ${q t.orange}, + accent = ${q t.accent}, brand = ${q t.brand}, + selection = { bg = ${q t.selection.bg}, fg = ${q t.selection.fg} }, + border = { active = ${q t.border.active}, inactive = ${q t.border.inactive}, urgent = ${q t.border.urgent} }, + } + ''; + + # key=value, read by ~/.local/bin/theme with `read`, never sourced. + # icon_suffix: the MacTahoe icon dirs are MacTahoe[-]-{dark,light}; + # the accent comes from `wallpaper`, the polarity from here. + meta = t: '' + name=${t.name} + polarity=${t.polarity} + claude_code=${t.claudeCode} + gtk_theme=${t.gtk.theme} + color_scheme=${t.gtk.colorScheme} + icon_suffix=${if t.polarity == "light" then "light" else "dark"} + ''; +in +rec { + themes = { + noir = import ./noir.nix; + claude-dark = import ./claude-dark.nix; + claude-light = import ./claude-light.nix; + }; + + # Every theme must fill every role: a missing key would surface as a Nix + # eval error deep in a string interpolation, so check up front. + roles = [ "name" "polarity" "catppuccinFlavour" "claudeCode" "ground" "fg" "fgDim" "comment" "muted" + "red" "green" "yellow" "blue" "magenta" "cyan" "orange" "accent" "brand" "ansi" "selection" + "cursor" "cursorText" "border" "tabIndicator" "insertHint" "gtk" ]; + checked = lib.mapAttrs (n: t: + let missing = lib.filter (r: !(t ? ${r})) roles; + in if missing == [ ] && t.name == n then t + else throw "home/themes/${n}.nix: name=${t.name}, missing roles: ${toString missing}") themes; + + render = t: { + "kitty.conf" = kitty t; + "ghostty" = ghostty t; + "niri.kdl" = niri t; + "colors.fish" = fish t; + "theme.lua" = lua t; + "meta" = meta t; + }; + + # { "themes//" = text; } for every theme — what theme.nix emits. + fragments = lib.concatMapAttrs (name: t: + lib.mapAttrs' (f: text: lib.nameValuePair "themes/${name}/${f}" text) (render t)) checked; +} diff --git a/home/themes/noir.nix b/home/themes/noir.nix new file mode 100644 index 000000000..2df31e362 --- /dev/null +++ b/home/themes/noir.nix @@ -0,0 +1,71 @@ +# noir — Tom's catppuccin-noir: Claude Dark's accents on pure OLED black. +# +# The accents are Claude Dark's 12-role syntax palette, bit for bit +# (~/colors/waves/capture/claude-code-theme/claude-themes-SOURCE.json, extracted +# from the claude.ai bundle). The grounds are #000000 — panel-off black on the +# Zenbook's OLED, and the habit the whole colorscheme investigation started from +# (~/colors/HANDOFF.md §1). noir differs from claude-dark ONLY in `ground` and +# `muted`; everything else is shared by construction, see claude-dark.nix. +{ + name = "noir"; + polarity = "dark"; + catppuccinFlavour = "mocha"; + claudeCode = "dark"; # ~/.claude.json "theme" + + ground = { + base = "#000000"; # terminal / editor background + dim = "#000000"; # tab bar, one step below base + raised = "#000000"; # popups, one step above base + desktop = "#000000"; # niri background-color + overview backdrop + }; + + fg = "#EAECF0"; # Claude Dark `fg` + fgDim = "#D3D7DE"; # Claude Dark `punctuation` + comment = "#818898"; # Claude Dark `comment` + muted = "#45475a"; # inactive UI text (catppuccin mocha surface1, as bufferline had it) + + red = "#F47B85"; # property + green = "#9BE963"; # string + yellow = "#f9e2af"; # catppuccin mocha yellow — Claude's palette has no yellow role + blue = "#70B8FF"; # function + magenta = "#CC7BF4"; # keyword + cyan = "#5EEDED"; # number + orange = "#FBAD60"; # variable + + accent = "#70B8FF"; # URLs, cursor trail + brand = "#D97757"; # Anthropic clay — identical in Claude's light and dark tokens + + # The 16 ANSI slots. Bright 1–6 equal normal 1–6: Claude's palette has exactly + # one hue per role, and that is how kitty.conf has always been. + ansi = { + black = "#000000"; + white = "#D3D7DE"; + brightBlack = "#818898"; + brightWhite = "#EAECF0"; + }; + + selection = { + bg = "#264F78"; # Claude Code's TUI selection highlight (also VS Code's default) + fg = "#EAECF0"; + }; + cursor = "#EAECF0"; + cursorText = "#000000"; + + border = { + active = "#555451"; + inactive = "#373735"; + urgent = "#F47B85"; + }; + tabIndicator = { + active = "#CC7BF4"; + inactive = "#818898"; + }; + insertHint = "#CC7BF480"; + + # GTK: the OLED-black MacTahoe (pkgs/mactahoe-gtk-theme.nix, variant oled). + gtk = { + package = "mactahoe-gtk-theme"; # pkgs attr, resolved in home/theme.nix + theme = "MacTahoe-Dark-grey"; + colorScheme = "prefer-dark"; + }; +} diff --git a/hosts/coordinator/disko.nix b/hosts/coordinator/disko.nix index 7140edc8e..e0f067d0d 100644 --- a/hosts/coordinator/disko.nix +++ b/hosts/coordinator/disko.nix @@ -64,6 +64,17 @@ # Wiped blank before the physical move (no partition table, no bootloader), # then laid out here. # + # ── OWNERSHIP (Tom, 2026-09-19) ───────────────────────────────────────────── + # THIS DISK BELONGS TO SODIMO. It is one of exactly two sodimo-owned items in + # the fleet, the other being the `worker` chassis. It is here only because the + # 2026-08-30 transition fitted Tom's own 1TB (26051Y809195) into worker and + # moved this one across. + # + # It LEAVES with worker (2+ months out as of 2026-09-19; per FRONT-10 no agent + # invents or enforces that date). /home lives on it, so that departure is a + # migration off this disk, not an unplug — plan it as one. Afterwards the + # coordinator runs on its own 1TB anchor 25140U804698 with no secondary. + # # ── The rule this disk exists to enforce ──────────────────────────────────── # The 1TB anchor holds the OS and only the OS: the nix store and everything # NixOS derives from it, /etc, /var, and the local-models weight collection. diff --git a/hosts/coordinator/services.nix b/hosts/coordinator/services.nix index 572a545d4..0ed3e5eca 100644 --- a/hosts/coordinator/services.nix +++ b/hosts/coordinator/services.nix @@ -43,9 +43,8 @@ in # satisfied WITHOUT a password file and the old immich-db.age secret is retired. # services.immich also subsumes the redis + machine-learning sidecars natively. # The navidrome-credentials secret is unrelated to the server — it is consumed -# client-side by the cliamp fish function — and is delivered in -# modules/secrets.nix (coordinator only since the zenbook-duo left the fleet, -# 2026-08-30; it was the second cliamp host). +# client-side by the navidrome-scan fish function — and is delivered in +# modules/secrets.nix (coordinator only). # # Reachability: both bind 0.0.0.0, but the firewall opens their ports ONLY on # tailscale0 (the trust model the wayvnc door used until the 2026-09-11 flip @@ -225,9 +224,7 @@ in # navidrome-credentials delivery moved to modules/secrets.nix (2026-07-13): # it's NOT consumed by the navidrome server here — only read client-side by - # the cliamp fish function. It moved there when cliamp gained a second host - # (zenbook-duo, retired 2026-08-30) and a single host-agnostic block beat - # duplicating the delivery; it stays there now that the coordinator is the - # only recipient again, because that is where secret delivery belongs. + # the navidrome-scan fish function. It stays there because that is where + # secret delivery belongs. }; } diff --git a/hosts/nas/disko.nix b/hosts/nas/disko.nix index 87df34a1e..6736258f4 100644 --- a/hosts/nas/disko.nix +++ b/hosts/nas/disko.nix @@ -42,6 +42,16 @@ # generated media (media.nix). Random-I/O state belongs here: it would be # wear on the eMMC and seek chatter on the HDD. The mountpoint move was # non-destructive — same filesystem, never reformatted. + # + # ── In the 2026-09-19 disk shuffle ────────────────────────────────────────── + # This Fanxiang is Tom's and is a CANDIDATE TO MOVE to the fanless desk NUC: + # 256GB is ample for a seat that only runs Chromium. In that option the 1TB + # freed by the worker return lands here instead, taking /mnt/fast to 1TB. + # The alternative sends the freed 1TB straight to the NUC and leaves this + # disk alone. Undecided as of 2026-09-19; either way nothing is purchased. + # If this disk does move, /mnt/fast is offline in the interim — it was 49% + # of 234G at the 2026-09-18 census and carries PostgreSQL, Navidrome state, + # Immich generated media and the remote journal. Check those before moving. disko.devices.disk.journal = { type = "disk"; device = "/dev/disk/by-id/nvme-Fanxiang_S500Pro_256GB_26040259615000015"; diff --git a/hosts/nas/media.nix b/hosts/nas/media.nix index f83d96420..315d0cb51 100644 --- a/hosts/nas/media.nix +++ b/hosts/nas/media.nix @@ -200,7 +200,7 @@ in # Enabled && ScanOnStartup). Upstream defaults this to TRUE, and this # is what was actually firing: the service is socket-activated, # StopWhenUnneeded, behind a proxy that exits after 15 min idle, so - # it restarts constantly and every cliamp launch or web-UI visit + # it restarts constantly and every client connect or web-UI visit # after a quiet spell triggered a full 34k-file walk. One was watched # taking 9+ minutes on 2026-08-18. ScanOnStartup = false; diff --git a/hosts/worker/disko.nix b/hosts/worker/disko.nix index 0f8f06405..9fd20028d 100644 --- a/hosts/worker/disko.nix +++ b/hosts/worker/disko.nix @@ -6,6 +6,22 @@ # 500GB paired against the coordinator's 1TB was the fleet's dual-node # capacity ceiling. # + # ── OWNERSHIP (Tom, 2026-09-19) ───────────────────────────────────────────── + # THIS DISK IS TOM'S, NOT SODIMO'S. sodimo owns exactly two items in this + # fleet: the `worker` chassis and the 500GB SN7100 260538801482 — and that + # 500GB is fitted in the COORDINATOR today, not here. When worker goes back + # (2+ months out as of 2026-09-19; per FRONT-10 no agent invents or enforces + # that date), the 500GB returns to this chassis and THIS 1TB COMES OUT. + # + # Do not read "worker's boot disk" as "unavailable". That is a filesystem + # fact, not an ownership fact, and conflating the two sent the silent-desk + # planning down a wrong path until it was corrected on 2026-09-19. + # + # The freed 1TB is the fleet's ENTIRE NVMe surplus. It is spoken for: either + # it becomes the fanless desk NUC's disk, or it upgrades the NAS /mnt/fast + # (256GB -> 1TB) and the NAS's Fanxiang 256GB goes to the NUC instead. + # Undecided. Standing constraint (Tom, 2026-09-19): NO NEW M.2 IS PURCHASED. + # # ── Why the attr is `w1t` and NOT `main` ──────────────────────────────────── # disko derives GPT partition NAMES from the attr: `main` yields # `disk-main-ESP` / `disk-main-root`. The coordinator's anchor uses those same diff --git a/modules/common.nix b/modules/common.nix index 2fcaa0bba..822b601a5 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -413,11 +413,15 @@ # --- env (qt + gtk theming) --- environment.sessionVariables = { QT_QPA_PLATFORMTHEME = "qt6ct"; - # GTK reads GTK_THEME with the highest priority. niri has no XSettings/settings - # daemon, so this system-wide export (reaching GUI apps via the PAM session) is - # what makes GTK3 apps like Nautilus honor the theme without nwg-look. It must be - # here, not home.sessionVariables (which only reaches interactive shells). - GTK_THEME = "MacTahoe-Dark-grey"; + # GTK_THEME is gone (2026-09-17). It pinned MacTahoe-Dark-grey for every GTK + # app because niri has no settings daemon — but GTK3's Wayland backend reads + # org.gnome.desktop.interface straight from gsettings/dconf and follows it + # LIVE, provided the schema is installed; it never was (no gschemas.compiled + # with that schema anywhere on XDG_DATA_DIRS), which is the actual reason + # gsettings-driven theming looked dead here. gsettings-desktop-schemas is in + # systemPackages below, and ~/.local/bin/theme sets gtk-theme / icon-theme / + # color-scheme per theme. An env var cannot change under a running session, + # so it had to go for the theme switcher to work without a re-login. # Chromium/Electron (google-chrome + PWA launchers) only run native Wayland # under niri with this set; otherwise they fall back to X11 and blur/fail. NIXOS_OZONE_WL = "1"; @@ -434,6 +438,19 @@ # --- base system packages (the rest are user packages in home/) --- environment.systemPackages = with pkgs; [ + # --- gsettings-driven GTK theming (theme switcher, 2026-09-17) --- + # glib: the `gsettings` CLI ~/.local/bin/theme calls (it was on no PATH in + # the session, so startup.kdl's old `spawn-at-startup "gsettings" …` lines + # had been failing silently). The schemas: nixpkgs installs them under + # share/gsettings-schemas//, which only wrapped apps see; GTK's own + # lookup is $XDG_DATA_DIRS/glib-2.0/schemas, so re-home them there + # (precompiled — no glib-compile-schemas pass depends on glib.dev). + glib + (runCommand "gsettings-desktop-schemas-on-xdg-data-dirs" { } '' + mkdir -p $out/share/glib-2.0/schemas + cp ${gsettings-desktop-schemas}/share/gsettings-schemas/*/glib-2.0/schemas/* $out/share/glib-2.0/schemas/ + test -f $out/share/glib-2.0/schemas/gschemas.compiled + '') git vim wl-clipboard diff --git a/modules/secrets.nix b/modules/secrets.nix index 20e6ede9f..135c0bd19 100644 --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -23,35 +23,12 @@ in config = lib.mkIf cfg.enable ( lib.mkMerge [ - # Claude Code OAuth credential — coordinator ONLY, and the recipient tier in - # ../secrets.nix now matches (aug04 ruling), so this MUST stay host-gated: - # no other host can decrypt the ciphertext, and declaring an undecryptable - # secret fails activation. The zenbook was already excluded (jul12 ruling: - # the laptop is a standalone backup operator for when the coordinator is - # unreachable, so it logs in with its OWN fresh OAuth session instead of - # inheriting the coordinator's token — two devices refreshing one shared - # token can race and sign each other out); the nas joined it aug04 for the - # simpler reason that it has no `claude` and never spent the token. - (lib.mkIf (config.networking.hostName == "coordinator") { - age.secrets.claude-credentials = { - file = ../secrets/claude-credentials.age; - owner = "tom"; - group = "users"; - mode = "600"; - }; - - # Seed the Claude Code OAuth credential once into a WRITABLE path Claude owns — - # agenix delivers a read-only /run/agenix symlink, but Claude must rewrite the - # file on token refresh, so copy rather than link, and only if absent. - system.userActivationScripts.seedClaudeCreds.text = '' - cred="$HOME/.claude/.credentials.json" - if [ ! -e "$cred" ] && [ -r "${config.age.secrets.claude-credentials.path}" ]; then - mkdir -p "$HOME/.claude" - cp "${config.age.secrets.claude-credentials.path}" "$cred" - chmod 600 "$cred" - fi - ''; - }) + # (claude-credentials — the Claude Code OAuth token seeded into + # ~/.claude/.credentials.json — was declared and copied here until + # 2026-09-22. The token had been dead since its August 4 rotation, and a + # seed that fires whenever the file is absent can only ever revert a fresh + # `/login` after a rebuild. Both Claude seats now log in by hand, once, + # on the coordinator: cc into ~/.claude, cc2 into ~/.claude-work.) # NOT ungated any more (2026-08-28). This block delivers ssh-user-key and # atuin-key, whose ciphertexts are encrypted to the `delivered` tier — and @@ -323,12 +300,10 @@ in # navidrome-credentials: NOT consumed by the navidrome server (which now # runs on the NAS, hosts/nas/media.nix, reached through the coordinator's - # navidrome-relay) — read client-side by the cliamp fish function, on - # whichever box cliamp runs from. Coordinator-only since the zenbook left - # the fleet (2026-08-30), matching the recipient tier in secrets.nix. The wrapper exports the - # file's NAVIDROME_PASSWORD under both that name (which config.toml's - # ${NAVIDROME_PASSWORD} placeholder interpolates) and NAVIDROME_PASS - # (which cliamp's config-less env fallback reads). + # navidrome-relay) — read client-side by the navidrome-scan fish function + # (NAVIDROME_USER / NAVIDROME_PASSWORD). Coordinator-only, matching the + # recipient tier in secrets.nix. (Its first reader, the cliamp TUI client, + # was removed 2026-09-17.) (lib.mkIf (config.networking.hostName == "coordinator") { age.secrets.navidrome-credentials = { file = ../secrets/navidrome-credentials.age; @@ -394,8 +369,7 @@ in }) # soundcloud-cookies: consumed by the music-consolidation drain's yt-dlp - # invocations (systemd user units, coordinator-only). NOT for cliamp — see - # secrets.nix for why cliamp needs no secret here. + # invocations (systemd user units, coordinator-only). (lib.mkIf (config.networking.hostName == "coordinator") { age.secrets.soundcloud-cookies = { file = ../secrets/soundcloud-cookies.age; diff --git a/overlays/default.nix b/overlays/default.nix index 7586ddccf..eec5f8be1 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -32,6 +32,9 @@ final: prev: { # the mactahoe-oled staging repo (since deleted 2026-07-04); pkgs/ is the home. # Icons: stock default (blue folders); GTK: light+dark grey, dark OLED-patched. mactahoe-gtk-theme = final.callPackage ../pkgs/mactahoe-gtk-theme.nix { }; + # the same source build recoloured to claude.ai's light/dark tokens, for the + # theme switcher's claude-dark / claude-light (home/themes/, docs/theme-switcher-2026-09-17.md) + mactahoe-claude-gtk-theme = final.callPackage ../pkgs/mactahoe-gtk-theme.nix { variant = "claude"; }; mactahoe-icon-theme = final.callPackage ../pkgs/mactahoe-icon-theme.nix { }; # Backlog.md — markdown-native task manager CLI (`backlog`). Not in nixpkgs; @@ -65,10 +68,6 @@ final: prev: { # Replaces paper-intake (paper-print-flush). See home/paper.nix. paper-daemon = final.callPackage ../pkgs/paper-daemon { }; - # cliamp — terminal music player (Winamp-inspired TUI). Not in nixpkgs (2026-07-06). - # Connects to navidrome via Subsonic API. Config at home/dot_config/cliamp/. - # CGO on Linux via ebitengine/oto → ALSA. See pkgs/cliamp.nix. - cliamp = final.callPackage ../pkgs/cliamp.nix { }; # CLI-Anything — pinned cli-hub Python app plus immutable Codex/Claude/Pi # integrations. Upstream has no flake; see modules/cli-anything.nix. diff --git a/pkgs/cliamp.nix b/pkgs/cliamp.nix deleted file mode 100644 index 68a2192f5..000000000 --- a/pkgs/cliamp.nix +++ /dev/null @@ -1,85 +0,0 @@ -{ - lib, - fetchFromGitHub, - buildGoModule, - pkg-config, - alsa-lib, - libogg, - libvorbis, - flac, - makeWrapper, - yt-dlp, - ffmpeg, -}: - -# cliamp — terminal music player (Winamp-inspired TUI). -# Packaged from source: not in nixpkgs (verified 2026-07-06). -# Uses CGO on Linux via ebitengine/oto → ALSA (libasound). -# -# Was pinned at v1.9.0 from 2026-07-06 until 2026-08-18. That release's -# Navidrome client was 143 lines exposing exactly three Subsonic endpoints — -# getPlaylists, getPlaylist, stream — so the library could only ever be browsed -# as playlists; there was no artist or album browsing at all. Its config loader -# was likewise a flat key=value scanner over ten hardcoded keys, which silently -# dropped every [navidrome]/[soundcloud] section and did no ${VAR} expansion, -# leaving the server configurable only through NAVIDROME_URL/USER/PASS. -# -# v1.63.2 fixes both: NavidromeClient now implements provider.ArtistBrowser and -# provider.AlbumBrowser over getArtists/getArtist/getAlbum/getAlbumList2/search3, -# and the config parser understands real sections plus ${VAR} interpolation from -# the environment. home/dot_config/cliamp/config.toml depends on both. -# -# vendorHash: from `go mod vendor` on the v1.63.2 source. If it drifts on a -# version bump, rebuild with lib.fakeHash and take the value from the error. -buildGoModule rec { - pname = "cliamp"; - version = "1.63.2"; - - src = fetchFromGitHub { - owner = "bjarneo"; - repo = "cliamp"; - rev = "v${version}"; - hash = "sha256-HqFDT8jGvrKqb6bupvXqZ5ECpvColRB5dXPwcKCX4RQ="; - }; - - vendorHash = "sha256-WYyv0w5KFA15axb+NA9tClfc1H4Znj8kI2boR8XziXg="; - - nativeBuildInputs = [ - pkg-config - makeWrapper - ]; - # alsa-lib for the oto audio backend; the ogg/vorbis/flac trio arrived with - # v1.63.2's go-librespot (Spotify) dependency, which cgo-links them via - # pkg-config and fails the build outright when they are absent. - buildInputs = [ - alsa-lib - libogg - libvorbis - flac - ]; - - ldflags = [ - "-s" - "-w" - "-X main.version=v${version}" - ]; - - # cliamp shells out to these by bare name for non-native formats (aac/opus/wma - # go through ffmpeg) and for SoundCloud/YouTube/Bandcamp URLs. Wrapping keeps - # it working regardless of what happens to be on the ambient PATH. - postInstall = '' - wrapProgram $out/bin/cliamp \ - --prefix PATH : ${lib.makeBinPath [ yt-dlp ffmpeg ]} - ''; - - meta = with lib; { - description = "Terminal music player (Winamp-inspired) with Navidrome/Subsonic, Spotify, and local file support"; - homepage = "https://www.cliamp.stream/"; - license = licenses.mit; - mainProgram = "cliamp"; - platforms = [ - "x86_64-linux" - "aarch64-linux" - ]; - }; -} diff --git a/pkgs/mactahoe-gtk-theme.nix b/pkgs/mactahoe-gtk-theme.nix index 4fb01032f..abb6a4ab7 100644 --- a/pkgs/mactahoe-gtk-theme.nix +++ b/pkgs/mactahoe-gtk-theme.nix @@ -1,19 +1,28 @@ -# MacTahoe GTK theme — light + dark, grey accent, with the custom OLED-black -# surfaces on the dark variants. +# MacTahoe GTK theme, recoloured at the SCSS source for the theme switcher. # # Built from upstream source (vinceliuice/MacTahoe-gtk-theme), NOT vendored. -# The ONLY customization is OLED black: upstream's dark surface colors in -# src/sass/_colors.scss (#242424 base/backdrop, #333333 bg/headerbar) forced to -# pure black ("rgba(5,5,5,0.96) → #000000 for true OLED black"). The -# swapped values sit in the dark-only branch of the if(...)s, so the Light -# variants are 100% stock. The grey accent and the solid opacity variant are -# STOCK install.sh flags, not customizations. +# One derivation per `variant`; the only customisation is a set of colour +# substitutions in src/sass/_colors.scss (and, for claude, the accent in +# _colors-palette.scss), applied with --replace-fail so upstream drift is +# caught at build time. The grey/orange accent and the solid opacity variant +# are STOCK install.sh flags, not customisations. +# +# oled (default) — Tom's noir: upstream's dark surfaces (#242424 +# base/backdrop, #333333 bg/headerbar) forced to pure black +# ("rgba(5,5,5,0.96) → #000000 for true OLED black"). Dark-only +# branch of the if(...)s, so its Light variants are 100% stock. +# Theme dirs: MacTahoe-{Dark,Light}[-solid]-grey[-(x)hdpi]. +# claude — both branches recoloured to claude.ai's own light and dark +# tokens (~/colors/waves/capture/claude-code-theme/claude-tokens- +# {light,dark}.json), accent = Anthropic clay #D97757 via the +# `orange` slot. Theme dirs: MacTahoe-Claude-{Dark,Light}[-solid]- +# orange[-(x)hdpi]. Consumed by home/themes/claude-{dark,light}.nix. # # Modeled on nixpkgs' whitesur-gtk-theme derivation — MacTahoe's install.sh is a # direct fork of WhiteSur's, so the same sudo/$HOME/shebang fixups apply. # # Builds once, then content-addressed: a normal `switch` reuses the store path -# (0s); it only rebuilds when `rev`/the OLED swap changes, and CI→cache means +# (0s); it only rebuilds when `rev`/a substitution changes, and CI→cache means # other devices substitute the result instead of building. { lib, @@ -25,10 +34,62 @@ libxml2, sassc, util-linux, + variant ? "oled", }: - +let + variants = { + oled = { + pname = "mactahoe-gtk-theme-oled-grey"; + themeName = "MacTahoe"; + accent = "grey"; + description = "MacTahoe GTK theme, light + dark, grey accent, custom OLED-black dark surfaces"; + # Dark variant only; $darker is off so only the 3rd value in each + # if(...) matters. base/backdrop → pure black, bg/headerbar → near-black + # so surfaces stay distinguishable. + colorsPatch = '' + substituteInPlace src/sass/_colors.scss \ + --replace-fail '#1f1f1f, #242424))' '#1f1f1f, #000000))' \ + --replace-fail '#282828, #333333))' '#282828, #0a0a0a))' \ + --replace-fail '#1e1e1e, #333333))' '#1e1e1e, #0a0a0a))' + ''; + }; + claude = { + pname = "mactahoe-gtk-theme-claude"; + themeName = "MacTahoe-Claude"; + accent = "orange"; + description = "MacTahoe GTK theme recoloured to claude.ai's light and dark tokens, clay accent"; + # Light = 1st value of each if(...), dark = 3rd. Tokens: bg-000/100/200 + # (#FFFFFF/#F9F9F7/#F3F3F0 light; #20201F/#151515 dark), text-000/200/400, + # cds-text-secondary, accent-100 (links). Dark page = bg-100 #151515 with + # bg-000 #20201F as the raised surface (headerbar, bg, sidebar), the + # relation MacTahoe's stock dark already has (base darker than bg). + colorsPatch = '' + substituteInPlace src/sass/_colors.scss \ + --replace-fail '#1f1f1f, #242424))' '#1f1f1f, #151515))' \ + --replace-fail "'light', #f5f5f5, if(" "'light', #F9F9F7, if(" \ + --replace-fail '#282828, #333333)' '#282828, #20201F)' \ + --replace-fail "'light', #f2f2f2," "'light', #F3F3F0," \ + --replace-fail "'light', #363636, #dadada)" "'light', #131313, #F9F9F7)" \ + --replace-fail "'light', #242424, #dedede)" "'light', #131313, #F9F9F7)" \ + --replace-fail "'light', #424242, #afafaf)" "'light', #383835, #C3C2B7)" \ + --replace-fail "'light', #565656, #999999)" "'light', #7B7974, #97958D)" \ + --replace-fail "'light', #ffffff, if(\$darker == 'true', #1e1e1e, #333333))" "'light', #F9F9F7, if(\$darker == 'true', #1e1e1e, #20201F))" \ + --replace-fail '#242424, #404040), #f5f5f5)' '#242424, #383835), #F3F3F0)' \ + --replace-fail "'light', #575757, #FDFDFD)" "'light', #52514E, #F9F9F7)" \ + --replace-fail '#eeeff2, #fefefe)' '#eeeff2, #F3F3F0)' \ + --replace-fail '#1a1a1a, #2a2a2a)' '#1a1a1a, #151515)' \ + --replace-fail '#5e81ac, #3484e2)' "#5e81ac, if(\$variant == 'light', #256ABF, #5598E7))" + # accent: the `orange` slot becomes Anthropic clay (--accent-brand, the + # same hex in Claude's light and dark tokens); built with --theme orange. + substituteInPlace src/sass/_colors-palette.scss \ + --replace-fail '$theme_color_orange: #E9873A;' '$theme_color_orange: #D97757;' + ''; + }; + }; + v = variants.${variant}; +in stdenvNoCC.mkDerivation { - pname = "mactahoe-gtk-theme-oled-grey"; + pname = v.pname; # Pinned rev (2026-06-19); bump deliberately to take upstream updates — then # `nix build` prints the new hash to paste below. The dark-only build was # verified in a nixos/nix container 2026-06-19 (6 variants, OLED-black @@ -71,14 +132,8 @@ stdenvNoCC.mkDerivation { --replace-fail 'prepare_deps() {' 'prepare_deps() { return 0;' \ --replace-fail 'installation_sorry() {' 'installation_sorry() { return 0;' - # ── the OLED customization (the whole reason this isn't just nixpkgs) ── - # Dark variant only; $darker is off so only the 3rd value in each - # if(...) matters. base/backdrop → pure black, bg/headerbar → near-black - # so surfaces stay distinguishable. --replace-fail catches upstream drift. - substituteInPlace src/sass/_colors.scss \ - --replace-fail '#1f1f1f, #242424))' '#1f1f1f, #000000))' \ - --replace-fail '#282828, #333333))' '#282828, #0a0a0a))' \ - --replace-fail '#1e1e1e, #333333))' '#1e1e1e, #0a0a0a))' + # ── the recolouring (the whole reason this isn't just nixpkgs) ── + ${v.colorsPatch} ''; dontBuild = true; @@ -89,8 +144,8 @@ stdenvNoCC.mkDerivation { # -c light/dark : both colors -t grey : grey accent -o normal/solid : # both opacities. install.sh also emits the -hdpi/-xhdpi variants # automatically, so all 6 dark directories are produced - # (MacTahoe-Dark-grey, -Dark-solid-grey, each + -hdpi/-xhdpi) plus the - # matching 6 Light dirs. + # (-Dark-, -Dark-solid-, each + -hdpi/-xhdpi) plus + # the matching 6 Light dirs. # MacTahoe's no-gnome-shell branch leaves SHELL_VERSION empty (upstream bug), # generating invalid SCSS ($GNOME_SHELL: ;). gnome-shell is never present in a # build sandbox, so set it explicitly — the overwriting line is gated behind @@ -100,8 +155,9 @@ stdenvNoCC.mkDerivation { # NB: --opacity/--color take ONE value per flag (install.sh does `shift 2`), # so multi-value variants must be passed as repeated flags, not space-listed. ./install.sh \ + --name ${v.themeName} \ --color light --color dark \ - --theme grey \ + --theme ${v.accent} \ --opacity normal --opacity solid \ --dest $out/share/themes jdupes --quiet --link-soft --recurse $out/share @@ -109,7 +165,7 @@ stdenvNoCC.mkDerivation { ''; meta = { - description = "MacTahoe GTK theme, light + dark, grey accent, custom OLED-black dark surfaces"; + description = v.description; homepage = "https://github.com/vinceliuice/MacTahoe-gtk-theme"; license = lib.licenses.gpl3Only; platforms = lib.platforms.linux; diff --git a/pkgs/mactahoe-icon-theme.nix b/pkgs/mactahoe-icon-theme.nix index 24f821a6f..289022b46 100644 --- a/pkgs/mactahoe-icon-theme.nix +++ b/pkgs/mactahoe-icon-theme.nix @@ -1,17 +1,20 @@ -# MacTahoe icon theme — stock default (blue folders). +# MacTahoe icon theme — stock default (blue folders) plus one folder-colour +# variant per claude.ai/imagine wallpaper accent. # -# 100% STOCK, zero flags beyond --name. Previously this built the `-t grey` -# variant (grey #686868 folders, itself also stock upstream); reverted to the -# default blue folder color 2026-07-04. The only reason this derivation exists -# at all is that MacTahoe (unlike its siblings whitesur-icon-theme / -# colloid-icon-theme) isn't in nixpkgs yet — re-check occasionally and drop -# this file when it lands. +# Stock build otherwise: MacTahoe (unlike whitesur-icon-theme / colloid-icon-theme) +# isn't in nixpkgs yet — re-check occasionally. Modeled on nixpkgs' +# whitesur-icon-theme derivation (same upstream author / same install.sh). # -# The default build emits all three dirs (COLOR_VARIANTS=('' '-light' '-dark')): -# MacTahoe, MacTahoe-light, MacTahoe-dark. -# -# Modeled on nixpkgs' whitesur-icon-theme derivation (same upstream author / -# same install.sh). No patch — so this could even be upstreamed to nixpkgs. +# The default build emits COLOR_VARIANTS=('' '-light' '-dark'): MacTahoe, +# MacTahoe-light, MacTahoe-dark. Folder colours are install.sh "themes": each +# stock one is a directory colors/color-/ of 18 folder SVGs drawn in ONE +# hex (grey = #686868) over white/black overlays, copied over places/scalable. +# The accents below add colors/color-/ generated from the grey set with +# that hex swapped for the wallpaper's own ground — the darker of the two +# grounds Anthropic ships per theme (~/colors/waves/capture/imagine-background.css +# --bg-primary-dark) — so `wallpaper olive` can put olive folders in Nautilus. +# Emitted as MacTahoe-{,-light,-dark}; everything but the folders +# dedupes to one copy under jdupes. Consumed by ~/.local/bin/theme (`theme icons`). { lib, stdenvNoCC, @@ -20,7 +23,24 @@ hicolor-icon-theme, jdupes, }: - +let + # accent → folder hex: the imagine theme's --bg-primary-dark. + accents = { + oat = "#d9d1c3"; + olive = "#697751"; + cactus = "#b6c8c1"; + sky = "#6488b4"; + fig = "#a55b74"; + heather = "#c0bed2"; + coral = "#dec4c4"; + }; + accentVariants = lib.concatMapStringsSep " " (a: "'-${a}'") (lib.attrNames accents); + mkAccent = a: hex: '' + cp -r colors/color-grey colors/color-${a} + sed -i "s/#686868/${hex}/g" colors/color-${a}/*.svg + grep -q "${hex}" colors/color-${a}/folder.svg + ''; +in stdenvNoCC.mkDerivation { pname = "mactahoe-icon-theme"; version = "0-unstable-2026-06-19"; @@ -46,13 +66,18 @@ stdenvNoCC.mkDerivation { postPatch = '' patchShebangs install.sh + # the accent folder sets, then make install.sh's `--theme all` mean + # "default + the accents" (the stock colours are not built). + ${lib.concatStrings (lib.mapAttrsToList mkAccent accents)} + substituteInPlace install.sh \ + --replace-fail "THEME_VARIANTS=(''' '-blue' '-purple' '-green' '-red' '-orange' '-yellow' '-grey' '-nord')" \ + "THEME_VARIANTS=(''' ${accentVariants})" ''; installPhase = '' runHook preInstall # --name is required: without it the theme dirs inherit the build-dir name. - # No --theme flag: default = blue folders. - ./install.sh --dest $out/share/icons --name MacTahoe + ./install.sh --dest $out/share/icons --name MacTahoe --theme all jdupes --link-soft --recurse $out/share runHook postInstall ''; @@ -60,10 +85,14 @@ stdenvNoCC.mkDerivation { # drop dangling symlinks upstream ships (same as nixpkgs whitesur-icon-theme) postFixup = '' find $out/share/icons -xtype l -delete + # every accent came out + for a in ${lib.concatStringsSep " " (lib.attrNames accents)}; do + test -d $out/share/icons/MacTahoe-$a-dark + done ''; meta = { - description = "MacTahoe icon theme (stock vinceliuice build, default blue folders)"; + description = "MacTahoe icon theme (stock vinceliuice build, default blue folders + imagine-accent folder variants)"; homepage = "https://github.com/vinceliuice/MacTahoe-icon-theme"; license = lib.licenses.gpl3Plus; platforms = lib.platforms.linux; diff --git a/secrets.nix b/secrets.nix index 03f43d3aa..5d94f6b0b 100644 --- a/secrets.nix +++ b/secrets.nix @@ -130,9 +130,7 @@ in # SoundCloud Go+ cookies.txt (Netscape format), consumed by the music-consolidation # drain's yt-dlp invocations (systemd user units on coordinator only — see that - # repo's docs/SPEC-2026-07-06-original.md). NOT for cliamp: cliamp shells to - # `yt-dlp --cookies-from-browser chrome` directly against a live signed-in browser - # and has no file-based cookie mode, so it needs no secret at all (dotfiles#70). + # repo's docs/SPEC-2026-07-06-original.md). "secrets/soundcloud-cookies.age".publicKeys = editors ++ coordinatorOnly; # YouTube Music cookies.txt (Netscape format), exported same sitting as the @@ -141,9 +139,9 @@ in # by DRM. Coordinator-only, same reasoning as soundcloud-cookies. "secrets/youtube-music-cookies.age".publicKeys = editors ++ coordinatorOnly; - # cliamp (client) reads this on the coordinator, where navidrome's relay - # lives. The zenbook was the second consumer until it left the fleet - # (2026-08-30); the laptops tier went with it. + # Read client-side on the coordinator, where navidrome's relay lives, by the + # navidrome-scan fish function (Subsonic API). Its first consumer, the cliamp + # TUI client, was removed 2026-09-17; the zenbook left the fleet 2026-08-30. "secrets/navidrome-credentials.age".publicKeys = editors ++ coordinatorOnly; # Immich full-permissions API key (photos.internal), read client-side by agent @@ -152,14 +150,8 @@ in # cleanup — as agenix ciphertext it survives reflash and never needs re-minting. "secrets/immich-api-key.age".publicKeys = editors ++ coordinatorOnly; - # Claude Code OAuth credential. Demoted from the common tier to coordinator-only - # (2026-08-04, Tom's ruling): the nas holds no `claude` binary and never ran an - # agent, so it had no use for the token; zenbook-duo was already excluded from - # delivery (jul12 ruling — it logs in with its OWN session, since two devices - # refreshing one shared token race and sign each other out). That left the - # coordinator as the only real consumer, so the recipient tier now says so — - # a token this wide should not be decryptable by boxes that never spend it. - "secrets/claude-credentials.age".publicKeys = editors ++ coordinatorOnly; + # (claude-credentials.age removed 2026-09-22: the seat logins are hand + # `/login`s on the coordinator, never a delivered secret; see modules/secrets.nix.) # Brother HL-L2445DW Web Based Management admin password, set 2026-08-21 when # the printer's forced default-password change gated its move onto the thomas diff --git a/secrets/claude-credentials.age b/secrets/claude-credentials.age deleted file mode 100644 index 8d0a443d1..000000000 Binary files a/secrets/claude-credentials.age and /dev/null differ