Conversation
Adds the two data flags the rest of the Omarchy work hangs off, and restructures the shell configuration so that files another owner also writes to never have to be contested. Detection: - isOmarchy reads ID=omarchy from /etc/os-release. Keying on the distro ID rather than on a path makes it false by construction in WSL (ID=arch/ubuntu), dev containers, macOS and Windows, so no Omarchy-only file can leak onto another host. - isThinkPad reads /sys/class/dmi/id/product_family, so the hardware pieces landing in later commits gate on the machine rather than on the distro. Drop-in directories: - ~/.bashrc and ~/.zshrc become four-line loaders over ~/.config/sh/rc.d/*.sh and should not need to change again. The content they held is now one numbered file per concern, with 00-shell.sh setting DOTFILES_SHELL so the rest can hand the right name to `starship init`, `mise activate` and friends. That granularity is what lets a single .chezmoiignore line drop exactly the pieces a host already provides -- on Omarchy, starship/mise/zoxide/history, all four of which its own default/bash/rc already sets up. - On Omarchy the loader sits below Omarchy's own /etc/skel/.bashrc preamble, kept byte-for-byte: env-bootstrap has to run above the interactivity guard or `bash -lc`, SSH commands and omarchy-launch-floating-terminal (a `bash -c`) lose OMARCHY_PATH and the mise/~/.local/bin PATH entries. - The mise tool pins move from ~/.config/mise/config.toml to conf.d/10-dotfiles.toml. Omarchy provisions 13 lazy tool wrappers via `omarchy mise install`, and each runs `mise use -g` on every invocation, which rewrites config.toml; mise structurally refuses to write into conf.d, so chezmoi and Omarchy stop clobbering each other. Because config.toml outranks conf.d, the Omarchy branch drops the tools Omarchy already ships rather than pinning versions that would be silently ignored -- herdr especially, where Omarchy has a migration deleting mise copies because a stale client shadows /usr/bin/herdr with an older wire protocol. A run_once script moves a pre-existing dotfiles-owned config.toml aside, recognising it by its minimum_release_age line so a `mise use -g` config survives untouched. Also on Omarchy: ~/.config/git/config is removed (git reads it before ~/.gitconfig, so its personal user.email and defaultBranch=master leak through on every key the dotfiles leave unset, and Omarchy writes it only on first run); ~/.config/ghostty/config is left unmanaged, since it is byte-identical to Omarchy's default and leaving it alone keeps `omarchy font set` and the theme system drift-free; and settings.json points Claude Code at the theme omarchy-theme-set regenerates on every theme change. Verified with render.sh: the non-Omarchy mise render resolves to the same 28 tools as before, settings.json is byte-identical off Omarchy, and the rendered Omarchy .bashrc sources cleanly under `bash -i` in a throwaway HOME (OMARCHY_PATH set, drop-ins layered above Omarchy's rc).
…dget Brings the desktop side of this machine into the repo. Everything here is ignored off Omarchy, so it cannot reach a WSL box, a dev container or a Mac. - ~/.config/hypr in full. Only input.lua (caps:escape, natural scroll, T14 touchpad tuning, three-finger workspace swipe), looknfeel.lua (tighter gaps, softer corners) and monitors.lua (1.25 scale -- "auto" resolves to 1.5 on this 14" 1080p panel, which reads too large) actually differ from Omarchy's defaults. The stock files come along to keep the directory whole; the trade-off is that an Omarchy migration editing one of them shows up as drift, which is exactly the case the sync widget's localEdits counter is there to surface. - shell.json: the bar layout with carson.agents in place of omarchy.agents, the third-party widgets, and omarchy.lock disabled. - carson.agents, a clone of Omarchy's agents plugin whose bar label is Claude Code's current 5-hour session percentage rather than a bare glyph. Panel.qml.orig -- the pristine copy left behind by the clone -- is deliberately not carried over. `omarchy plugin validate` passes on the result. - The portland-head theme (colors sampled from the background photograph) and the omazed theme-set hook that follows it. run_onchange_after_07-omarchy-theme.sh re-applies the theme once its files land, keyed on a hash of the whole theme folder. Omarchy discovers a user theme from the directory alone but only regenerates the derived per-application colour files on a set/refresh, so this closes that gap: it switches themes when the current one differs and refreshes in place when it does not, then reloads Hyprland and rescans plugins. All of it is skipped without a HYPRLAND_INSTANCE_SIGNATURE, so a TTY bootstrap or a container apply is a no-op rather than a pile of errors.
…uchpad modprobe drop-in
The T14-specific hardware pieces. All three gate on `and .isOmarchy
.isThinkPad` rather than on the distro alone, so a future Omarchy
desktop does not get a laptop keyboard-LED service -- and so a WSL
install on this same ThinkPad, where the DMI check would also pass,
gets none of it.
- dot_local/src/kbd-backlight-auto/{kbd-backlight-auto.c,Makefile}. The
compiled binary and the superseded .py prototype stay out of the repo.
- The systemd user unit. Only the unit file is managed; the
default.target.wants/ symlinks belong to systemd, and checking them in
would fight `systemctl disable`.
- system/etc/modprobe.d/99-thinkpad-touchpad.conf, outside root/ because
chezmoi manages $HOME only.
Ordering follows chezmoi's before/after split so a broken build cannot
leave a half-applied machine:
- run_onchange_before_10 compiles first, out of a temporary copy of the
source rather than ~/.local/src -- run_before runs before chezmoi has
written that directory, so on a fresh machine it isn't there yet. With
set -e a compile failure aborts the apply before any config is
touched.
- run_onchange_after_05 installs the /etc file, comparing before it
escalates so an unchanged apply never puts up a password prompt. sudo
when there is a terminal to type into, pkexec otherwise (an apply
triggered from the bar widget has no TTY, and Omarchy's polkit agent
shows a dialog).
- run_onchange_after_06 reloads and enables the unit, skipping cleanly
when there is no user bus.
Verified: the C source compiles clean with the Makefile's flags
(-Wall -Wextra, no warnings), and the content 05 embeds is byte-identical
to the file currently on this machine, so cmp short-circuits and no
escalation happens on a no-op apply.
.chezmoidata/omarchy.toml lists what this machine needs that isn't a file: nine repo packages, two AUR packages, four of Omarchy's opinionated installers, and the four third-party shell plugins shell.json places on the bar. Source-only, so it is template data and never lands in $HOME. To add something, add a line. The installers are listed with a sentinel package rather than as bare commands, because they do more than install: `omarchy install terminal ghostty` also makes ghostty the Omarchy default and wires it into the theme system. The plugin entries close a gap the plan hadn't covered -- shell.json references four git-cloned plugins, so without this a restore-from-scratch came up with four dead bar entries. run_before_00-omarchy-packages.sh installs whatever is missing, before any file is written, so a fresh machine has the packages its config refers to by the time the config lands. Deliberately a plain run_ rather than run_onchange_. Every action is presence-guarded, so on a settled machine this is a handful of `pacman -Q` calls that exit silently -- whereas a run_onchange keyed on the manifest hash records itself as done even on a run that had to skip (no terminal for the sudo prompt, AUR unreachable) and then never retries. Always-run plus cheap checks cannot get stuck half-applied. Privilege handling: plugins are git clones needing none, so they are installed either way. The rest takes a single `sudo -v` up front rather than one prompt per package, and when there is no terminal to type into it names exactly what it skipped and leaves the work for the next interactive apply. `--enable` is deliberately not passed to `omarchy plugin add`: it would edit shell.json, which chezmoi overwrites moments later with its own copy. Verified against command stubs: a fully-provisioned machine produces no output and never invokes sudo; a bare machine adds the plugins, prompts once, and then runs the package and installer steps in order.
One backend, three front ends, so the bar, the keyboard and the CLI can
never disagree about the state of the dotfiles.
`dotfiles-status [--fetch]` prints a one-line JSON summary:
{"applyNeeded":3,"localEdits":1,"behind":2,"ahead":0,"uncommitted":4,"ok":true}
It follows three rules, because a bar widget polls it unattended: never
prompt (GIT_TERMINAL_PROMPT=0 turns a credential prompt into an
immediate failure rather than a hang), never block for long (the fetch
is wrapped in `timeout 15`), and never exit nonzero -- a failure sets
"ok":false and still prints valid JSON.
The two `chezmoi status` columns mean different things and the split
matters. Column 2 is an ordinary pending apply. Column 1 is a target
that changed after chezmoi last wrote it -- an Omarchy migration,
`omarchy refresh`, a hand edit -- and chezmoi refuses to overwrite one
of those without being told to. That is the only state needing a
decision rather than a command, so it is the only one the widget takes
urgent.
`dotfiles-sync` is a gum picker bare and fully non-interactive with a
subcommand (pull/apply/diff/re-add/commit/update/status), which is what
the widget and menu invoke. `--help` works before chezmoi is installed.
carson.dotfiles is modelled on the first-party omarchy.system-update
widget: invisible while there is nothing to do, so it costs no bar space
on a machine in step. Left click opens the picker, right pulls and
applies, middle re-checks. The pull deliberately runs in a floating
terminal rather than a silent Process -- it can stop on a conflict or an
out-of-band edit, and swallowing that would leave the bar quietly wrong.
`omarchy plugin validate` passes.
The post-update hook pokes the widget after `omarchy update`, since
migrations rewrite the very files under ~/.config/hypr and
~/.config/omarchy that chezmoi manages; that is exactly when the drift
counter is most likely to have moved, and the half-hourly poll would
otherwise be slow to notice. `omarchy-shell -q` makes it a no-op when
the shell isn't running, so it can never fail the update it hooks.
Verified against a real chezmoi driving a throwaway destination: an
empty destination reports applyNeeded 124; after apply it drops to the
14 excluded scripts; tampering with an applied file flips it to `MM` and
the counters to localEdits 1; `chezmoi apply` then refuses the overwrite
until forced, which is the behaviour the urgent colour advertises.
Omarchy does not merely prefer its update path, it enforces it: the 00-omarchy-update-guard libalpm hook aborts any direct `pacman -Syu`. Both scripts previously did exactly that on Arch and would have failed outright on this machine. `update` now hands the whole system-package step to `omarchy update -y`, which takes a snapshot, refreshes keyrings, upgrades packages, runs migrations and post-update hooks, and owns the restart prompt. The chezmoi step gained a guard of the same shape as the existing mise one, but keyed on whether the binary is writable rather than on the distro: `chezmoi upgrade` rewrites the binary in place, so a root-owned package-managed copy can never self-upgrade, and testing writability covers Omarchy, Arch's extra/ and Homebrew in one condition instead of enumerating packagers. `mise upgrade` still runs after the pull -- `omarchy update` did run `mise up`, but that was before the freshly pulled pins landed. `install.sh` gains the same detection and three branches: bootstrap packages go through `omarchy pkg add` (installs only what is missing, no -Syu to be aborted); chezmoi comes from the Arch repos rather than the curl installer, so `omarchy update` keeps it current thereafter (extra/ carries 2.72, past .chezmoiversion 2.71); and the Claude Code installer is skipped, because Omarchy already provisions a mise wrapper for it and the mise shims sit ahead of ~/.local/bin on PATH, so a native install would just be shadowed. fish is unchanged: it is installed and chsh'd on Omarchy like anywhere else. SDDM launches `uwsm start` directly and uwsm sources env-bootstrap itself, so the graphical session never goes through the login shell. Verified by running both scripts against command stubs and a synthetic /etc/os-release: ID=omarchy takes the omarchy branches, ID=arch falls through to pacman, ID=ubuntu to apt, and the chezmoi step flips between upgrading and deferring purely on the binary's write bit.
`curl claude.ai/install.sh | bash` was the last hand-rolled binary install in the bootstrap, and it was there on every platform. mise has the tool as aqua:anthropics/claude-code, so it becomes one line in the tool list like everything else: one place that records the version, one update path (`mise upgrade`, driven by the pins the dotfiles pull), and no install step that only some machines have run. Declared alongside the other tools Omarchy also provides, so it lands off Omarchy and is left to Omarchy's own `omarchy mise install claude` wrapper there -- config.toml outranks conf.d, so a pin in our file would be silently ignored on that host anyway. Side effect worth noting: 04-claude-plugins used to no-op on a first install, because install.sh installed Claude Code *after* chezmoi applied. 01-mise-install now installs it earlier in the same apply, so plugins are set up on the first pass rather than the second. AGENTS.md gains the general rule this is an instance of: anything with a mise backend goes in the tool list rather than getting its own installer, with mise and chezmoi called out as the two bootstrappers that necessarily predate it.
Dropping the six stock files (hyprland.lua, bindings.lua, autostart.lua, hyprsunset.conf, xdph.conf, .luarc.json) and keeping input.lua, looknfeel.lua and monitors.lua. The earlier "keeps the directory whole" rationale had it backwards. Omarchy's migrations decide what to touch by hashing: migration 1781485962 carries stock_input_sha and stock_bindings_sha constants and rewrites a user's file only when it still matches one of them. That makes the two cases opposites. A stock file under chezmoi is a standing conflict -- Omarchy matches the hash, rewrites the file, the next apply reverts it, and that repeats on every update for as long as both sides care. A customized file is skipped by the migration outright, so chezmoi is its only writer and there is nothing to fight over. So managing the stock six bought nothing and cost the one thing the whole layout is trying to avoid. They are byte-identical to what Omarchy installs, so a rebuilt machine gets them from Omarchy regardless; the restore story is unchanged. This also puts the hypr directory back in line with the rule already stated for ghostty and written into AGENTS.md: only manage a file that differs from its $OMARCHY_PATH/config default.
`omarchy update` ends in omarchy-update-restart, which calls `gum confirm "Linux kernel has been updated. Reboot?"` and, on yes, runs omarchy-system-reboot immediately. `-y` does not prevent this. It exports OMARCHY_UPDATE_UNATTENDED and its own comment describes that as "a promise not to ask anything" -- but nothing under $OMARCHY_PATH ever reads the variable, so the prompt appears regardless. With that step first, any kernel or Hyprland upgrade could take the machine down before `chezmoi update` had run, leaving an update that reported success and had in fact skipped every dotfiles step. Moving it to the end costs nothing: chezmoi and mise are both already installed by then, and the two package-managed guards skip their self-updates precisely because this step is what updates them. Non-Omarchy ordering is untouched -- packages still go first there, where the step neither prompts nor reboots. Verified against command stubs on both hosts: ID=omarchy now emits mise → chezmoi → pull → mise upgrade → omarchy update -y, and ID=arch is byte-identical to its previous output.
Two bugs found auditing the cross-platform story.
dotfiles-sync's interactive picker calls gum, but gum was only ever
present on Omarchy, where it is a pacman package. Everywhere else
`dotfiles-sync` with no subcommand died on "gum is not installed". It
joins the tool list next to jq and gh, in the group Omarchy already
provides.
`clean` never got the Omarchy branch that `update` did, and both halves
of its pacman path are destructive there rather than merely untidy:
- `pacman -Sc` empties the package cache. That cache is Omarchy's only
offline downgrade path, which is why omarchy-update-pkg-prune keeps
two versions per package with `paccache -rk2`. Emptying it takes the
ability to roll back a bad update with it.
- `pacman -Rns` on -Qtdq orphans, unattended. Omarchy reviews orphans
instead: omarchy-update-orphan-pkgs lists them and asks, defaulting
to no. Removing them silently undoes that.
Also adds shellcheck and shfmt to the tool list, on every platform, so
the repo's own linting is pinned and reproducible rather than depending
on whatever happens to be installed.
Verified against stubs: ID=omarchy takes paccache -rk2 plus the orphan
review, ID=arch is byte-identical to its previous output.
The repo had no automated checks. This adds ./test.sh, which is the
harness that was being run by hand during the Omarchy work -- and which
caught a real bug there: after the mise config moved to a conf.d
drop-in, three .chezmoiscripts still `include`d the old path, and every
apply would have failed. Rendering the templates did not catch it;
driving a real apply did.
Four stages, cheapest first:
render every *.tmpl against all five environments (omarchy-thinkpad,
omarchy-desktop, macos, linux, devcontainer)
apply a real `chezmoi apply --exclude=scripts` into a throwaway
destination per environment, asserting nothing is left
unapplied afterwards
lint shellcheck and shfmt over every shell script, including
templates rendered first so the guarded bodies get checked
parse every JSON/TOML/JSONC file, templated JSON in each environment
Nothing touches $HOME: each apply gets its own temp destination and its
own chezmoi persistent state.
CI runs it on Ubuntu and macOS, and adds the one thing test.sh
deliberately will not do -- a genuine `chezmoi init --apply` with
scripts enabled, in Debian and Arch containers, as an unprivileged user.
That job then applies a second time and fails if `chezmoi status` is
non-empty, which is what would catch a script that rewrites a managed
file or a run_onchange whose hash is not stable.
A previous workflow was removed in a5b1068 as overkill, and it was: two
runners doing `chezmoi init --apply` with no assertions. The value here
is in the assertions and the environment matrix, neither of which that
had.
The formatting commit folded in here is shfmt's doing over five files
that were already inconsistent with the rest (trailing whitespace,
spacing inside arithmetic). shfmt runs with the repo's existing case
style rather than -ci, so the check matches how these scripts were
already written instead of demanding a reflow.
Three related gaps on the Windows side. The drop-in reorganization covered bash and zsh but left the PowerShell profile a monolith duplicating PATH, mise, starship and zoxide setup. It now loads ~/.config/pwsh/rc.d/*.ps1 through the same four-line loop, and the profile itself is one line. Windows had no `update` and no `clean`. .chezmoiignore drops .local/bin there, correctly -- those are bash -- but nothing replaced them, so a Windows machine could not update itself at all. They arrive as PowerShell functions in 60-commands.ps1, alongside set-work-email which moves out of the profile. Renumbering surfaced a latent ordering bug in the bash/zsh drop-ins: mise was at 41, after starship at 40, so `command -v starship` was evaluated before mise's shim directory reached PATH. On a host where mise is the only thing providing starship, zoxide or mcfly, their setup was silently skipped -- not just on first run, but every shell. mise moves to 25, ahead of every tool activation, in both shells. fish was already correct by accident: conf.d loads alphabetically and mise sorts before starship and zoxide. test.sh grows a PowerShell stage. There is no shellcheck for pwsh, but its own parser rejects a syntax error, which is the failure that matters for files nothing on this machine ever loads; GitHub's runners ship pwsh, and the stage skips cleanly without it. Verified it is not a no-op by feeding it a deliberate parse error and watching it fail.
`fish -c "fundle install"` only ever ran from install.sh, which is Linux-only. macOS applies chezmoi directly, so every plugin listed in config.fish -- bass, fzf.fish, autopair -- silently never installed there. Moving it into a run_onchange script keyed on the fish config hash fixes that and also picks up plugin-list changes on any host, which the one-shot call in install.sh never did. Network failures warn instead of aborting the apply; the next apply retries. The other half of the macOS gap does not have a clean fix and is now documented rather than papered over. The per-host signing key cannot move into a chezmoi script: gitSigningKeyFile resolves when .chezmoi.toml.tmpl renders, which is `chezmoi init` time, before any script runs -- so a script that generated the key would not be reflected until the next init. It stays in install.sh, and the README tells macOS users to run scripts/generate-signing-key.sh beforehand if they want a key on disk rather than the agent-based fallback (which does work). Making fish the login shell is likewise noted as manual there.
The bootstrap job asserted that `chezmoi status` is empty after a second apply, which would have failed on every run. Plain run_ and run_after_ scripts are always-run by design, so chezmoi lists them in status even immediately after executing them -- three of them here (install-claude-config, mise-update.sh, mise-update.ps1), plus the Omarchy package script on that host. Their presence says nothing about idempotency. The assertion that actually matters is that no managed FILE is left out of sync, which is what catches a script rewriting something chezmoi owns or a run_onchange whose hash is not stable. Filter script rows out and assert on the rest -- the same thing test.sh's apply stage already does. Validated by running the real sequence locally: two full applies with scripts enabled, in a sandbox where HOME and every external command were redirected, leaves no file out of sync and exactly the three always-run scripts listed.
cloud-init.yaml carried a seven-package bootstrap list that install.sh installs again moments later -- two places to keep in step, for no gain. It now installs only what cloud-init itself needs to fetch the repo (ca-certificates, curl, git) and leaves the rest to install.sh. That exposed a real gap rather than just duplication: build-essential was in cloud-init's list and nowhere else, so a Debian or Arch host provisioned by install.sh directly had no compiler at all -- mise builds some tools from source, and on Omarchy the keyboard-backlight daemon is compiled during the apply. install.sh now installs a toolchain in each branch, named per distro since build-essential and base-devel do not share a name. .editorconfig encodes the conventions the repo already follows, so an editor agrees with what ./test.sh enforces (shfmt -i 4, default case indentation) without depending on the VS Code settings. Two carve-outs: *.tmpl keeps trailing whitespace, because Go templates control their own rendering, and the vendored fundle.fish is left byte-identical so it stays diffable against new upstream releases.
dotfiles-status gains a "files" array -- path, status code, added,
removed, binary -- and the bar widget lists it on hover, capped by a new
tooltipFiles setting, with a `!` against anything edited outside
chezmoi. Enough to see what an apply would do without opening a
terminal.
Counts come from parsing a single `chezmoi diff` rather than shelling
out to git, so it works outside a work tree. Two things that parse
caught, both verified against `git apply --numstat`:
- Matching removed lines with /^--- / misreads a deleted Lua comment
as a diff header. The hypr configs are full of `-- like this`, so
input.lua reported 3 removals where git said 4. Fixed by counting
only inside hunks, which excludes headers structurally.
- chezmoi emits directory entries as mode 40xxx with no line diff.
On a fresh machine those were 39 of 112 rows, all `+0 -0`, pushing
real files out of the tooltip. Now dropped from the list; the
counters still include them, since creating a directory is a real
pending change.
Cost is one extra `chezmoi diff` per poll: 60ms with drift, 258ms on a
fresh machine with 112 changes, and binaries are summarized rather than
embedded so the payload stays ~10KB.
Verified by running the tooltip's real QML through a Qt engine against
live dotfiles-status output, in both the few-files and truncated cases.
Machines should be able to look different while sharing these dotfiles. Theme *definitions* stay managed -- portland-head's colors.toml and background are still applied, so every machine has it in the switcher -- but nothing here decides which theme is selected. 07-omarchy-theme.sh previously ran `omarchy theme set portland-head` whenever the current theme differed, which was actively wrong: this machine is on Catppuccin Latte, and the next apply would have switched it back. The script still has one job, because Omarchy compiles a theme's colors.toml into per-application files (ghostty.conf, claude.json, btop.theme, ...) at set/refresh time rather than on demand -- so a colors.toml that chezmoi just changed leaves those derived files stale. It now refreshes if and only if the changed theme is the one active on this machine, via `omarchy theme refresh`, which re-runs theme-set against the current selection and never changes what is selected. Any other active theme is left alone with a log line saying so. The managed theme list is enumerated from the source tree at render time, so dropping in a second custom theme needs no edit here -- verified by adding one and watching it appear. Nothing else pins a theme: the active selection lives in ~/.local/state/omarchy/current/theme.name, which is not managed, and the two theme-adjacent pieces that are managed both follow the live theme rather than naming one -- the omazed hook (Zed) and the "theme": "custom:omarchy" key in settings.json (Claude Code). Verified against stubs across all four paths: a managed theme active refreshes, an unmanaged one is left alone, no graphical session is a no-op, and a newly added theme is picked up automatically.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.