chezmoi: split tags into compositor/apps, data-drive workspace placement

Collapse redundant template tags into a cleaner data model:
- drop the desktop tag (use "not laptop"); replace hyprland/niri with
  compositor = "hyprland"|"niri"; replace cs2/entertainment with an
  apps list (gated via has); drop the dead waybar tag.
- move app->workspace->monitor routing into a portable [[data.placement]]
  table keyed by monitor role (left/right/primary), resolved per machine
  with fallback to the primary monitor. workspaces.lua.tmpl and
  rules.lua.tmpl now generate the workspace/window rules from it, so
  single-monitor machines work with no hardcoded monitor names.

Update CLAUDE.md / AGENTS.md / GEMINI.md schema references to match.
This commit is contained in:
2026-06-03 22:53:36 +02:00
parent d8001d6e19
commit 7b1ff73004
14 changed files with 111 additions and 122 deletions
+10 -5
View File
@@ -1,17 +1,19 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{ if not (index $tags "cs2") -}}
{{- $apps := .chezmoi.config.data.apps -}}
{{- $compositor := .chezmoi.config.data.compositor -}}
{{ if not (has "steam" $apps) -}}
.local/share/Steam/steamapps/common/Counter-Strike Global Offensive/game/csgo/cfg/autoexec.cfg
{{ end -}}
{{ if not (index $tags "pipewire") -}}
.config/pipewire/**
{{ end -}}
{{ if not (index $tags "hyprland") -}}
{{ if ne $compositor "hyprland" -}}
.config/autostart/swayosd-server.desktop
.config/autostart/waybar.desktop
.config/hypr/**
{{ end -}}
{{ if not (index $tags "niri") -}}
{{ if ne $compositor "niri" -}}
.config/niri/**
{{ end -}}
@@ -19,8 +21,10 @@
.config/autostart/jetbrains-toolbox.desktop
{{ end -}}
{{ if not (index $tags "entertainment") -}}
{{ if not (has "jellyfin" $apps) -}}
.config/autostart/jellyfin-mpv-shim.desktop
{{ end -}}
{{ if not (has "spotify" $apps) -}}
.config/spicetify/**
{{ end -}}
@@ -28,7 +32,7 @@
.config/autostart/nm-applet.desktop
{{ end -}}
{{ if not (index $tags "desktop") -}}
{{ if index $tags "laptop" -}}
.config/autostart/info.mumble.Mumble.desktop
.config/autostart/vesktop.desktop
.config/autostart/steam.desktop
@@ -40,6 +44,7 @@ pkglist/
# system/ contains files staged for /etc — installed manually via system/install-greeter.sh
system/
README.md
AGENTS.md
GEMINI.md
CLAUDE.md
+24 -21
View File
@@ -18,38 +18,41 @@ This is a chezmoi dotfiles source tree. Key paths:
## Chezmoi Config (chezmoi.toml)
- Source of truth: `~/.config/chezmoi/chezmoi.toml` (not tracked here).
- Tag keys are stable across machines; templates must access them via `.chezmoi.config.data.tags.<tag>`.
- `microphone` is only present when `pipewire` is true; guard access with the tag.
- `data.monitors` is an array and the number of entries varies by machine.
- The data model splits machine traits from structured config:
- `tags` — booleans only: `laptop`, `touchscreen`, `pipewire`, `dev`, `bluetooth`. No `desktop` tag; use `not (index $tags "laptop")`.
- `compositor` `"hyprland"` | `"niri"`; branch with `eq …data.compositor "hyprland"`.
- `apps` — optional programs present, e.g. `["steam","spotify","jellyfin"]`; gate with `has "steam" …data.apps`.
- `data.monitors` — array (varies per machine); each has `name`, optional `primary`, and a `role` (`"primary"`/`"left"`/`"right"`).
- `data.placement` — app-home workspaces keyed by monitor `role` (resolves per machine, falls back to primary). See `dot_config/hypr/hyprland.d.lua/{workspaces,rules}.lua.tmpl`.
- `data.microphones` — array, meaningful only when `pipewire = true`.
- Reference structure (current machine):
```
```toml
[data]
tags = { desktop = true, laptop = false, hyprland = true, waybar = true, pipewire = true, dev = true, entertainment = true, cs2 = true, bluetooth = false }
microphone = "alsa_input.usb-DCMT_Technology_USB_Condenser_Microphone_214b206000000178-00.mono-fallback"
tags = { laptop = false, touchscreen = false, pipewire = true, dev = true, bluetooth = false }
compositor = "hyprland"
apps = ["steam", "spotify", "jellyfin"]
[[data.placement]]
id = 4; name = "joplin"; role = "left"; apps = ["joplin-app-desktop"]; silent = true
[[data.placement]]
id = 5; name = "steam"; role = "left"; apps = ["steam"]; silent = true; layout = "scrolling"; requires = "steam"
# --- Primary Monitor ---
[[data.monitors]]
name = "DP-1"
primary = true
width = 1920
height = 1080
refresh_rate = 60
position = "0x0"
scale = 1.0
workspaces = [1, 2, 3, 4, 5]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/rosepinesuccubus11.png"
role = "primary"
width = 2560
height = 1440
position = "1920x0"
workspaces = [{ id = 24, name = "4" }, { id = 25, name = "5" }]
# --- Secondary Monitor ---
[[data.monitors]]
name = "DP-2"
width = 1920
height = 1080
refresh_rate = 144
position = "1920x0"
scale = 1.0
vrr = 1
workspaces = [6, 7, 8, 9, 10]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/witch_autumn.png"
role = "right"
position = "4480x0"
workspaces = [{ id = 28, name = "8" }, { id = 29, name = "9", layout = "scrolling" }]
```
## Build, Test, and Development Commands
+8 -4
View File
@@ -29,12 +29,16 @@ After applying, reload the affected tool:
## Template data (`chezmoi.toml`)
Config lives at `~/.config/chezmoi/chezmoi.toml` (not tracked). Access tags via `.chezmoi.config.data.tags.<tag>`.
Config lives at `~/.config/chezmoi/chezmoi.toml` (not tracked). Access via `.chezmoi.config.data.<key>`.
Current tags: `desktop`, `laptop`, `hyprland`, `waybar`, `pipewire`, `dev`, `entertainment`, `cs2`, `bluetooth`
The data model separates concerns:
- `microphone` is only present when `pipewire = true` — guard with `{{- if (index $tags "pipewire") }}`.
- `data.monitors` is an array; number of entries varies by machine.
- **`tags`** — boolean machine traits only: `laptop`, `touchscreen`, `pipewire`, `dev`, `bluetooth`. The desktop case is `not (index $tags "laptop")` (there is no `desktop` tag).
- **`compositor`** — string, `"hyprland"` or `"niri"`. Branch with `eq .chezmoi.config.data.compositor "hyprland"`.
- **`apps`** — list of optional programs present on this machine (e.g. `["steam", "spotify", "jellyfin"]`). Gate with `has "steam" .chezmoi.config.data.apps`.
- **`data.monitors`** — array (size varies per machine). Each has `name`, optional `primary = true`, and a `role` (`"primary"`/`"left"`/`"right"`) used by placement.
- **`data.placement`** — array of app-home workspaces: `id`, `name`, `role`, `apps` (window classes routed here), `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`. A `role` resolves to the monitor carrying it, falling back to the primary monitor when absent (single-monitor machines "just work"). `requires` drops the entry unless that program is in `apps`. Consumed by `hypr/hyprland.d.lua/workspaces.lua.tmpl` (workspace_rule) and `rules.lua.tmpl` (window_rule).
- **`data.microphones`** — array; only meaningful when `pipewire = true`.
## Architecture
+1 -1
View File
@@ -24,7 +24,7 @@ The directory structure follows standard `chezmoi` conventions:
* **`dot_local/`**: Maps to `~/.local/`. Contains scripts and local data.
* **`dot_local/share/docs/`**: Local docs area populated by git submodules using the `external_` attribute (renders to `~/.local/share/docs/`).
* **`.chezmoiscripts/`**: Contains scripts that run when `chezmoi apply` is executed (e.g., `run_onchange_...`).
* **`dot_profile.tmpl`**: A template that renders to `~/.profile`, handling environment variables. It uses tags (e.g., `dev`, `desktop`) to conditionally include configurations.
* **`dot_profile.tmpl`**: A template that renders to `~/.profile`, handling environment variables. It uses tags (e.g., `dev`, and `not laptop` for the desktop case) to conditionally include configurations.
## Documentation
+20 -27
View File
@@ -12,38 +12,31 @@ Hyprland configuration uses the v0.55+ **Lua** config: entry point `hyprland.lua
## Chezmoi Config (chezmoi.toml)
- Source of truth: `~/.config/chezmoi/chezmoi.toml` (not tracked here).
- Tag keys are stable across machines; templates must access them via `.chezmoi.config.data.tags.<tag>`.
- `microphone` is only present when `pipewire` is true; guard access with the tag.
- `data.monitors` is an array and the number of entries varies by machine.
- Data model: `tags` (booleans: `laptop`, `touchscreen`, `pipewire`, `dev`, `bluetooth` — no `desktop`, use `not laptop`), `compositor` (`"hyprland"`/`"niri"`), `apps` (optional programs, gate with `has "x" .apps`), `data.monitors` (array; each `name` + optional `primary` + `role`), `data.placement` (app-home workspaces), `data.microphones`.
- **Workspace placement is data-driven.** `data.placement[]` entries define the app-home workspaces (`id`, `name`, monitor `role`, `apps` = window classes routed there, `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`). `workspaces.lua.tmpl` turns each into a `workspace_rule` (resolving `role` → monitor name, falling back to the `primary` monitor when that role is absent), and `rules.lua.tmpl` turns each `apps` class into a `window_rule` routing to that workspace. To move an app or add a new app-home, edit `data.placement` — do **not** hardcode monitor names in the templates.
- Reference structure (current machine):
```
```toml
[data]
tags = { desktop = true, laptop = false, hyprland = true, waybar = true, pipewire = true, dev = true, entertainment = true, cs2 = true, bluetooth = false }
microphone = "alsa_input.usb-DCMT_Technology_USB_Condenser_Microphone_214b206000000178-00.mono-fallback"
tags = { laptop = false, touchscreen = false, pipewire = true, dev = true, bluetooth = false }
compositor = "hyprland"
apps = ["steam", "spotify", "jellyfin"]
# --- Primary Monitor ---
[[data.monitors]]
name = "DP-1"
primary = true
width = 1920
height = 1080
refresh_rate = 60
position = "0x0"
scale = 1.0
workspaces = [1, 2, 3, 4, 5]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/rosepinesuccubus11.png"
[[data.placement]]
id = 2; name = "comms"; role = "right"; silent = true; layout = "scrolling"; layoutopts = ["direction:down"]
apps = ["info.mumble.Mumble", "discord", "vesktop", "teamspeak-client", "TeamSpeak", "TeamSpeak 3", "teamspeak3"]
[[data.placement]]
id = 4; name = "joplin"; role = "left"; apps = ["joplin-app-desktop"]; silent = true
# --- Secondary Monitor ---
# --- Monitors (role drives placement; primary = fallback) ---
[[data.monitors]]
name = "DP-2"
width = 1920
height = 1080
refresh_rate = 144
position = "1920x0"
scale = 1.0
vrr = 1
workspaces = [6, 7, 8, 9, 10]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/witch_autumn.png"
name = "DP-1"; primary = true; role = "primary"
workspaces = [{ id = 24, name = "4" }, { id = 25, name = "5" }]
[[data.monitors]]
name = "DP-2"; role = "right"
workspaces = [{ id = 28, name = "8" }, { id = 29, name = "9", layout = "scrolling" }]
[[data.monitors]]
name = "HDMI-A-2"; role = "left"
workspaces = [{ id = 21, name = "1" }]
```
## Build, Test, and Development Commands
@@ -4,7 +4,7 @@ hl.config({
input = {
kb_layout = "ultimatekeys",
kb_options = "caps:escape_shifted_capslock",
{{- if (index $tags "desktop") }}
{{- if (not (index $tags "laptop")) }}
numlock_by_default = true,
{{- end }}
repeat_rate = 25,
@@ -25,7 +25,7 @@ hl.config({
}
})
{{- if (index $tags "desktop") }}
{{- if (not (index $tags "laptop")) }}
hl.device({
name = "Logitech Gaming Mouse G502",
sensitivity = 0.0,
+7 -18
View File
@@ -1,4 +1,4 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{- $apps := .chezmoi.config.data.apps -}}
hl.config({
group = {
@@ -50,24 +50,13 @@ hl.window_rule({
})
hl.window_rule({ match = { xwayland = true }, no_initial_focus = true })
-- Communication
hl.window_rule({ match = { class = "^(info\\.mumble\\.Mumble|discord|vesktop|teamspeak-client|TeamSpeak|TeamSpeak 3|teamspeak3)$" }, workspace = "2 silent" })
hl.window_rule({ match = { class = "^(Element)$" }, workspace = "3 silent" })
-- Mail
hl.window_rule({ match = { class = "^(org\\.mozilla\\.Thunderbird)$" }, workspace = "1 silent" })
-- Notes
hl.window_rule({ match = { class = "^(joplin-app-desktop)$" }, workspace = "4 silent" })
{{- if index $tags "entertainment" }}
-- Multimedia
hl.window_rule({ match = { class = "Spotify" }, workspace = "6 silent" })
-- App placement (data-driven from .placement; one workspace per entry, monitor
-- binding lives on the matching workspace_rule in workspaces.lua).
{{- range $p := .placement }}
{{- if or (not (hasKey $p "requires")) (has $p.requires $apps) }}
{{- $classes := list }}{{ range $c := $p.apps }}{{ $classes = append $classes ($c | replace "." "\\\\.") }}{{ end }}
hl.window_rule({ match = { class = "^({{ join "|" $classes }})$" }, workspace = "{{ $p.id }}{{ if $p.silent }} silent{{ end }}" })
{{- end }}
{{- if index $tags "cs2" }}
-- Gaming
hl.window_rule({ match = { class = "^(steam)$" }, workspace = "5 silent" })
{{- end }}
-- Game Content Bypass (Option A - per-monitor CTM bypass)
@@ -1,20 +1,22 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{- $apps := .chezmoi.config.data.apps -}}
{{- /* Resolve placement role -> monitor name, falling back to the primary monitor. */ -}}
{{- $primary := "" -}}
{{- range .monitors }}{{ if index . "primary" }}{{ $primary = .name }}{{ end }}{{ end -}}
{{- if not $primary }}{{ $primary = (index .monitors 0).name }}{{ end -}}
{{- $role2mon := dict -}}
{{- range .monitors }}{{ if hasKey . "role" }}{{ $role2mon = set $role2mon .role .name }}{{ end }}{{ end -}}
-- Special Workspaces
hl.workspace_rule({ workspace = "special:passwordmgr", on_created_empty = "uwsm app -- bitwarden-desktop" })
-- Named Workspaces (IDs 1-6, sorted before numbered)
hl.workspace_rule({ workspace = "1", default_name = "mail", monitor = "HDMI-A-2" })
hl.workspace_rule({ workspace = "2", default_name = "comms", monitor = "DP-2", layout = "scrolling", layout_opts = { direction = "down" } })
hl.workspace_rule({ workspace = "3", default_name = "element", monitor = "DP-2", layout = "scrolling", layout_opts = { direction = "down" } })
hl.workspace_rule({ workspace = "4", default_name = "joplin", monitor = "HDMI-A-2" })
{{- if index $tags "cs2" }}
hl.workspace_rule({ workspace = "5", default_name = "steam", monitor = "HDMI-A-2", layout = "scrolling" })
-- Named app-home workspaces (data-driven via .placement; monitor resolved from role)
{{- range $p := .placement }}
{{- if or (not (hasKey $p "requires")) (has $p.requires $apps) }}
hl.workspace_rule({ workspace = "{{ $p.id }}", default_name = "{{ $p.name }}", monitor = "{{ index $role2mon $p.role | default $primary }}"
{{- if hasKey $p "layout" }}, layout = "{{ $p.layout }}"{{ end }}
{{- if hasKey $p "layoutopts" }}, layout_opts = { {{ range $i, $opt := $p.layoutopts }}{{ $parts := splitList ":" $opt }}{{ if $i }}, {{ end }}{{ index $parts 0 }} = "{{ index $parts 1 }}"{{ end }} }{{ end }}
{{- if hasKey $p "on_created_empty" }}, on_created_empty = "{{ $p.on_created_empty }}"{{ end }} })
{{- end }}
{{- if index $tags "entertainment" }}
hl.workspace_rule({ workspace = "6", default_name = "spotify", monitor = "DP-2", layout = "monocle", on_created_empty = "uwsm app -- spotify-launcher" })
{{- end }}
-- Monitor Workspaces
+1 -1
View File
@@ -1,5 +1,5 @@
# --- Video & Quality ---
{{- if .chezmoi.config.data.tags.desktop }}
{{- if not .chezmoi.config.data.tags.laptop }}
profile=high-quality
vo=gpu-next
gpu-api=vulkan
+1 -1
View File
@@ -14,7 +14,7 @@ interpolation=yes
tscale=oversample
# --- Hardware & Quality (Chezmoi) ---
{{- if .chezmoi.config.data.tags.desktop }}
{{- if not .chezmoi.config.data.tags.laptop }}
profile=high-quality
vo=gpu-next
gpu-api=vulkan
+1 -1
View File
@@ -1,7 +1,7 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{- $monitors := .chezmoi.config.data.monitors -}}
{{- $isLaptop := index $tags "laptop" -}}
{{- $isDesktop := index $tags "desktop" -}}
{{- $isDesktop := not (index $tags "laptop") -}}
pragma Singleton
import Quickshell
+14 -22
View File
@@ -7,38 +7,30 @@ This is a chezmoi dotfiles source tree scoped to Waybar. The current directory m
## Chezmoi Config (chezmoi.toml)
- Source of truth: `~/.config/chezmoi/chezmoi.toml` (not tracked here).
- Tag keys are stable across machines; templates must access them via `.chezmoi.config.data.tags.<tag>`.
- `microphone` is only present when `pipewire` is true; guard access with the tag.
- `data.monitors` is an array and the number of entries varies by machine.
- Data model: `tags` (booleans: `laptop`, `touchscreen`, `pipewire`, `dev`, `bluetooth` — no `desktop`; use `not laptop`), `compositor` (`"hyprland"`/`"niri"``config.tmpl` branches the workspace module on this), `apps` (optional programs), `data.monitors` (array; each `name` + optional `primary`/`waybar` + `role`), `data.placement`, `data.microphones`.
- The waybar bar attaches to the monitor whose entry has `waybar = true` (else the `primary` monitor).
- Reference structure (current machine):
```
```toml
[data]
tags = { desktop = true, laptop = false, hyprland = true, waybar = true, pipewire = true, dev = true, entertainment = true, cs2 = true, bluetooth = false }
microphone = "alsa_input.usb-DCMT_Technology_USB_Condenser_Microphone_214b206000000178-00.mono-fallback"
tags = { laptop = false, touchscreen = false, pipewire = true, dev = true, bluetooth = false }
compositor = "hyprland"
apps = ["steam", "spotify", "jellyfin"]
# --- Primary Monitor ---
[[data.monitors]]
name = "DP-1"
primary = true
width = 1920
height = 1080
refresh_rate = 60
position = "0x0"
scale = 1.0
workspaces = [1, 2, 3, 4, 5]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/rosepinesuccubus11.png"
role = "primary"
position = "1920x0"
workspaces = [{ id = 24, name = "4" }, { id = 25, name = "5" }]
# --- Secondary Monitor ---
# --- Secondary Monitor (hosts the bar) ---
[[data.monitors]]
name = "DP-2"
width = 1920
height = 1080
refresh_rate = 144
position = "1920x0"
scale = 1.0
vrr = 1
workspaces = [6, 7, 8, 9, 10]
wallpaper = "/home/mpuchstein/Pictures/wallpaper/ki/1920x1080/witch_autumn.png"
role = "right"
waybar = true
position = "4480x0"
workspaces = [{ id = 28, name = "8" }]
```
## Build, Test, and Development Commands
+7 -6
View File
@@ -1,4 +1,5 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{- $compositor := .chezmoi.config.data.compositor -}}
{{- $waybar_output := "eDP-1" -}}
{{- range .monitors -}}
{{- if index . "waybar" -}}
@@ -8,9 +9,9 @@
{{- end -}}
{{- end -}}
{{- $workspace_module := "" -}}
{{- if $tags.hyprland -}}
{{- if (eq $compositor "hyprland") -}}
{{- $workspace_module = "hyprland/workspaces" -}}
{{- else if $tags.niri -}}
{{- else if (eq $compositor "niri") -}}
{{- $workspace_module = "niri/workspaces" -}}
{{- end -}}
{
@@ -39,7 +40,7 @@
"modules-right": [
"idle_inhibitor",
{{- if $tags.hyprland }}
{{- if (eq $compositor "hyprland") }}
"custom/hyprsunset",
{{- end }}
"gamemode",
@@ -55,7 +56,7 @@
{{- end }}
],
{{- if $tags.hyprland }}
{{- if (eq $compositor "hyprland") }}
"hyprland/workspaces": {
"format": "{icon}",
"all-outputs": true,
@@ -83,7 +84,7 @@
"on-scroll-up": "hyprctl dispatch workspace e+1",
"on-scroll-down": "hyprctl dispatch workspace e-1"
},
{{- else if $tags.niri }}
{{- else if (eq $compositor "niri") }}
"niri/workspaces": {
"format": "{value}",
"all-outputs": true
@@ -330,7 +331,7 @@
"on-click-middle": "swaync-client -C"
},
{{- if $tags.hyprland }}
{{- if (eq $compositor "hyprland") }}
"custom/hyprsunset": {
"format": "SUN",
"tooltip": true,
+1 -1
View File
@@ -37,7 +37,7 @@ export FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS \
--color=spinner:#ff007c \
"
{{- if (index $tags "desktop") }}
{{- if not (index $tags "laptop") }}
export ROCM_PATH=/opt/rocm
export HSA_OVERRIDE_GFX_VERSION=10.3.0
# export HIP_VISIBLE_DEVICES=1