#+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 + =. 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?