diff --git a/README.md b/README.md index 64fa067..89f7a08 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,14 @@ instant. Internally it's two keys: `settings.nightlight.installed` (sticky — gates the unit, the first enable rebuilds) and `settings.nightlight.on` (the instant on/off). Expect more `nomarchy.*` toggles to migrate to this pattern. +**Auto-commit (opt-in):** System › Auto-commit makes every `apply`/`set` +mutation also `git commit` theme-state.json in your flake — *only* that +file, so unrelated dirty work is never swept up — turning your settings +history into `git log`. Off by default; the toggle is instant (nothing in +Nix consumes the flag), the off-write is itself committed so history stays +consistent, and wallpaper cycling (`bg next`) is deliberately excluded — +the current wallpaper rides along with the next real commit. + ### Per-theme app assets (`themes//`) Recoloring covers 95% of theming; the rest is one optional assets directory diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index e718037..587588f 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -9,6 +9,9 @@ next, in what order*. - Agents take the topmost actionable item (see LOOP.md). Finished items are **deleted** here — the journal + git log are the record; durable design notes get a ✓-entry in docs/ROADMAP.md if worth keeping. +- Item numbers are **stable IDs** — never renumbered or reused. A gap in + the sequence means shipped (or dropped) work; new items take the next + free number regardless of tier. - Tags: `[blocked:hw]` needs real hardware (see HARDWARE-QUEUE.md) · `[human]` needs Bernardo · `[stuck]` two failed attempts, needs help · `[big]` must be split before starting. @@ -19,19 +22,6 @@ next, in what order*. ## NOW -### 1. Opt-in auto-commit of the flake on state mutations -Phase 4 of the in-flake-state principle (phases 1–3: night-light, -keyboard memory, display resolution — shipped). Every menu write -(`nomarchy-theme-sync set/apply`) already does `git add -N`; add an -opt-in (a `settings.autoCommit` flag, itself menu-toggled) that commits -**owned files only** (theme-state.json) with a generated message after -each mutation. **Why:** closes the loop on "your checkout is your -machine" — settings changes become history. **Done when:** off by -default; toggleable from the System menu; commits fire on apply/set; -never commits unrelated dirty files; graceful when the repo has no user -git identity. **Verify:** V1 + a round-trip on the theme-sync CLI in a -sandbox repo; V2 if wired into a check. - ### 2. CI: run the check suite on every push (Forgejo Actions) First slice of ROADMAP § Automated upstream lock bumps, promoted to NOW because agents now push to `main` autonomously — an always-on net under diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index f17608d..f684b9e 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -43,6 +43,12 @@ QA machine), the **T14s** (webcam case). - [ ] **Visual theme pass** — live ISO: all six identity themes (bars) + the four authored rofi `.rasi` (nord/retro-82/lumon/kanagawa) look right, not just parse. +- [ ] **Auto-commit on a real machine** — System › Auto-commit toggles on + (row shows state, notification fires); a theme apply from SUPER+T then + shows a `nomarchy: apply theme …` commit in `~/.nomarchy` (`git log`); + unrelated dirty files in the checkout stay uncommitted; toggle off is + itself the last commit. Also: the "Auto timezone (on/off)" row label + now reflects the real state (the `= true` comparison fix). ## AMD dev box only - [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 1f2db69..a84dcf3 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,24 @@ Template: --- +## 2026-07-04 — Opt-in auto-commit shipped (loop iteration #1) +- **Task:** BACKLOG NOW#1 — auto-commit of state mutations (in-flake + state Phase 4). First autonomous /loop iteration. +- **Did:** `settings.autoCommit` live-read by nomarchy-theme-sync; + pathspec-limited commit of theme-state.json on apply/set (fires if flag + on before OR after the write → the off-toggle commits too); identity + fallback; same-value no-op; `bg` excluded. Menu: System › Auto-commit + (instant, self-gated on `.git`). Rider: `get` prints booleans as JSON + `true`/`false`, fixing the stuck "Auto timezone (on/off)" menu label. + BACKLOG numbering switched to stable IDs (no renumbering). +- **Verified:** V0 (py_compile, flake check) + V1 (HM generation builds; + generated menu `bash -n`) + a 7-assertion sandbox git-repo round-trip + (enable/set/same-value/disable/post-disable/apply/identity-fallback). +- **Pending:** V3 queued in HARDWARE-QUEUE (on-machine toggle + apply). +- **Next suggestion:** NOW#2 (CI checks-on-push) — but its runner half + may need Bernardo to register a Forgejo runner; deliver the workflow + regardless. + ## 2026-07-04 — Backlog revision pass (human-requested) - **Task:** Bernardo asked for a philosophy-aligned revision of BACKLOG.md ("I'm sure I'm forgetting important items"). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e3b55d1..105bb97 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -784,6 +784,24 @@ how to override it. Items marked ✓ are shipped. has **no CI today** (manual `nix flake update` only). Explicitly *not* a binary cache: compile-from-source is a deliberate values call (Gentoo-style), so this automates the *config/lock* channel, not artifact distribution. +- ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):** + `settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the + flake being a git repo) makes every `apply`/`set` also `git commit` + theme-state.json in the downstream flake. Design decisions: the flag is + **live-read by the tool** on each write — nothing in Nix consumes it, so + the toggle is instant, no rebuild, no option-surface addition; the commit + is **pathspec-limited** (`git commit -- theme-state.json`) so unrelated + dirty files are never swept up; it fires when the flag is on before *or* + after the write, so the disable-toggle itself lands in history instead of + staying forever-dirty; a same-value `set` no-ops (diff against HEAD); a + missing git identity falls back to `Nomarchy `; + `bg next` is deliberately excluded (runtime wallpaper churn — the path + rides along with the next real commit). Verified: V1 (HM generation + builds, generated menu `bash -n` green) + a 7-assertion sandbox-repo + round-trip incl. the apply path; on-machine check queued. Rider fix: + `get` now prints booleans JSON-style (`true`, not Python's `True`) — + which also un-sticks the System menu's "Auto timezone (on/off)" label, + whose `= true` comparison could never match before. - **"nomarchy" control center:** a single TUI/GUI front-end over the common toggles — theme, power profile, opt-in services, display, DND — built on the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 8ce3b75..ea3da46 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -387,6 +387,22 @@ ${themeRows} || { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; } exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;; + autocommit) + # Toggle opt-in auto-commit: every menu/theme mutation also commits + # theme-state.json (that file only) in the downstream flake, so + # settings history is `git log`. Nothing in Nix consumes the flag — + # the tool reads the live state on each write — so the toggle is + # instant, no rebuild. The off-write commits too (the tool fires + # when the flag was on before OR after), keeping history consistent. + if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]; then + nomarchy-theme-sync --quiet set settings.autoCommit false --no-switch + notify-send "Auto-commit" "Off — menu changes stay uncommitted in your flake." + else + nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch + notify-send "Auto-commit" "On — each change commits theme-state.json in your flake." + fi + exit 0 ;; + snapshot) # btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch # the keyboard-driven snapper browser instead: a terminal running it @@ -443,6 +459,12 @@ ${themeRows} then row "Auto timezone (on)" preferences-system-time else row "Auto timezone (off)" preferences-system-time fi + if [ -e "''${NOMARCHY_PATH:-$HOME/.nomarchy}/.git" ]; then + if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ] + then row "Auto-commit (on)" git + else row "Auto-commit (off)" git + fi + fi command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then row "Power profile" preferences-system-power @@ -460,6 +482,7 @@ ${themeRows} *"Do Not Disturb"*) exec "$0" dnd ;; *"Night light"*) exec "$0" nightlight ;; *"Auto timezone"*) exec "$0" autotimezone ;; + *"Auto-commit"*) exec "$0" autocommit ;; *Snapshots*) exec "$0" snapshot ;; *"Power profile"*) exec "$0" power-profile ;; esac ;; @@ -486,7 +509,7 @@ ${themeRows} esac ;; *) - echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|vpn|snapshot]" >&2 + echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot]" >&2 exit 64 ;; esac ''; diff --git a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py index 60b940c..efe0a3e 100644 --- a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py +++ b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py @@ -116,6 +116,39 @@ def write_state(state: dict) -> None: ) +def auto_commit_enabled(state: dict) -> bool: + return bool((state.get("settings") or {}).get("autoCommit")) + + +def auto_commit(message: str) -> None: + """Opt-in (settings.autoCommit): commit theme-state.json — and nothing + else — after a mutation, so settings history is `git log`. The pathspec + keeps unrelated dirty files out of the commit; a missing git identity + falls back to a Nomarchy one so a fresh machine never errors. Callers + fire this when the flag is on before OR after the write, so the + disable-toggle itself lands in history instead of staying dirty. + `bg` is deliberately excluded (runtime wallpaper churn); the wallpaper + path rides along with the next apply/set commit.""" + if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None: + return + git = ["git", "-C", str(FLAKE_DIR)] + # No-op when the file already matches HEAD (a `set` to the same value). + # On a repo with no commits yet this diff errors — then just commit. + if subprocess.run(git + ["diff", "--quiet", "HEAD", "--", "theme-state.json"], + capture_output=True).returncode == 0: + return + if not subprocess.run(git + ["config", "user.email"], + capture_output=True, text=True).stdout.strip(): + git += ["-c", "user.name=Nomarchy", "-c", "user.email=nomarchy@localhost"] + result = subprocess.run( + git + ["commit", "--quiet", "-m", message, "--", "theme-state.json"], + capture_output=True, text=True) + if result.returncode == 0: + log(f"auto-committed: {message}") + else: + log(f"auto-commit skipped: {(result.stderr or result.stdout).strip()}") + + def deep_merge(base: dict, override: dict) -> dict: out = dict(base) for k, v in override.items(): @@ -264,8 +297,11 @@ def cmd_apply(args) -> None: preset = json.loads(preset_path.read_text()) # Merge over current state: presets define palette/name/wallpaper, # user tweaks (gaps, fonts) outside the preset survive. - state = deep_merge(load_state(), preset) + old = load_state() + state = deep_merge(old, preset) write_state(state) + if auto_commit_enabled(old) or auto_commit_enabled(state): + auto_commit(f"nomarchy: apply theme {state.get('name', args.theme)}") log(f"theme: {state.get('name', args.theme)}") check_fonts(state) if not args.no_switch: @@ -280,6 +316,7 @@ def cmd_set(args) -> None: except json.JSONDecodeError: value = args.value # bare string ("#7aa2f7", font names, paths) + was_on = auto_commit_enabled(state) node = state keys = args.path.split(".") for key in keys[:-1]: @@ -289,6 +326,8 @@ def cmd_set(args) -> None: node[keys[-1]] = value write_state(state) + if was_on or auto_commit_enabled(state): + auto_commit(f"nomarchy: set {args.path} = {json.dumps(value)}") log(f"set {args.path} = {value!r}") if args.path.startswith("fonts."): check_fonts(state) @@ -310,7 +349,10 @@ def cmd_get(args) -> None: node = node[key] except (KeyError, TypeError): die(f"no such key: {args.path}") - print(json.dumps(node, indent=2) if isinstance(node, (dict, list)) else node) + # Booleans print JSON-style (true/false, not Python's True/False) so + # shell consumers can compare against the same literal they `set`. + print(json.dumps(node, indent=2) + if isinstance(node, (dict, list, bool)) else node) else: print(json.dumps(state, indent=2))