aboutsummaryrefslogtreecommitdiff
path: root/docs/PLAN-per-host-overrides.org
blob: 6816a217e52a759346339608238a6542b568eb35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#+TITLE: Per-Host Override Mechanism for the Dotfiles Repo
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-05-26

* Status

| Field        | Value                                                       |
|--------------+-------------------------------------------------------------|
| State        | Implemented 2026-06-11 (dotfiles =c5e699b=) — review decisions: auto-detect via uname -n with HOST= override; foot via native include with the font made per-host; pypr whole-file per host; waybar stays shared (velox's copy was stale, not divergent); phases 1-4 in one pass; installer integration deferred to a follow-on task |
| Trigger      | Zoom launched enormous on ratio after a per-app QT_SCALE_FACTOR=1.5 patch meant for velox |
| Supersedes   | The fragile "local real files shadowing stow" pattern on velox |
| Related task | =todo.org= → "Cleaner per-machine override mechanism for the dotfiles repo" |
| Related spec | [[file:PLAN-dotfiles-separation.org][PLAN-dotfiles-separation.org]] (the repo this extends) |

* Problem

The =~/.dotfiles= repo stows the same =common/= + =hyprland/= trees to every
machine. Both hosts (ratio = desktop, velox = Framework 13 laptop) symlink
identical files. There is no mechanism for a setting to differ per machine, and
the two machines genuinely differ in one structural way: *velox is HiDPI
(2256×1504), ratio is not.*

Two concrete failures this causes:

1. *Per-app scaling patches leak across hosts.* Someone set
   =Exec=env QT_SCALE_FACTOR=1.5 /usr/bin/zoom %U= in
   =hyprland/.local/share/applications/Zoom.desktop= to enlarge Zoom on velox's
   HiDPI panel. Because that desktop file is shared via stow, the 1.5× also
   applied on ratio, where Zoom then opened enormous. (Reverted to plain
   =/usr/bin/zoom %U= on 2026-05-26; this spec is the durable fix for the
   underlying need.)

2. *velox's laptop overrides are fragile real files.* velox keeps
   laptop-specific configs — =foot.ini= font size, =pypr= scratchpad sizing for
   2256×1504, waybar battery module — as *local real files shadowing the stow
   symlinks*. Any =make restow= on velox re-conflicts: stow aborts on the real
   files (hit exactly this during the 2026-05-22 migration). The override
   survives only via manual backup/restore around every restow.

* Background — how scaling works today

Two lines in =hyprland/.config/hypr/hyprland.conf= set the scaling regime:

- =monitor=,preferred,auto,auto= — Hyprland auto-scales each monitor by its DPI.
  *Wayland-native apps already adapt per-machine automatically* — velox gets
  fractional scaling, ratio gets 1×, with no config difference. Native apps are
  not the problem.
- =xwayland { force_zero_scaling = true }= — XWayland apps (Zoom, any X11 app)
  are deliberately *not* scaled by the compositor, so they stay sharp instead of
  blurry-upscaled. The cost: on a HiDPI display they render tiny unless the app
  scales itself via toolkit env vars (=GDK_SCALE=, =QT_SCALE_FACTOR=).

So XWayland scaling has to come from environment variables, and *those env vars
need to differ per host.* The per-app =QT_SCALE_FACTOR= hack was the wrong layer
(one app, shared across hosts). The right layer is one per-host setting that
covers every XWayland app at once.

* The seam already exists

=hyprland.conf= line 342 already sources a glob:

#+begin_src conf
source = $HOME/.config/hypr/conf.d/*.conf
#+end_src

and the repo already ships =hyprland/.config/hypr/conf.d/local.conf= as a
"machine-local overrides" template (monitor scaling, keybinds, etc.). Two
properties make this the ideal mechanism:

- *Glob source tolerates absence.* A host with no matching file just sources
  nothing extra — no Hyprland error banner.
- *Last-wins.* Values set in =conf.d/local.conf= override the same keys set
  earlier in =hyprland.conf=.

The only flaw: =local.conf= currently lives in the *shared* =hyprland/= tier, so
it is one symlink shared by both machines. Editing it (through the symlink)
rewrites the single repo file and applies everywhere — and re-conflicts on
restow. *The fix is to make =local.conf= a per-host file instead of a shared
one.*

* Goal

A per-machine override that:

1. Survives =make restow= without manual backup/restore.
2. Is version-controlled in the dotfiles repo (a fresh install of that host
   gets its overrides automatically).
3. Covers the immediate HiDPI/XWayland-scaling need on velox.
4. Generalizes to the other per-host divergences (foot font, pypr sizing,
   waybar battery) without per-app hacks.

* Proposed mechanism — a per-host stow tier

Add host-named stow packages alongside the existing =common/ dwm/ hyprland/
minimal/= tiers:

#+begin_example
~/.dotfiles/
  common/
  dwm/
  hyprland/
  minimal/
  ratio/      ← new: ratio-only files
  velox/      ← new: velox-only files
#+end_example

=make stow hyprland= stows =common + hyprland + <hostname>=. The host tier holds
*only files that exist nowhere else in the stow set*, so stow never conflicts.

** Why no conflict

GNU Stow refuses when two packages provide the same target path. So the host
tier cannot re-own a file already in =common/= or =hyprland/=. That constraint
is exactly why =conf.d/local.conf= is the right first tenant: once it is
*removed* from =hyprland/=, it exists *only* in the host tiers, and each host
symlinks its own copy with zero conflict. The Hyprland glob does the merge at
runtime.

** First tenant: Hyprland per-host config (solves the scaling trigger)

- Remove =local.conf= from =hyprland/.config/hypr/conf.d/=.
- Add =ratio/.config/hypr/conf.d/local.conf= and
  =velox/.config/hypr/conf.d/local.conf=.

=velox/...local.conf=:

#+begin_src conf
# velox — Framework 13, HiDPI 2256×1504
monitor = eDP-1,preferred,auto,1.566667

# XWayland apps don't scale via the compositor (force_zero_scaling=true),
# so scale the toolkits explicitly. Inherited by every app launched in the
# session, including XWayland clients spawned by the browser (e.g. Zoom).
env = GDK_SCALE,1.5
env = QT_SCALE_FACTOR,1.5
env = XCURSOR_SIZE,36
#+end_src

=ratio/...local.conf=:

#+begin_src conf
# ratio — desktop, 1× scaling. Defaults in hyprland.conf are correct;
# nothing to override yet. File present so the host tier always has content.
#+end_src

This removes the need for any per-app =QT_SCALE_FACTOR= in =.desktop= files.

** Open design question: whole-file app configs

=conf.d/local.conf= works cleanly because Hyprland merges fragments at runtime.
The other per-host divergences are *monolithic files* already present in a
shared tier, which a host tier cannot re-own via stow:

| File                         | Native include?            | Approach |
|------------------------------+----------------------------+----------|
| =foot.ini=                   | Yes — =include=path=       | Keep base in shared tier; add =include=~/.config/foot/host.ini=; ship =host.ini= per host tier |
| waybar =config= (JSON)       | No include directive       | Either move the whole file into host tiers, or keep a documented local-override + =.stow-local-ignore= |
| pypr =config.toml=           | No include directive       | Same choice as waybar |

Three candidate strategies for the no-include files, to decide in review:

1. *Move the whole file into host tiers.* Cleanest at runtime, but duplicates
   the ~95% shared content across =ratio/= and =velox/= — drift risk.
2. *Documented local-override + =.stow-local-ignore=.* Keep the shared file,
   let the host keep a real file, and add the path to =.stow-local-ignore= so
   restow skips it instead of aborting. Removes the fragility (no more abort)
   but the override stays out of version control.
3. *Generator step.* A =make host-config= target (or an archsetup install step)
   that assembles the per-host file from a shared base + a host fragment. Most
   flexible, most machinery.

Recommendation to start: strategy 1 for files with no include directive (small
count, low churn), native =include=​= for foot. Revisit if duplication drift
becomes real.

* Implementation outline

Gated on review. Rough phases:

1. *Makefile — host tier.* Detect the host (=HOST := $(shell uname -n)= — note
   the =hostname= binary is absent on these boxes; =uname -n= and
   =cat /etc/hostname= both work). Add =$(HOST)= to the package list in =stow=,
   =restow=, =reset=, =unstow=, guarded so a host with no package is skipped
   with a clear message rather than a stow error. Update =help= text and the
   archsetup =CLAUDE.md= Makefile section.

2. *Hyprland per-host config.* Remove =local.conf= from =hyprland/=; create
   =ratio/= and =velox/= host tiers each with =.config/hypr/conf.d/local.conf=.
   Populate velox's with the HiDPI monitor scale + toolkit env; ratio's minimal.

3. *Verify XWayland env propagation.* Confirm =env =​= lines in =conf.d=
   actually reach XWayland clients launched outside the compositor (e.g. Zoom
   spawned by the browser). Hyprland sets =env= for its children; the browser is
   a session child, so its children should inherit. Verify on velox: launch Zoom
   from a link, confirm normal size with no per-app patch.

4. *Migrate velox's fragile overrides.* Move foot font / pypr sizing / waybar
   battery off the local-real-file pattern into the chosen strategy from the
   open question above.

5. *archsetup install integration (optional follow-on).* Teach the installer to
   stow the host tier for =DESKTOP_ENV in {dwm,hyprland}=. Decide whether the
   host name comes from =/etc/hostname= at install time or a new conf key.

* Open questions for Craig

1. Host detection: =uname -n= / =/etc/hostname= auto-detect (proposed), or an
   explicit =HOST== Makefile override / conf key?
2. Whole-file overrides (waybar, pypr): strategy 1 (whole file in host tier),
   strategy 2 (=.stow-local-ignore= + local real file), or strategy 3
   (generator)? See the table above.
3. Scope now: just the Hyprland scaling fix (phases 1–3), or migrate velox's
   existing overrides in the same pass (phase 4)?
4. Should =make stow= *always* include the host tier silently, or require it be
   named (=make stow hyprland velox=) to keep the behavior explicit?
5. Does the archsetup installer need to know about host tiers (phase 5), or is
   this a post-install =make= concern only?