From 5f62dbf50aa3f0c8e0de41e90946b4d6af3f6dd4 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Thu, 6 Aug 2026 13:26:07 -0700 Subject: [PATCH] A new Epoch. --- flake.lock | 6 +- modules/home/wm/aerospace/aerospace.toml | 134 +++++++---------------- modules/home/wm/paneru/default.nix | 33 ++++++ modules/home/wm/paneru/paneru-startup.sh | 69 ++++++++++++ modules/home/wm/paneru/paneru.toml | 31 ++++-- 5 files changed, 162 insertions(+), 111 deletions(-) create mode 100755 modules/home/wm/paneru/paneru-startup.sh diff --git a/flake.lock b/flake.lock index 8eb7bf2..f99555e 100644 --- a/flake.lock +++ b/flake.lock @@ -270,11 +270,11 @@ ] }, "locked": { - "lastModified": 1786042321, - "narHash": "sha256-dZkRkUklY3xmt95coJ8QLvQ8Y2eBF3peSOC1k2kSlX8=", + "lastModified": 1786044312, + "narHash": "sha256-m5JAoOvQqxRqlJs1JoXtxqMkszqYixLfmYARtOsPWPA=", "owner": "noctalia-dev", "repo": "noctalia", - "rev": "54688c63aba4c624c41ca9a315e46e9df03ded51", + "rev": "d3135ed7a45c7d97b5ec254cf6fcfc26eaceebaa", "type": "github" }, "original": { diff --git a/modules/home/wm/aerospace/aerospace.toml b/modules/home/wm/aerospace/aerospace.toml index 6871505..e83f849 100644 --- a/modules/home/wm/aerospace/aerospace.toml +++ b/modules/home/wm/aerospace/aerospace.toml @@ -44,7 +44,7 @@ tell application "Zen" delay 0.8 end tell -tell application "Ghostty" +tell application "Zed" activate delay 0.8 end tell @@ -69,11 +69,6 @@ tell application "Spotify" activate delay 0.8 end tell - -tell application "Calendar" - activate - delay 0.8 -end tell ''' ] @@ -174,6 +169,9 @@ automatically-unhide-macos-hidden-apps = false end tell' ''' + # Mirrors niri's Mod+Shift+Return: open Ghostty with a zellij "welcome" layout + alt-shift-enter = 'exec-and-forget ghostty -e zellij -l welcome' + alt-f = 'fullscreen' # See: https://nikitabobko.github.io/AeroSpace/commands#layout @@ -207,28 +205,13 @@ automatically-unhide-macos-hidden-apps = false alt-8 = 'workspace 8' alt-9 = 'workspace 9' alt-0 = 'workspace 10' - # alt-a = 'workspace A' # In your config, you can drop workspace bindings that you don't need - alt-b = 'workspace B' # browser (zen) - alt-c = 'workspace C' # calendar - alt-d = 'workspace D' # discord - alt-e = 'workspace E' # emergence - # alt-f = 'workspace F' - # alt-g = 'workspace G' - alt-i = 'workspace I' # Zed (IDE) - alt-m = 'workspace M' # mail - # alt-n = 'workspace N' - # alt-o = 'workspace O' - # alt-p = 'workspace P' - # alt-q = 'workspace Q' - # alt-r = 'workspace R' - alt-s = 'workspace S' # spotify - alt-t = 'workspace T' # terminal - # alt-u = 'workspace U' - # alt-v = 'workspace V' - # alt-w = 'workspace W' - # alt-x = 'workspace X' - # alt-y = 'workspace Y' - alt-z = 'workspace Z' # zulip + # Named workspaces mirroring modules/home/wm/niri/config.kdl + # (there: Mod+B/W/D/M/T -> here: alt-b/w/d/m/t) + alt-b = 'workspace browser' # zen + alt-w = 'workspace work' # Zed + alt-d = 'workspace messaging' # discord, zulip + alt-m = 'workspace inbox' # mail + alt-t = 'workspace tunes' # spotify # See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace alt-shift-1 = ['move-node-to-workspace 1', 'workspace 1'] @@ -241,27 +224,11 @@ automatically-unhide-macos-hidden-apps = false alt-shift-8 = ['move-node-to-workspace 8', 'workspace 8'] alt-shift-9 = ['move-node-to-workspace 9', 'workspace 9'] alt-shift-0 = ['move-node-to-workspace 10', 'workspace 10'] - alt-shift-a = ['move-node-to-workspace A', 'workspace A'] - alt-shift-b = ['move-node-to-workspace B', 'workspace B'] - alt-shift-c = ['move-node-to-workspace C', 'workspace C'] - alt-shift-d = ['move-node-to-workspace D', 'workspace D'] - alt-shift-e = ['move-node-to-workspace E', 'workspace E'] - alt-shift-f = ['move-node-to-workspace F', 'workspace F'] - alt-shift-g = ['move-node-to-workspace G', 'workspace G'] - alt-shift-i = ['move-node-to-workspace I', 'workspace I'] - alt-shift-m = ['move-node-to-workspace M', 'workspace M'] - alt-shift-n = ['move-node-to-workspace N', 'workspace N'] - alt-shift-o = ['move-node-to-workspace O', 'workspace O'] - alt-shift-p = ['move-node-to-workspace P', 'workspace P'] - alt-shift-q = ['move-node-to-workspace Q', 'workspace Q'] - alt-shift-r = ['move-node-to-workspace R', 'workspace R'] - alt-shift-s = ['move-node-to-workspace S', 'workspace S'] - alt-shift-t = ['move-node-to-workspace T', 'workspace T'] - alt-shift-u = ['move-node-to-workspace U', 'workspace U'] - alt-shift-w = ['move-node-to-workspace W', 'workspace W'] - alt-shift-x = ['move-node-to-workspace X', 'workspace X'] - alt-shift-y = ['move-node-to-workspace Y', 'workspace Y'] - alt-shift-z = ['move-node-to-workspace Z', 'workspace Z'] + alt-shift-b = ['move-node-to-workspace browser', 'workspace browser'] + alt-shift-w = ['move-node-to-workspace work', 'workspace work'] + alt-shift-d = ['move-node-to-workspace messaging', 'workspace messaging'] + alt-shift-m = ['move-node-to-workspace inbox', 'workspace inbox'] + alt-shift-t = ['move-node-to-workspace tunes', 'workspace tunes'] # See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth alt-tab = 'workspace-back-and-forth' @@ -304,68 +271,43 @@ automatically-unhide-macos-hidden-apps = false 9 = 1 10 = 1 -S = 1 -C = 1 - -D = 3 -M = 3 -Z = 3 - -T = 2 -B = 2 -I = 2 +# Mirrors modules/home/wm/niri/config.kdl's workspace layout +tunes = 1 +messaging = 3 +inbox = 3 +browser = 2 +work = 2 +# Mirrors modules/home/wm/niri/config.kdl's window-rule -> workspace assignments [[on-window-detected]] if.app-id = 'app.zen-browser.zen' - run = ['move-node-to-workspace B'] - -[[on-window-detected]] - if.window-title-regex-substring = 'Discord' - run = ['move-node-to-workspace D'] - -[[on-window-detected]] - if.window-title-regex-substring = 'simulator' - run = ['move-node-to-workspace 0'] - -[[on-window-detected]] - if.app-id = 'com.spotify.client' - run = ['move-node-to-workspace S'] + run = ['move-node-to-workspace browser'] [[on-window-detected]] - if.app-id = 'org.zulip.zulip-electron' - run = ['move-node-to-workspace Z'] + if.app-id = 'app.zen-browser.zen' + if.window-title-regex-substring = 'Picture-in-Picture' + run = ['layout floating'] [[on-window-detected]] - if.app-id = 'com.tinyspeck.slackmacgap' - run = ['move-node-to-workspace 8'] + if.app-id = 'dev.zed.Zed' + run = ['move-node-to-workspace work'] [[on-window-detected]] if.app-id = 'com.apple.mail' - run = ['move-node-to-workspace M'] - -[[on-window-detected]] - if.app-id = 'com.mitchellh.ghostty' - run = ['move-node-to-workspace T'] + run = ['move-node-to-workspace inbox'] [[on-window-detected]] - if.app-id = 'com.apple.iCal' - run = ['move-node-to-workspace C'] - -[[on-window-detected]] - if.app-id = 'com.zed.Zed' - run = ['move-node-to-workspace I'] - + if.app-id = 'com.spotify.client' + run = ['move-node-to-workspace tunes'] [[on-window-detected]] - if.window-title-regex-substring = '(?i)fil' - run = ['move-node-to-workspace 7'] + if.app-id = 'com.hnc.Discord' + run = ['move-node-to-workspace messaging'] [[on-window-detected]] -if.app-id = 'app.zen-browser.zen' -if.window-title-regex-substring = 'Picture-in-Picture' -run = ['layout floating'] - + if.app-id = 'org.zulip.zulip-electron' + run = ['move-node-to-workspace messaging'] [[on-window-detected]] -if.window-title-regex-substring = 'simulator' -run = 'layout floating' + if.app-id = 'net.whatsapp.WhatsApp' + run = ['move-node-to-workspace messaging'] diff --git a/modules/home/wm/paneru/default.nix b/modules/home/wm/paneru/default.nix index 94c8ef7..c8fa2b9 100644 --- a/modules/home/wm/paneru/default.nix +++ b/modules/home/wm/paneru/default.nix @@ -15,5 +15,38 @@ in config = mkIf (cfg.enable && pkgs.stdenv.isDarwin) { home.file.".config/paneru/paneru.toml".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nyx/modules/home/wm/paneru/paneru.toml"; + + home.file.".config/paneru/paneru-startup.sh".source = + config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nyx/modules/home/wm/paneru/paneru-startup.sh"; + + # paneru comes from the "paneru" homebrew brew (see hosts/Daedalus), not + # nixpkgs, and its formula defines no `service do` block, so `brew + # services` can't start it. This runs the daemon itself at login and + # restarts it if it dies; paneru-startup below waits for it to be up. + launchd.agents.paneru = { + enable = true; + config = { + ProgramArguments = [ "/opt/homebrew/bin/paneru" "launch" ]; + RunAtLoad = true; + KeepAlive = true; + StandardOutPath = "/tmp/paneru.out.log"; + StandardErrorPath = "/tmp/paneru.err.log"; + }; + }; + + # Launches the workspace apps and routes each one to its virtual + # workspace, mirroring the per-app placement in the niri config. See + # paneru-startup.sh for why this has to be a launch-then-poll-focus + # script instead of a static paneru.toml window rule. + launchd.agents.paneru-startup = { + enable = true; + config = { + ProgramArguments = [ "${config.home.homeDirectory}/.config/paneru/paneru-startup.sh" ]; + RunAtLoad = true; + KeepAlive = false; + StandardOutPath = "/tmp/paneru-startup.out.log"; + StandardErrorPath = "/tmp/paneru-startup.err.log"; + }; + }; }; } diff --git a/modules/home/wm/paneru/paneru-startup.sh b/modules/home/wm/paneru/paneru-startup.sh new file mode 100755 index 0000000..a473b8a --- /dev/null +++ b/modules/home/wm/paneru/paneru-startup.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Launches the apps paneru doesn't have a native "open on workspace" rule for, +# then routes each one onto the same virtual workspace number niri would put +# it on (see modules/home/wm/niri/config.kdl's window-rules), plus WhatsApp +# (Daedalus-only, not in the niri config since it's a Mac App Store app): +# 1 = browser (Zen) 3 = messaging (Discord, Zulip, WhatsApp) 5 = tunes (Spotify) +# 2 = work (Zed) 4 = inbox (Mail) +# +# paneru's send-cmd move commands only operate on the currently focused +# window (there's no move-by-id), so this launches apps one at a time and +# waits for each to actually take focus before routing it - mirroring what +# aerospace.toml's after-startup-command does with `delay 0.8`, but polling +# instead of guessing a fixed sleep. +set -u + +export PATH="/opt/homebrew/bin:$PATH" + +log() { echo "[paneru-startup] $*" >&2; } + +wait_for_paneru() { + local tries=60 + while (( tries > 0 )); do + if paneru query state --json >/dev/null 2>&1; then + return 0 + fi + tries=$(( tries - 1 )) + sleep 0.5 + done + log "paneru daemon never came up, giving up" + return 1 +} + +# Waits until the focused window's bundle id matches, or times out. +wait_for_focus() { + local bundle_id="$1" + local tries=30 + while (( tries > 0 )); do + local focused + focused=$(paneru query active --json 2>/dev/null | jq -r '.focused_bundle_id // empty') + if [[ "$focused" == "$bundle_id" ]]; then + return 0 + fi + tries=$(( tries - 1 )) + sleep 0.5 + done + return 1 +} + +route() { + local bundle_id="$1" app_name="$2" workspace="$3" + + open -a "$app_name" 2>/dev/null + + if wait_for_focus "$bundle_id"; then + paneru send-cmd window virtualmovenum "$workspace" + else + log "$app_name ($bundle_id) never took focus, leaving it wherever it landed" + fi +} + +wait_for_paneru || exit 1 + +route "app.zen-browser.zen" "Zen" 1 +route "dev.zed.Zed" "Zed" 2 +route "com.hnc.Discord" "Discord" 3 +route "org.zulip.zulip-electron" "Zulip" 3 +route "net.whatsapp.WhatsApp" "WhatsApp" 3 +route "com.apple.mail" "Mail" 4 +route "com.spotify.client" "Spotify" 5 diff --git a/modules/home/wm/paneru/paneru.toml b/modules/home/wm/paneru/paneru.toml index 180df6f..9f78729 100644 --- a/modules/home/wm/paneru/paneru.toml +++ b/modules/home/wm/paneru/paneru.toml @@ -10,7 +10,9 @@ # - There's no "focus monitor" without moving a window, no overview, and no # per-workspace naming (paneru's virtual workspaces are numbered rows, not # named strips like niri's "browser"/"work"/etc) - that naming already lives -# in aerospace.toml's workspace letters. +# in aerospace.toml's workspace letters. Niri's Mod+B/W/D/M/T named-workspace +# jumps are still approximated below, bound to the numbered workspace each +# name corresponds to. # - Fine 10%-step resizing (Mod+Minus/Equal) doesn't exist; paneru only cycles # preset widths, so those are mapped to the closest grow/shrink action. @@ -23,8 +25,8 @@ preset_column_widths = [0.33333, 0.5, 0.66667, 1.0] # Focus (niri: Mod+H/J/K/L and arrows) window_focus_west = ["alt - h", "alt - leftarrow"] window_focus_east = ["alt - l", "alt - rightarrow"] -window_focus_north = ["alt - k", "alt - uparrow"] -window_focus_south = ["alt - j", "alt - downarrow"] +window_focus_north = "alt - uparrow" +window_focus_south = "alt - downarrow" window_focus_first = "alt - home" window_focus_last = "alt - end" @@ -42,18 +44,23 @@ window_nextdisplay = "alt + shift - n" window_nextdisplaysend = "alt + shift + ctrl - n" mouse_nextdisplay = "alt - n" -# Virtual workspaces (niri: Mod+Page_Up/Down, Mod+Ctrl+Page_Up/Down) -window_virtual_north = "alt - pageup" -window_virtual_south = "alt - pagedown" +# Virtual workspaces (niri: Mod+Page_Up/Down, Mod+Ctrl+Page_Up/Down). +# alt-k/alt-j are layered on here (rather than on window_focus_north/south) +# so they step -1/+1 through virtual workspaces instead of column-stack focus. +window_virtual_north = ["alt - pageup", "alt - k"] +window_virtual_south = ["alt - pagedown", "alt - j"] window_virtualsend_north = "alt + ctrl - pageup" window_virtualsend_south = "alt + ctrl - pagedown" -# Direct workspace numbers (niri: Mod+1..9, Mod+Ctrl+1..9) -window_virtualnum_1 = "alt - 1" -window_virtualnum_2 = "alt - 2" -window_virtualnum_3 = "alt - 3" -window_virtualnum_4 = "alt - 4" -window_virtualnum_5 = "alt - 5" +# Direct workspace numbers (niri: Mod+1..9, Mod+Ctrl+1..9). Workspaces +# 1-5 double as niri's named workspaces (browser/work/messaging/inbox/tunes, +# in the order they're declared at the top of config.kdl), so the same +# Mod+B/W/D/M/T letter binds are layered on here too. +window_virtualnum_1 = ["alt - 1", "alt - b"] +window_virtualnum_2 = ["alt - 2", "alt - w"] +window_virtualnum_3 = ["alt - 3", "alt - d"] +window_virtualnum_4 = ["alt - 4", "alt - m"] +window_virtualnum_5 = ["alt - 5", "alt - t"] window_virtualnum_6 = "alt - 6" window_virtualnum_7 = "alt - 7" window_virtualnum_8 = "alt - 8" -- 2.51.2