@sebastienrousseau/dotfiles 0.2.481 → 0.2.500

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/CHANGELOG.md +122 -32
  2. package/LICENSE +1 -1
  3. package/README.md +211 -242
  4. package/docs/.vitepress/reports/localization-readability-audit.md +69 -0
  5. package/docs/AI.md +102 -0
  6. package/docs/COPYRIGHT +2 -2
  7. package/docs/NAMING_CONVENTIONS.md +95 -0
  8. package/docs/README.md +58 -249
  9. package/docs/adr/ADR-004-cli-architecture.md +2 -2
  10. package/docs/adr/ADR-006-shell-selection.md +2 -2
  11. package/docs/adr/ADR-007-multi-shell-parity.md +53 -0
  12. package/docs/adr/ADR-008-alias-system-architecture.md +85 -0
  13. package/docs/adr/README.md +2 -3
  14. package/docs/architecture/ARCHITECTURE.md +113 -0
  15. package/docs/architecture/INTEROP.md +36 -0
  16. package/docs/architecture/REPO_LAYOUT.md +237 -0
  17. package/docs/architecture/WALKTHROUGH.md +82 -0
  18. package/docs/architecture/fleet-deployment.md +73 -0
  19. package/docs/archive/EUXIS_2026_REVIEW.md +119 -0
  20. package/docs/archive/MILESTONE_v0.2.493.md +43 -0
  21. package/docs/{PLAN.md → archive/PLAN.md} +1 -1
  22. package/docs/guides/INSTALL.md +140 -0
  23. package/docs/{NEOVIM_IDE_GUIDE.md → guides/NEOVIM_IDE_GUIDE.md} +12 -12
  24. package/docs/guides/THEMING.md +222 -0
  25. package/docs/{TROUBLESHOOTING.md → guides/TROUBLESHOOTING.md} +36 -0
  26. package/docs/{WSL2_NIX_TROUBLESHOOTING.md → guides/WSL2_NIX_TROUBLESHOOTING.md} +8 -8
  27. package/docs/index.md +39 -0
  28. package/docs/interop/A2A.md +32 -0
  29. package/docs/manual/00-introduction.md +85 -0
  30. package/docs/manual/01-concepts/01-architecture.md +134 -0
  31. package/docs/manual/01-concepts/02-trust-model.md +178 -0
  32. package/docs/manual/01-concepts/03-theme-engine.md +182 -0
  33. package/docs/manual/01-concepts/04-fleet.md +142 -0
  34. package/docs/manual/01-concepts/05-self-healing.md +197 -0
  35. package/docs/manual/02-tutorials/01-first-install.md +192 -0
  36. package/docs/manual/02-tutorials/02-add-wallpaper.md +211 -0
  37. package/docs/manual/02-tutorials/03-create-profile.md +238 -0
  38. package/docs/manual/02-tutorials/04-encrypt-secret.md +275 -0
  39. package/docs/manual/02-tutorials/05-deploy-fleet.md +279 -0
  40. package/docs/manual/03-reference/01-dot-cli.md +413 -0
  41. package/docs/manual/03-reference/02-config-files.md +259 -0
  42. package/docs/manual/03-reference/03-environment.md +120 -0
  43. package/docs/manual/03-reference/04-templates.md +184 -0
  44. package/docs/manual/03-reference/05-feature-flags.md +118 -0
  45. package/docs/manual/04-cookbook/01-recipes.md +281 -0
  46. package/docs/manual/04-cookbook/02-troubleshooting.md +319 -0
  47. package/docs/manual/04-cookbook/03-faq.md +167 -0
  48. package/docs/manual/05-appendices/A-platform-matrix.md +97 -0
  49. package/docs/manual/05-appendices/B-security-checklist.md +78 -0
  50. package/docs/manual/05-appendices/C-glossary.md +36 -0
  51. package/docs/manual/05-appendices/D-bibliography.md +54 -0
  52. package/docs/manual/05-appendices/E-license.md +34 -0
  53. package/docs/manual/_toc.yml +58 -0
  54. package/docs/manual/command-index.md +58 -0
  55. package/docs/manual/concept-index.md +164 -0
  56. package/docs/operations/ATTESTATION.md +39 -0
  57. package/docs/operations/MAINTENANCE.md +59 -0
  58. package/docs/operations/MIGRATION.md +101 -0
  59. package/docs/operations/OPERATIONS.md +164 -0
  60. package/docs/operations/RELIABILITY.md +116 -0
  61. package/docs/{ROADMAP.md → operations/ROADMAP.md} +11 -11
  62. package/docs/operations/TESTING.md +195 -0
  63. package/docs/operations/TRACEABILITY.md +31 -0
  64. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +61 -0
  65. package/docs/operations/VERSION_SYNC.md +344 -0
  66. package/docs/{ALIASES.md → reference/ALIASES.md} +2 -6
  67. package/docs/reference/ALIASES_CHEATSHEET.md +28 -0
  68. package/docs/reference/ALIASES_DEPRECATIONS.md +9 -0
  69. package/docs/reference/FEATURES.md +62 -0
  70. package/docs/reference/FONTS.md +108 -0
  71. package/docs/reference/PROFILES.md +65 -0
  72. package/docs/{SCREENSHOTS.md → reference/SCREENSHOTS.md} +26 -25
  73. package/docs/reference/SCRIPTS.md +67 -0
  74. package/docs/reference/SUPPORT_MATRIX.md +74 -0
  75. package/docs/reference/THEMES.md +113 -0
  76. package/docs/reference/TOOLS.md +105 -0
  77. package/docs/reference/UTILS.md +151 -0
  78. package/docs/security/AI_ACT_COMPLIANCE.md +90 -0
  79. package/docs/security/AUTOMATION_SECRETS.md +22 -0
  80. package/docs/security/COMPLIANCE.md +453 -0
  81. package/docs/security/ENCRYPTION.md +52 -0
  82. package/docs/security/FMEA.md +155 -0
  83. package/docs/security/INCIDENT_RESPONSE.md +575 -0
  84. package/docs/security/KEYS.md +45 -0
  85. package/docs/security/MCP_POLICY.md +69 -0
  86. package/docs/security/POLICY_RELEASES.md +33 -0
  87. package/docs/security/README.md +24 -0
  88. package/docs/security/SECRETS.md +146 -0
  89. package/docs/security/SECURITY.md +41 -0
  90. package/docs/security/SECURITY_CHECKLIST.md +44 -0
  91. package/docs/security/SOUP_REGISTER.md +32 -0
  92. package/docs/security/THREAT_MODEL.md +120 -0
  93. package/docs/security/VERIFICATION_VALIDATION.md +224 -0
  94. package/docs/themes/README.md +3 -17
  95. package/docs/themes/VISUAL_INTEGRITY_REPORT.md +26 -0
  96. package/docs/themes/hero-shot.svg +78 -0
  97. package/dot_config/.module-manifest.json +69 -0
  98. package/dot_config/ai/identity.md +20 -0
  99. package/dot_config/ai/patterns/architect.md +14 -0
  100. package/dot_config/ai/patterns/hardener.md +13 -0
  101. package/dot_config/ai/patterns/refactor.md +13 -0
  102. package/dot_config/aider/aider.conf.yml +1 -1
  103. package/dot_config/alacritty/alacritty.toml.tmpl +41 -469
  104. package/dot_config/atuin/config.toml +18 -1
  105. package/dot_config/bat/config +16 -1
  106. package/dot_config/claude/mcp_servers.json +3 -32
  107. package/dot_config/dotfiles/agent-card.json +29 -0
  108. package/dot_config/dotfiles/agent-profiles.json +92 -0
  109. package/dot_config/dotfiles/boot/README.md +1 -1
  110. package/dot_config/dotfiles/grub/README.md +1 -1
  111. package/dot_config/dotfiles/lock/README.md +1 -1
  112. package/dot_config/dotfiles/mcp-lock.json +33 -0
  113. package/dot_config/dotfiles/mcp-policy.json +52 -0
  114. package/dot_config/dotfiles/mcp-registry.json +47 -0
  115. package/dot_config/dotfiles/model-registry.json +20 -0
  116. package/dot_config/dotfiles/policy-bundles.json +38 -0
  117. package/dot_config/dotfiles/prompt-registry.json +16 -0
  118. package/dot_config/dotfiles/versions.env +18 -0
  119. package/dot_config/environment.d/50-ssh-agent.conf +2 -0
  120. package/dot_config/environment.d/90-wayland.conf +19 -0
  121. package/dot_config/firefox/user.js.tmpl +58 -0
  122. package/dot_config/fish/completions/dot-theme-sync.fish.tmpl +9 -0
  123. package/dot_config/fish/completions/dot.fish.tmpl +115 -0
  124. package/dot_config/fish/conf.d/000-perf-cache.fish +56 -0
  125. package/dot_config/fish/conf.d/aliases.fish.tmpl +77 -0
  126. package/dot_config/fish/conf.d/env.fish.tmpl +48 -0
  127. package/dot_config/fish/conf.d/functions.fish.tmpl +22 -0
  128. package/dot_config/fish/conf.d/init.fish.tmpl +151 -0
  129. package/dot_config/fish/config.fish.tmpl +44 -0
  130. package/dot_config/fish/fish_plugins +1 -0
  131. package/dot_config/fish/functions/_.fish +3 -0
  132. package/dot_config/fish/functions/_cached_eval.fish +58 -0
  133. package/dot_config/fish/functions/als.fish +46 -0
  134. package/dot_config/fish/functions/bg-upgrade.fish +7 -0
  135. package/dot_config/fish/functions/c.fish +3 -0
  136. package/dot_config/fish/functions/cat.fish +9 -0
  137. package/dot_config/fish/functions/d.fish +7 -0
  138. package/dot_config/fish/functions/dot.fish +34 -0
  139. package/dot_config/fish/functions/h.fish +3 -0
  140. package/dot_config/fish/functions/l.fish +7 -0
  141. package/dot_config/fish/functions/la.fish +7 -0
  142. package/dot_config/fish/functions/ll.fish +7 -0
  143. package/dot_config/fish/functions/log_error.fish +13 -0
  144. package/dot_config/fish/functions/log_info.fish +13 -0
  145. package/dot_config/fish/functions/lr.fish +7 -0
  146. package/dot_config/fish/functions/lra.fish +7 -0
  147. package/dot_config/fish/functions/ls.fish +7 -0
  148. package/dot_config/fish/functions/lt.fish +7 -0
  149. package/dot_config/fish/functions/lta.fish +7 -0
  150. package/dot_config/fish/functions/notify-run.fish +11 -0
  151. package/dot_config/fish/functions/q.fish +3 -0
  152. package/dot_config/fish/functions/silent-run.fish +7 -0
  153. package/dot_config/fish/functions/yy.fish +8 -0
  154. package/dot_config/fontconfig/fonts.conf.tmpl +13 -0
  155. package/dot_config/foot/foot.ini.tmpl +33 -0
  156. package/dot_config/fuzzel/fuzzel.ini.tmpl +18 -0
  157. package/dot_config/fzf/fzf.zsh +48 -0
  158. package/dot_config/ghostty/config.tmpl +69 -565
  159. package/dot_config/git/allowed_signers.tmpl +18 -0
  160. package/dot_config/git/commit-template +10 -0
  161. package/dot_config/gnupg/{gpg-agent.conf.tmpl → gpg-agent.conf} +3 -3
  162. package/dot_config/goose/config.yaml +2 -2
  163. package/dot_config/gtk-3.0/gtk.css.tmpl +59 -180
  164. package/dot_config/gtk-3.0/settings.ini.tmpl +5 -44
  165. package/dot_config/gtk-4.0/gtk.css.tmpl +79 -136
  166. package/dot_config/gtk-4.0/settings.ini.tmpl +5 -8
  167. package/dot_config/ipython/profile_default/ipython_config.py +19 -0
  168. package/dot_config/just/justfile +18 -0
  169. package/dot_config/kanshi/config.tmpl +22 -0
  170. package/dot_config/kitty/kitty.conf.tmpl +44 -508
  171. package/dot_config/lazygit/config.yml +18 -6
  172. package/dot_config/mako/config +47 -0
  173. package/dot_config/micro/settings.json +3 -0
  174. package/dot_config/mimeapps.list +77 -0
  175. package/dot_config/mise/config.toml +63 -5
  176. package/dot_config/mongosh/mongoshrc.js +1 -0
  177. package/dot_config/mpv/input.conf +47 -0
  178. package/dot_config/mpv/mpv.conf +47 -0
  179. package/dot_config/niri/config.kdl.tmpl +327 -0
  180. package/dot_config/nushell/aliases.nu +18 -0
  181. package/dot_config/nushell/completions.nu.tmpl +52 -0
  182. package/dot_config/nushell/config.nu.tmpl +86 -0
  183. package/dot_config/nushell/env.nu.tmpl +94 -0
  184. package/dot_config/nushell/functions.nu.tmpl +17 -0
  185. package/dot_config/nvim/init.lua +14 -0
  186. package/dot_config/nvim/lazy-lock.json +45 -14
  187. package/dot_config/nvim/lua/config/autocmds.lua +45 -1
  188. package/dot_config/nvim/lua/config/keymaps.lua +13 -8
  189. package/dot_config/nvim/lua/config/lazy.lua +22 -12
  190. package/dot_config/nvim/lua/config/options.lua +1 -0
  191. package/dot_config/nvim/lua/plugins/ai.lua +75 -0
  192. package/dot_config/nvim/lua/plugins/coding.lua +11 -29
  193. package/dot_config/nvim/lua/plugins/completion.lua +60 -0
  194. package/dot_config/nvim/lua/plugins/dap.lua +215 -4
  195. package/dot_config/nvim/lua/plugins/editor.lua +6 -10
  196. package/dot_config/nvim/lua/plugins/git.lua +1 -0
  197. package/dot_config/nvim/lua/plugins/lsp.lua +16 -81
  198. package/dot_config/nvim/lua/plugins/markdown.lua +1 -0
  199. package/dot_config/nvim/lua/plugins/rust.lua +1 -0
  200. package/dot_config/nvim/lua/plugins/sessions.lua +1 -0
  201. package/dot_config/nvim/lua/plugins/ui.lua +23 -51
  202. package/dot_config/paru/paru.conf +10 -0
  203. package/dot_config/powershell/Microsoft.PowerShell_profile.ps1.tmpl +81 -0
  204. package/dot_config/pueue/pueue.yml +34 -0
  205. package/dot_config/raycast/README.md +1 -1
  206. package/dot_config/ripgrep/ripgreprc +21 -0
  207. package/dot_config/shell/00-core-paths.sh.tmpl +62 -24
  208. package/dot_config/shell/05-core-safety.sh +47 -0
  209. package/dot_config/shell/10-secrets.sh +40 -0
  210. package/dot_config/shell/40-fzf-defaults.sh.tmpl +16 -0
  211. package/dot_config/shell/40-ls-colors.sh +48 -0
  212. package/dot_config/shell/50-logic-functions-core.sh.tmpl +10 -0
  213. package/dot_config/shell/51-logic-functions-extra.sh.tmpl +64 -0
  214. package/dot_config/shell/90-ux-aliases.sh.tmpl +90 -5
  215. package/dot_config/shell/91-ux-aliases-lazy.sh.tmpl +33 -2
  216. package/dot_config/shell/Brewfile.cli +8 -0
  217. package/dot_config/shell/README.md +9 -9
  218. package/dot_config/shell/als_data.txt.tmpl +65 -0
  219. package/dot_config/shell/custom/auto_ls.zsh +20 -0
  220. package/dot_config/shell/custom/context_suggest.zsh +50 -50
  221. package/dot_config/shell/custom/error_analysis.zsh +29 -29
  222. package/dot_config/starship.toml.tmpl +85 -7
  223. package/dot_config/swaylock/config +43 -0
  224. package/dot_config/systemd/user/dotfiles-maintenance.service +8 -0
  225. package/dot_config/systemd/user/dotfiles-maintenance.timer +11 -0
  226. package/dot_config/systemd/user/pueued.service +12 -0
  227. package/dot_config/systemd/user/ssh-agent.service +12 -0
  228. package/dot_config/tmux/tmux.conf.tmpl +80 -38
  229. package/dot_config/topgrade.toml.tmpl +54 -0
  230. package/dot_config/user-dirs.dirs +9 -0
  231. package/dot_config/vscode/settings.json.tmpl +9 -50
  232. package/dot_config/waybar/config.jsonc.tmpl +90 -0
  233. package/dot_config/waybar/style.css.tmpl +211 -0
  234. package/dot_config/wezterm/wezterm.lua.tmpl +31 -203
  235. package/dot_config/xdg-desktop-portal/niri-portals.conf +5 -0
  236. package/dot_config/yazi/keymap.toml +67 -0
  237. package/dot_config/yazi/theme.toml +102 -0
  238. package/dot_config/zathura/zathurarc +52 -0
  239. package/dot_config/zellij/{config.kdl → config.kdl.tmpl} +1 -1
  240. package/dot_config/zsh/dot_zprofile +1 -7
  241. package/dot_config/zsh/dot_zshenv +2 -3
  242. package/dot_config/zsh/dot_zshrc.tmpl +368 -69
  243. package/dot_config/zsh/rc.d/00-alias-shims.zsh +77 -0
  244. package/dot_config/zsh/rc.d/05-ssh-agent.zsh +17 -4
  245. package/dot_config/zsh/rc.d/10-env.zsh.tmpl +4 -4
  246. package/dot_config/zsh/rc.d/20-zinit.zsh +65 -0
  247. package/dot_config/zsh/rc.d/30-options.zsh.tmpl +101 -30
  248. package/dot_config/zsh/rc.d/99-alias-wrapper.zsh +32 -0
  249. package/dot_local/bin/__pycache__/executable_dot-load-benchmark-ptycpython-312.pyc +0 -0
  250. package/dot_local/bin/executable_ai-update +195 -0
  251. package/dot_local/bin/executable_ai_core +113 -91
  252. package/dot_local/bin/executable_antigravity +11 -0
  253. package/dot_local/bin/executable_b64 +61 -36
  254. package/dot_local/bin/executable_bm +77 -0
  255. package/dot_local/bin/executable_cb +39 -0
  256. package/dot_local/bin/executable_dot +543 -132
  257. package/dot_local/bin/executable_dot-ai +36 -0
  258. package/dot_local/bin/executable_dot-bootstrap +77 -0
  259. package/dot_local/bin/executable_dot-launch-or-focus +52 -0
  260. package/dot_local/bin/executable_dot-load-benchmark +31 -0
  261. package/dot_local/bin/executable_dot-load-benchmark-pty +77 -0
  262. package/dot_local/bin/executable_dot-theme-sync +952 -0
  263. package/dot_local/bin/executable_dot_completion +2 -0
  264. package/dot_local/bin/executable_dtags +28 -0
  265. package/dot_local/bin/executable_epoch +43 -38
  266. package/dot_local/bin/executable_extract +32 -99
  267. package/dot_local/bin/executable_gbd +52 -0
  268. package/dot_local/bin/executable_gd +42 -0
  269. package/dot_local/bin/executable_git-ai-commit +35 -20
  270. package/dot_local/bin/executable_git-ai-diff +33 -18
  271. package/dot_local/bin/executable_gl +101 -0
  272. package/dot_local/bin/executable_hash +56 -18
  273. package/dot_local/bin/executable_hashsum +160 -0
  274. package/dot_local/bin/executable_hex +44 -8
  275. package/dot_local/bin/executable_jsonv +34 -10
  276. package/dot_local/bin/executable_jwt +22 -6
  277. package/dot_local/bin/executable_kill-port +18 -2
  278. package/dot_local/bin/executable_lorem +21 -6
  279. package/dot_local/bin/executable_mkscript +35 -0
  280. package/dot_local/bin/executable_monitor +56 -0
  281. package/dot_local/bin/{executable_ip → executable_myip} +16 -2
  282. package/dot_local/bin/executable_notify +22 -0
  283. package/dot_local/bin/executable_open +36 -0
  284. package/dot_local/bin/executable_pw +27 -0
  285. package/dot_local/bin/executable_rec-start +46 -0
  286. package/dot_local/bin/executable_rec-stop +35 -0
  287. package/dot_local/bin/executable_regex +49 -13
  288. package/dot_local/bin/executable_start-niri +8 -0
  289. package/dot_local/bin/executable_tmux-sessionizer +276 -149
  290. package/dot_local/bin/executable_tour +112 -74
  291. package/dot_local/bin/executable_up +21 -0
  292. package/dot_local/bin/executable_update +45 -11
  293. package/dot_local/bin/executable_uuid +37 -7
  294. package/dot_local/bin/executable_win +32 -0
  295. package/dot_local/bin/executable_yamlv +25 -7
  296. package/dot_local/share/bash-completion/completions/dot +62 -44
  297. package/dot_local/share/icons/hicolor/128x128/apps/rousseau.png +0 -0
  298. package/dot_local/share/icons/hicolor/16x16/apps/rousseau.png +0 -0
  299. package/dot_local/share/icons/hicolor/24x24/apps/rousseau.png +0 -0
  300. package/dot_local/share/icons/hicolor/256x256/apps/rousseau.png +0 -0
  301. package/dot_local/share/icons/hicolor/32x32/apps/rousseau.png +0 -0
  302. package/dot_local/share/icons/hicolor/48x48/apps/rousseau.png +0 -0
  303. package/dot_local/share/icons/hicolor/512x512/apps/rousseau.png +0 -0
  304. package/dot_local/share/icons/hicolor/64x64/apps/rousseau.png +0 -0
  305. package/dot_local/share/icons/hicolor/96x96/apps/rousseau.png +0 -0
  306. package/dot_local/share/icons/hicolor/scalable/apps/rousseau.png +0 -0
  307. package/dot_local/share/man/man1/dot.1 +149 -0
  308. package/dot_local/share/mise/plugins/kiro-cli/bin/executable_download +64 -0
  309. package/dot_local/share/mise/plugins/kiro-cli/bin/executable_install +18 -0
  310. package/dot_local/share/mise/plugins/kiro-cli/bin/executable_list-all +13 -0
  311. package/dot_local/share/wayland-sessions/niri.desktop.tmpl +6 -0
  312. package/dot_local/share/zsh/completions/_dot +283 -0
  313. package/install.sh +297 -210
  314. package/package.json +17 -5
  315. package/scripts/ci/check-copyright-headers.sh +96 -0
  316. package/scripts/ci/guard-gitleaks-checkout.sh +57 -0
  317. package/scripts/ci/install-chezmoi-verified.sh +64 -0
  318. package/scripts/ci/validate-ci-config.sh +41 -39
  319. package/scripts/demo/record.sh +3 -2
  320. package/scripts/diagnostics/a2a-conformance.sh +162 -0
  321. package/scripts/diagnostics/alias-governance.sh +137 -0
  322. package/scripts/diagnostics/aliases-cheatsheet.sh +73 -0
  323. package/scripts/diagnostics/aliases-manifest.sh +31 -0
  324. package/scripts/diagnostics/benchmark.sh +209 -29
  325. package/scripts/diagnostics/conflicts.sh +72 -0
  326. package/scripts/diagnostics/doctor-unified.sh +38 -0
  327. package/scripts/diagnostics/doctor.sh +491 -65
  328. package/scripts/diagnostics/drift-dashboard.sh +17 -6
  329. package/scripts/diagnostics/health.sh +351 -110
  330. package/scripts/diagnostics/history-analysis.sh +12 -4
  331. package/scripts/diagnostics/mcp-doctor.sh +575 -0
  332. package/scripts/diagnostics/perf.sh +225 -0
  333. package/scripts/diagnostics/scorecard.sh +118 -0
  334. package/scripts/diagnostics/secret-governance.sh +58 -0
  335. package/scripts/diagnostics/security-score.sh +139 -93
  336. package/scripts/diagnostics/smoke-test.sh +87 -0
  337. package/scripts/diagnostics/snapshot.sh +89 -0
  338. package/scripts/diagnostics/verify.sh +107 -0
  339. package/scripts/diagnostics/verify_state.sh +2 -2
  340. package/scripts/diagnostics/version-locks.sh +79 -0
  341. package/scripts/diagnostics/workstation-attestation.sh +181 -0
  342. package/scripts/docs/build-manual.sh +883 -0
  343. package/scripts/docs/check-manual.sh +213 -0
  344. package/scripts/dot/commands/agent.sh +465 -0
  345. package/scripts/dot/commands/ai.sh +416 -0
  346. package/scripts/dot/commands/aliases.sh +250 -0
  347. package/scripts/dot/commands/appearance.sh +21 -4
  348. package/scripts/dot/commands/core.sh +59 -4
  349. package/scripts/dot/commands/diagnostics.sh +132 -3
  350. package/scripts/dot/commands/fleet.sh +399 -0
  351. package/scripts/dot/commands/lint.sh +177 -0
  352. package/scripts/dot/commands/manual.sh +168 -0
  353. package/scripts/dot/commands/meta.sh +148 -17
  354. package/scripts/dot/commands/patterns.sh +54 -0
  355. package/scripts/dot/commands/restore.sh +57 -35
  356. package/scripts/dot/commands/secrets.sh +151 -9
  357. package/scripts/dot/commands/security.sh +3 -0
  358. package/scripts/dot/commands/tools.sh +268 -27
  359. package/scripts/dot/data/alias-deprecations.tsv +2 -0
  360. package/scripts/dot/lib/bento.sh +49 -0
  361. package/scripts/dot/lib/log.sh +194 -0
  362. package/scripts/dot/lib/platform.sh +98 -0
  363. package/scripts/dot/lib/ui.sh +507 -0
  364. package/scripts/dot/lib/utils.sh +318 -18
  365. package/scripts/fonts/install-nerd-fonts.sh +19 -10
  366. package/scripts/fonts/patch-fonts.sh +1 -0
  367. package/scripts/git-hooks/install.sh +11 -0
  368. package/scripts/git-hooks/pre-commit +12 -0
  369. package/scripts/git-hooks/pre-commit-audit.sh +145 -0
  370. package/scripts/git-hooks/pre-push +29 -0
  371. package/scripts/git-hooks/prepare-commit-msg +29 -0
  372. package/scripts/lib/secrets_provider.sh +158 -0
  373. package/scripts/maintenance/check-updates.sh +131 -0
  374. package/scripts/ops/ai-setup.sh +55 -0
  375. package/scripts/ops/bundle.sh +77 -0
  376. package/scripts/ops/chaos.sh +49 -0
  377. package/scripts/ops/chezmoi-apply.sh +245 -17
  378. package/scripts/ops/chezmoi-diff.sh +1 -0
  379. package/scripts/ops/chezmoi-remove.sh +2 -0
  380. package/scripts/ops/chezmoi-update.sh +34 -2
  381. package/scripts/ops/heal-chezmoi.sh +51 -0
  382. package/scripts/ops/heal-system.sh +128 -0
  383. package/scripts/ops/heal-tools.sh +340 -0
  384. package/scripts/ops/heal.sh +98 -342
  385. package/scripts/ops/post-apply-repair.sh +106 -0
  386. package/scripts/ops/prewarm.sh +127 -0
  387. package/scripts/ops/release.sh +252 -0
  388. package/scripts/ops/rollback.sh +86 -28
  389. package/scripts/ops/setup.sh +137 -0
  390. package/scripts/ops/teleport.sh +2 -1
  391. package/scripts/qa/coverage-baseline.sh +60 -0
  392. package/scripts/qa/docs-coverage.sh +111 -0
  393. package/scripts/qa/reliability-audit.sh +138 -0
  394. package/scripts/qa/traceability-coverage.sh +116 -0
  395. package/scripts/qa/validate-examples.sh +26 -0
  396. package/scripts/qa/wsl-contract.sh +11 -0
  397. package/scripts/release/package-policy-bundles.sh +125 -0
  398. package/scripts/secrets/age-init.sh +1 -0
  399. package/scripts/secrets/create-secrets-file.sh +1 -0
  400. package/scripts/secrets/encrypt-ssh-key.sh +1 -0
  401. package/scripts/security/backup.sh +44 -4
  402. package/scripts/security/dns-doh.sh +36 -14
  403. package/scripts/security/encryption-check.sh +27 -12
  404. package/scripts/security/enforce-policies.sh +11 -16
  405. package/scripts/security/firewall.sh +76 -20
  406. package/scripts/security/lock-configs.sh +2 -2
  407. package/scripts/security/lock-screen.sh +40 -18
  408. package/scripts/security/manage-secrets.sh +42 -29
  409. package/scripts/security/ssh-cert.sh +203 -0
  410. package/scripts/security/telemetry-kill.sh +40 -18
  411. package/scripts/security/usb-safety.sh +37 -15
  412. package/scripts/theme/apply-gnome-theme.sh +87 -56
  413. package/scripts/theme/extract-theme.py +677 -0
  414. package/scripts/theme/install-boot-logo.sh +1 -0
  415. package/scripts/theme/install-catppuccin-themes.sh +57 -33
  416. package/scripts/theme/install-cursors.sh +1 -0
  417. package/scripts/theme/install-file-icons.sh +1 -0
  418. package/scripts/theme/install-grub-theme.sh +11 -1
  419. package/scripts/theme/install-lock-icon.sh +1 -0
  420. package/scripts/theme/merge-wallpaper.sh +141 -0
  421. package/scripts/theme/rebuild-themes.sh +309 -0
  422. package/scripts/theme/switch.sh +308 -137
  423. package/scripts/theme/wallpaper-rotate.sh +66 -12
  424. package/scripts/theme/wallpaper-sync.sh +386 -31
  425. package/scripts/tools/cmatrix.sh +1 -0
  426. package/scripts/tools/detect-collisions.py +1 -1
  427. package/scripts/tools/emoji-picker.sh +4 -1
  428. package/scripts/tools/figlet-banner.sh +1 -0
  429. package/scripts/tools/log-rotate.sh +2 -1
  430. package/scripts/tools/lolcat-wrap.sh +1 -0
  431. package/scripts/tools/pipes.sh +1 -0
  432. package/scripts/tuning/linux.sh +23 -13
  433. package/scripts/tuning/macos.sh +13 -4
  434. package/scripts/uninstall.sh +85 -0
  435. package/scripts/version-sync.sh +487 -0
  436. package/templates/chezmoi-data/geekom-a9.toml.example +21 -0
  437. package/templates/chezmoi-data/mac-m1.toml.example +16 -0
  438. package/templates/chezmoi-data/mac-t2-linux.toml.example +21 -0
  439. package/templates/chezmoi-data/surface-pro-7p.toml.example +21 -0
  440. package/templates/projects/go/.github/workflows/ci.yml +31 -0
  441. package/templates/projects/go/README.md +1 -1
  442. package/templates/projects/go/cmd/__PROJECT_NAME__/main.go +1 -0
  443. package/templates/projects/go/go.sum +0 -0
  444. package/templates/projects/molecule/README.md +1 -1
  445. package/templates/projects/node/.github/workflows/ci.yml +30 -0
  446. package/templates/projects/node/README.md +1 -1
  447. package/templates/projects/node/package-lock.json +12 -0
  448. package/templates/projects/node/src/index.js +1 -0
  449. package/templates/projects/packer/README.md +1 -1
  450. package/templates/projects/python/.github/workflows/ci.yml +34 -0
  451. package/templates/projects/python/README.md +1 -1
  452. package/templates/projects/python/src/__PROJECT_NAME__/__init__.py +1 -0
  453. package/templates/projects/python/tests/test_basic.py +1 -0
  454. package/docs/ARCHITECTURE.md +0 -229
  455. package/docs/COMPLIANCE.md +0 -47
  456. package/docs/FEATURES.md +0 -166
  457. package/docs/FONTS.md +0 -147
  458. package/docs/INSTALL.md +0 -119
  459. package/docs/KEYS.md +0 -40
  460. package/docs/OPERATIONS.md +0 -130
  461. package/docs/PR_DESCRIPTION.md +0 -149
  462. package/docs/SECRETS.md +0 -76
  463. package/docs/SECURITY.md +0 -30
  464. package/docs/SECURITY_CHECKLIST.md +0 -25
  465. package/docs/TASK.md +0 -99
  466. package/docs/TESTING.md +0 -190
  467. package/docs/TOOLS.md +0 -146
  468. package/docs/UTILS.md +0 -105
  469. package/docs/WALKTHROUGH.md +0 -165
  470. package/dot_config/clippy.toml +0 -55
  471. package/dot_config/dotfiles/versions.env.tmpl +0 -19
  472. package/dot_config/firefox/user.js +0 -8
  473. package/dot_config/git/config.tmpl +0 -77
  474. package/dot_config/go/aliases.sh +0 -61
  475. package/dot_config/go/env.sh +0 -24
  476. package/dot_config/ice/README.md +0 -13
  477. package/dot_config/ipython/profile_default/ipython_config.py.tmpl +0 -5
  478. package/dot_config/lsd/config.yaml +0 -19
  479. package/dot_config/nvim/snippets/all.lua +0 -9
  480. package/dot_config/nvim/snippets/python.lua +0 -16
  481. package/dot_config/shell/00-container-detect.sh +0 -53
  482. package/dot_config/shell/05-core-safety.sh.tmpl +0 -13
  483. package/dot_config/shell/40-ls-colors.sh.tmpl +0 -31
  484. package/dot_config/shell/90-theme-switch.sh +0 -70
  485. package/dot_config/topgrade/topgrade.toml.tmpl +0 -42
  486. package/dot_config/waybar/style.css +0 -8
  487. package/dot_config/zsh/rc.d/20-zinit.zsh.tmpl +0 -22
  488. package/dot_local/bin/executable_zig-install +0 -89
  489. package/dot_local/bin/voice_ops +0 -7
  490. package/scripts/ops/health-check.sh +0 -380
  491. package/scripts/tests/README.md +0 -241
  492. package/scripts/tests/benchmark.sh +0 -91
  493. package/scripts/tests/framework/assertions.sh +0 -283
  494. package/scripts/tests/framework/mocks.sh +0 -201
  495. package/scripts/tests/framework/test_runner.sh +0 -189
  496. package/scripts/tests/integration/test_install.sh +0 -237
  497. package/scripts/tests/performance/benchmark_runner.sh +0 -156
  498. package/scripts/tests/performance/regression_check.sh +0 -73
  499. package/scripts/tests/performance/stress_test.sh +0 -65
  500. package/scripts/tests/test-aliases.sh +0 -25
  501. package/scripts/tests/test-docker.sh +0 -48
  502. package/scripts/tests/unit/test_ai_aliases.sh +0 -158
  503. package/scripts/tests/unit/test_ai_cli_checks.sh +0 -47
  504. package/scripts/tests/unit/test_backup.sh +0 -227
  505. package/scripts/tests/unit/test_case_functions.sh +0 -238
  506. package/scripts/tests/unit/test_cd_aliases.sh +0 -367
  507. package/scripts/tests/unit/test_dot_cli.sh +0 -217
  508. package/scripts/tests/unit/test_encode64.sh +0 -67
  509. package/scripts/tests/unit/test_environment.sh +0 -78
  510. package/scripts/tests/unit/test_extract.sh +0 -163
  511. package/scripts/tests/unit/test_framework_edge_cases.sh +0 -113
  512. package/scripts/tests/unit/test_genpass.sh +0 -298
  513. package/scripts/tests/unit/test_install_edge_cases.sh +0 -167
  514. package/scripts/tests/unit/test_installers.sh +0 -172
  515. package/scripts/tests/unit/test_keygen.sh +0 -68
  516. package/scripts/tests/unit/test_logging.sh +0 -96
  517. package/scripts/tests/unit/test_os_detection_comprehensive.sh +0 -388
  518. package/scripts/tests/unit/test_package_managers_comprehensive.sh +0 -321
  519. package/scripts/tests/unit/test_prependpath.sh +0 -66
  520. package/scripts/tests/unit/test_prependpath_refactored.sh +0 -89
  521. package/scripts/tests/unit/test_rd.sh +0 -230
  522. package/scripts/tests/unit/test_security_fixes.sh +0 -130
  523. package/scripts/tests/unit/test_security_scripts.sh +0 -87
  524. package/scripts/tests/unit/test_template_validation.sh +0 -238
  525. package/scripts/tests/unit/test_utility_functions.sh +0 -144
  526. package/scripts/tests/unit/test_wave1_alias_split.sh +0 -132
  527. package/scripts/tests/unit/test_wave1_ci_pinning.sh +0 -61
  528. package/scripts/tests/unit/test_wave1_gitleaks_config.sh +0 -52
  529. package/scripts/tests/unit/test_wave1_heal_fixes.sh +0 -97
  530. package/scripts/tests/unit/test_wave1_install_gpg.sh +0 -35
  531. package/scripts/tests/unit/test_wave1_zshenv_path.sh +0 -89
  532. package/scripts/tests/unit/test_wave1_zshrc_lazy_hook.sh +0 -115
  533. package/scripts/tests/unit/test_wave2_dot_new_guard.sh +0 -75
  534. package/scripts/tests/unit/test_zipf.sh +0 -76
  535. /package/docs/{LEGACY_ROADMAP.md → archive/LEGACY_ROADMAP.md} +0 -0
  536. /package/docs/{REPO_AUDIT.md → archive/REPO_AUDIT.md} +0 -0
  537. /package/docs/{KEY_ROTATION.md → security/KEY_ROTATION.md} +0 -0
  538. /package/dot_config/ansible/{ansible.cfg.tmpl → ansible.cfg} +0 -0
  539. /package/dot_config/docker/{config.json.tmpl → config.json} +0 -0
  540. /package/dot_config/k9s/{config.yaml.tmpl → config.yaml} +0 -0
  541. /package/dot_config/pypoetry/{config.toml.tmpl → config.toml} +0 -0
  542. /package/dot_config/{pyrightconfig.json.tmpl → pyrightconfig.json} +0 -0
  543. /package/dot_config/tflint/{tflint.hcl.tmpl → tflint.hcl} +0 -0
  544. /package/dot_config/yarn/{yarnrc.yml.tmpl → yarnrc.yml} +0 -0
  545. /package/dot_config/zsh/rc.d/{40-bell.zsh.tmpl → 40-bell.zsh} +0 -0
  546. /package/dot_config/zsh/rc.d/{50-login-fortune.zsh.tmpl → 50-login-fortune.zsh} +0 -0
@@ -0,0 +1,677 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # extract-theme.py — Generate a terminal theme from wallpaper dominant colors.
4
+ #
5
+ # Uses K-Means clustering in CIELAB color space for perceptually accurate
6
+ # dominant color extraction. No external dependencies — stdlib only.
7
+ # ImageMagick is used for image downsampling and pixel extraction.
8
+ #
9
+ # Usage:
10
+ # python3 extract-theme.py <image-path> [--name <theme-name>] [--format toml|json]
11
+ # python3 extract-theme.py /System/Library/Desktop\ Pictures/Sonoma.heic
12
+ # python3 extract-theme.py ~/Pictures/Wallpapers/macos-tahoe-dark.heic --name macos-tahoe-dark
13
+
14
+ import sys
15
+ import subprocess # nosec B404 — used only with fixed magick command, no shell
16
+ import math
17
+ import random # nosec B311 — used for K-Means seeding, not security
18
+ import json
19
+ import os
20
+ from typing import List, Tuple, Dict
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Color space conversions (RGB ↔ XYZ ↔ CIELAB)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def srgb_to_linear(c: float) -> float:
27
+ """Linearize an sRGB component (0-1)."""
28
+ return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
29
+
30
+
31
+ def linear_to_srgb(c: float) -> float:
32
+ """Gamma-compress a linear component to sRGB (0-1)."""
33
+ c = max(0.0, min(1.0, c))
34
+ return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1.0 / 2.4)) - 0.055
35
+
36
+
37
+ def rgb_to_xyz(r: int, g: int, b: int) -> Tuple[float, float, float]:
38
+ """Convert sRGB (0-255) to CIE XYZ (D65 illuminant)."""
39
+ rl = srgb_to_linear(r / 255.0)
40
+ gl = srgb_to_linear(g / 255.0)
41
+ bl = srgb_to_linear(b / 255.0)
42
+ x = 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl
43
+ y = 0.2126729 * rl + 0.7151522 * gl + 0.0721750 * bl
44
+ z = 0.0193339 * rl + 0.1191920 * gl + 0.9503041 * bl
45
+ return x, y, z
46
+
47
+
48
+ def xyz_to_lab(x: float, y: float, z: float) -> Tuple[float, float, float]:
49
+ """Convert CIE XYZ to CIELAB (D65 reference white)."""
50
+ xn, yn, zn = 0.95047, 1.00000, 1.08883
51
+
52
+ def f(t):
53
+ return t ** (1.0 / 3.0) if t > 0.008856 else 7.787 * t + 16.0 / 116.0
54
+
55
+ fx, fy, fz = f(x / xn), f(y / yn), f(z / zn)
56
+ L = 116.0 * fy - 16.0
57
+ a = 500.0 * (fx - fy)
58
+ b = 200.0 * (fy - fz)
59
+ return L, a, b
60
+
61
+
62
+ def lab_to_xyz(L: float, a: float, b: float) -> Tuple[float, float, float]:
63
+ """Convert CIELAB to CIE XYZ (D65 reference white)."""
64
+ xn, yn, zn = 0.95047, 1.00000, 1.08883
65
+ fy = (L + 16.0) / 116.0
66
+ fx = a / 500.0 + fy
67
+ fz = fy - b / 200.0
68
+
69
+ def inv_f(t):
70
+ return t ** 3 if t ** 3 > 0.008856 else (t - 16.0 / 116.0) / 7.787
71
+
72
+ return inv_f(fx) * xn, inv_f(fy) * yn, inv_f(fz) * zn
73
+
74
+
75
+ def xyz_to_rgb(x: float, y: float, z: float) -> Tuple[int, int, int]:
76
+ """Convert CIE XYZ to sRGB (0-255)."""
77
+ rl = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
78
+ gl = -0.9692660 * x + 1.8760108 * y + 0.0415561 * z
79
+ bl = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
80
+ r = int(round(linear_to_srgb(rl) * 255))
81
+ g = int(round(linear_to_srgb(gl) * 255))
82
+ b = int(round(linear_to_srgb(bl) * 255))
83
+ return max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))
84
+
85
+
86
+ def rgb_to_lab(r: int, g: int, b: int) -> Tuple[float, float, float]:
87
+ return xyz_to_lab(*rgb_to_xyz(r, g, b))
88
+
89
+
90
+ def lab_to_rgb(L: float, a: float, b: float) -> Tuple[int, int, int]:
91
+ return xyz_to_rgb(*lab_to_xyz(L, a, b))
92
+
93
+
94
+ def rgb_to_hex(r: int, g: int, b: int) -> str:
95
+ return f"#{r:02x}{g:02x}{b:02x}"
96
+
97
+
98
+ def hex_to_rgb(h: str) -> Tuple[int, int, int]:
99
+ h = h.lstrip("#")
100
+ return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Perceptual utilities
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def lab_distance(a: Tuple[float, ...], b: Tuple[float, ...]) -> float:
108
+ """Euclidean distance in CIELAB (ΔE*ab)."""
109
+ return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
110
+
111
+
112
+ def relative_luminance(r: int, g: int, b: int) -> float:
113
+ """WCAG relative luminance from sRGB."""
114
+ def ch(c):
115
+ s = c / 255.0
116
+ return s / 12.92 if s <= 0.03928 else ((s + 0.055) / 1.055) ** 2.4
117
+ return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b)
118
+
119
+
120
+ def contrast_ratio(c1: Tuple[int, int, int], c2: Tuple[int, int, int]) -> float:
121
+ l1 = relative_luminance(*c1)
122
+ l2 = relative_luminance(*c2)
123
+ return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
124
+
125
+
126
+ def lab_chroma(L: float, a: float, b: float) -> float:
127
+ """Chroma (saturation) in CIELAB."""
128
+ return math.sqrt(a * a + b * b)
129
+
130
+
131
+ def lab_hue(L: float, a: float, b: float) -> float:
132
+ """Hue angle in CIELAB (0-360)."""
133
+ h = math.degrees(math.atan2(b, a))
134
+ return h if h >= 0 else h + 360.0
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # K-Means clustering in CIELAB
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def _kmeans_init(pixels, k, rng):
142
+ """Initialize k centroids using K-Means++ seeding."""
143
+ n = len(pixels)
144
+ centroids = [pixels[rng.randint(0, n - 1)]]
145
+ for _ in range(1, k):
146
+ dists = [min(lab_distance(p, c) ** 2 for c in centroids) for p in pixels]
147
+ total = sum(dists)
148
+ if total == 0:
149
+ centroids.append(pixels[rng.randint(0, n - 1)])
150
+ continue
151
+ r = rng.random() * total
152
+ cumulative = 0.0
153
+ for i, d in enumerate(dists):
154
+ cumulative += d
155
+ if cumulative >= r:
156
+ centroids.append(pixels[i])
157
+ break
158
+ return centroids
159
+
160
+
161
+ def _assign_labels(pixels, centroids, labels):
162
+ """Assign each pixel to its nearest centroid. Returns number of changed labels."""
163
+ changed = 0
164
+ for i, p in enumerate(pixels):
165
+ best_j = 0
166
+ best_d = lab_distance(p, centroids[0])
167
+ for j in range(1, len(centroids)):
168
+ d = lab_distance(p, centroids[j])
169
+ if d < best_d:
170
+ best_d = d
171
+ best_j = j
172
+ if labels[i] != best_j:
173
+ changed += 1
174
+ labels[i] = best_j
175
+ return changed
176
+
177
+
178
+ def _update_centroids(pixels, labels, centroids):
179
+ """Recompute centroids as the mean of assigned pixels."""
180
+ n = len(pixels)
181
+ for j, _ in enumerate(centroids):
182
+ members = [pixels[i] for i in range(n) if labels[i] == j]
183
+ if members:
184
+ centroids[j] = tuple(
185
+ sum(m[d] for m in members) / len(members) for d in range(3)
186
+ )
187
+
188
+
189
+ def _kmeans_single_run(pixels, k, max_iter, rng):
190
+ """Run one K-Means iteration loop. Returns (centroids, labels, inertia)."""
191
+ centroids = _kmeans_init(pixels, k, rng)
192
+ labels = [0] * len(pixels)
193
+ for _ in range(max_iter):
194
+ changed = _assign_labels(pixels, centroids, labels)
195
+ if changed == 0:
196
+ break
197
+ _update_centroids(pixels, labels, centroids)
198
+ inertia = sum(
199
+ lab_distance(pixels[i], centroids[labels[i]]) ** 2 for i in range(len(pixels))
200
+ )
201
+ return centroids, labels, inertia
202
+
203
+
204
+ def kmeans_lab(
205
+ pixels: List[Tuple[float, float, float]],
206
+ k: int = 8,
207
+ max_iter: int = 20,
208
+ runs: int = 3,
209
+ ) -> List[Tuple[Tuple[float, float, float], int]]:
210
+ """Run K-Means clustering in CIELAB space with best-of-N initialization."""
211
+ n = len(pixels)
212
+ if n == 0:
213
+ return []
214
+
215
+ best_centroids = None
216
+ best_labels = None
217
+ best_inertia = float("inf")
218
+
219
+ for run_idx in range(runs):
220
+ rng = random.Random(run_idx * 42 + 7) # nosec B311
221
+ centroids, labels, inertia = _kmeans_single_run(pixels, k, max_iter, rng)
222
+ if inertia < best_inertia:
223
+ best_inertia = inertia
224
+ best_centroids = centroids[:]
225
+ best_labels = labels[:]
226
+
227
+ populations = [0] * k
228
+ for label in best_labels:
229
+ populations[label] += 1
230
+
231
+ result = [(best_centroids[j], populations[j]) for j in range(k)]
232
+ result.sort(key=lambda x: x[1], reverse=True)
233
+ return result
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Pixel extraction via ImageMagick
238
+ # ---------------------------------------------------------------------------
239
+
240
+ def extract_pixels(image_path: str, max_dim: int = 80) -> List[Tuple[int, int, int]]:
241
+ """Downsample image and extract RGB pixels using ImageMagick."""
242
+ # Validate path contains no shell metacharacters (defense in depth)
243
+ base_path = image_path.split("[")[0] if "[" in image_path else image_path
244
+ if not os.path.isfile(base_path):
245
+ raise FileNotFoundError(f"Image not found: {base_path}")
246
+ cmd = [
247
+ "magick", image_path,
248
+ "-resize", f"{max_dim}x{max_dim}>",
249
+ "-depth", "8",
250
+ "txt:-",
251
+ ]
252
+ # cmd is a fixed list with validated image_path, shell=False by default
253
+ result = subprocess.run( # nosec B603
254
+ cmd, capture_output=True, text=True, timeout=30, check=False
255
+ )
256
+ if result.returncode != 0:
257
+ raise RuntimeError(f"ImageMagick failed: {result.stderr}")
258
+
259
+ pixels = []
260
+ for line in result.stdout.splitlines():
261
+ if line.startswith("#"):
262
+ continue
263
+ # Format: "x,y: (R,G,B) #RRGGBB srgb(R,G,B)" or with alpha
264
+ parts = line.split("(")
265
+ if len(parts) < 2:
266
+ continue
267
+ color_part = parts[1].split(")")[0]
268
+ components = [c.strip() for c in color_part.split(",")]
269
+ if len(components) >= 3:
270
+ try:
271
+ r, g, b = int(components[0]), int(components[1]), int(components[2])
272
+ pixels.append((r, g, b))
273
+ except ValueError:
274
+ continue
275
+ return pixels
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Theme generation from dominant colors
280
+ # ---------------------------------------------------------------------------
281
+
282
+ # ANSI hue targets in CIELAB hue angle
283
+ ANSI_HUES = {
284
+ "red": 30.0,
285
+ "green": 145.0,
286
+ "yellow": 95.0,
287
+ "blue": 275.0,
288
+ "magenta": 330.0,
289
+ "cyan": 210.0,
290
+ }
291
+
292
+
293
+ def find_nearest_hue(hue: float) -> str:
294
+ """Map a CIELAB hue angle to the nearest ANSI color name."""
295
+ best_name = "red"
296
+ best_dist = 360.0
297
+ for name, target in ANSI_HUES.items():
298
+ dist = min(abs(hue - target), 360 - abs(hue - target))
299
+ if dist < best_dist:
300
+ best_dist = dist
301
+ best_name = name
302
+ return best_name
303
+
304
+
305
+ def adjust_lightness(lab: Tuple[float, float, float], target_L: float) -> Tuple[float, float, float]:
306
+ """Adjust L* while preserving hue and chroma."""
307
+ L, a, b = lab
308
+ if L == 0:
309
+ return (target_L, a, b)
310
+ scale = target_L / L
311
+ return (target_L, a * min(scale, 1.5), b * min(scale, 1.5))
312
+
313
+
314
+ def ensure_contrast(
315
+ fg_rgb: Tuple[int, int, int],
316
+ bg_rgb: Tuple[int, int, int],
317
+ min_ratio: float,
318
+ is_dark: bool,
319
+ ) -> Tuple[int, int, int]:
320
+ """Adjust fg lightness until contrast ratio meets min_ratio against bg."""
321
+ fl, fa, fb = rgb_to_lab(*fg_rgb)
322
+ for _ in range(80):
323
+ cr = contrast_ratio(lab_to_rgb(fl, fa, fb), bg_rgb)
324
+ if cr >= min_ratio:
325
+ break
326
+ fl += 2.0 if is_dark else -2.0
327
+ fl = max(0.0, min(100.0, fl))
328
+ return lab_to_rgb(fl, fa, fb)
329
+
330
+
331
+ def _nvim_from_hue(hue: float, is_dark: bool) -> Tuple[str, str]:
332
+ """Map accent hue angle to nearest Neovim colorscheme."""
333
+ if 60 <= hue < 150:
334
+ return ("everforest", "hard" if is_dark else "soft")
335
+ if 210 <= hue < 270:
336
+ return ("tokyonight", "night" if is_dark else "day")
337
+ return ("catppuccin", "mocha" if is_dark else "latte")
338
+
339
+
340
+ def _macos_accent_from_hue(hue: float) -> int:
341
+ """Map accent hue angle to macOS accent color integer."""
342
+ if hue < 30 or hue >= 345:
343
+ return 0 # Red
344
+ if hue < 60:
345
+ return 1 # Orange
346
+ if hue < 105:
347
+ return 2 # Yellow
348
+ if hue < 165:
349
+ return 3 # Green
350
+ if hue < 255:
351
+ return 4 # Blue
352
+ if hue < 300:
353
+ return 5 # Purple
354
+ return 6 # Pink
355
+
356
+
357
+ def _compute_bg_fg(clusters, is_dark):
358
+ """Select background and foreground from clustered colors."""
359
+ sorted_by_L = sorted(clusters, key=lambda c: c[0][0])
360
+ if is_dark:
361
+ bg_lab = sorted_by_L[0][0]
362
+ bg_lab = (min(bg_lab[0], 15.0), bg_lab[1] * 0.3, bg_lab[2] * 0.3)
363
+ fg_lab = (88.0, bg_lab[1] * 0.1, bg_lab[2] * 0.1)
364
+ else:
365
+ bg_lab = sorted_by_L[-1][0]
366
+ bg_lab = (max(bg_lab[0], 92.0), bg_lab[1] * 0.15, bg_lab[2] * 0.15)
367
+ fg_lab = (18.0, bg_lab[1] * 0.15, bg_lab[2] * 0.15)
368
+ bg_rgb = lab_to_rgb(*bg_lab)
369
+ fg_rgb = ensure_contrast(lab_to_rgb(*fg_lab), bg_rgb, 7.0, is_dark)
370
+ return bg_lab, bg_rgb, fg_rgb
371
+
372
+
373
+ def _compute_accent(clusters, is_dark):
374
+ """Select and AAA-compliant-darken the most saturated cluster for accent."""
375
+ sorted_by_chroma = sorted(clusters, key=lambda c: lab_chroma(*c[0]), reverse=True)
376
+ accent_lab = sorted_by_chroma[0][0]
377
+ if is_dark:
378
+ accent_lab = (max(accent_lab[0], 35.0), accent_lab[1], accent_lab[2])
379
+ else:
380
+ accent_lab = (min(accent_lab[0], 45.0), accent_lab[1], accent_lab[2])
381
+ # Darken until white text has 7:1 contrast (AAA)
382
+ al, aa, ab = accent_lab
383
+ for _ in range(80):
384
+ if contrast_ratio((255, 255, 255), lab_to_rgb(al, aa, ab)) >= 7.0:
385
+ break
386
+ al = max(0.0, al - 2.0)
387
+ return (al, aa, ab), lab_to_rgb(al, aa, ab)
388
+
389
+
390
+ def _compute_panel_border(bg_lab, bg_rgb, is_dark):
391
+ """Compute panel and border with enforced contrast ranges against bg."""
392
+ if is_dark:
393
+ panel_lab = (min(bg_lab[0] + 3, 100), bg_lab[1], bg_lab[2])
394
+ border_lab = (bg_lab[0] + 8, bg_lab[1] * 0.5, bg_lab[2] * 0.5)
395
+ else:
396
+ panel_lab = (max(bg_lab[0] - 3, 0), bg_lab[1], bg_lab[2])
397
+ border_lab = (bg_lab[0] - 6, bg_lab[1] * 0.3, bg_lab[2] * 0.3)
398
+ panel_rgb = lab_to_rgb(*panel_lab)
399
+ for _ in range(20):
400
+ pr = contrast_ratio(panel_rgb, bg_rgb)
401
+ if 1.03 <= pr <= 2.0:
402
+ break
403
+ if pr < 1.03:
404
+ panel_lab = (panel_lab[0] + (2 if is_dark else -2), panel_lab[1], panel_lab[2])
405
+ else:
406
+ panel_lab = (panel_lab[0] + (-1 if is_dark else 1), panel_lab[1], panel_lab[2])
407
+ panel_lab = (max(0, min(100, panel_lab[0])), panel_lab[1], panel_lab[2])
408
+ panel_rgb = lab_to_rgb(*panel_lab)
409
+ return panel_rgb, lab_to_rgb(*border_lab)
410
+
411
+
412
+ def _build_ansi_color(base_lab, accent_lab, bg_rgb, is_dark):
413
+ """Build normal + bright ANSI variant from a base Lab color."""
414
+ if is_dark:
415
+ normal_L = max(55.0, min(75.0, base_lab[0]))
416
+ bright_L = normal_L + 12
417
+ else:
418
+ normal_L = max(30.0, min(50.0, base_lab[0]))
419
+ bright_L = normal_L - 8
420
+ normal = adjust_lightness(base_lab, normal_L)
421
+ bright = adjust_lightness(base_lab, bright_L)
422
+ normal_rgb = ensure_contrast(lab_to_rgb(*normal), bg_rgb, 3.0, is_dark)
423
+ bright_rgb = ensure_contrast(lab_to_rgb(*bright), bg_rgb, 4.5, is_dark)
424
+ return normal_rgb, bright_rgb
425
+
426
+
427
+ def _ansi_palette(clusters, accent_lab, bg_rgb, is_dark):
428
+ """Generate the 6 chromatic ANSI hues (red, green, yellow, blue, magenta, cyan)."""
429
+ chromatic = [(c, pop) for c, pop in clusters if lab_chroma(*c) > 10]
430
+ hue_assignments: Dict[str, List[Tuple[float, float, float]]] = {h: [] for h in ANSI_HUES}
431
+ for c_lab, _ in chromatic:
432
+ hue_assignments[find_nearest_hue(lab_hue(*c_lab))].append(c_lab)
433
+
434
+ accent_chroma = lab_chroma(*accent_lab)
435
+ ansi = {}
436
+ for hue_name, target_hue in ANSI_HUES.items():
437
+ candidates = hue_assignments[hue_name]
438
+ if candidates:
439
+ base = max(candidates, key=lambda c: lab_chroma(*c))
440
+ else:
441
+ new_a = accent_chroma * math.cos(math.radians(target_hue))
442
+ new_b = accent_chroma * math.sin(math.radians(target_hue))
443
+ base = (50.0, new_a, new_b)
444
+ ansi[hue_name] = _build_ansi_color(base, accent_lab, bg_rgb, is_dark)
445
+ return ansi
446
+
447
+
448
+ def _structural_colors(bg_lab, bg_rgb, is_dark):
449
+ """Compute c0, c7, c8, c15 structural ANSI colors."""
450
+ if is_dark:
451
+ return (
452
+ ensure_contrast(lab_to_rgb(bg_lab[0] + 10, bg_lab[1], bg_lab[2]), bg_rgb, 1.5, True),
453
+ ensure_contrast(lab_to_rgb(75.0, bg_lab[1] * 0.1, bg_lab[2] * 0.1), bg_rgb, 5.0, True),
454
+ ensure_contrast(lab_to_rgb(bg_lab[0] + 25, bg_lab[1], bg_lab[2]), bg_rgb, 2.5, True),
455
+ ensure_contrast(lab_to_rgb(90.0, bg_lab[1] * 0.05, bg_lab[2] * 0.05), bg_rgb, 7.0, True),
456
+ )
457
+ return (
458
+ ensure_contrast(lab_to_rgb(18.0, bg_lab[1] * 0.2, bg_lab[2] * 0.2), bg_rgb, 7.0, False),
459
+ ensure_contrast(lab_to_rgb(bg_lab[0] - 8, bg_lab[1], bg_lab[2]), bg_rgb, 1.3, False),
460
+ ensure_contrast(lab_to_rgb(35.0, bg_lab[1] * 0.2, bg_lab[2] * 0.2), bg_rgb, 4.5, False),
461
+ ensure_contrast(lab_to_rgb(8.0, 0, 0), bg_rgb, 10.0, False),
462
+ )
463
+
464
+
465
+ def generate_theme(
466
+ clusters: List[Tuple[Tuple[float, float, float], int]],
467
+ name: str,
468
+ is_dark: bool,
469
+ ) -> Dict:
470
+ """Generate a full theme definition from clustered dominant colors."""
471
+ bg_lab, bg_rgb, fg_rgb = _compute_bg_fg(clusters, is_dark)
472
+ accent_lab, accent_rgb = _compute_accent(clusters, is_dark)
473
+ accent_text = (255, 255, 255)
474
+ cursor_rgb = accent_rgb
475
+
476
+ # Selection background
477
+ if is_dark:
478
+ sel_lab = (bg_lab[0] + 15, accent_lab[1] * 0.4, accent_lab[2] * 0.4)
479
+ else:
480
+ sel_lab = (bg_lab[0] - 12, accent_lab[1] * 0.3, accent_lab[2] * 0.3)
481
+ sel_rgb = lab_to_rgb(*sel_lab)
482
+
483
+ panel_rgb, border_rgb = _compute_panel_border(bg_lab, bg_rgb, is_dark)
484
+ ansi = _ansi_palette(clusters, accent_lab, bg_rgb, is_dark)
485
+ c0_rgb, c7_rgb, c8_rgb, c15_rgb = _structural_colors(bg_lab, bg_rgb, is_dark)
486
+
487
+ accent_hue = lab_hue(*accent_lab)
488
+ nvim_theme = _nvim_from_hue(accent_hue, is_dark)
489
+ macos_accent = _macos_accent_from_hue(accent_hue)
490
+
491
+ mode = "dark" if is_dark else "light"
492
+
493
+ return {
494
+ "name": name,
495
+ "mode": mode,
496
+ "family": name.rsplit("-", 1)[0] if name.endswith(f"-{mode}") else name,
497
+ "macos_accent": macos_accent,
498
+ "wallpaper": "", # Set by caller
499
+ "source": "custom", # Set by caller
500
+ "term": {
501
+ "bg": rgb_to_hex(*bg_rgb),
502
+ "fg": rgb_to_hex(*fg_rgb),
503
+ "cursor": rgb_to_hex(*cursor_rgb),
504
+ "cursor_text": rgb_to_hex(*bg_rgb),
505
+ "sel_bg": rgb_to_hex(*sel_rgb),
506
+ "sel_fg": rgb_to_hex(*fg_rgb),
507
+ "c0": rgb_to_hex(*c0_rgb),
508
+ "c1": rgb_to_hex(*ansi["red"][0]),
509
+ "c2": rgb_to_hex(*ansi["green"][0]),
510
+ "c3": rgb_to_hex(*ansi["yellow"][0]),
511
+ "c4": rgb_to_hex(*ansi["blue"][0]),
512
+ "c5": rgb_to_hex(*ansi["magenta"][0]),
513
+ "c6": rgb_to_hex(*ansi["cyan"][0]),
514
+ "c7": rgb_to_hex(*c7_rgb),
515
+ "c8": rgb_to_hex(*c8_rgb),
516
+ "c9": rgb_to_hex(*ansi["red"][1]),
517
+ "c10": rgb_to_hex(*ansi["green"][1]),
518
+ "c11": rgb_to_hex(*ansi["yellow"][1]),
519
+ "c12": rgb_to_hex(*ansi["blue"][1]),
520
+ "c13": rgb_to_hex(*ansi["magenta"][1]),
521
+ "c14": rgb_to_hex(*ansi["cyan"][1]),
522
+ "c15": rgb_to_hex(*c15_rgb),
523
+ },
524
+ "ui": {
525
+ "accent": rgb_to_hex(*accent_rgb),
526
+ "accent_text": rgb_to_hex(*accent_text),
527
+ "error": rgb_to_hex(*ansi["red"][0]),
528
+ "warning": rgb_to_hex(*ansi["yellow"][0]),
529
+ "success": rgb_to_hex(*ansi["green"][0]),
530
+ "info": rgb_to_hex(*ansi["blue"][0]),
531
+ "panel": rgb_to_hex(*panel_rgb),
532
+ "border": rgb_to_hex(*border_rgb),
533
+ },
534
+ "app": {
535
+ "nvim": nvim_theme[0],
536
+ "nvim_style": nvim_theme[1],
537
+ "lualine": nvim_theme[0],
538
+ "gtk_theme": "Adwaita-dark" if is_dark else "Adwaita",
539
+ "gtk_icon": "Papirus-Dark" if is_dark else "Papirus-Light",
540
+ "gnome_shell": "",
541
+ "gnome_gtk": "Adwaita-dark" if is_dark else "Adwaita",
542
+ "vscode": f"{nvim_theme[0].replace('-', ' ').title()} {'Mocha' if is_dark else 'Latte'}",
543
+ "vscode_dark": f"{nvim_theme[0].replace('-', ' ').title()} Mocha",
544
+ "vscode_light": f"{nvim_theme[0].replace('-', ' ').title()} Latte",
545
+ "cat_wallpaper": "",
546
+ "starship_palette": f"catppuccin_{'mocha' if is_dark else 'latte'}",
547
+ },
548
+ }
549
+
550
+
551
+ # ---------------------------------------------------------------------------
552
+ # TOML output
553
+ # ---------------------------------------------------------------------------
554
+
555
+ def theme_to_toml(theme: Dict) -> str:
556
+ """Render a theme dict as TOML sections."""
557
+ name = theme["name"]
558
+ lines = []
559
+ lines.append(f'[themes.{name}]')
560
+ lines.append(f'mode = "{theme["mode"]}"')
561
+ lines.append(f'family = "{theme["family"]}"')
562
+ lines.append(f'macos_accent = {theme["macos_accent"]}')
563
+ lines.append(f'wallpaper = "{theme["wallpaper"]}"')
564
+ lines.append(f'source = "{theme["source"]}"')
565
+ lines.append("")
566
+
567
+ lines.append(f"[themes.{name}.term]")
568
+ for key in ["bg", "fg", "cursor", "cursor_text", "sel_bg", "sel_fg"]:
569
+ lines.append(f'{key} = "{theme["term"][key]}"')
570
+ for i in range(16):
571
+ key = f"c{i}"
572
+ pad = " " * (4 - len(key))
573
+ lines.append(f'{key}{pad}= "{theme["term"][key]}"')
574
+ lines.append("")
575
+
576
+ lines.append(f"[themes.{name}.ui]")
577
+ for key in ["accent", "accent_text", "error", "warning", "success", "info", "panel", "border"]:
578
+ lines.append(f'{key} = "{theme["ui"][key]}"')
579
+ lines.append("")
580
+
581
+ lines.append(f"[themes.{name}.app]")
582
+ for key, val in theme["app"].items():
583
+ lines.append(f'{key} = "{val}"')
584
+
585
+ return "\n".join(lines)
586
+
587
+
588
+ # ---------------------------------------------------------------------------
589
+ # Main
590
+ # ---------------------------------------------------------------------------
591
+
592
+ def determine_mode(pixels: List[Tuple[int, int, int]]) -> bool:
593
+ """Determine if image is dark (True) or light (False) from average luminance."""
594
+ if not pixels:
595
+ return True
596
+ avg_lum = sum(relative_luminance(*p) for p in pixels) / len(pixels)
597
+ return avg_lum < 0.35
598
+
599
+
600
+ def derive_name(image_path: str) -> str:
601
+ """Derive a theme name from the image filename."""
602
+ base = os.path.splitext(os.path.basename(image_path))[0]
603
+ # Normalize: lowercase, replace spaces with hyphens, strip non-alphanum
604
+ name = base.lower().replace(" ", "-").replace("_", "-")
605
+ name = "".join(c for c in name if c.isalnum() or c == "-")
606
+ # Remove consecutive hyphens
607
+ while "--" in name:
608
+ name = name.replace("--", "-")
609
+ return name.strip("-")
610
+
611
+
612
+ def main():
613
+ import argparse
614
+
615
+ parser = argparse.ArgumentParser(
616
+ description="Extract a terminal theme from a wallpaper image."
617
+ )
618
+ parser.add_argument("image", help="Path to wallpaper image")
619
+ parser.add_argument("--name", help="Theme name (default: derived from filename)")
620
+ parser.add_argument("--format", choices=["toml", "json"], default="toml")
621
+ parser.add_argument("--clusters", type=int, default=8, help="Number of K-Means clusters")
622
+ parser.add_argument("--source", choices=["system", "custom"], default="custom", help="Wallpaper source type")
623
+ args = parser.parse_args()
624
+
625
+ # Extract pixels
626
+ try:
627
+ pixels = extract_pixels(args.image)
628
+ except FileNotFoundError as exc:
629
+ print(f"Error: {exc}", file=sys.stderr)
630
+ sys.exit(1)
631
+ if not pixels:
632
+ print("Error: no pixels extracted", file=sys.stderr)
633
+ sys.exit(1)
634
+
635
+ # Derive name
636
+ name = args.name or derive_name(args.image)
637
+
638
+ # Determine dark/light: trust name suffix if present, else detect from pixels
639
+ if name.endswith("-dark"):
640
+ is_dark = True
641
+ elif name.endswith("-light"):
642
+ is_dark = False
643
+ else:
644
+ is_dark = determine_mode(pixels)
645
+ name += "-dark" if is_dark else "-light"
646
+
647
+ # Subsample for speed — 2000 pixels is enough for accurate K-Means
648
+ if len(pixels) > 2000:
649
+ rng = random.Random(42) # nosec B311 — deterministic sampling
650
+ pixels = rng.sample(pixels, 2000)
651
+
652
+ # Convert to CIELAB
653
+ lab_pixels = [rgb_to_lab(*p) for p in pixels]
654
+
655
+ # Filter near-black and near-white to avoid muddy palettes
656
+ filtered = [p for p in lab_pixels if 5 < p[0] < 95]
657
+ if len(filtered) < len(lab_pixels) * 0.1:
658
+ filtered = lab_pixels # Image is mostly black/white, use all
659
+
660
+ # K-Means clustering
661
+ clusters = kmeans_lab(filtered, k=args.clusters, runs=3)
662
+
663
+ # Generate theme
664
+ theme = generate_theme(clusters, name, is_dark)
665
+ wp_base = args.image.split("[")[0] if "[" in args.image else args.image
666
+ theme["wallpaper"] = os.path.abspath(wp_base)
667
+ theme["source"] = args.source
668
+
669
+ # Output
670
+ if args.format == "json":
671
+ print(json.dumps(theme, indent=2))
672
+ else:
673
+ print(theme_to_toml(theme))
674
+
675
+
676
+ if __name__ == "__main__":
677
+ main()
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
2
3
  set -euo pipefail
3
4
 
4
5
  logo_path="${DOTFILES_BOOT_LOGO:-$HOME/.config/dotfiles/boot/logo.png}"