From 8d54eecd67fca152c42e7fac4b2446a8c092be87 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sun, 5 Jul 2026 20:56:01 +0100 Subject: [PATCH] feat(hyprland): launch-or-focus binds (item 17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nomarchy.launchOrFocus (opt-in, commented template example): each entry gives a key that FOCUSES the app's existing window (case-insensitive class match via jq + a "(?i)" focuswindow regex) or launches it when none is open. One shared nomarchy-focus-or-launch script; a bind whose command was removed from the suite fires a "not installed" toast instead of failing silently. The same entries render into the SUPER+? cheatsheet through prettyKeys — bind and documentation stay one source. README row added. Verified: V0; V1 — a scratch downstream with two entries renders both binds (default command = lowercased class; custom mods/command/desc honored), the script passes bash -n with a usage guard, and the cheatsheet carries both rows. Focus behaviour needs a session -> V3 (HARDWARE-QUEUE). Co-Authored-By: Claude Fable 5 --- README.md | 1 + agent/BACKLOG.md | 5 ---- agent/HARDWARE-QUEUE.md | 6 +++++ agent/JOURNAL.md | 22 +++++++++++++++++ modules/home/hyprland.nix | 31 ++++++++++++++++++++++- modules/home/options.nix | 46 +++++++++++++++++++++++++++++++++++ modules/home/rofi.nix | 6 +++++ templates/downstream/home.nix | 5 ++++ 8 files changed, 116 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b9d9ea6..49bfd6d 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ two tables below are split along exactly that line. | `nomarchy.mime.enable` | `true` | Default "open with" associations (PDF/image/video/text/browser/directory); entries for apps you removed are skipped, so it degrades with the suite | | `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback | | `nomarchy.displayProfiles` | `{}` | Named layouts for the same outputs (docked/undocked/…), switched from System › Display › Profiles: instant via hyprctl, persisted in-flake (`settings.displayProfile`), baked over `nomarchy.monitors` at the next rebuild. The menu's Auto-switch row (`settings.displayProfileAuto`) makes plugging/unplugging outputs apply the matching profile automatically | +| `nomarchy.launchOrFocus` | `[]` | Launch-or-focus binds: `SUPER+` focuses the app's window (case-insensitive class match) or launches it; entries land in the SUPER+? cheatsheet, and a bind whose app was removed notifies instead of failing silently | | `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed | | `nomarchy.package` | overlay's `nomarchy-theme-sync` | The theme/state tool package, overridable if you fork it | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 0a5003b..43955ea 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -214,11 +214,6 @@ the *same* outputs. **Plan (designed 2026-07-05, iteration #27):** - **Non-goals:** no kanshi (fights Hyprland's output management — decided in ROADMAP), nwg-displays stays a find-the-values helper. -### 17. Launch-or-focus UX scripts -ROADMAP § launch-or-focus. Hyprland dispatch scripts: a bind launches an -app or focuses its existing window. Curate which apps get binds; -cheatsheet entries via keybinds.nix. - ### 18. "nomarchy" control center + first-boot welcome `[big]` ROADMAP § control center. A single front-end over the common toggles on the same `nomarchy-theme-sync` surface, + a first-boot guided diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index d517de1..cd499a7 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -167,6 +167,12 @@ QA machine), the **T14s** (webcam case). (tooltips, fastfetch labels) is visible on summer-day + flexoki-light, kanagawa floats are lighter than the bg (upstream sumiInk4). Anything that reads worse than before → reopen 28b. +- [ ] **Launch-or-focus (item 17)** — uncomment the template's + `nomarchy.launchOrFocus` firefox example (with firefox + installed), `home-update` + reload: SUPER+B launches firefox + when closed, FOCUSES the existing window when open (also from + another workspace); with firefox removed the bind fires a + "not installed" toast; the row shows under SUPER+?. - [ ] **Themed greeter + console (item 16)** — after `sys-rebuild` + logout: tuigreet renders in theme colors (dark container, accent border, themed prompt/time) and a raw tty (CTRL+ALT+F2) shows the diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index f96c18e..ec8fd63 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,28 @@ Template: --- +## 2026-07-05 — launch-or-focus binds (iteration #39, item 17) +- **Task:** BACKLOG NEXT#17 (the UI review is Bernardo-gated; back to + the queue head). Item deleted. +- **Did:** nomarchy.launchOrFocus (listOf submodule: key/mods/class/ + command/desc; opt-in, template example) → hyprland.nix generates + binds through a shared nomarchy-focus-or-launch script (jq + case-insensitive class match → focuswindow "(?i)" regex, else + launch; uninstalled command → toast, so binds survive suite edits); + rofi.nix renders the same entries into the SUPER+? cheatsheet via + prettyKeys (single source). README row. +- **Verified:** V0; V1 — scratch downstream with 2 entries: both + binds rendered (default command = lowercased class; custom + mods/command/desc honored), tool bash -n + usage exit 64, + cheatsheet rows present. Focus behaviour needs a session → V3 + queued. +- **Pending:** V3. Queue after this: #18 control center [big, needs + the form-factor Decision], #19 Look&Feel menu (gated on ≥3 + entries), #20 [human], LATER tier. +- **Next suggestion:** a QA sweep (#18/#19/#20 are blocked-ish) — or + item 19's precondition check (does Look & Feel earn 3 entries + yet?). + ## 2026-07-05 — placeholder dim + latte review + backdrop probe (iteration #38, item 28c) - **Task:** item 28 slice (c) — the summer-rasi quickie + the light- theme capture. diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index a514cbe..98f3026 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -31,6 +31,34 @@ let ${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2" ''; + # Launch-or-focus (nomarchy.launchOrFocus, item 17): focus the app's + # window if one exists (case-insensitive class match), launch it + # otherwise. Self-gates: an uninstalled command notifies instead of + # failing silently, so a bind survives the app being removed from the + # suite. rofi.nix renders the same entries into the SUPER+? cheatsheet. + focusOrLaunch = pkgs.writeShellScriptBin "nomarchy-focus-or-launch" '' + class="''${1:-}"; shift || true + [ -n "$class" ] && [ $# -gt 0 ] || { + echo "usage: nomarchy-focus-or-launch " >&2; exit 64 + } + if hyprctl clients -j 2>/dev/null \ + | ${pkgs.jq}/bin/jq -e --arg c "$class" \ + 'any(.[]; (.class | ascii_downcase) == ($c | ascii_downcase))' >/dev/null; then + exec hyprctl dispatch focuswindow "class:(?i)^$class\$" + fi + command -v "$1" >/dev/null 2>&1 \ + || { notify-send "Launch" "$1 is not installed."; exit 0; } + exec "$@" + ''; + lofEntries = map + (e: e // { + command = if e.command == "" then lib.toLower e.class else e.command; + }) + config.nomarchy.launchOrFocus; + lofBinds = map + (e: "${e.mods}, ${e.key}, exec, ${focusOrLaunch}/bin/nomarchy-focus-or-launch ${e.class} ${e.command}") + lofEntries; + # SUPER+1..9 / SUPER+SHIFT+1..9 workspace binds, generated. workspaceBinds = builtins.concatLists (builtins.genList (i: @@ -393,7 +421,8 @@ in bind = map mkBind (keybinds.binds ++ lib.optionals (lib.hasInfix "," config.nomarchy.keyboard.layout) keybinds.multiLayoutBinds) - ++ workspaceBinds; + ++ workspaceBinds + ++ lofBinds; # Media keys via swayosd-client: it performs the action AND shows # the on-screen display (the nomarchy.osd module). e = repeat, diff --git a/modules/home/options.nix b/modules/home/options.nix index 577b8d0..3066bc8 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -237,6 +237,52 @@ in ''; }; + launchOrFocus = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + key = lib.mkOption { + type = lib.types.str; + example = "B"; + description = "The key (with `mods`) that focuses-or-launches the app."; + }; + mods = lib.mkOption { + type = lib.types.str; + default = "$mod"; + description = "Modifier string, Hyprland syntax (\"$mod\", \"$mod SHIFT\")."; + }; + class = lib.mkOption { + type = lib.types.str; + example = "firefox"; + description = "Window class to focus (case-insensitive; see `hyprctl clients`)."; + }; + command = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Command to launch when no window matches (defaults to the class)."; + }; + desc = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Cheatsheet label (defaults to \"Focus or launch \")."; + }; + }; + }); + default = [ ]; + example = lib.literalExpression '' + [ + { key = "B"; class = "firefox"; } + { key = "O"; class = "obsidian"; } + ] + ''; + description = '' + Launch-or-focus binds: the key focuses the app's existing window + (case-insensitive class match) or launches it if none is open. Each + entry generates a Hyprland bind AND a SUPER+? cheatsheet row. The + launcher self-gates: a bind whose command isn't installed notifies + instead of failing silently. + ''; + }; + displayProfiles = lib.mkOption { type = lib.types.attrsOf (lib.types.listOf monitorType); default = { }; diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 5d1673c..d89c182 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -125,6 +125,12 @@ let map (b: padRight 22 (prettyKeys b) + b.desc) (keybinds.binds ++ lib.optionals (lib.hasInfix "," cfg.keyboard.layout) keybinds.multiLayoutBinds) + # Launch-or-focus binds (nomarchy.launchOrFocus, generated in + # hyprland.nix) — same renderer, so they land in the cheatsheet too. + ++ map (e: padRight 22 (prettyKeys e) + + (if e.desc != "" then e.desc + else "Focus or launch ${if e.command == "" then lib.toLower e.class else e.command}")) + cfg.launchOrFocus ++ map (e: padRight 22 e.keys + e.desc) keybinds.extra; cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt" (lib.concatStringsSep "\n" cheatRows + "\n"); diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index bbf1e90..cdce838 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -34,6 +34,11 @@ # { name = "HDMI-A-1"; position = "auto-right"; } # ]; # names from `hyprctl devices` # + # nomarchy.launchOrFocus = [ # SUPER+ focuses the app's window, + # { key = "B"; class = "firefox"; } # or launches it if none is open; + # { key = "O"; class = "obsidian"; }# rows appear in the SUPER+? cheatsheet + # ]; # (class from `hyprctl clients`) + # # nomarchy.displayProfiles = { # named layouts for the SAME outputs — # docked = [ # switch: System › Display › Profiles # { name = "eDP-1"; resolution = "disable"; }