diff --git a/dotfiles/niri/.config/niri/cfg/display.kdl b/dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded similarity index 100% rename from dotfiles/niri/.config/niri/cfg/display.kdl rename to dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded diff --git a/dotfiles/niri/.config/niri/cfg/layout.kdl b/dotfiles/niri/.config/niri/cfg/layout.kdl index d439c8d..7bea5d3 100644 --- a/dotfiles/niri/.config/niri/cfg/layout.kdl +++ b/dotfiles/niri/.config/niri/cfg/layout.kdl @@ -5,10 +5,13 @@ background-color "transparent" // <- needed for noctalia-shell to set wallpaper preset-column-widths { + proportion 0.125 + proportion 0.25 proportion 0.33333 proportion 0.5 proportion 0.66667 - proportion 0.875 + proportion 0.75 + proportion 0.875 } struts { diff --git a/dotfiles/niri/.config/niri/config.kdl b/dotfiles/niri/.config/niri/config.kdl index aee1c00..176892b 100644 --- a/dotfiles/niri/.config/niri/config.kdl +++ b/dotfiles/niri/.config/niri/config.kdl @@ -8,10 +8,11 @@ gestures { include "./cfg/autostart.kdl" include "./cfg/keybinds.kdl" include "./cfg/input.kdl" -include "./cfg/display.kdl" +include optional=true "./cfg/display.kdl" include "./cfg/layout.kdl" include "./cfg/rules.kdl" include "./cfg/misc.kdl" +include optional=true "./cfg/switch-events.kdl" include "noctalia.kdl" diff --git a/dotfiles/noctalia/.config/noctalia/config.toml b/dotfiles/noctalia/.config/noctalia/config.toml new file mode 100644 index 0000000..40de7e5 --- /dev/null +++ b/dotfiles/noctalia/.config/noctalia/config.toml @@ -0,0 +1,5 @@ +[shell] +polkit_agent = true + +[wallpaper] +directory = "/home/blank/.local/share/noctalia/wallpapers" diff --git a/dotfiles/noctalia/.local/state/noctalia/settings.toml b/dotfiles/noctalia/.local/state/noctalia/settings.toml new file mode 100644 index 0000000..0f20afc --- /dev/null +++ b/dotfiles/noctalia/.local/state/noctalia/settings.toml @@ -0,0 +1,253 @@ +config_version = 14 + +[audio] +enable_overdrive = true +enable_sounds = true + +[bar.default] +center = [ "taskbar", "cat" ] +end = [ "notifications", "clipboard", "network", "bluetooth", "tray", "battery", "session", "control-center", "clock" ] +font_family = "Terminess Nerd Font" +font_weight = 700 + +[location] +auto_locate = true + +[lockscreen_widgets] +enabled = false +schema_version = 2 +widget_order = [ + "lockscreen-login-box@DP-1", + "lockscreen-login-box@HDMI-A-1", + "lockscreen-login-box@DP-2", + "lockscreen-login-box@eDP-1" +] + + [lockscreen_widgets.grid] + cell_size = 16 + major_interval = 4 + visible = true + + [lockscreen_widgets.widget."lockscreen-login-box@DP-1"] + box_height = 196.0 + box_width = 810.0 + cx = 640.0 + cy = 538.0 + output = "DP-1" + placement_height = 720.0 + placement_width = 1280.0 + rotation = 0.0 + type = "login_box" + + [lockscreen_widgets.widget."lockscreen-login-box@DP-1".settings] + background_color = "surface_variant" + background_opacity = 0.88 + background_radius = 12.0 + center_password_text = false + input_opacity = 1.0 + input_radius = 6.0 + layout = "regular" + show_caps_lock = true + show_keyboard_layout = true + show_login_button = true + show_media = true + show_session_buttons = true + show_unlock_hint = true + show_weather = true + + [lockscreen_widgets.widget."lockscreen-login-box@DP-2"] + box_height = 196.0 + box_width = 810.0 + cx = 1719.999755859375 + cy = 1221.60009765625 + output = "DP-2" + placement_height = 1440.0 + placement_width = 3440.0 + rotation = 0.0 + type = "login_box" + + [lockscreen_widgets.widget."lockscreen-login-box@DP-2".settings] + background_color = "surface_variant" + background_opacity = 0.88 + background_radius = 12.0 + center_password_text = false + input_opacity = 1.0 + input_radius = 6.0 + layout = "regular" + show_caps_lock = true + show_keyboard_layout = true + show_login_button = true + show_media = true + show_session_buttons = true + show_unlock_hint = true + show_weather = true + + [lockscreen_widgets.widget."lockscreen-login-box@HDMI-A-1"] + box_height = 196.0 + box_width = 720.0 + cx = 600.0 + cy = 1628.800048828125 + output = "HDMI-A-1" + placement_height = 1920.0 + placement_width = 1200.0 + rotation = 0.0 + type = "login_box" + + [lockscreen_widgets.widget."lockscreen-login-box@HDMI-A-1".settings] + background_color = "surface_variant" + background_opacity = 0.88 + background_radius = 12.0 + center_password_text = false + input_opacity = 1.0 + input_radius = 6.0 + layout = "regular" + show_caps_lock = true + show_keyboard_layout = true + show_login_button = true + show_media = true + show_session_buttons = true + show_unlock_hint = true + show_weather = true + + [lockscreen_widgets.widget."lockscreen-login-box@eDP-1"] + box_height = 196.0 + box_width = 810.0 + cx = 960.000244140625 + cy = 852.500244140625 + output = "eDP-1" + placement_height = 1080.0 + placement_width = 1920.0 + rotation = 0.0 + type = "login_box" + + [lockscreen_widgets.widget."lockscreen-login-box@eDP-1".settings] + background_color = "surface_variant" + background_opacity = 0.88 + background_radius = 12.0 + center_password_text = false + input_opacity = 1.0 + input_radius = 6.0 + layout = "regular" + show_caps_lock = true + show_keyboard_layout = true + show_login_button = true + show_media = true + show_session_buttons = true + show_unlock_hint = true + show_weather = true + +[nightlight] +enabled = true + +[plugin_settings."dotnetrob/cat"] +panel_open_near_click = false +panel_placement = "floating" +panel_position = "center" + +[plugins] +enabled = [ + "noctalia/wallhaven", + "prponkshe/umbriel-displays", + "ashur-d/nvim-projects", + "thepunkoff/pomodoro", + "umedbazarov/webapp-maker", + "emiliovenegas/omp-launcher", + "yugaaank/better-clock", + "dotnetrob/cat", + "yuuto/calculator", + "joshuaslate/shelly" +] + +[shell] +external_ip_enabled = true +font_family = "Terminess Nerd Font Propo" + + [shell.panel] + transparency_mode = "glass" + +[theme] +builtin = "Kanagawa" +community_palette = "Gruber Darker" +custom_palette = "Gruber Red" +mode = "dark" +shell_mode = "dark" +source = "custom" + + [theme.templates] + builtin_ids = [ + "alacritty", + "btop", + "cava", + "emacs", + "foot", + "gtk3", + "gtk4", + "ghostty", + "helix", + "hyprland", + "kcolorscheme", + "kitty", + "labwc", + "mango", + "niri", + "qt", + "scroll", + "starship", + "sway", + "umbriel", + "wezterm" + ] + community_ids = [ + "pi-agent", + "ytm-player", + "pywalfox", + "pywalfox-beta4", + "ungoogled-chromium", + "micro", + "neovim", + "obsidian", + "vscode", + "steam", + "rofi", + "fastfetch", + "bat", + "fzf", + "glow", + "herdr", + "lazygit", + "tmux" + ] + +[wallpaper] +transition = [ "fade" ] + + [wallpaper.automation] + enabled = true + interval_seconds = 1200 + + [wallpaper.default] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" + + [wallpaper.last] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" + + [wallpaper.monitors.DP-1] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_nzz3gy.jpg" + + [wallpaper.monitors.DP-2] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" + + [wallpaper.monitors.HDMI-A-1] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" + + [wallpaper.monitors.eDP-1] + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" + +[weather] +unit = "imperial" + +[widget.cat] +type = "dotnetrob/cat:cat" + +[widget.tray] +drawer = true \ No newline at end of file diff --git a/dotfiles/zz-blankpad/.config/systemd/user/blankpad-desk-autoswitch.service b/dotfiles/zz-blankpad/.config/systemd/user/blankpad-desk-autoswitch.service new file mode 100644 index 0000000..4bc7cc4 --- /dev/null +++ b/dotfiles/zz-blankpad/.config/systemd/user/blankpad-desk-autoswitch.service @@ -0,0 +1,17 @@ +[Unit] +Description=Desk Profile Auto-Switch for Blankpad +After=graphical-session.target +ConditionHost=blankpad + +[Service] +Type=simple +ExecStart=%h/.local/bin/desk-autoswitch.sh watch +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +Environment=DESK_NIRI=/usr/bin/niri +Environment="DESK_NOTIFY=noctalia msg notification-show" + +[Install] +WantedBy=default.target diff --git a/dotfiles/zz-blankpad/.local/bin/desk-autoswitch.sh b/dotfiles/zz-blankpad/.local/bin/desk-autoswitch.sh new file mode 120000 index 0000000..e64742a --- /dev/null +++ b/dotfiles/zz-blankpad/.local/bin/desk-autoswitch.sh @@ -0,0 +1 @@ +../../../../scripts/desk-autoswitch.sh \ No newline at end of file diff --git a/dotfiles/zz-blankpad/.local/bin/desk.sh b/dotfiles/zz-blankpad/.local/bin/desk.sh new file mode 120000 index 0000000..a7a55e7 --- /dev/null +++ b/dotfiles/zz-blankpad/.local/bin/desk.sh @@ -0,0 +1 @@ +../../../../scripts/desk.sh \ No newline at end of file diff --git a/dotfiles/zz-blankpad/.local/share/com.pais.handy/README.md b/dotfiles/zz-blankpad/.local/share/com.pais.handy/README.md new file mode 100644 index 0000000..9a2b8b0 --- /dev/null +++ b/dotfiles/zz-blankpad/.local/share/com.pais.handy/README.md @@ -0,0 +1,34 @@ +# Handy Settings Template + +Handy (Tauri v2) keeps its store at `~/.local/share/com.pais.handy/settings_store.json`, +shaped `{"settings": {...}}`, and **rewrites the live file on every change** — so the +repo carries a merge patch (`settings_store.template.json`), not a full store. + +## Apply the fleet wiring + +Quit Handy first (it caches settings in memory and overwrites external edits): + +```bash +pkill -f /usr/bin/handy +jq --slurpfile p /dotfiles/zz-blankpad/.local/share/com.pais.handy/settings_store.template.json ' + .settings |= ( + $p[0].settings + | .post_process_models = ((.post_process_models // {}) + $p[0].settings.post_process_models) + | .post_process_providers = (.post_process_providers + | map(if .id == "custom" then .base_url = $p[0].custom_provider_base_url else . end)) + )' settings_store.json > store.tmp && mv store.tmp settings_store.json +setsid nohup handy --start-hidden >/dev/null 2>&1 < /dev/null & +``` + +Niri also spawns Handy at startup (`cfg/autostart.kdl`: +`spawn-at-startup "handy" "--start-hidden"`), hence `autostart_enabled: false` — +the compositor owns autostart. + +## Known breaker (2026-10-09) + +`frank-secondary` (`http://192.168.0.143:7771/v1`) is a thinking model: default +requests return `content: ""` with the answer locked in `reasoning_content`. +Handy reads `content`, so post-processed transcripts come back empty. The +per-request cure is `chat_template_kwargs: {"enable_thinking": false}` (verified), +which Handy cannot send. Endpoint-side fix required — see +`omp-docs/tools/handy.md` → Known issues. diff --git a/dotfiles/zz-blankpad/.local/share/com.pais.handy/settings_store.template.json b/dotfiles/zz-blankpad/.local/share/com.pais.handy/settings_store.template.json new file mode 100644 index 0000000..b555b86 --- /dev/null +++ b/dotfiles/zz-blankpad/.local/share/com.pais.handy/settings_store.template.json @@ -0,0 +1,14 @@ +{ + "//": "Handy settings-store wiring patch (blankpad, 2026-10-09).", + "//shape": "The live store is {\"settings\": {...}}; the app rewrites it, so this file is a MERGE PATCH, not a drop-in store.", + "//apply": "see README.md", + "//knob": "frank-secondary is a thinking model: it answers via reasoning_content and leaves content empty unless chat_template_kwargs.enable_thinking=false. Handy cannot send that; the fix must live on the endpoint (see omp-docs/tools/handy.md Known issues).", + "settings": { + "post_process_enabled": true, + "autostart_enabled": false, + "post_process_provider_id": "custom", + "post_process_models": { "custom": "frank-secondary" }, + "post_process_selected_prompt_id": "default_improve_transcriptions" + }, + "custom_provider_base_url": "http://192.168.0.143:7771/v1" +} diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/autostart.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/autostart.kdl index 1fea9d7..ec34e05 100644 --- a/dotfiles/zz-blankpad/niri/.config/niri/cfg/autostart.kdl +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/autostart.kdl @@ -2,3 +2,4 @@ // https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-sh-at-startup spawn-at-startup "noctalia" +spawn-at-startup "handy" "--start-hidden" diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl new file mode 100644 index 0000000..abf3d76 --- /dev/null +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl @@ -0,0 +1,32 @@ +// ────────────── Desk Profile: home (current rig) ────────────── +// Applied by symlinking this file to cfg/display.kdl (see scripts/desk.sh). +// Panels are keyed by EDID identity (manufacturer model serial), NOT by +// connector name — connector names reshuffle between desks; identity travels. +// +// requires: Sceptre Tech Inc Sceptre M34 Unknown +// requires: Lenovo Group Limited 0x40A9 Unknown +// optional: HDMI-A-1 (PM1601, dock-dependent) + +output "Sceptre Tech Inc Sceptre M34 Unknown" { + mode "3440x1440@239.999" + scale 1 + transform "normal" + position x=1581 y=-600 + focus-at-startup +} + +// PM1601 portrait panel (dock-dependent). EDID string NOT captured as of +// 2026-10-09 (panel disconnected) — connector-keyed placeholder. +// When plugged in: run `desk.sh capture`, re-key this block to the printed +// long name, update #optional above. +output "HDMI-A-1" { + scale 1 + transform "90" + position x=1200 y=0 +} + +output "Lenovo Group Limited 0x40A9 Unknown" { + scale 1.0 + position x=5021 y=-240 + focus-at-startup +} diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-2-portable.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-2-portable.kdl new file mode 100644 index 0000000..1f1f7e6 --- /dev/null +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-2-portable.kdl @@ -0,0 +1,18 @@ +// ────────────── Desk Profile: portable (skeleton) ────────────── +// SKELETON — portable monitor EDID not captured yet. On first use at the +// portable desk: run `desk.sh capture`, replace PORTABLE-EDID-TBD below and +// in the requires list with the printed long name, adjust position to taste. +// requires: Lenovo Group Limited 0x40A9 Unknown +// requires: PORTABLE-EDID-TBD + +output "Lenovo Group Limited 0x40A9 Unknown" { + scale 1.0 + position x=0 y=0 + focus-at-startup +} + +// Assumed: 1920x1080 landscape panel right of the laptop panel. +output "PORTABLE-EDID-TBD" { + scale 1.0 + position x=1920 y=0 +} diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/display.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/display.kdl deleted file mode 100644 index 814166d..0000000 --- a/dotfiles/zz-blankpad/niri/.config/niri/cfg/display.kdl +++ /dev/null @@ -1,29 +0,0 @@ -// ────────────── Output Configuration ────────────── -// You can run `niri msg outputs` to get the correct name for your displays. -// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs -// -// eDP-1 (laptop panel) is pinned too: an output block resets unspecified -// properties to defaults, so its 1.25 scale must be repeated here. -// -// Both external PM1601 panels run portrait. niri's transform is -// counter-clockwise: "90" = panel's top bezel faces left, "270" = top bezel -// faces right. In portrait a 180° flip is the 90<->270 swap. - -output "DP-2" { - mode "3440x1440@239.999" - - scale 1 - transform "normal" - position x=1581 y=-600 -} - -output "HDMI-A-1" { - scale 1 - transform "90" - position x=1200 y=0 -} - -output "eDP-1" { - scale 1.0 // MUST restate: dashed-out by the block otherwise - position x=5021 y=-240 -} diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/keybinds.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/keybinds.kdl index ab08974..de6cc4d 100644 --- a/dotfiles/zz-blankpad/niri/.config/niri/cfg/keybinds.kdl +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/keybinds.kdl @@ -16,9 +16,13 @@ binds { Mod+Shift+Return hotkey-overlay-title="Open Wallpaper Selector: noctalia wallpaper toggle" { spawn-sh "noctalia msg panel-toggle wallpaper"; } Mod+S hotkey-overlay-title="Open Control Center: noctalia controlCenter" { spawn-sh "noctalia msg panel-toggle control-center"; } Mod+Shift+S hotkey-overlay-title="Open Settings: noctalia settings" { spawn-sh "noctalia msg settings-toggle"; } - Mod+CTRL+Return hotkey-overlay-title="Open App Launcher: noctalia launcher" { spawn-sh "noctalia msg panel-toggle launcher"; } + Mod+D hotkey-overlay-title="Open App Launcher: noctalia launcher" { spawn-sh "noctalia msg panel-toggle launcher"; } Mod+ALT+L hotkey-overlay-title="Lock Screen: noctalia lock" { spawn-sh "noctalia msg session lock"; } Mod+Shift+Q hotkey-overlay-title="Session Menu: noctalia sessionMenu" { spawn-sh "noctalia msg panel-toggle session"; } + Mod+V hotkey-overlay-title="Clipboard History: noctalia panel" { spawn-sh "noctalia msg panel-toggle clipboard"; } + Alt+TAB hotkey-overlay-title="Window Switcher: noctalia" { spawn-sh "noctalia msg window-switcher"; } + Mod+Shift+Space hotkey-overlay-title="Handy: toggle post-process recording" allow-when-locked=true { spawn-sh "handy --toggle-post-process"; } + Mod+Shift+D hotkey-overlay-title="Cycle Desk Profile" { spawn-sh "desk.sh cycle"; } // Please set the text editor of your choice. // Mod+Shift+E hotkey-overlay-title="Open Text Editor: Zed" { spawn "zed"; } @@ -52,10 +56,15 @@ binds { Mod+Down { focus-window-down; } Mod+J { focus-window-down; } - Mod+CTRL+Left { move-column-left; } - Mod+CTRL+H { move-column-left; } - Mod+CTRL+Right { move-column-right; } - Mod+CTRL+L { move-column-right; } + // swap-window = window walks sideways, joining/exiting columns it passes + Mod+CTRL+Left { swap-window-left; } + Mod+CTRL+H { swap-window-left; } + Mod+CTRL+Right { swap-window-right; } + Mod+CTRL+L { swap-window-right; } + Mod+CTRL+Alt+Left { move-column-left; } + Mod+CTRL+Alt+H { move-column-left; } + Mod+CTRL+Alt+Right { move-column-right; } + Mod+CTRL+Alt+L { move-column-right; } Mod+CTRL+UP { move-window-up; } Mod+CTRL+K { move-window-up; } Mod+CTRL+Down { move-window-down; } @@ -149,6 +158,9 @@ binds { CTRL+Shift+1 hotkey-overlay-title="Screenshot: flameshot full" { spawn-sh "flameshot full"; } CTRL+Shift+2 hotkey-overlay-title="Screenshot: flameshot screen" { spawn-sh "flameshot screen"; } CTRL+Shift+3 hotkey-overlay-title="Screenshot: flameshot region" { spawn-sh "flameshot gui"; } + Print hotkey-overlay-title="Screenshot: flameshot full" { spawn-sh "flameshot full"; } + Ctrl+Print hotkey-overlay-title="Screenshot: flameshot region" { spawn-sh "flameshot gui"; } + Alt+Print hotkey-overlay-title="Screenshot: flameshot screen" { spawn-sh "flameshot screen"; } // ─── Emergency Escape Key ─── // Use this when a fullscreen app blocks your keybinds. diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/switch-events.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/switch-events.kdl new file mode 100644 index 0000000..44d85fc --- /dev/null +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/switch-events.kdl @@ -0,0 +1,7 @@ +// ────────────── Switch (lid) Events — blankpad laptop ────────────── +// Included by config.kdl via include optional=true; only blankpad links this. +// Pair with logind: if HandleLidSwitch=ignore is set, this is the sole path. + +switch-events { + lid-close { spawn "noctalia" "msg" "session" "lock-and-suspend"; } +} diff --git a/omp-docs/log.md b/omp-docs/log.md index a980471..0b467fa 100644 --- a/omp-docs/log.md +++ b/omp-docs/log.md @@ -11,6 +11,25 @@ timestamp: 2026-08-28 # Bundle Change Log +## 2026-10-09 — Blankpad DE redesign: desk profiles, keybind rework, Handy fleet wiring + +Branch `blankpad-de-redesign` (worktree `wt/de-redesign-20261009`). Full sweep + redesign of the niri/noctalia/Handy stack on blankpad. + +* **Desk profiles.** `cfg/display.kdl` retired to a symlink; per-desk profiles `desk-1-home.kdl` / `desk-2-portable.kdl` declare monitors by **EDID long name** (`// requires:` lines) — connector names reshuffle between docks, identity travels. `scripts/desk.sh` = list/current/switch/cycle/match/capture; apply = relink → `niri validate` (rollback on failure) → `load-config-file` → noctalia notification. `scripts/desk-autoswitch.sh` + `blankpad-desk-autoswitch.service` watch `niri msg --json event-stream`: niri 26.04 has **no hotplug event** (`OutputConfigChanged` is only an ACK that an IPC/config output change was Applied, per niri-ipc); the daemon diffs the sorted output set carried by `WorkspacesChanged` and hot-applies the matching profile (debounced 1.5 s). Proven live with an eDP-1 off/on cycle: `[DP-2 eDP-1] → [DP-2] → [DP-2 eDP-1]`, apply + reload each time. PM1601 EDID still uncaptured (panel disconnected) — connector-keyed placeholder, `desk.sh capture` pending. +* **Keybinds.** `swap-window-left/right` on Mod+CTRL+H/L; move-column demoted to Mod+CTRL+Alt+H/L; 7 column-width presets 12.5–87.5 %; Print trio (full / gui / screen) via noctalia screenshot IPC; launcher Mod+D; Alt+Tab → noctalia window-switcher; Mod+V → clipboard panel (id verified live); Mod+Shift+D → desk cycle; Mod+Shift+Space → `handy --toggle-post-process`; lid-close → `noctalia msg session lock-and-suspend` (pairs with logind `HandleLidSwitch=ignore`). Cheat sheet now GENERATED: `scripts/gen-keybinds.sh` → `omp-docs/tools/niri/keybindings.md` (119 bindings). +* **Noctalia.** `config.toml` + `settings.toml` (lives in `~/.local/state/`, not `~/.config/`) tracked; `taskbar` widget added to bar center, applied live via `noctalia msg config-reload`. Docs corrected to the v5 `noctalia msg` API. +* **Handy.** Self-autostart killed (`~/.config/autostart/Handy.desktop` → `.AGENTTRASH`; `autostart_enabled=false`); niri owns autostart (`spawn-at-startup "handy" "--start-hidden"`). Fleet post-processing wired into the live store: provider `custom` → `http://192.168.0.143:7771/v1`, model `frank-secondary`, prompt `default_improve_transcriptions`. **Open breaker:** frank-secondary answers in `reasoning_content` and leaves `content` empty; per-request cure `chat_template_kwargs.enable_thinking=false` verified, but Handy cannot send it and the server is shared with fleet compaction — dedicated no-think port proposed. 0.9.6→0.9.8 upgrade is operator-owned (`yay -Syu handy-bin`). +* **Scripts + tests.** Two subagents died mid-run (one mid-test-write; one crashed **looping `sudo yay` without a TTY** — see next entry). Their scripts/tests were rebuilt from live-format ground truth: `tests/desk.bats` (16), `tests/gen-keybinds.bats` (6), `tests/niri-config.bats` (5, incl. real `niri validate` on the assembled base+zz tree with each desk profile active). Suite: 27/27 green. + +## 2026-10-09 — Incident: agent sudo-loop tripped pam_faillock and locked out the operator's sudo + +While attempting the Handy 0.9.8 install, a subagent ran `sudo yay -Syu handy-bin` in a retry loop for ~15 minutes. Harness sudo has no TTY, so every attempt logged 3 `pam_unix` auth failures; `pam_faillock` (deny=3) locked user `blank` out — the operator's own correct-password `sudo`/`pacman -Syu` began failing with "temporarily locked out", which looked exactly like a changed password. + +* **No password change:** `/etc/shadow` mtime 2026-09-10 23:31, untouched; no `passwd`/`chpasswd`/`usermod` in the journal. +* **Recovery:** loop process died with the agent; `faillock --user blank --reset` (unprivileged for own user, dir `/run/faillock/blank` is 0700) cleared the counter; verified empty. +* **New law:** agents NEVER run `sudo`/`yay`/`pacman`/`pkexec` on Frank's workstations — root-requiring steps are handed to the operator as exact copy-paste commands. Codified as managed skill `faillock-sudo-lockout-recovery`. + + ## 2026-10-09 — Full secret audit of the command-center repo: clean Five-layer audit of `dank/command-center` at `79e780f`, driven by the four live values in `~/.config/secrets/` (gitea, hf_token, hf_token_write, kagi_api_key) plus 21 provider shapes (sk-, hf_, gh[pousr]_, glpat-, AKIA/ASIA, xox, npm_, pypi-, AIza, JWT, PEM/OpenSSH keys, `scheme://user:pass@host` basic-auth URLs, `password|secret|api_key = ` assignments). Findings printed redacted (prefix + length + sha256 prefix). diff --git a/omp-docs/tools/handy.md b/omp-docs/tools/handy.md new file mode 100644 index 0000000..199cc22 --- /dev/null +++ b/omp-docs/tools/handy.md @@ -0,0 +1,233 @@ +--- +type: reference +title: Handy +description: Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI. +resource: https://github.com/cjpais/Handy +tags: + - handy + - stt + - speech-to-text + - transcription + - wayland + - tauri +timestamp: 2026-10-09 +verified: 2026-10-09 +--- + +# Handy + +Handy is an offline speech-to-text (STT) transcription tool built with Tauri v2 and WebKitGTK. It runs entirely offline, supports multiple GPU backends, and provides real-time transcription with optional post-processing. + +**Installed version:** 0.9.6-1 (`local/handy-bin`, `/usr/bin/handy`) — **0.9.8 upgrade pending**: `yay -Syu handy-bin` (root work is operator-owned; agents never sudo). +**Upstream latest:** 0.9.8 (as of 2026-10-03) +**Binary:** `/usr/bin/handy` +**Config:** `~/.config/com.pais.handy/` (upstream docs) / `~/.local/share/com.pais.handy/settings_store.json` (live observed) + +--- + +## Architecture + +Handy uses Tauri v2 for its UI (WebKitGTK for rendering) and a native Rust backend for STT processing. On Wayland, it requires compositor-level global shortcuts. + +**Live settings snapshot (blankpad):** + +| Setting | Value | +|---------|-------| +| `paste_method` | `none` | +| `copy_to_clipboard` | `true` | +| `push_to_talk` | `true` | +| `bindings` | transcribe=`ctrl+space`, transcribe_with_post_process=`ctrl+shift+space`, cancel=`escape` | +| `keyboard_implementation` | `handy_keys` | +| `overlay` | `bottom` (live), `true` (config) | +| `model` | `handy-computer/canary-180m-flash-gguf` (Q8_0) | +| `transcribe_gpu` | `vulkan` (PCI 07:00.0) | +| `autostart_enabled` | `false` (niri-managed) | +| `experimental` | `true` | +| `filler_word_removal` | `true` | +| `post_process_enabled` | `true` (fleet-wired) | +| `clipboard_handling` | `copy_to_clipboard` | +| `provider` | `custom` (base_url=`http://192.168.0.143:7771/v1`, model=`frank-secondary`) | +| `prompt` | `default_improve_transcriptions` | + +--- + +## Configuration + +### Config Location + +**Upstream docs:** `~/.config/com.pais.handy/` +**Live observed:** `~/.local/share/com.pais.handy/settings_store.json` + +Both locations may contain settings; the live data directory is the authoritative runtime store. + +### Live Settings (blankpad) + +The store is nested: everything lives under a top-level `settings` object. +Post-processing wiring (applied 2026-10-09) touches five keys plus the +`custom` entry inside `post_process_providers`: + +```json +{ "settings": { + "post_process_enabled": true, + "post_process_provider_id": "custom", + "post_process_models": { "custom": "frank-secondary", "openai": "", "...": "" }, + "post_process_selected_prompt_id": "default_improve_transcriptions", + "post_process_providers": [ + { "id": "custom", "base_url": "http://192.168.0.143:7771/v1", + "allow_base_url_edit": true, "models_endpoint": "/models", "...": "" } + ], + "post_process_api_keys": { "custom": "", "...": "" }, + "autostart_enabled": false, + "push_to_talk": true, + "paste_method": "none", + "clipboard_handling": "copy_to_clipboard", + "keyboard_implementation": "handy_keys", + "overlay_position": "bottom", + "overlay_style": "live" +} } +``` + +Repo-side merge patch + apply procedure: +`dotfiles/zz-blankpad/.local/share/com.pais.handy/` (README.md). + +--- + +## Command-Line Interface + +### Startup Flags + +```bash +handy --start-hidden # Start without showing the UI +handy --no-tray # Hide the system tray icon +handy --debug # Enable debug mode (Ctrl+Shift+D) +``` + +### Remote Control + +Send commands to a running instance: + +```bash +handy --toggle-transcription # Toggle transcription on/off +handy --toggle-post-process # Toggle post-processing on/off +handy --cancel # Cancel the current transcription +``` + +--- + +## Wayland Integration + +### Global Shortcuts + +On Wayland, global shortcuts **must** be defined in the compositor (niri), not in Handy itself. + +**File:** `~/.config/niri/cfg/keybinds.kdl` + +```kdl +// Live (cfg/keybinds.kdl): +Mod+Shift+Space hotkey-overlay-title="Handy: toggle post-process recording" allow-when-locked=true { spawn-sh "handy --toggle-post-process"; } +// Transcription itself stays on Handy's own ctrl+space (keyboard_implementation="handy_keys"). +``` + +### Text Input + +**Requirement:** `wtype` or `dotool` for text input (NOT installed on blankpad). +**Current config:** `paste_method=none` + manual `Ctrl+V` paste. + +### Overlay + +**Default:** Disabled on Linux (can steal focus and break paste). +**Live config:** `mode=bottom`, `live=true`. + +--- + +## Troubleshooting + +### WebKit Renderer Issue + +**Symptom:** Blank or corrupted rendering. + +**Fix:** + +```bash +export WEBKIT_DISABLE_DMABUF_RENDERER=1 +handy +``` + +### SIGUSR1 Crash + +**Trap:** The upstream SIGUSR1 handler was **REMOVED** (WebKitGTK now owns it). + +```bash +# WRONG (may crash the app): +pkill -USR1 handy + +# CORRECT (toggle transcription): +pkill -USR2 -n handy +# or use CLI: +handy --toggle-transcription +``` + +### Debug Mode + +```bash +handy --debug +# or press Ctrl+Shift+D in a running instance +``` + +--- + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `handy` | Main application | +| `tauri-apps` | Tauri v2 runtime | +| `webkit2gtk` | WebKitGTK rendering | +| `gtk-layer-shell` | Wayland layer-shell support | +| `vulkan` | GPU-accelerated STT | +| `ffmpeg` | Audio processing | + +--- + +## Fleet LLM Endpoint + +**Post-processing pipeline:** `handy` → `http://192.168.0.143:7771/v1`, model id +`frank-secondary` (the served id; harness callsign for the same unit is +`frank-zealot` — same llama.cpp server, also the fleet compaction endpoint). + +**Endpoint configuration:** +- **Provider:** `custom` (the one preset with `allow_base_url_edit: true`) +- **Base URL:** `http://192.168.0.143:7771/v1` +- **Model:** `frank-secondary` +- **Prompt:** `default_improve_transcriptions` + +### Known issues + +**Thinking-model empty content (2026-10-09, open).** `frank-secondary` is a +thinking model. Default chat completions return `content: ""` with the answer +trapped in `reasoning_content` — Handy reads `content`, so post-processed +transcripts arrive empty. Verified per-request cure: +`"chat_template_kwargs": {"enable_thinking": false}` (probe below returns +cleaned text in `content`). Handy has no field to send extra body keys, and +the cure CANNOT be set server-global on :7771 without degrading fleet +compaction (same server). Proposed fix (not yet implemented): a dedicated +no-think unit/port for Handy traffic, e.g. llama-server on `:7772` with +thinking disabled at the template level, then repoint `custom.base_url`. + +```bash +curl -s http://192.168.0.143:7771/v1/chat/completions -d '{ + "model": "frank-secondary", + "messages": [{"role":"user","content":"Clean this transcript: twenty five items cost ten dollars um ok."}], + "max_tokens": 200, + "chat_template_kwargs": {"enable_thinking": false}}' +``` + +See [[noctalia-shell]] for IPC integration patterns. + +--- + +## See Also + +- [[noctalia-shell|noctalia-shell.md]] — Desktop environment integration +- [[niri|niri/index.md]] — Compositor configuration +- [`handy --help`](https://github.com/cjpais/Handy) — CLI reference diff --git a/omp-docs/tools/index.md b/omp-docs/tools/index.md index b954fc1..fee4d75 100644 --- a/omp-docs/tools/index.md +++ b/omp-docs/tools/index.md @@ -13,7 +13,8 @@ Configuration guides and reference material for installed software. Each tool ha * ](gitea/index.md) — Private Git hosting on dankclaw. Quadlet-managed containers: Gitea, PostgreSQL, Caddy. * ](llama-cpp/index.md) — Local LLM inference server supporting GGUF models with OpenAI-compatible API. * [[linux-power-management|linux-power-management]] — Sleep/wake troubleshooting, NVIDIA GPU power management, and diagnostics for Linux systems. -* [[noctalia-shell|noctalia-shell]] — Desktop shell for Wayland built with Quickshell — launcher, wallpaper, session menu, lock screen, hotkey overlay, and media controls. +* [[noctalia-shell|noctalia-shell]] — Native desktop environment for Niri — launcher, control center, settings, lock screen, session menu, hotkey overlay, media controls, and system management. +* [handy](handy.md) — Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI. * [niri](niri/index.md) — Scrollable-tiling Wayland compositor — configuration, keybindings, and architecture for the workstation. * [npx Skills](npx-skills/index.md) — Distribution and installation tips for pi skill packages on npm, ecosystem overview, and skill specification. * [ornith-llama-cpp-setup](ornith-llama-cpp-setup/index.md) — Setup and reference for running Ornith MoE models with llama.cpp server for local inference. @@ -34,6 +35,8 @@ Configuration guides and reference material for installed software. Each tool ha * [Linux Power Management](linux-power-management.md) — Sleep/wake troubleshooting, NVIDIA GPU power management, and diagnostics for Linux systems. -* [Noctalia Shell](noctalia-shell.md) — Desktop shell for Wayland built with Quickshell — launcher, wallpaper, session menu, lock screen, hotkey overlay, and media controls. +* [Noctalia Shell](noctalia-shell.md) — Native desktop environment for Niri — launcher, control center, settings, lock screen, session menu, hotkey overlay, media controls, and system management. + +* [Handy](handy.md) — Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI. * [tmux](tmux.md) — Terminal multiplexer configuration, extended-keys setup, keyboard input best practices, and reload/restart procedures. diff --git a/omp-docs/tools/niri/action-reference.md b/omp-docs/tools/niri/action-reference.md index 13b68aa..22426b4 100644 --- a/omp-docs/tools/niri/action-reference.md +++ b/omp-docs/tools/niri/action-reference.md @@ -1,16 +1,18 @@ --- type: reference title: Niri Action Reference -description: Complete reference of all available niri actions — window operations, focus, movement, spawning, and system actions. +description: Curated reference of niri actions — window operations, focus, movement, spawning, and system actions. For complete command-line options, see `niri msg action --help`. tags: - niri - actions - reference -verified: 2026-08-28 +verified: 2026-10-09 --- # Action Reference +> **Note:** This is a curated subset. For complete options and parameters, run `niri msg action --help` (authoritative, live). + ## Window/Column Actions - `close-window` — Close focused window @@ -55,15 +57,49 @@ verified: 2026-08-28 - `spawn "program"` — Run program (no shell expansion) - `spawn-sh "command"` — Run command through shell - `spawn-at-startup "program"` — Run at startup -- `quit` — Quit compositor (with confirmation) +- `quit` — Quit compositor (with confirmation, `--skip-confirmation` available) - `show-hotkey-overlay` — Show keybind cheat sheet - `toggle-overview` — Toggle overview - `screenshot` — Interactive screenshot -- `screenshot-screen` — Screenshot monitor -- `screenshot-window` — Screenshot window +- `screenshot-screen` — Screenshot monitor (`--write-to-disk`, `--show-pointer` options) +- `screenshot-window` — Screenshot window (`--write-to-disk`, `--show-pointer` options) - `power-off-monitors` — Turn off displays - `toggle-keyboard-shortcuts-inhibit` — Toggle inhibitor -- `do-screen-transition` — Smooth screen transition (for theme switching) +- `do-screen-transition` — Smooth screen transition (for theme switching, `--delay-ms` option) +- `stop-cast --session-id ` — Stop a PipeWire screencast +- `toggle-window-rule-opacity` — Toggle opacity of focused window +- `focus-column-or-monitor-left/right` — Focus column or monitor horizontally +- `focus-column-or-monitor-up/down` — Focus column or monitor vertically +- `focus-window-or-monitor-up/down` — Focus window or monitor vertically +- `focus-window-or-monitor-down/up` — Focus window or monitor vertically +- `focus-column-or-monitor-left/right` — Focus column or monitor horizontally +- `focus-window-or-monitor-down/up` — Focus window or monitor vertically +- `focus-window-or-workspace-down/up` — Focus window or workspace vertically +- `focus-window-or-workspace-up/down` — Focus window or workspace vertically + +## Bind Properties + +Actions and bindings support optional properties: + +| Property | Description | +|----------|-------------| +| `repeat=false` | Disable repeat for the binding | +| `cooldown-ms=N` | Rate-limit to N milliseconds (commonly 150 for wheel binds) | +| `allow-when-locked=true` | Execute even when session is locked | +| `allow-inhibiting=false` | Process even when inhibitor is active | +| `hotkey-overlay-title="..."` | Custom title for overlay (supports `=null` to hide or Pango markup) | + +### Mod+Mouse* Binds (25.01+) + +Since 25.01, `Mod+Mouse*` binds are supported for advanced navigation. + +### TouchpadScroll Binds + +`Mod+TouchpadScroll*` binds are available for touchpad-based navigation. + +### Mouse-Click Binds + +Mouse-click actions operate on the **focused window**, not the clicked window. ## Related diff --git a/omp-docs/tools/niri/default-bindings.md b/omp-docs/tools/niri/default-bindings.md index 7aee934..85105b9 100644 --- a/omp-docs/tools/niri/default-bindings.md +++ b/omp-docs/tools/niri/default-bindings.md @@ -6,7 +6,7 @@ tags: - niri - keybindings - reference - - unverified +verified: 2026-10-09 --- # Default Key Bindings @@ -61,7 +61,7 @@ tags: | Key | Action | |-----|--------| -| `Mod+R` | Switch preset column width (1/3, 1/2, 2/3) | +| `Mod+R` | Switch preset column width (1/3, 1/2, 2/3, 87.5%) | | `Mod+Shift+R` | Switch preset width (reverse) | | `Mod+Minus` | Decrease column width by 10% | | `Mod+Equal` | Increase column width by 10% | @@ -129,12 +129,12 @@ tags: | `Mod+WheelScrollLeft` | Focus column left | | `Mod+Ctrl+WheelScrollDown` | Move column to workspace down | | `Mod+Ctrl+WheelScrollUp` | Move column to workspace up | -| `Mod+Ctrl+WheelScrollRight` | Move column right | -| `Mod+Ctrl+WheelScrollLeft` | Move column left | +| `Mod+Ctrl+WheelScrollRight` | Move column to workspace right | +| `Mod+Ctrl+WheelScrollLeft` | Move column to workspace left | | `Mod+Shift+WheelScrollDown` | Focus column right | | `Mod+Shift+WheelScrollUp` | Focus column left | -| `Mod+Ctrl+Shift+WheelScrollDown` | Move column right | -| `Mod+Ctrl+Shift+WheelScrollUp` | Move column left | +| `Mod+Ctrl+Shift+WheelScrollDown` | Move column to workspace right | +| `Mod+Ctrl+Shift+WheelScrollUp` | Move column to workspace left | ## Overview diff --git a/omp-docs/tools/niri/displays.md b/omp-docs/tools/niri/displays.md index 69a44fa..e686f5b 100644 --- a/omp-docs/tools/niri/displays.md +++ b/omp-docs/tools/niri/displays.md @@ -1,126 +1,135 @@ --- type: reference title: Display Configuration -description: Niri output configuration for a multi-monitor desk rig — portrait transforms, position pinning, scale, and iGPU display ownership. +description: Niri output configuration on blankpad — EDID-identity-keyed desk profiles, hotplug auto-switching, portrait transforms, position pinning, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). tags: - niri - display - monitors - rotation -verified: 2026-08-28 + - desk-profiles +verified: 2026-10-09 --- # Display Configuration -How a multi-monitor desk rig is configured in niri: portrait external panels -flanking a laptop panel, positions pinned by hand, and the integrated GPU kept -as the display owner. +Niri output config on **blankpad** is a **desk-profile system**: the laptop +travels between desks, connector names (`DP-2`, `HDMI-A-1`) reshuffle per +dock, so panels are configured by **EDID identity** (manufacturer model +serial), and *positions* live in per-desk files. A hotplug watcher and a +manual picker keep the live layout matched to reality. -**Config:** `~/.config/niri/cfg/display.kdl` -**Probe:** `wlr-randr` (installed by CachyOS) reports what the compositor -actually committed — connectors, preferred modes, transforms, scale, positions. +**Mechanism:** `~/.config/niri/cfg/display.kdl` is a **symlink** to the +active profile (`desk-1-home.kdl`, `desk-2-portable.kdl`, …). +`config.kdl` includes it with `include optional=true` so a missing link is a +warning, not a parse failure. +**Probe:** `niri msg outputs` is authoritative (connector, full EDID long +name, modes, transform, scale, position, VRR). `wlr-randr` remains available. --- -## The rig - -Three outputs in a row: two external monitors rotated to portrait, one -internal laptop panel in landscape at the right. +## The desk-1 rig (home) ``` -┌──────────────┬──────────────┬──────────────┐ -│ │ │ │ -│ DP-1 │ DP-3 │ eDP-1 │ -│ external │ external │ internal │ -│ 1920x1200 │ 1920x1200 │ 2560x1600 │ -│ Portrait │ Portrait │ Landscape │ -│ 90° rotate │ 270° rotate │ 1.5x scale │ -│ │ │ │ -└──────────────┴──────────────┴──────────────┘ - x=0,0 x=1200,0 auto-placed +┌──────────────┬──────────────────────────────┬──────────┐ +│ PM1601 │ Sceptre M34 ultrawide │ eDP-1 │ +│ portrait │ 3440x1440 @ 239.999 │ 1920x1080│ +│ 1200x1920 │ landscape, scale 1 │ scale 1 │ +│ rot 90 │ position 1581,-600 │ 5021,-240│ +└──────────────┴──────────────────────────────┴──────────┘ + HDMI-A-1 DP-2 (focus-at-startup) eDP-1 (fallback focus) ``` -| Output | Role | Transform | Position | Scale | -|--------|------|-----------|----------|-------| -| `DP-1` | External, left | `90` (portrait) | `x=0 y=0` | 1.0 | -| `DP-3` | External, center | `270` (portrait) | `x=1200 y=0` | 1.0 | -| `eDP-1` | Internal panel | none (landscape) | auto-placed | 1.5 | +| Panel | EDID long name | Keyed by | Notes | +|-------|----------------|----------|-------| +| Sceptre M34 ultrawide | `Sceptre Tech Inc Sceptre M34 Unknown` | identity | `mode "3440x1440@239.999"`, `focus-at-startup` | +| PM1601 portrait | *not captured* | connector `HDMI-A-1` | dock-dependent; run `desk.sh capture` when plugged in, then re-key | +| eDP-1 laptop panel | `Lenovo Group Limited 0x40A9 Unknown` | identity | scale **1.0** (deliberate; Ghostty grid at 1.25 rejected 2026-10-09), second `focus-at-startup` | -The two portrait panels are 1200 px wide after rotation, which is why the -second one starts at `x=1200`. The internal panel declares no position at -all — niri auto-places it to the right of the explicitly positioned outputs. -That is the only reliable way to keep a third panel from landing on top of -the rig when a cable is re-seated. +VRR: DP-2 supports it; **left disabled by decision** (2026-10-09). Add +`variable-refresh-rate` per output if that ever changes. ---- +`focus-at-startup` on both M34 and eDP-1: niri focuses in config order, so +the ultrawide wins when present and the laptop panel is the fallback. + +## Desk switching + +```mermaid +graph LR + P[plug / unplug] --> E[niri event-stream
WorkspacesChanged → output-set diff] + E --> M[desk-autoswitch.sh
debounced matcher] + M --> R{profile requires
subset of connected?} + R -->|best match| A[desk.sh apply:
validate - relink - reload - notify] + R -->|none| K[keep current + notify] + D[Mod+Shift+D] --> C[desk.sh cycle] --> A +``` + +- **Matcher:** each profile declares `// requires: ` (all must be + connected) and `// optional:` comment lines; the most-specific eligible + profile wins. +- **Apply order is load-config-safe:** `niri validate` the *current* config + first, relink `display.kdl`, then `niri msg action load-config-file`, + then a noctalia notification names the active desk. A profile that fails + validation can never go live. +- **Manual override:** `Mod+Shift+D` cycles profiles (wrong auto-pick, or + same panels, new layout). +- **New desk:** `desk.sh capture` prints connected long names → copy into a + new `desk-N-.kdl` (see `desk-2-portable.kdl` skeleton, portable + monitor EDID pending first capture). +- Unit: `blankpad-desk-autoswitch.service` (systemd user, + `ConditionHost=blankpad`). ## The config pattern ```kdl -output "DP-1" { - transform "90" - position x=0 y=0 -} - -output "DP-3" { - transform "270" - position x=1200 y=0 -} - -output "eDP-1" { - // no position — auto-placed to the right of the DP-* outputs - scale 1.5 +// desk-1-home.kdl — one block per panel, keyed by identity +output "Sceptre Tech Inc Sceptre M34 Unknown" { + mode "3440x1440@239.999" + scale 1 + transform "normal" + position x=1581 y=-600 + focus-at-startup } ``` -Three rules learned the hard way: +Four rules learned the hard way: -1. **One output, one block.** A second block keyed by the make/model string - (`output "Samsung Electric Company …"`) that also sets `transform` fights - the connector-keyed block. Use the make/model form only to pin a `mode` - for an output whose connector name is unstable across boots, and never - set a transform in both places. -2. **Positions are absolute, not relational.** Niri does not reflow the rig - when one panel is disconnected — a missing output leaves a hole at its - coordinates. Verify with `wlr-randr` after any cable or dock change. -3. **Re-assert rotation after a config reload, then confirm.** A transform - silently reverting to landscape is the usual symptom of a duplicate - output block, not of a failed reload. - -Modes must match what the panel reports. Check `wlr-randr` for the preferred -mode before writing a `mode` line; a `mode` the panel cannot drive leaves the -output dark or drops it. - ---- +1. **One output, one block.** Two blocks matching the same output (connector + name *and* long name) fight over properties — transform reverts are the + classic symptom. Desk profiles are self-contained: a panel's identity, + mode, transform, **and** position live in exactly one block per file. +2. **Key by EDID identity, not connector.** Connector names swap between + docks; the EDID long name travels with the panel. Serials on these panels + read `Unknown`, so the full long name (manufacturer model serial) is the + match string — copy it verbatim from `niri msg outputs`. +3. **Positions are absolute, not relational.** A disconnected panel leaves + a hole; that is exactly why positions belong to the *desk profile*, not a + global file. +4. **Modes must match what the panel reports** (`niri msg outputs` → + `Mode: current …`). A mode the panel cannot drive leaves the output dark. ## GPU assignment -The integrated GPU drives all display output; the discrete GPU is available -for compute but must not take the outputs. Pin the render node explicitly in -`~/.config/niri/cfg/misc.kdl`: +blankpad is a **single AMD Cezanne iGPU** (`/dev/dri/renderD128`, PCI +`07:00.0`). No render-node pin exists in `misc.kdl` — and must not: the +blankpad override file deliberately deletes the pin. -```kdl -environment { - render-node "/dev/dri/renderD129" -} -``` - -Without the pin, the session's render node follows probe order and can land -on the discrete GPU — which costs display-linked copies and, on hybrid -graphics laptops, is a common trigger of the suspend/resume deadlock covered -in the sleep-wake investigation report. - ---- +The `environment { render-node "/dev/dri/renderD129" }` pin found in the +*base* `misc.kdl` is **dankware-legacy** (Intel Arc iGPU + discrete GPU +hybrid; pin kept the iGPU as display owner and sidesteps the suspend/resume +deadlock documented in the sleep-wake investigation). Deploying that base +file on blankpad pins a render node that does not exist — this is why +blankpad runs the `zz-blankpad` override layer (see [[index|the per-host +symlink matrix]]). ## Reload `niri msg action load-config-file` reloads live. Never `SIGHUP` the process — see [[tips]]. ---- - ## See Also - [Niri](/tools/niri/index.md) — compositor overview and config layout -- [[tips|tips]] — reload path and other gotchas +- [[tips|tips]] — reload path, `niri msg layers/casts`, includes +- [[noctalia-shell|noctalia-shell]] — notification shown on desk switch - [[cachyos|cachyos]] — operating system, including `wlr-randr` diff --git a/omp-docs/tools/niri/index.md b/omp-docs/tools/niri/index.md index 0f683db..3a208fb 100644 --- a/omp-docs/tools/niri/index.md +++ b/omp-docs/tools/niri/index.md @@ -12,15 +12,19 @@ Niri is a scrollable-tiling Wayland compositor. Windows are arranged in columns ``` ~/.config/niri/ ├── config.kdl # Main entry, includes all modules +├── noctalia.kdl # Generated by noctalia (theme colors) └── cfg/ ├── animation.kdl # Spring and curve animations - ├── autostart.kdl # Startup applications - ├── display.kdl # Output configuration + ├── autostart.kdl # Startup applications (noctalia, handy --start-hidden) + ├── display.kdl # SYMLINK to the active desk-*.kdl profile (see displays) + ├── desk-1-home.kdl # Desk profile: home rig (EDID-keyed) + ├── desk-2-portable.kdl # Desk profile: portable monitor (skeleton) ├── input.kdl # Keyboard, touchpad, mouse ├── keybinds.kdl # All key bindings ├── layout.kdl # Gaps, column widths, struts ├── rules.kdl # Window and layer rules - └── misc.kdl # Environment, cursor, debug + ├── switch-events.kdl # Lid-close -> noctalia lock-and-suspend (blankpad) + └── misc.kdl # Environment, cursor, blur ``` ## Layout @@ -32,7 +36,7 @@ Column-based scrollable tiling. Columns are cycled with Mod+Left/Right (or Mod+H | **Gaps** | 16px | | **Center focused column** | Never | | **Background** | Transparent (for Noctalia wallpaper) | -| **Column presets** | 33%, 50%, 67% | +| **Column presets** | 12.5%, 25%, 33%, 50%, 67%, 75%, 87.5% (Mod+R / Mod+Shift+R) | ## Input @@ -42,7 +46,6 @@ Column-based scrollable tiling. Columns are cycled with Mod+Left/Right (or Mod+H | **Workspace back-and-forth** | Enabled | | **Numlock** | Enabled on startup | | **Touchpad tap** | Enabled | -| **Natural scroll** | Enabled | ## Window Rules @@ -52,6 +55,8 @@ Column-based scrollable tiling. Columns are cycled with Mod+Left/Right (or Mod+H | Steam (non-window) | `app-id="steam"`, exclude title `Steam` | Open floating | | Steam notifications | `app-id="steam"`, title `notificationtoasts_*` | Floating, bottom-right | | Noctalia wallpaper | namespace `noctalia-wallpaper*` | Place in backdrop | +| Noctalia settings | `app-id="dev.noctalia.Noctalia"` | Floating 1080x920 | +| Noctalia layers | namespace `^noctalia-(bar|notification|dock|panel|attached-panel|osd)` | blur xray=false | ## Environment Variables @@ -63,17 +68,16 @@ Column-based scrollable tiling. Columns are cycled with Mod+Left/Right (or Mod+H | `QT_WAYLAND_DISABLE_WINDOWDECORATION` | `1` | Remove Qt CSD | | `XDG_CURRENT_DESKTOP` | `niri` | Desktop identification | | `XDG_SESSION_TYPE` | `wayland` | Session type | -| `render-node` | `/dev/dri/renderD129` | Intel Arc iGPU | ## Animations | Animation | Type | Parameters | |-----------|------|------------| -| Workspace switch | Spring | damping=1.0, stiffness=1000 | +| Workspace switch | Spring | damping-ratio=1.0 stiffness=1000 | | Window open | Duration | 200ms, ease-out-quad | | Window close | Duration | 200ms, ease-out-cubic | -| Window movement | Spring | damping=1.0, stiffness=800 | -| Window resize | Spring | damping=1.0, stiffness=1000 | +| Window movement | Spring | damping-ratio=1.0 stiffness=800 | +| Window resize | Spring | damping-ratio=1.0 stiffness=1000 | --- @@ -81,11 +85,12 @@ Column-based scrollable tiling. Columns are cycled with Mod+Left/Right (or Mod+H | File | Type | Description | |------|------|-------------| -| [[action-reference|action-reference.md]] | reference | Complete reference of all available niri actions | +| [[action-reference|action-reference.md]] | reference | Curated reference of niri actions | | [[default-bindings|default-bindings.md]] | reference | Default key bindings organized by category | | [[key-bindings-syntax|key-bindings-syntax.md]] | reference | KDL configuration syntax for key bindings | | [[tips|tips.md]] | howto | Practical tips for working with niri | -| [[displays|displays.md]] | reference | Multi-monitor display rig: portrait transforms, positions, scale, iGPU display ownership | +| [[displays|displays.md]] | reference | Desk-profile system: EDID-keyed outputs, hotplug auto-switch, portrait transforms, GPU notes | +| [[keybindings|keybindings.md]] | reference | GENERATED keybinding cheat sheet (scripts/gen-keybinds.sh) | --- diff --git a/omp-docs/tools/niri/keybindings.md b/omp-docs/tools/niri/keybindings.md new file mode 100644 index 0000000..046bbe0 --- /dev/null +++ b/omp-docs/tools/niri/keybindings.md @@ -0,0 +1,183 @@ +# Niri Keybindings + +> GENERATED by `scripts/gen-keybinds.sh` from `keybinds.kdl` — do not edit by hand. + +## Keybindings + +| Key | Action | +|-----|--------| + +## noctalia-shell keybinds + +| Key | Action | +|-----|--------| +| `Mod+Shift+ESCAPE` | show-hotkey-overlay | + +## Applications + +| Key | Action | +|-----|--------| +| `Mod+Return` | Open Terminal: Ghostty | +| `Mod+B` | Open Browser: firefox | +| `Mod+Shift+Return` | Open Wallpaper Selector: noctalia wallpaper toggle | +| `Mod+S` | Open Control Center: noctalia controlCenter | +| `Mod+Shift+S` | Open Settings: noctalia settings | +| `Mod+D` | Open App Launcher: noctalia launcher | +| `Mod+ALT+L` | Lock Screen: noctalia lock | +| `Mod+Shift+Q` | Session Menu: noctalia sessionMenu | +| `Mod+V` | Clipboard History: noctalia panel | +| `Alt+TAB` | Window Switcher: noctalia | +| `Mod+Shift+Space` | Handy: toggle post-process recording | +| `Mod+Shift+D` | Cycle Desk Profile | +| `Mod+E` | File Manager: Nautilus | + +## Media Controls + +| Key | Action | +|-----|--------| +| `XF86AudioRaiseVolume` | spawn-sh "noctalia msg volume-up" | +| `XF86AudioLowerVolume` | spawn-sh "noctalia msg volume-down" | +| `XF86AudioMute` | spawn-sh "noctalia msg volume-mute" | +| `XF86AudioMicMute` | spawn-sh "noctalia msg mic-mute" | +| `XF86AudioNext` | spawn-sh "noctalia msg media next" | +| `XF86AudioPrev` | spawn-sh "noctalia msg media previous" | +| `XF86AudioPlay` | spawn-sh "noctalia msg media play" | +| `XF86AudioPause` | spawn-sh "noctalia msg media stop" | + +## Brightness Controls + +| Key | Action | +|-----|--------| +| `XF86MonBrightnessUp` | spawn-sh "noctalia msg brightness-up" | +| `XF86MonBrightnessDown` | spawn-sh "noctalia msg brightness-down" | + +## Window Movement and Focus + +| Key | Action | +|-----|--------| +| `Mod+Q` | close-window | +| `Mod+Left` | focus-column-left | +| `Mod+H` | focus-column-left | +| `Mod+Right` | focus-column-right | +| `Mod+L` | focus-column-right | +| `Mod+Up` | focus-window-up | +| `Mod+K` | focus-window-up | +| `Mod+Down` | focus-window-down | +| `Mod+J` | focus-window-down | +| `Mod+CTRL+Left` | swap-window-left | +| `Mod+CTRL+H` | swap-window-left | +| `Mod+CTRL+Right` | swap-window-right | +| `Mod+CTRL+L` | swap-window-right | +| `Mod+CTRL+Alt+Left` | move-column-left | +| `Mod+CTRL+Alt+H` | move-column-left | +| `Mod+CTRL+Alt+Right` | move-column-right | +| `Mod+CTRL+Alt+L` | move-column-right | +| `Mod+CTRL+UP` | move-window-up | +| `Mod+CTRL+K` | move-window-up | +| `Mod+CTRL+Down` | move-window-down | +| `Mod+CTRL+J` | move-window-down | +| `Mod+Home` | focus-column-first | +| `Mod+End` | focus-column-last | +| `Mod+CTRL+Home` | move-column-to-first | +| `Mod+CTRL+End` | move-column-to-last | +| `Mod+Shift+Left` | focus-monitor-left | +| `Mod+Shift+Right` | focus-monitor-right | +| `Mod+Shift+UP` | focus-monitor-up | +| `Mod+Shift+Down` | focus-monitor-down | +| `Mod+Shift+CTRL+Left` | move-column-to-monitor-left | +| `Mod+Shift+CTRL+Right` | move-column-to-monitor-right | +| `Mod+Shift+CTRL+UP` | move-column-to-monitor-up | +| `Mod+Shift+CTRL+Down` | move-column-to-monitor-down | +| `Mod+Shift+V` | switch-focus-between-floating-and-tiling | +| `Mod+BracketLeft` | consume-or-expel-window-left | +| `Mod+BracketRight` | consume-or-expel-window-right | +| `Mod+Comma` | consume-window-into-column | +| `Mod+Period` | expel-window-from-column | +| `Mod+R` | switch-preset-column-width | +| `Mod+Shift+R` | switch-preset-column-width-back | +| `Mod+Ctrl+Shift+R` | switch-preset-window-height | +| `Mod+Ctrl+R` | reset-window-height | + +## Workspace Switching + +| Key | Action | +|-----|--------| +| `Mod+WheelScrollDown` | focus-workspace-down | +| `Mod+WheelScrollUp` | focus-workspace-up | +| `Mod+CTRL+WheelScrollDown` | move-column-to-workspace-down | +| `Mod+CTRL+WheelScrollUp` | move-column-to-workspace-up | +| `Mod+WheelScrollRight` | focus-column-right | +| `Mod+WheelScrollLeft` | focus-column-left | +| `Mod+CTRL+WheelScrollRight` | move-column-right | +| `Mod+CTRL+WheelScrollLeft` | move-column-left | +| `Mod+Shift+WheelScrollDown` | focus-column-right | +| `Mod+Shift+WheelScrollUp` | focus-column-left | +| `Mod+CTRL+Shift+WheelScrollDown` | move-column-right | +| `Mod+CTRL+Shift+WheelScrollUp` | move-column-left | +| `Mod+1` | focus-workspace 1 | +| `Mod+2` | focus-workspace 2 | +| `Mod+3` | focus-workspace 3 | +| `Mod+4` | focus-workspace 4 | +| `Mod+5` | focus-workspace 5 | +| `Mod+6` | focus-workspace 6 | +| `Mod+7` | focus-workspace 7 | +| `Mod+8` | focus-workspace 8 | +| `Mod+9` | focus-workspace 9 | +| `Mod+CTRL+1` | move-column-to-workspace 1 | +| `Mod+CTRL+2` | move-column-to-workspace 2 | +| `Mod+CTRL+3` | move-column-to-workspace 3 | +| `Mod+CTRL+4` | move-column-to-workspace 4 | +| `Mod+CTRL+5` | move-column-to-workspace 5 | +| `Mod+CTRL+6` | move-column-to-workspace 6 | +| `Mod+CTRL+7` | move-column-to-workspace 7 | +| `Mod+CTRL+8` | move-column-to-workspace 8 | +| `Mod+CTRL+9` | move-column-to-workspace 9 | +| `Mod+TAB` | focus-workspace-previous | + +## Layout Controls + +| Key | Action | +|-----|--------| +| `Mod+CTRL+F` | expand-column-to-available-width | +| `Mod+C` | center-column | +| `Mod+CTRL+C` | center-visible-columns | +| `Mod+Minus` | set-column-width "-10%" | +| `Mod+Equal` | set-column-width "+10%" | +| `Mod+Shift+Minus` | set-window-height "-10%" | +| `Mod+Shift+Equal` | set-window-height "+10%" | + +## Modes + +| Key | Action | +|-----|--------| +| `Mod+T` | toggle-window-floating | +| `Mod+F` | maximize-column | +| `Mod+Shift+F` | fullscreen-window | +| `Mod+W` | toggle-column-tabbed-display | +| `Mod+M` | maximize-window-to-edges | + +## Screenshots + +| Key | Action | +|-----|--------| +| `CTRL+Shift+1` | Screenshot: flameshot full | +| `CTRL+Shift+2` | Screenshot: flameshot screen | +| `CTRL+Shift+3` | Screenshot: flameshot region | +| `Print` | Screenshot: flameshot full | +| `Ctrl+Print` | Screenshot: flameshot region | +| `Alt+Print` | Screenshot: flameshot screen | + +## Emergency Escape Key + +| Key | Action | +|-----|--------| +| `Mod+ESCAPE` | toggle-keyboard-shortcuts-inhibit | + +## Exit / Power + +| Key | Action | +|-----|--------| +| `CTRL+ALT+Delete` | quit | +| `Mod+Shift+P` | power-off-monitors | +| `Mod+O` | toggle-overview | + diff --git a/omp-docs/tools/niri/tips.md b/omp-docs/tools/niri/tips.md index 3a8fcf9..2db7a2c 100644 --- a/omp-docs/tools/niri/tips.md +++ b/omp-docs/tools/niri/tips.md @@ -6,7 +6,7 @@ tags: - niri - tips - gotchas -verified: 2026-08-28 +verified: 2026-10-09 --- # Helpful Tips @@ -30,6 +30,12 @@ Correct reload options, in order of preference: Niri reads `~/.config/niri/config.kdl` (or `config.kdl` relative to its CWD). Split config lives in `~/.config/niri/cfg/` — each `.kdl` file is `include`d from the main config, so a reload picks up edits to any included module. +### 26.04 Config Loading Features + +`niri msg action load-config-file --path ` switches configs at runtime (26.04). It includes: +- `optional=true` support — allows loading configs that may not exist +- `~` expansion — tilde paths are expanded to the niri user's home directory + ## 4. Alternative navigation Vim-like keys (H/J/K/L) are bound by default alongside arrow keys. @@ -38,9 +44,16 @@ Vim-like keys (H/J/K/L) are bound by default alongside arrow keys. Every action can be invoked via `niri msg action `. -## 6. Find XKB keys +### 26.04 IPC Enhancements -Use `wev` to discover key names. +- `niri msg layers` — lists layer-shell namespaces per output +- `niri msg casts` — lists active PipeWire screencasts +- `Escape` cancels drag-and-drop operations +- Duplicate-bind parse errors now show the **first definition** in the file + +### Pointer Behavior + +Pointer warps across the screen during `view-scroll` gestures. ## Related diff --git a/omp-docs/tools/noctalia-shell.md b/omp-docs/tools/noctalia-shell.md index 0f64aa8..9768bad 100644 --- a/omp-docs/tools/noctalia-shell.md +++ b/omp-docs/tools/noctalia-shell.md @@ -1,59 +1,283 @@ --- type: reference title: Noctalia Shell -description: Desktop shell for Wayland built with Quickshell — launcher, wallpaper, session menu, lock screen, hotkey overlay, and media controls. +description: Native desktop environment for Niri — launcher, control center, settings, lock screen, session menu, hotkey overlay, media controls, and system management. +resource: https://github.com/Noctalia/noctalia tags: - noctalia - - quickshell + - niri - wayland - - shell + - desktop-environment - launcher -timestamp: 2026-08-06 + - control-center + - session-management +timestamp: 2026-10-09 +verified: 2026-10-09 --- # Noctalia Shell -Noctalia Shell is a sleek, minimal desktop shell for Wayland built with [Quickshell](https://github.com/outfoxxed/quickshell). It provides an app launcher, wallpaper manager, session menu, lock screen, hotkey overlay, media controls, and brightness/volume controls. +Noctalia is a native, minimal desktop environment for the [Niri](niri/index.md) compositor. It provides an application launcher, control center, settings GUI, lock screen, session menu, hotkey overlay, media controls, brightness/volume controls, wallpaper management, and system-level utilities. -**Version:** 4.7.7 -**Dependency:** noctalia-qs / qs 0.0.12 +**Version:** 5.2.1-1.1 +**Binary:** `/usr/bin/noctalia` (native Qt application, no Quickshell dependency) +**Config:** `~/.config/noctalia/` --- ## Architecture -Noctalia communicates through an IPC system accessed via the `qs` command: +Noctalia is a native Qt binary that communicates with Niri and system services via direct IPC (`noctalia msg`) and shell scripts (`spawn-sh`). It does not depend on Quickshell. + +### IPC Interface + +All primary functionality is exposed via the `noctalia msg` command: ```bash -qs -c noctalia-shell ipc call [params] +noctalia msg ``` -## Components +See `noctalia msg --help` for the complete command list. -| Component | IPC Command | Description | -|-----------|-------------|-------------| -| **Shell** | `qs -c noctalia-shell` | Main shell process | -| **Launcher** | `ipc call launcher toggle` | Application launcher | -| **Lock Screen** | `ipc call lockScreen lock` | Screen locker | -| **Session Menu** | `ipc call sessionMenu toggle` | Power/logout menu | -| **Hotkey Overlay** | `ipc call overlay toggle` | Visual keybind reference | -| **Volume** | `ipc call volume increase/decrease/muteOutput` | Audio controls | -| **Media** | `ipc call media playPause/next/previous` | Media playback | -| **Brightness** | `ipc call brightness increase/decrease` | Display brightness | -| **Wallpaper** | Layer namespace `noctalia-wallpaper*` | Desktop wallpaper | +**Historical Note:** v4.x used Quickshell IPC (`qs -c noctalia-shell ipc call ...`), but v5+ is a native binary with its own IPC interface. + +--- + +## Command Reference + +Commands are grouped by functional area. + +### Shells / Surfaces + +| Command | Description | +|---------|-------------| +| `bar-show [bar-name]` | Show a named bar or all bars | +| `bar-hide [bar-name]` | Hide a named bar or all bars | +| `bar-layer-set ` | Set bar layer (e.g., `top`, `bottom`) | +| `bar-reserve-toggle [bar-name]` | Temporarily toggle reserve space | +| `bar-toggle [bar-name]` | Toggle visibility for a named bar or all bars | +| `dock-hide` | Hide the dock (persists override) | +| `dock-reload` | Reload dock configuration | +| `dock-show` | Show the dock (persists override) | +| `dock-toggle` | Toggle dock visibility (persists override) | +| `panel-close [id]` | Close the active or named panel | +| `panel-open [context]` | Open a panel by id, optionally with context (e.g., `launcher /emo`, `control-center audio`) | +| `panel-toggle [context]` | Toggle a panel by id, optionally with context | +| `settings-close` | Close the settings window | +| `settings-open [context]` | Open the settings window, optionally at a specific section | +| `settings-open-plugin ` | Open settings at a plugin's settings (e.g., `noctalia/notes`) | +| `settings-open-widget [bar-name] [widget-name]` | Open settings at a bar widget | +| `settings-toggle [context]` | Toggle the settings window, optionally at a specific section | +| `window-switcher [action]` | Open or close the window switcher overlay | + +### Lockscreen / Session + +| Command | Description | +|---------|-------------| +| `session lock` | Lock the screen | +| `session lock-and-suspend` | Lock and suspend the system | +| `lockscreen-widgets-edit` | Open the lockscreen widgets editor | +| `lockscreen-widgets-exit` | Close the lockscreen widgets editor | +| `lockscreen-widgets-toggle-edit` | Toggle lockscreen widgets edit mode | + +### Desktop Widgets + +| Command | Description | +|---------|-------------| +| `desktop-widgets-edit` | Open the desktop widgets editor | +| `desktop-widgets-exit` | Close the desktop widgets editor | +| `desktop-widgets-hide` | Hide desktop widgets (runtime only) | +| `desktop-widgets-show` | Show desktop widgets (runtime only) | +| `desktop-widgets-toggle` | Toggle desktop widgets visibility (runtime only) | +| `desktop-widgets-toggle-edit` | Toggle desktop widgets edit mode | + +### Greeter + +| Command | Description | +|---------|-------------| +| `greeter-sync` | Sync wallpaper, colors, and monitor layout to Noctalia Greeter | + +### Media / UI + +| Command | Description | +|---------|-------------| +| `notification-show ` | Show an internal Noctalia notification | +| `notification-clear-active` | Dismiss all currently active notifications | +| `notification-clear-history` | Clear notification history | +| `notification-dnd-set ` | Set notification Do Not Disturb state | +| `notification-dnd-status` | Print notification DND state | +| `notification-dnd-toggle` | Toggle notification DND | +| `notification-invoke-latest` | Invoke the default action of the most recent active notification | +| `clipboard-copy ` | Copy text to the clipboard | +| `clipboard-text` | Print the most recent clipboard text | +| `clipboard-clear` | Clear clipboard history | +| `wallpaper-set [connector] ` | Set wallpaper for all or a specific output (persisted) | +| `wallpaper-next [connector]` | Switch to the next wallpaper immediately | +| `wallpaper-previous [connector]` | Switch to the previous wallpaper immediately | +| `wallpaper-random [connector]` | Switch to a random wallpaper immediately | +| `wallpaper-get [connector]` | Print default wallpaper path, or effective path for an output | +| `theme-mode-set ` | Set theme mode and persist to `settings.toml` | +| `theme-mode-toggle` | Toggle theme mode between dark and light | +| `theme-mode-get` | Print the current resolved theme mode | +| `color-scheme-set <{builtin\|wallpaper\|community\|custom}> ...` | Set palette source and selection in `settings.toml` | +| `color-scheme-get` | Print active color scheme: ` ` | +| `screenshot-region` | Start an interactive region screenshot | +| `screenshot-fullscreen [mode]` | Capture the focused monitor by default, `pick` interactively, or `all` outputs | +| `screenshot-annotate` | Freeze the screen and annotate it, then copy or save | +| `annotate [path]` | Draw on the screen over running apps, or annotate an image file; press F or the Freeze button to capture the background | + +### System Controls + +| Command | Description | +|---------|-------------| +| `volume-up [step]` | Increase speaker volume | +| `volume-down [step]` | Decrease speaker volume | +| `volume-set ` | Set speaker volume | +| `volume-mute` | Toggle speaker mute | +| `volume-osd [value]` | Show the volume OSD without changing volume | +| `mic-mute` | Toggle microphone mute | +| `mic-volume-up [step]` | Increase microphone volume | +| `mic-volume-down [step]` | Decrease microphone volume | +| `mic-volume-set ` | Set microphone volume | +| `mic-volume-osd [value]` | Show the microphone volume OSD | +| `brightness-up [target] [step]` | Increase brightness (defaults to current monitor) | +| `brightness-down [target] [step]` | Decrease brightness (defaults to current monitor) | +| `brightness-set [target] ` | Set brightness (defaults to current monitor) | +| `brightness-osd ` | Show brightness OSD without changing brightness | +| `brightness-list-backlight-devices` | List available sysfs backlight device names | +| `keyboard-backlight-up` | Increase all keyboard backlights by one level | +| `keyboard-backlight-down` | Decrease all keyboard backlights by one level | +| `keyboard-backlight-toggle` | Toggle all keyboard backlights on/off | +| `keyboard-backlight-set ` | Set all keyboard backlights (0-100 percentage) | +| `keyboard-backlight-osd ` | Show keyboard backlight OSD without changing brightness | +| `keyboard-layout-cycle` | Switch to the next keyboard layout | +| `network-toggle` | Disconnect the active network, or reconnect when nothing is connected | +| `wifi-toggle` | Toggle Wi-Fi | +| `wifi-enable` | Enable Wi-Fi | +| `wifi-disable` | Disable Wi-Fi | +| `wifi-status` | Print Wi-Fi state | +| `bluetooth-toggle` | Toggle Bluetooth | +| `bluetooth-enable` | Enable Bluetooth | +| `bluetooth-disable` | Disable Bluetooth | +| `bluetooth-status` | Print Bluetooth state | +| `power-cycle [direction]` | Step through UPower's ordered profile list, forward by default (wraps) | +| `power-set ` | Set the UPower power profile (e.g., `performance`, `balanced`, `power-saver`) | +| `caffeine-toggle` | Toggle caffeine (idle inhibitor) | +| `caffeine-enable` | Enable caffeine (idle inhibitor) | +| `caffeine-disable` | Disable caffeine (idle inhibitor) | +| `dpms-on` | Turn monitors on | +| `dpms-off` | Turn monitors off | +| `effects-profile-set ` | Set the EasyEffects output or input profile | +| `log-level-set ` | Set the console log level | +| `log-level-status` | Print the current console log level | +| `session ` | Run a built-in session action (e.g., `lock`, `lock-and-suspend`) | +| `config-reload` | Reload the config file | +| `status` | Print current state as JSON | +| `taskbar-cycle ` | Step to the adjacent task or workspace group in the invoking taskbar | +| `templates-apply` | Apply configured theme templates for the current palette | +| `osd-toggle` | Toggle OSD popups | +| `osd-enable` | Enable OSD popups | +| `osd-disable` | Disable OSD popups | +| `plugin [payload]` | Dispatch an event to a plugin entry | +| `plugins <{list\|enable\|disable\|update\|source}> ...` | Manage plugins and sources (`list`/`enable`/`disable`/`update`, `source` list/add/remove) | + +--- + +## Configuration Files + +### `~/.config/noctalia/config.toml` + +Main configuration file for runtime and persisted settings. + +**Live config (tracked in repo):** + +```toml +[shell] +polkit_agent = true + +[wallpaper] +directory = "/home/blank/.local/share/noctalia/wallpapers" +``` + +### `~/.local/state/noctalia/settings.toml` + +Settings GUI configuration (bar widgets, theme, wallpaper automation, plugins). +Note the location: **`~/.local/state/`**, not `~/.config/`. + +**Location:** `~/.local/state/noctalia/settings.toml` +**Status:** Tracked in repo — `dotfiles/noctalia/.local/state/noctalia/settings.toml`; +the Settings GUI rewrites the live file, so deploy adopts divergents. +Bar layout lives in `[bar.default]`: `center = [ "taskbar", "cat" ]`, +`end = [ "notifications", "clipboard", "network", "bluetooth", "tray", +"battery", "session", "control-center", "clock" ]`. The taskbar widget lists +open windows of the focused workspace (added 2026-10-09). Apply changes with +`noctalia msg config-reload`. + +### `~/.config/noctalia/palettes/` + +Custom palette folders. + +**Live config (tracked in repo):** +- `~/.config/noctalia/palettes/Gruber Red.json` → `/home/blank/.omp/dotfiles/noctalia/.config/noctalia/palettes/Gruber Red.json` + +**Usage:** +```bash +noctalia msg color-scheme-set custom "Gruber Red" +``` + +Custom palettes are folder-based and immune to the community-palette catalog sync mechanism. + +### `~/.config/niri/noctalia.kdl` + +Niri-specific integration file (generated by Noctalia, untracked in repo). +Contains theme colors (live: `#f43841` red theme), layer rules, and wallpaper backdrop rules. --- ## Niri Integration -Noctalia is deeply integrated with the Niri compositor configuration: +Noctalia is deeply integrated with the Niri compositor configuration. ### Autostart **File:** `~/.config/niri/cfg/autostart.kdl` ```kdl -spawn-sh-at-startup "qs -c noctalia-shell" +spawn-at-startup "noctalia" +``` + +### Window Rules + +**File:** `~/.config/niri/cfg/rules.kdl` + +```kdl +// Geometry and clipping +window-rule { + geometry-corner-radius 20 + clip-to-geometry true +} + +// Floating settings window +window-rule { + match app-id="dev.noctalia.Noctalia" + open-floating true + default-column-width { fixed 1080; } + default-window-height { fixed 920; } +} + +// Wallpaper backdrop +layer-rule { + match namespace="^noctalia-wallpaper" + place-within-backdrop true +} + +// Panels and overlays +layer-rule { + match namespace="^noctalia-(bar-[^\"]+|notification|dock|panel|attached-panel|osd)$" + background-effect { + xray false + } +} ``` ### Keybindings @@ -62,67 +286,150 @@ spawn-sh-at-startup "qs -c noctalia-shell" | Keybind | Action | IPC Command | |---------|--------|-------------| -| `Mod+Return` | Terminal | `spawn "ghostty"` | -| `Mod+CTRL+Return` | App Launcher | `noctalia-shell ipc call launcher toggle` | -| `Mod+B` | Browser | `spawn "firefox"` | -| `Mod+ALT+L` | Lock Screen | `noctalia-shell ipc call lockScreen lock` | -| `Mod+Shift+Q` | Session Menu | `noctalia-shell ipc call sessionMenu toggle` | -| `Mod+Shift+Escape` | Hotkey Overlay | `show-hotkey-overlay` | -| `Mod+E` | File Manager | `spawn "nautilus"` | +| `Mod+Shift+Return` | Wallpaper panel | `noctalia msg panel-toggle wallpaper` | +| `Mod+S` | Control center | `noctalia msg panel-toggle control-center` | +| `Mod+Shift+S` | Settings | `noctalia msg settings-toggle` | +| `Mod+D` | App launcher | `noctalia msg panel-toggle launcher` | +| `Mod+ALT+L` | Lock screen | `noctalia msg session lock` | +| `Mod+Shift+Q` | Session menu | `noctalia msg panel-toggle session` | +| `Mod+V` | Clipboard history panel | `noctalia msg panel-toggle clipboard` | +| `Alt+Tab` | Window switcher (app icons + titles) | `noctalia msg window-switcher` | +| `XF86AudioRaiseVolume` | Volume up | `noctalia msg volume-up` | +| `XF86AudioLowerVolume` | Volume down | `noctalia msg volume-down` | +| `XF86AudioMute` | Mute | `noctalia msg volume-mute` | +| `XF86AudioMicMute` | Mic mute | `noctalia msg mic-mute` | +| `XF86AudioNext` | Next track | `noctalia msg media next` | +| `XF86AudioPrev` | Previous track | `noctalia msg media previous` | +| `XF86AudioPlay` | Play | `noctalia msg media play` | +| `XF86AudioPause` | Pause/stop | `noctalia msg media stop` | +| `XF86MonBrightnessUp` | Brightness up | `noctalia msg brightness-up` | +| `XF86MonBrightnessDown` | Brightness down | `noctalia msg brightness-down` | +| `Mod+Shift+ESCAPE` | Show hotkey overlay | `niri show-hotkey-overlay` | -### Media and Brightness +**Recommended IPC binds** (from docs): +- `Mod+Space` — Launcher +- `Mod+S` — Control center +- `Mod+Comma` — Settings +- `Alt+Tab` — Window switcher (hold) -| Key | Action | IPC Command | -|-----|--------|-------------| -| `XF86AudioRaiseVolume` | Volume Up | `volume increase` | -| `XF86AudioLowerVolume` | Volume Down | `volume decrease` | -| `XF86AudioMute` | Mute | `volume muteOutput` | -| `XF86AudioMicMute` | Mic Mute | `volume muteInput` | -| `XF86AudioPlay` | Play/Pause | `media playPause` | -| `XF86AudioNext` | Next Track | `media next` | -| `XF86AudioPrev` | Previous | `media previous` | -| `XF86MonBrightnessUp` | Brightness Up | `brightness increase` | -| `XF86MonBrightnessDown` | Brightness Down | `brightness decrease` | - -### Wallpaper +### Wallpaper Backdrop The wallpaper layer is placed in the backdrop: +**File:** `~/.config/niri/cfg/rules.kdl` + ```kdl -// In ~/.config/niri/cfg/rules.kdl layer-rule { - match namespace="^noctalia-wallpaper*" + match namespace="^noctalia-wallpaper" place-within-backdrop true } ``` -The background color is set to transparent to allow the wallpaper to show: +### Background Color + +**File:** `~/.config/niri/cfg/layout.kdl` ```kdl -// In ~/.config/niri/cfg/layout.kdl layout { - background-color "transparent" + background-color "transparent" // <- needed for noctalia-shell to set wallpaper } ``` +### Per-Widget Layer Namespaces + +Widgets use namespace-based layer rules: + +```kdl +layer-rule { + match namespace="^noctalia-(bar-[^\"]+|notification|dock|panel|attached-panel|osd)$" + background-effect { + xray false + } +} +``` + +### Laptop / Lid Policy + +**File:** `~/.config/niri/cfg/keybinds.kdl` + +```kdl +Mod+ALT+L hotkey-overlay-title="Lock Screen: noctalia lock" { spawn-sh "noctalia msg session lock"; } +``` + +**File:** `~/.config/niri/cfg/autostart.kdl` + +```kdl +// Lid close policy (optional) +switch-events { + lid-close { + spawn "noctalia" "msg" "session" "lock-and-suspend"; + } +} +``` + +Also requires logind configuration: `HandleLidSwitch=ignore` in `/etc/logind.conf`. + +--- + +## Theme / Palette Workflow + +### Builtin Themes + +```bash +# List available builtin themes +noctalia msg theme-mode-get + +# Set a builtin theme +noctalia msg theme-mode-set dark +noctalia msg theme-mode-set light + +# Toggle between dark and light +noctalia msg theme-mode-toggle +``` + +### Community Palettes + +```bash +# Set a community palette by ID +noctalia msg color-scheme-set community "Palette Name" +``` + +### Custom Palettes + +```bash +# Set a custom palette from a folder +noctalia msg color-scheme-set custom "Gruber Red" +``` + +Custom palettes live in `~/.config/noctalia/palettes/` and are immune to the community-palette catalog sync. + +**Apply templates:** + +```bash +noctalia msg templates-apply +``` + --- ## Dependencies | Package | Purpose | |---------|---------| -| `noctalia-qs` / `qs` | Quickshell runtime | -| `imagemagick` | Image processing | -| `brightnessctl` | Brightness control | -| `ffmpeg` | Media processing | -| `qt6-multimedia` | Qt multimedia | -| `python` | Python runtime | +| `noctalia` (Qt) | Native desktop environment | +| `qt6-base` | Qt6 runtime | +| `qt6-multimedia` | Multimedia support | +| `niri` | Wayland compositor | | `wlr-randr` | Display information | +| `brightnessctl` | Brightness control | +| `imagemagick` | Image processing | +| `ffmpeg` | Media processing | +| `polkit` | PolicyKit integration | --- ## See Also -- [niri](niri/index.md) — Compositor configuration +- [[niri|niri/index.md]] — Compositor configuration - [[displays|displays]] — Display layout - [[cachyos|cachyos]] — Operating system documentation +- [`noctalia msg --help`](https://github.com/Noctalia/noctalia) — Full command reference diff --git a/scripts/deploy.sh b/scripts/deploy.sh index ea6986a..34c2a68 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -107,7 +107,7 @@ for pkg in "${PKGS[@]}"; do # override packages nest base-package dirs internally: strip that segment for targets if [ "${pkgname#zz-}" != "$pkgname" ]; then case "$rel" in */*) [ -d "$PKGS_DIR/${rel%%/*}" ] && rel="${rel#*/}";; esac; fi deploy_file "$f" "$rel" - done < <(find "$pkg" -type f ! -name 'dump.txt' -print0) + done < <(find "$pkg" \( -type f -o -type l \) ! -name 'dump.txt' ! -path '*/.AGENTTRASH/*' -print0) done if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" ]; then diff --git a/scripts/desk-autoswitch.sh b/scripts/desk-autoswitch.sh new file mode 100755 index 0000000..29e48e8 --- /dev/null +++ b/scripts/desk-autoswitch.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# scripts/desk-autoswitch.sh — hotplug daemon: apply the matching desk profile +# when the SET OF OUTPUTS changes. +# +# niri has no dedicated hotplug event (verified on 26.04): +# - WorkspacesChanged carries every workspace's `output`, so the sorted +# unique set of outputs changes exactly on connect/disconnect. +# - OutputConfigChanged is only an ACK that an IPC/config output change was +# Applied (niri-ipc), NOT a hotplug signal. +# +# Usage: desk-autoswitch.sh watch +# +# Environment overrides: +# DESK_AUTO_HOSTNAME only run on this hostname (default: blankpad) +# DESK_SCRIPT path to desk.sh (default ~/.local/bin/desk.sh) +# DESK_NIRI niri binary (default niri) +# DESK_DEBOUNCE_S min seconds between applies (default 1.5) + +set -euo pipefail + +HOST="${DESK_AUTO_HOSTNAME:-blankpad}" +DESK="${DESK_SCRIPT:-$HOME/.local/bin/desk.sh}" +NIRI="${DESK_NIRI:-niri}" +DEBOUNCE="${DESK_DEBOUNCE_S:-1.5}" + +usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; } + +outputs_from_event() { # $1 = one JSON event line + jq -r '[.WorkspacesChanged.workspaces[]?.output // empty] | unique | join(" ")' <<<"$1" 2>/dev/null || true +} + +watch_mode() { + [ "$(hostname)" = "$HOST" ] || { echo "desk-autoswitch: not on $HOST, exiting"; exit 0; } + echo "desk-autoswitch: initial match" + "$DESK" match --apply || true + + local seen="" last_seen="" last=0 now + # One JSON event per line; reconnect forever (niri restart drops the stream). + while "$NIRI" msg --json event-stream 2>/dev/null | while IFS= read -r line; do + case "$line" in + *WorkspacesChanged*) + seen="$(outputs_from_event "$line")" + [ -n "$seen" ] || continue + [ "$seen" = "$last_seen" ] && continue + last_seen="$seen" + now="$(date +%s.%N)" + # swallow bursts inside the debounce window + if awk -v l="$last" -v n="$now" -v d="$DEBOUNCE" 'BEGIN { exit !(n - l >= d) }'; then + last="$now" + echo "desk-autoswitch: outputs changed [$seen]" + "$DESK" match --apply || true + fi + ;; + esac + done; do :; done +} + +case "${1:-}" in + watch) watch_mode ;; + -h|--help) usage ;; + *) usage >&2; exit 1 ;; +esac diff --git a/scripts/desk.sh b/scripts/desk.sh new file mode 100755 index 0000000..4012786 --- /dev/null +++ b/scripts/desk.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# scripts/desk.sh — Desk profile management for niri (blankpad) +# +# Desk profiles are cfg/desk-*.kdl files. The active profile is whatever +# file cfg/display.kdl symlinks to. Profiles declare the monitors they +# need via comment lines: +# +# // requires: +# // optional: +# +# Subcommands: +# list list available desk profiles +# current show currently active profile (basename of display.kdl) +# switch apply a specific profile (name or *.kdl) +# cycle apply the next profile (wraps) +# match [--apply] pick the profile matching connected outputs +# capture print `// requires:` lines for current outputs +# +# Environment overrides (for tests and deploys): +# DESK_CFG_DIR config dir (default ~/.config/niri/cfg) +# DESK_NIRI niri binary (default niri) +# DESK_NOTIFY notification command prefix (default "noctalia msg notification-show") + +set -euo pipefail + +CFG_DIR="${DESK_CFG_DIR:-$HOME/.config/niri/cfg}" +NIRI="${DESK_NIRI:-niri}" +NOTIFY="${DESK_NOTIFY:-noctalia msg notification-show}" + +usage() { + sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' +} + +# Profile names (basenames without .kdl), sorted. +profiles() { + local f + for f in "$CFG_DIR"/desk-*.kdl; do + [ -e "$f" ] || continue + basename "$f" .kdl + done | sort +} + +# Active profile name, or "none". +current() { + if [ -L "$CFG_DIR/display.kdl" ]; then + basename "$(readlink -f "$CFG_DIR/display.kdl")" .kdl + else + echo none + fi +} + +# Connected output long names, one per line. +outputs() { + "$NIRI" msg outputs 2>/dev/null | grep -oP 'Output "\K[^"]+' || true +} + +# requires lines of a profile, one per line. +requires() { + grep -oP '^//\s*requires:\s*\K.+' "$CFG_DIR/$1.kdl" 2>/dev/null || true +} + +notify() { + # shellcheck disable=SC2086 # word splitting of the prefix is intended + $NOTIFY "Desk" "$1" >/dev/null 2>/dev/null || true +} + +# Apply a profile: relink, validate live config, roll back on failure, +# then reload + notify. +apply_profile() { + local name="$1" + local file="$CFG_DIR/$name.kdl" + local link="$CFG_DIR/display.kdl" + + [ -f "$file" ] || { echo "error: no such profile: $name" >&2; return 1; } + + local old="" + [ -L "$link" ] && old="$(readlink "$link")" + + ln -sfn "$name.kdl" "$link" + + if ! "$NIRI" validate >/dev/null 2>/dev/null; then + if [ -n "$old" ]; then ln -sfn "$old" "$link"; else rm -f "$link"; fi + echo "error: niri validate failed; kept $( [ -n "$old" ] && basename "$old" .kdl || echo 'no' ) profile" >&2 + return 1 + fi + + "$NIRI" msg action load-config-file >/dev/null 2>/dev/null || true + notify "switched to $name" + echo "switched to $name" +} + +cmd_switch() { + local target="${1:-}" + [ -n "$target" ] || { echo "usage: desk.sh switch " >&2; return 1; } + target="${target%.kdl}" + apply_profile "$target" +} + +cmd_match() { + local apply="${1:-}" + local connected best="" best_n=0 + connected="$(outputs)" + + local name req count + while IFS= read -r name; do + [ -n "$name" ] || continue + count=0 + local ok=1 + while IFS= read -r req; do + [ -n "$req" ] || continue + if grep -qxF "$req" <<<"$connected"; then + count=$((count + 1)) + else + ok=0 + break + fi + done < <(requires "$name") + # requires=0 profiles never match; ties go to more requires, then alphabetical + [ "$ok" -eq 1 ] && [ "$count" -gt 0 ] && [ "$count" -gt "$best_n" ] && { best="$name"; best_n=$count; } + done < <(profiles) + + if [ -z "$best" ]; then + echo "no desk profile matches connected outputs" >&2 + return 2 + fi + if [ "$apply" = "--apply" ]; then + apply_profile "$best" + else + echo "$best" + fi +} + +cmd_cycle() { + local list cur next + list="$(profiles)" + [ -n "$list" ] || { echo "no desk profiles found in $CFG_DIR" >&2; return 1; } + cur="$(current)" + next="$(awk -v cur="$cur" ' + { a[NR]=$0 } + END { for (i=1;i<=NR;i++) if (a[i]==cur) { print a[(i%NR)+1]; exit } + print a[1] }' <<<"$list")" + apply_profile "$next" +} + +cmd_capture() { + local n + n="$(outputs | wc -l)" + [ "$n" -gt 0 ] || { echo "error: no connected outputs (is niri running?)" >&2; return 1; } + echo "// paste into the profile:" + outputs | sed 's/^/\/\/ requires: /' +} + +main() { + case "${1:-}" in + list) profiles ;; + current) current ;; + switch) shift; cmd_switch "$@" ;; + cycle) cmd_cycle ;; + match) shift; cmd_match "${1:-}" ;; + capture) cmd_capture ;; + -h|--help) usage ;; + *) usage >&2; exit 1 ;; + esac +} + +main "$@" diff --git a/scripts/gen-keybinds.sh b/scripts/gen-keybinds.sh new file mode 100755 index 0000000..45c948d --- /dev/null +++ b/scripts/gen-keybinds.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# scripts/gen-keybinds.sh — generate the niri keybinding cheat sheet (markdown) +# from a keybinds.kdl file. +# +# Usage: +# gen-keybinds.sh [keybinds.kdl] # markdown to stdout +# gen-keybinds.sh [keybinds.kdl] --write OUT # markdown to OUT +# +# Default input: ~/.config/niri/cfg/keybinds.kdl +# Recognized: section comments `// ─── Name ───`, bind lines with optional +# hotkey-overlay-title="..."; the action inside { } is used when there is no title. + +set -euo pipefail + +usage() { sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; } + +KBD="" +OUT="" +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage; exit 0 ;; + --write) OUT="${2:?--write needs an output path}"; shift 2 ;; + *) KBD="$1"; shift ;; + esac +done +KBD="${KBD:-$HOME/.config/niri/cfg/keybinds.kdl}" +[ -f "$KBD" ] || { echo "error: no such file: $KBD" >&2; exit 1; } + +gen() { + echo "# Niri Keybindings" + echo + echo "> GENERATED by \`scripts/gen-keybinds.sh\` from \`$(basename "$KBD")\` — do not edit by hand." + echo + awk ' + # section header: // ─── Name ─── + /^[[:space:]]*\/\/[[:space:]]*─+ (.+) ─+[[:space:]]*$/ { + s = $0 + sub(/^[[:space:]]*\/\/[[:space:]]*─+ /, "", s) + sub(/ ─+[[:space:]]*$/, "", s) + if (in_table) print "" + print "## " s + print "" + print "| Key | Action |" + print "|-----|--------|" + in_table = 1 + next + } + # bind line: KEY [flags] [hotkey-overlay-title="..."] { action; } + /^[[:space:]]*[A-Z][A-Za-z0-9+]*( |$)/ && /\{/ { + key = $1 + title = "" + if (match($0, /hotkey-overlay-title="[^"]*"/)) { + title = substr($0, RSTART + 22, RLENGTH - 23) + } + act = "" + if (match($0, /\{[^}]*\}/)) { + act = substr($0, RSTART + 1, RLENGTH - 2) + sub(/;[[:space:]]*$/, "", act) + gsub(/[[:space:]]+/, " ", act) + sub(/^ /, "", act); sub(/ $/, "", act) + } + out = (title != "") ? title : act + gsub(/\|/, "\\|", out) + print "| `" key "` | " out " |" + n++ + next + } + END { + if (in_table) print "" + printf("\n", n) > "/dev/stderr" + } + ' "$KBD" +} + +if [ -n "$OUT" ]; then + mkdir -p "$(dirname "$OUT")" + gen > "$OUT" + echo "wrote $OUT" >&2 +else + gen +fi diff --git a/tests/desk.bats b/tests/desk.bats new file mode 100755 index 0000000..0fec5ca --- /dev/null +++ b/tests/desk.bats @@ -0,0 +1,223 @@ +#!/usr/bin/env bats +# tests/desk.bats — black-box tests for scripts/desk.sh +# +# desk.sh contract (see script header): subcommands list/current/switch/ +# cycle/match/capture driven by env overrides DESK_CFG_DIR, DESK_NIRI, +# DESK_NOTIFY. Everything observable is via stdout/stderr/exit status, +# the display.kdl symlink, and the stub invocation log. + +setup() { + TEST_TMP="$(mktemp -d)" + CFG="$TEST_TMP/cfg" + LOG="$TEST_TMP/stub.log" + mkdir -p "$CFG" + + # canned `niri msg outputs` payload (live niri human format) + cat > "$TEST_TMP/outputs.txt" <<'EOF' +Output "Sceptre Tech Inc Sceptre M34 Unknown" (DP-2) + Current mode: 3440x1440 @ 239.999 Hz (current) +Output "Lenovo Group Limited 0x40A9 Unknown" (eDP-1) + Current mode: 1920x1080 @ 60.001 Hz (current, preferred) +EOF + + # stub niri: logs every invocation, serves canned outputs, + # validate exit code controlled by $STUB_VALIDATE (default 0) + cat > "$TEST_TMP/niri" <<'EOF' +#!/usr/bin/env bash +echo "niri $*" >> "$STUB_LOG" +case "$*" in + "msg outputs") cat "$STUB_OUTPUTS" ;; + "validate") exit "${STUB_VALIDATE:-0}" ;; + "msg action load-config-file") exit 0 ;; + *) exit 0 ;; +esac +EOF + chmod +x "$TEST_TMP/niri" + + # stub notifier: records what it was shown + cat > "$TEST_TMP/notify" <<'EOF' +#!/usr/bin/env bash +echo "notify $*" >> "$STUB_LOG" +EOF + chmod +x "$TEST_TMP/notify" + + export STUB_LOG="$LOG" + export STUB_OUTPUTS="$TEST_TMP/outputs.txt" + export DESK_CFG_DIR="$CFG" + export DESK_NIRI="$TEST_TMP/niri" + export DESK_NOTIFY="$TEST_TMP/notify" + + DESK="$BATS_TEST_DIRNAME/../scripts/desk.sh" +} + +teardown() { + rm -rf "$TEST_TMP" +} + +make_profiles() { + cat > "$CFG/desk-1-home.kdl" <<'EOF' +// requires: Sceptre Tech Inc Sceptre M34 Unknown +// requires: Lenovo Group Limited 0x40A9 Unknown +output "Sceptre Tech Inc Sceptre M34 Unknown" { scale 1 } +EOF + cat > "$CFG/desk-2-portable.kdl" <<'EOF' +// requires: Sceptre Tech Inc Sceptre M34 Unknown +// requires: PM1601 Portable Monitor Unknown +output "Sceptre Tech Inc Sceptre M34 Unknown" { scale 1 } +EOF + # non-profile files must be ignored + echo 'include "display.kdl"' > "$CFG/config.kdl" + echo 'output "X" {}' > "$CFG/keybinds.kdl" +} + +@test "list: shows desk profiles only, sorted" { + make_profiles + run "$DESK" list + [ "$status" -eq 0 ] + [ "$output" = "$(printf 'desk-1-home\ndesk-2-portable')" ] +} + +@test "list: no profiles yields empty output" { + run "$DESK" list + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "current: reports none without symlink, profile with symlink" { + make_profiles + run "$DESK" current + [ "$status" -eq 0 ] + [ "$output" = "none" ] + + ln -sfn desk-1-home.kdl "$CFG/display.kdl" + run "$DESK" current + [ "$output" = "desk-1-home" ] +} + +@test "switch: relinks, validates, reloads, notifies" { + make_profiles + run "$DESK" switch desk-1-home + [[ "$output" == *"switched to desk-1-home"* ]] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ] + + run cat "$LOG" + [[ "$output" == *"niri validate"* ]] + [[ "$output" == *"niri msg action load-config-file"* ]] + [[ "$output" == *"notify Desk switched to desk-1-home"* ]] +} + +@test "switch: accepts .kdl suffix" { + make_profiles + run "$DESK" switch desk-2-portable.kdl + [ "$status" -eq 0 ] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-2-portable.kdl" ] +} + +@test "switch: unknown profile fails without touching the link" { + make_profiles + ln -sfn desk-1-home.kdl "$CFG/display.kdl" + run "$DESK" switch nonexistent + [ "$status" -ne 0 ] + [[ "$output" == *"no such profile"* ]] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ] + # validate must never have run for a missing profile + run cat "$LOG" + [[ "$output" != *"niri validate"* ]] +} + +@test "switch: failed validate rolls the link back to the previous profile" { + make_profiles + ln -sfn desk-1-home.kdl "$CFG/display.kdl" + export STUB_VALIDATE=1 + run "$DESK" switch desk-2-portable + [ "$status" -ne 0 ] + [[ "$output" == *"validate failed"* ]] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ] + # no reload, no notification after a failed validate + [[ "$output" != *"load-config-file"* ]] + run cat "$LOG" + [[ "$output" != *"notify"* ]] +} + +@test "switch: failed validate with no previous profile removes the link" { + make_profiles + export STUB_VALIDATE=1 + run "$DESK" switch desk-1-home + [ "$status" -ne 0 ] + [ ! -L "$CFG/display.kdl" ] +} + +@test "match: picks the profile whose requires are all connected" { + make_profiles + run "$DESK" match + [ "$status" -eq 0 ] + [ "$output" = "desk-1-home" ] + # dry run: must not touch the symlink + [ ! -L "$CFG/display.kdl" ] +} + +@test "match --apply: applies the winner" { + make_profiles + run "$DESK" match --apply + [ "$status" -eq 0 ] + [[ "$output" == *"switched to desk-1-home"* ]] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ] +} + +@test "match: no matching profile exits nonzero and keeps the link" { + make_profiles + ln -sfn desk-2-portable.kdl "$CFG/display.kdl" + cat > "$TEST_TMP/outputs.txt" <<'EOF' +Output "Some Other Monitor 0x1234 Unknown" (DP-9) +EOF + run "$DESK" match + [ "$status" -ne 0 ] + [[ "$output" == *"no desk profile matches"* ]] + [ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-2-portable.kdl" ] +} + +@test "match: prefers the profile with more matched requires" { + # both profiles match the connected set; desk-1 requires 2, desk-2 requires 1 + cat > "$CFG/desk-1-home.kdl" <<'EOF' +// requires: Sceptre Tech Inc Sceptre M34 Unknown +// requires: Lenovo Group Limited 0x40A9 Unknown +EOF + cat > "$CFG/desk-9-guest.kdl" <<'EOF' +// requires: Sceptre Tech Inc Sceptre M34 Unknown +EOF + run "$DESK" match + [ "$status" -eq 0 ] + [ "$output" = "desk-1-home" ] +} + +@test "cycle: advances to the next profile and wraps around" { + make_profiles + ln -sfn desk-1-home.kdl "$CFG/display.kdl" + run "$DESK" cycle + [ "$status" -eq 0 ] + [[ "$output" == *"switched to desk-2-portable"* ]] + + run "$DESK" cycle + [ "$status" -eq 0 ] + [[ "$output" == *"switched to desk-1-home"* ]] +} + +@test "cycle: with no active profile applies the first" { + make_profiles + run "$DESK" cycle + [ "$status" -eq 0 ] + [[ "$output" == *"switched to desk-1-home"* ]] +} + +@test "cycle: no profiles at all fails cleanly" { + run "$DESK" cycle + [ "$status" -ne 0 ] + [[ "$output" == *"no desk profiles"* ]] +} + +@test "capture: prints requires lines for connected outputs" { + run "$DESK" capture + [ "$status" -eq 0 ] + [[ "$output" == *"// requires: Sceptre Tech Inc Sceptre M34 Unknown"* ]] + [[ "$output" == *"// requires: Lenovo Group Limited 0x40A9 Unknown"* ]] +} diff --git a/tests/gen-keybinds.bats b/tests/gen-keybinds.bats new file mode 100755 index 0000000..2c3831b --- /dev/null +++ b/tests/gen-keybinds.bats @@ -0,0 +1,70 @@ +#!/usr/bin/env bats +# tests/gen-keybinds.bats — black-box tests for scripts/gen-keybinds.sh +# +# Contract: parse a keybinds.kdl into a markdown cheat sheet on stdout, +# or into a file with --write OUT. Observable surface: markdown rows, +# section headers, exit status, written file. + +setup() { + GEN="$BATS_TEST_DIRNAME/../scripts/gen-keybinds.sh" + TMP="$(mktemp -d)" + cat > "$TMP/keybinds.kdl" <<'EOF' +binds { + // ─── Applications ─── + Mod+Return hotkey-overlay-title="Open Terminal: Ghostty" { spawn "ghostty"; } + Mod+D hotkey-overlay-title="Open App Launcher: noctalia launcher" { spawn-sh "noctalia msg panel-toggle launcher"; } + Mod+E { spawn "nautilus"; } + + // ─── Screenshots ─── + Print hotkey-overlay-title="Fullscreen screenshot" { spawn-sh "noctalia msg screenshot-fullscreen"; } + + // a plain comment, not a section + Mod+Shift+ESCAPE { show-hotkey-overlay; } +} +EOF +} + +teardown() { + rm -rf "$TMP" +} + +@test "renders a row per bind with title preferred over action" { + run "$GEN" "$TMP/keybinds.kdl" + [ "$status" -eq 0 ] + [[ "$output" == *'| `Mod+D` | Open App Launcher: noctalia launcher |'* ]] + [[ "$output" == *'| `Mod+Return` | Open Terminal: Ghostty |'* ]] +} + +@test "binds without a title render their action" { + run "$GEN" "$TMP/keybinds.kdl" + [[ "$output" == *'| `Mod+E` | spawn "nautilus" |'* ]] + [[ "$output" == *'| `Mod+Shift+ESCAPE` | show-hotkey-overlay |'* ]] +} + +@test "section comments become headers; plain comments are ignored" { + run "$GEN" "$TMP/keybinds.kdl" + [[ "$output" == *"## Applications"* ]] + [[ "$output" == *"## Screenshots"* ]] + [[ "$output" != *"a plain comment"* ]] +} + +@test "row count matches bind count" { + run "$GEN" "$TMP/keybinds.kdl" + local rows + rows="$(grep -c '^| `' <<<"$output")" + [ "$rows" -eq 5 ] +} + +@test "--write writes the markdown to the given path" { + run "$GEN" "$TMP/keybinds.kdl" --write "$TMP/out/keybindings.md" + [ "$status" -eq 0 ] + [ -f "$TMP/out/keybindings.md" ] + grep -q '| `Mod+D` | Open App Launcher: noctalia launcher |' "$TMP/out/keybindings.md" + grep -q 'GENERATED' "$TMP/out/keybindings.md" +} + +@test "missing input file fails loudly" { + run "$GEN" "$TMP/nope.kdl" + [ "$status" -ne 0 ] + [[ "$output" == *"no such file"* ]] +} diff --git a/tests/niri-config.bats b/tests/niri-config.bats new file mode 100755 index 0000000..6825035 --- /dev/null +++ b/tests/niri-config.bats @@ -0,0 +1,66 @@ +#!/usr/bin/env bats +# tests/niri-config.bats — the assembled niri config must pass `niri validate` +# +# Assembly mirrors deploy: base dotfiles/niri config + cfg files, overlaid +# by the per-host dotfiles/zz-blankpad overrides, display.kdl symlinked to a +# desk profile, live noctalia.kdl copied in (the repo include is not optional). + +setup() { + REPO="$BATS_TEST_DIRNAME/.." + NIRI_BIN="$(command -v niri || true)" + TMP="$(mktemp -d)" +} + +teardown() { + rm -rf "$TMP" +} + +# Assemble a config tree with the given desk profile active. +assemble() { + local profile="${1:-desk-1-home}" + mkdir -p "$TMP/niri" + cp -r "$REPO/dotfiles/niri/.config/niri/." "$TMP/niri/" + cp -f "$REPO/dotfiles/zz-blankpad/niri/.config/niri/cfg/"*.kdl "$TMP/niri/cfg/" + ln -sfn "$profile.kdl" "$TMP/niri/cfg/display.kdl" + [ -f "$HOME/.config/niri/noctalia.kdl" ] && + cp "$HOME/.config/niri/noctalia.kdl" "$TMP/niri/noctalia.kdl" +} + +require_niri() { + [ -n "$NIRI_BIN" ] || skip "niri binary not found" +} + +@test "assembly: host overrides win over base files" { + assemble + # the cycle-desk bind only exists in the zz-blankpad keybinds + grep -q 'desk.sh cycle' "$TMP/niri/cfg/keybinds.kdl" + ! grep -q 'desk.sh cycle' "$REPO/dotfiles/niri/.config/niri/cfg/keybinds.kdl" +} + +@test "validate: assembled config with desk-1-home active" { + require_niri + assemble desk-1-home + run "$NIRI_BIN" validate -c "$TMP/niri/config.kdl" + [ "$status" -eq 0 ] +} + +@test "validate: assembled config with desk-2-portable active" { + require_niri + assemble desk-2-portable + run "$NIRI_BIN" validate -c "$TMP/niri/config.kdl" + [ "$status" -eq 0 ] +} + +@test "validate: config is valid with the optional includes absent" { + require_niri + assemble + rm -f "$TMP/niri/cfg/switch-events.kdl" "$TMP/niri/cfg/display.kdl" + run "$NIRI_BIN" validate -c "$TMP/niri/config.kdl" + [ "$status" -eq 0 ] +} + +@test "desk profiles: every profile declares at least one requires line" { + for f in "$REPO"/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-*.kdl; do + grep -q '^// requires:' "$f" + done +}