Chezmoi-managed dotfiles for my Hyprland desktop. A palette-driven theme system wires one JSON into every app (Hyprland, Strand, GTK, Qt, Kitty, Zed, Yazi, btop, Obsidian), so switching from Everforest to Catppuccin is a single command, and the shell animates to the new colours.
- Window Manager: Hyprland
- Desktop Shell: Strand (island bar, launcher, notifications, lock screen, polkit password prompt, wallpaper picker, screen recording, Upkeep package manager; hyprlock is the fallback locker)
- Terminal: Kitty (its drag-and-drop protocol lets Yazi drag files in and out)
- Editor: Zed with hand-crafted themes per palette
- File Manager: Yazi, in Kitty (Super+Shift+F)
- System Monitor: btop
- Theme System: Palette-based runtime theme switching across all apps
- GTK Theming: libadwaita color names for GTK4 apps, and the same for GTK3 apps through the adw-gtk3 theme
- Qt Theming: Qt5 apps (VLC) through qt5ct, Fusion coloured from the palette
- Monitors and Thunderbolt: Strand, named monitor profiles with their workspaces, several a set and one its default (
monitors.toml; it writesmonitors.lua), and bolt's devices allowed from the launcher - Login Manager: greetd + tuigreet
chezmoi init --apply https://github.com/jakeb-grant/dotfiles.git
exec bash # or log out and back in
theme-switch everforestchezmoi overwrites the files it manages. To see what would change first, run chezmoi init with the same URL, then chezmoi diff, then chezmoi apply.
These dotfiles are built for arch-quickstart, a custom Arch ISO, which installs everything below: the packages from its package lists, and Strand and a Rust toolchain as user installs. On another Arch system, the configs and scripts need:
- Strand, the shell:
curl -fsSL https://strand.jacobgrant.dev/install.sh | sh -s -- --edge. Edge, because these configs track Strand's main branch (monitors.strandneedsd9d726f4or later). - Apps:
kitty,yazi,zed,btop, and1password(AUR), whose SSH agentprivate_1Password/configures - Desktop:
awww(wallpapers),bolt(Thunderbolt; withjqandglib2'sgdbusforbolt-devices),hypridle,hyprlock,phinger-cursors(AUR),adw-gtk-theme(the GTK3 base thattheme-switchsets andgtk-3.0/gtk.cssrecolours),qt5ct - Shell features:
wf-recorder(screen recording),libqalculate(the launcher's sums),grim,slurpandwl-clipboard(screenshots),hyprpicker - Scripts:
uv(theme-switchandwallpaper-upscalerun asuv run --script),jqand ImageMagick (wallpaper-split),acl(setfacl, for the Chromium policy script),rustupwith a default toolchain (rustup default stable; the Zed extension build);fzfandfnmare used by.bashrcwhen present - Login:
greetd,greetd-tuigreetand agreeteruser; the greetd setup script writes/etc/greetd/config.tomlpointing at them and atstart-hyprland, which ships with hyprland
Install notes:
- The
run_oncesetup scripts (greetd, Chromium policies) and therun_onchangescripts for the Thunderbolt resume service, the IPU7 camera relay (on machines with that camera; it also needsv4l2-relaydfrom the AUR) and GPU symlinks (onprimemachines) need interactive sudo — a fully unattendedchezmoi init --applywill stop and prompt. arch-quickstart's installer gives them passwordless sudo for the length of its apply, and setsARCH_QUICKSTART_INSTALL=1so a script can tell it runs in the installer's chroot. - The greetd script heredocs its config straight into
/etc, so later out-of-band edits there are invisible tochezmoi diff— accepted trade-off for a personal repo; the script is the source of truth. - chezmoi only manages the
.obsidiantheme files inside the~/knowledgeObsidian vault, so an install that applies before the vault is cloned (arch-quickstart does, in the chroot) leaves a bare~/knowledge/.obsidian, andgit clonethen refuses the non-empty directory. Clone into it instead:git -C ~/knowledge init && git -C ~/knowledge remote add origin https://github.com/jakeb-grant/knowledge.git && git -C ~/knowledge fetch && git -C ~/knowledge checkout -f main, thenchezmoi applyto put the theme back.
dot_config/
├── hypr/ # Hyprland window manager + hypridle + hyprlock (fallback)
├── strand/ # Desktop shell (Strand)
│ ├── island.strand # The island: workspaces, status, clock, OSD, polkit prompt
│ ├── launcher.strand # Launcher: apps, Wi-Fi, Bluetooth, sound, themes, wallpapers, power…
│ ├── lock.strand # Lock screen (answers loginctl lock-session)
│ ├── monitors.strand # Monitor profiles, the launcher's Displays page
│ ├── notify.strand # Notification server
│ ├── record.strand # Screen recording (wf-recorder)
│ ├── upkeep.strand # Upkeep: updates, installed, orphans, search, Arch news
│ └── vm.strand # Windows VM start-up on the island
├── kitty/ # Terminal emulator + a colour theme per palette; launch-actions
│ # opens folders handed to it (the system folder handler) in Yazi
├── zed/ # Zed editor settings + hand-crafted themes
├── yazi/ # Yazi file manager theme
├── btop/ # System monitor theme
├── gtk-3.0/ # GTK3 color overrides
├── gtk-4.0/ # GTK4/libadwaita color overrides
├── qt5ct/ # Qt5 apps (VLC): Fusion, coloured from the palette
├── age/ # age recipients (public key) — ~/knowledge's git clean
│ # filter encrypts personal/finance/** to this key
│ # (decrypt side reads the private key from 1Password)
├── private_1Password/ # 1Password SSH agent config (serves keys from the Developer vault)
├── windows-vm/ # Windows VM (docker-compose)
├── wireplumber/ # Hides the IPU7 raw camera nodes (see run_onchange_after_setup-ipu7-camera)
├── palette/ # Theme palette definitions (JSON)
└── wallpapers/ # Flat image pool + wallpapers.json config
knowledge/
└── dot_obsidian/ # Obsidian vault theme (~/knowledge/ must be the vault)
├── appearance.json # Selects "Palette" theme
└── themes/Palette/ # Theme CSS template + manifest
dot_local/share/applications/
└── win-vm.desktop # "Windows VM" in the launcher
dot_local/bin/
├── ipu7-camera-check # Check the IPU7 camera relay end to end
├── screenshot # Capture (region/full) + clipboard + notification
├── term # Kitty's single instance; Strand's launcher runs terminal apps through it
├── theme-switch # Theme switching utility
├── upkeep-tx # Upkeep's package changes (pkexec pacman, yay, or Kitty)
├── wallpaper-split # Split wide/panoramic images into per-monitor sets
├── wallpaper-upscale # AI upscale images using EDSR 4x (CPU)
└── win-vm # Windows VM management
dot_bashrc # Shell config (PATH, Zed as $EDITOR, history, fzf, y for Yazi, fnm)
dot_claude/ # Claude Code config (writing skills)
.chezmoitemplates/ # Colour helpers, GTK's shared colour block, the IPU7 camera's /etc files
run_* # Setup scripts: greetd, Chromium policies, GPU symlinks, IPU7 camera
Palette JSONs are the single source of truth. theme-switch applies them across all apps in four ways:
Templated apps (chezmoi templates reading the active palette):
Palette JSON active.json Chezmoi Template Final Config
(everforest.json) ---> written by theme-switch ---> (style.css.tmpl reads ---> (style.css)
active.json) chezmoi apply
Used by: GTK3/4, Qt5 (qt5ct), Kitty, Yazi, Zed settings, Obsidian, hyprlock, btop. btop gets a single templated theme (palette.theme) that always matches the active palette; its config points at it statically.
Direct-write apps (config written by theme-switch itself):
Palette JSON theme-switch Final Config
(everforest.json) ---> reads base and _variant ---> writes a managed policy JSON
Used by: Chromium/Chrome (managed color policy) and GTK (gsettings). (Zed theme selection is templated via the _zed_theme meta key; theme-switch syncs Zed's settings in place before apply so its inode-level hot-reload watch survives.)
Lua-required (Hyprland):
active.json palette.lua.tmpl hyprland.lua
(written by ---> renders palette.lua ---> local p = require("palette") ---> p.surface0_rgba(0.93)
theme-switch)
hyprland.lua requires palette.lua (a chezmoi template like the rest) and reads role-helpers like p.surface0_rgba(0.93).
Strand reads active.json through a from file service and springs to the new colours — no restart needed.
- Palette files (
dot_config/palette/*.json) define color roles using Catppuccin-style naming theme-switchwritesactive.json(palette + derived_slug,_accent*and_on_accentkeys), updates direct-write appschezmoi applyrenders themed.tmplconfigs fromactive.jsonand machine-specific variables (GPU config)- Strand sees the
active.jsonchange and animates to the new colours instantly
| Palette | Variant |
|---|---|
| Catppuccin Mocha | dark |
| Catppuccin Frappé | dark |
| Catppuccin Macchiato | dark |
| Catppuccin Latte | light |
| Rosé Pine | dark |
| Rosé Pine Moon | dark |
| Rosé Pine Dawn | light |
| Everforest | dark |
| Everforest Light | light |
| Nord | dark |
| Gruvbox | dark |
Theme colors (chezmoi Go templates, rendered by chezmoi apply):
{{- $p := include "dot_config/palette/active.json" | fromJson -}}
{{ $p.crust }} # Direct color value: #11111b
{{ template "rgba" (list $p.crust 0.95) }} # CSS rgba with opacity
{{ template "hypr_rgba" (list $p.surface0 0.93) }} # Hyprland format (hyprlock)
Lua-side colors in hyprland.lua.tmpl (via palette.lua):
local p = require("palette")
p.surface0_rgba(0.93) -- "rgba(313244ed)"
p.crust_rgb() -- "rgb(11111b)"Chezmoi variables (Go templates, processed by chezmoi apply):
{{ .graphics }} # Machine-specific data
{{ if eq .graphics "nvidia" }}...{{ end }}
| Helper | Output Example |
|---|---|
rgba (color, opacity) |
rgba(61, 219, 217, 0.90) |
rgb_values (color) |
61, 219, 217 |
hypr_rgb (color) |
rgb(3ddbd9) |
hypr_rgba (color, opacity) |
rgba(3ddbd9e5) |
theme-switch everforest # switch (updates active.json, applies chezmoi, reloads apps)
theme-switch # apply the theme in use again
theme-switch --list # the themesWallpapers live in a flat directory (~/.config/wallpapers/) with a wallpapers.json config that defines singles and sets with palette assignments.
{
"wallpapers": [
{ "file": "forest.jpg", "palettes": ["everforest*"] },
{ "file": "sunset.jpg", "palettes": ["*"] }
],
"sets": [
{
"name": "Cyberpunk",
"images": ["cyberpunk-1.png", "cyberpunk-2.png", "cyberpunk-3.png"],
"palettes": ["catppuccin*"]
}
],
"defaults": [
{ "palettes": ["everforest*"], "file": "forest.jpg" },
{ "palettes": ["*"], "file": "sunset.jpg" }
]
}- Singles apply one image across all monitors
- Sets assign images to monitors left-to-right, cycling with modulo if there are fewer images than monitors
- Palettes supports exact names (
catppuccin-mocha), globs (catppuccin*), or*for all themes - Defaults name the wallpaper (its file, or a set's name) a theme switch falls back to: one listing the theme by exact name wins, then a trailing glob (
everforest*), then* - The launcher's Wallpapers page lists those suited to the theme in use and puts one up with awww; after a theme switch it keeps the wallpaper if it suits the new theme, else puts up the theme's default
- Strand reads
wallpapers.jsonlive;awww-daemonrestores the last images at login
# Split panoramic images into per-monitor sets (default: 3 monitors, jpg)
wallpaper-split ~/Downloads/panoramas/ --palettes "catppuccin*,rose-pine*"
# AI upscale images using EDSR 4x (CPU-only, tiled processing)
wallpaper-upscale ~/Downloads/lowres/ -o ~/Downloads/upscaled/On first run, chezmoi prompts for machine-specific settings stored in ~/.config/chezmoi/chezmoi.toml:
| Variable | Options | Description |
|---|---|---|
graphics |
amd, intel, prime, nvidia |
GPU driver configuration |
igpu_pci |
PCI ID (auto-detected) | Integrated GPU — only prompted for prime |
dgpu_pci |
PCI ID (auto-detected) | Discrete GPU — only prompted for prime |
To change settings:
chezmoi edit-config
chezmoi applyMIT