From 0fe46221ea8c7c8afdc809db67456fa0b8dd7c02 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 4 Jul 2026 20:58:40 +0100 Subject: [PATCH] =?UTF-8?q?feat(capture):=20screen=20recording=20=E2=80=94?= =?UTF-8?q?=20menu=20start,=20bar-click=20stop=20(item=2012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nomarchy-record owns the lifecycle: start prefers wl-screenrec (VAAPI hardware encode) and falls back to wf-recorder if it dies at startup (the no-VAAPI case), stop SIGINTs so the file finalizes cleanly, and status feeds a self-gating Waybar ⏺ REC (alert red) whose click is the one stop surface — signal 8 pokes make it appear/vanish instantly. Capture gains Record region/screen (+ audio variants) and self-swaps to '■ Stop recording' while one runs. Parity: both summer whole-swap bars carry the same module. Recordings land in ~/Videos/Recordings with the screenshot timestamp naming (deviation from 'next to Screenshots': an .mp4 in Pictures felt wrong). V1: rendered bar config + CSS asserted, summer jsonc parse, recorder dry-run state machine correct (idle/active/usage exits), both recorders in home-path. Real capture needs a session → V3 queued (incl. the VAAPI-vs-fallback check on the AMD box). Co-Authored-By: Claude Fable 5 --- agent/BACKLOG.md | 12 +--- agent/HARDWARE-QUEUE.md | 9 +++ agent/JOURNAL.md | 22 +++++++ modules/home/default.nix | 1 + modules/home/recording.nix | 99 ++++++++++++++++++++++++++++++++ modules/home/rofi.nix | 17 ++++++ modules/home/waybar.nix | 20 ++++++- themes/summer-day/waybar.css | 6 ++ themes/summer-day/waybar.jsonc | 10 +++- themes/summer-night/waybar.css | 6 ++ themes/summer-night/waybar.jsonc | 10 +++- 11 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 modules/home/recording.nix diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index ea67fd4..f2e8398 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -26,15 +26,6 @@ next, in what order*. ## NEXT -### 12. Screen recording in the Capture submenu -New; screenshots ship, recording doesn't — a standard workstation need. -Extend `nomarchy-menu capture`: record region/full → `wl-screenrec` -(fallback `wf-recorder`), saved next to Screenshots, with a self-gating -Waybar recording indicator that stops the recording on click (the only -sane "stop" surface). Optional audio toggle. Parity rule applies. -**Verify:** V1 + `bash -n`; V2 headless if the software-GL recipe -supports it, else V3 queue. - ### 13. Small niceties batch (one slice per iteration) Each is a small, self-contained polish item in the existing patterns: - **Idle-inhibit (caffeine) toggle:** Waybar `idle_inhibitor` module @@ -106,7 +97,8 @@ earns ≥3 entries — keep the root at six. - **Per-theme icon overrides** / more icon packs (ROADMAP § Icon themes). - **MIPI/IPU software-ISP camera** support (no-UVC machines). - **OCR screenshot-to-text**: a Capture › entry (grim region → tesseract - → clipboard) — cheap once recording (#12) reshapes the submenu. + → clipboard) — recording (#12, shipped 2026-07-04) already reshaped + the submenu, so this is now a two-row addition. - **Doctor Waybar warning**: a self-gating bar indicator fed by `nomarchy-doctor` (shipped 2026-07-04) — appears only when the sheet has a ✖; click opens the sheet. diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 279c86d..483921f 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -86,6 +86,15 @@ QA machine), the **T14s** (webcam case). `sudo nomarchy-snapshots`, restore a single file (`undochange`) and walk a root-config rollback up to (or through) the typed-`yes` gate. +- [ ] **Screen recording (item 12)** — Tools › Capture: Record region + (slurp) and Record screen produce a playable .mp4 in + ~/Videos/Recordings; the red ⏺ REC appears in the bar instantly + and CLICKING IT stops + notifies the path; the + audio variants + carry sound. While recording, the Capture menu offers only + "■ Stop recording". On the AMD box wl-screenrec (VAAPI) should + do the work; if it dies at start the wf-recorder fallback kicks + in silently — check the notification says which. Summer bars + have the same ⏺ (parity). - [ ] **Doctor (item 10)** — menu › System › Doctor opens the sheet in a terminal; `nomarchy-doctor` over SSH shows the same minus user units. On a healthy machine everything is ✔/– (dev-box run diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 983473a..0edfb73 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,28 @@ Template: --- +## 2026-07-04 — screen recording in Capture (iteration #22, item 12) +- **Task:** BACKLOG NEXT#12 — record region/screen from the menu, stop + from the bar. +- **Did:** modules/home/recording.nix: `nomarchy-record` + start/stop/active/status — wl-screenrec first, wf-recorder fallback + if it dies at startup (no-VAAPI case), SIGINT-finalized stop, + runtime pidfile, signal-8 bar pokes. Capture menu: 4 record rows that + self-swap to "■ Stop recording" while one runs. Waybar: self-gating + custom/recording (⏺ REC, @bad red), click = stop — the one stop + surface. Parity: both summer whole-swap bars got the module + CSS + (@red). DEVIATIONS: saves to ~/Videos/Recordings (an .mp4 in + Pictures/Screenshots felt wrong — same timestamp naming though); + audio = two extra explicit rows, not a toggle state. +- **Verified:** V0; V1 — template-home builds; rendered bar config has + the module (first in modules-right) + CSS rule; summer jsonc still + parse; nomarchy-record dry-run: status empty+exit 0 idle, active + exit 1, usage exit 2; both recorders in home-path. Real capture + needs a session → V3 (headless GL recipe can't confirm an encoder). +- **Pending:** V3 recording exercise (incl. VAAPI-vs-fallback check). +- **Next suggestion:** NEXT#13 niceties, one slice: idle-inhibit + (caffeine) toggle — reverse-parity gap already noted in the item. + ## 2026-07-04 — state-file validation & friendly errors (iteration #21, item 11) - **Task:** BACKLOG NEXT#11 — hand-edited state must fail with field + problem + fix, never a Nix stack. diff --git a/modules/home/default.nix b/modules/home/default.nix index 1d27f27..4e94d2c 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -25,6 +25,7 @@ ./fastfetch.nix # system info with the themed Nomarchy logo ./viewers.nix # zathura (Stylix-themed) + imv — PDF/image viewing ./mime.nix # default applications (mimeapps.list), degrades with the suite + ./recording.nix # nomarchy-record: screen recording behind Capture + the bar ⏺ ]; # Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V diff --git a/modules/home/recording.nix b/modules/home/recording.nix new file mode 100644 index 0000000..a23624c --- /dev/null +++ b/modules/home/recording.nix @@ -0,0 +1,99 @@ +# Screen recording (menu › Tools › Capture). One helper owns the whole +# lifecycle: `start` launches wl-screenrec (VAAPI hardware encode) and +# falls back to wf-recorder (software x264) if it dies on the spot — +# e.g. no usable VAAPI device; `status` feeds the self-gating Waybar +# indicator (waybar.nix, signal 8), whose click is the ONE stop surface +# (`stop` SIGINTs the recorder so it finalizes the file cleanly). State +# is a runtime pidfile — nothing persists across sessions. +{ config, lib, pkgs, ... }: + +lib.mkIf config.nomarchy.rofi.enable { + home.packages = [ + pkgs.wl-screenrec + pkgs.wf-recorder + + (pkgs.writeShellScriptBin "nomarchy-record" '' + run="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + pidfile="$run/nomarchy-record.pid" + filefile="$run/nomarchy-record.file" + + alive() { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; } + poke_bar() { pkill -RTMIN+8 waybar 2>/dev/null || true; } + + case "''${1:-}" in + start) + if alive; then + notify-send "Screen recording" "Already recording — stop it via the ⏺ in the bar." + exit 1 + fi + target="''${2:-screen}"; audio="''${3:-}" + geo="" + if [ "$target" = region ]; then + geo=$(slurp) || exit 0 # Esc in slurp = cancel, silently + fi + dir="$HOME/Videos/Recordings" + mkdir -p "$dir" + file="$dir/$(date +%Y%m%d-%H%M%S).mp4" + + launch() { # launch -> 0 if it survived startup + case "$1" in + wl-screenrec) + setsid wl-screenrec ''${geo:+-g "$geo"} \ + ''${audio:+--audio} -f "$file" >/dev/null 2>&1 & ;; + wf-recorder) + setsid wf-recorder ''${geo:+-g "$geo"} \ + ''${audio:+-a} -f "$file" >/dev/null 2>&1 & ;; + esac + echo $! > "$pidfile" + sleep 0.7 + alive + } + + started="" + if command -v wl-screenrec >/dev/null 2>&1 && launch wl-screenrec; then + started=wl-screenrec + elif command -v wf-recorder >/dev/null 2>&1 && launch wf-recorder; then + started=wf-recorder # software fallback (no VAAPI device) + fi + if [ -z "$started" ]; then + rm -f "$pidfile" + notify-send "Screen recording" "Recorder failed to start (no wl-screenrec/wf-recorder able to run)." + exit 1 + fi + printf '%s' "$file" > "$filefile" + poke_bar + notify-send "Screen recording" "Recording ($started)''${audio:+ with audio} — click the ⏺ in the bar to stop." ;; + + stop) + if ! alive; then + rm -f "$pidfile" "$filefile"; poke_bar + notify-send "Screen recording" "No recording is running." + exit 0 + fi + pid=$(cat "$pidfile") + kill -INT "$pid" 2>/dev/null # graceful: both recorders finalize on INT + for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done + kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null + file=$(cat "$filefile" 2>/dev/null || echo "?") + rm -f "$pidfile" "$filefile" + poke_bar + notify-send "Screen recording saved" "$file" ;; + + active) + alive ;; + + status) + # Waybar JSON while recording; nothing => module hidden. + if alive; then + file=$(cat "$filefile" 2>/dev/null || echo "") + printf '{"text":"⏺ REC","class":"recording","tooltip":"Recording to %s — click to stop"}\n' "''${file##*/}" + fi + exit 0 ;; + + *) + echo "usage: nomarchy-record start [region|screen] [audio] | stop | active | status" >&2 + exit 2 ;; + esac + '') + ]; +} diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index c5bb0aa..a82f458 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -341,11 +341,23 @@ ${themeRows} fi ;; capture) + # Screenshots + screen recording. Recording rows self-swap: while + # one runs, the only offer is Stop (same surface as the bar's ⏺). choice=$( { row "Region → clipboard" applets-screenshooter row "Region → file" applets-screenshooter row "Full screen → clipboard" camera-photo row "Full screen → file" camera-photo + if command -v nomarchy-record >/dev/null 2>&1; then + if nomarchy-record active; then + row "■ Stop recording" media-playback-stop + else + row "Record region" media-record + row "Record screen" media-record + row "Record region + audio" audio-input-microphone + row "Record screen + audio" audio-input-microphone + fi + fi back } | rofi -dmenu -show-icons -p Capture) || exit 0 dir="$HOME/Pictures/Screenshots" @@ -358,6 +370,11 @@ ${themeRows} *"Full screen → clipboard"*) grim - | wl-copy ;; *"Full screen → file"*) mkdir -p "$dir"; grim "$file" \ && notify-send "Screenshot saved" "$file" ;; + *"Stop recording"*) exec nomarchy-record stop ;; + *"Record region + audio"*) exec nomarchy-record start region audio ;; + *"Record screen + audio"*) exec nomarchy-record start screen audio ;; + *"Record region"*) exec nomarchy-record start region ;; + *"Record screen"*) exec nomarchy-record start screen ;; esac ;; keybinds) diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index 0dd5276..38f4170 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -121,7 +121,7 @@ let modules-left = [ "hyprland/workspaces" "hyprland/window" ]; modules-center = [ "clock" ]; - modules-right = [ "tray" "custom/vpn" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ] + modules-right = [ "custom/recording" "tray" "custom/vpn" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ] ++ lib.optional showLanguage "hyprland/language" ++ [ "battery" "custom/updates" "custom/notification" ]; @@ -184,6 +184,19 @@ let on-click = "nomarchy-vpn"; }; + # Screen recording indicator. Self-gates: visible only while + # nomarchy-record runs (status prints nothing otherwise), and the + # click IS the stop surface — the menu only starts recordings. + # signal 8: the recorder pokes the bar on start/stop so the ⏺ + # appears/vanishes instantly (the interval is just a safety net). + "custom/recording" = { + exec = "nomarchy-record status"; + return-type = "json"; + interval = 10; + signal = 8; + on-click = "nomarchy-record stop"; + }; + # Night-light (hyprsunset) indicator. Self-gates: the moon shows only while # the schedule runs; otherwise the status helper prints nothing => hidden # (enable / re-enable from the System menu). Click toggles instantly — writes @@ -283,11 +296,14 @@ let font-weight: bold; } - #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #custom-vpn, #language, #battery, #custom-notification { + #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #custom-vpn, #custom-recording, #language, #battery, #custom-notification { color: alpha(@text, 0.85); padding: 0 10px; } + /* Recording in progress → the alert red; the ⏺ is also the stop button. */ + #custom-recording.recording { color: @bad; } + /* VPN active → accent green, reading as "connected / protected". */ #custom-vpn.on { color: @good; } diff --git a/themes/summer-day/waybar.css b/themes/summer-day/waybar.css index 4d484b1..b71faca 100755 --- a/themes/summer-day/waybar.css +++ b/themes/summer-day/waybar.css @@ -43,6 +43,7 @@ window#waybar { } #custom-launcher, +#custom-recording, #clock, #clock-date, #workspaces, @@ -114,3 +115,8 @@ window#waybar { padding-left: 20px; padding-right: 23px; } + +/* Recording in progress: alert red; the module is also the stop button. */ +#custom-recording.recording { + color: @red; +} diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc index f72c440..8e15a3e 100644 --- a/themes/summer-day/waybar.jsonc +++ b/themes/summer-day/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/launcher", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], + "modules-right": ["custom/recording", "pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -79,6 +79,14 @@ "on-click": "nomarchy-powerprofile-cycle" }, + "custom/recording": { + "return-type": "json", + "interval": 10, + "signal": 8, + "exec": "nomarchy-record status", + "on-click": "nomarchy-record stop" + }, + "custom/vpn": { "return-type": "json", "interval": 5, diff --git a/themes/summer-night/waybar.css b/themes/summer-night/waybar.css index f68ab66..bea7a80 100644 --- a/themes/summer-night/waybar.css +++ b/themes/summer-night/waybar.css @@ -49,6 +49,7 @@ window#waybar { #backlight, #tray, #custom-nomarchy, +#custom-recording, #clock, #clock.date, #workspaces, @@ -133,3 +134,8 @@ window#waybar { padding-right: 10px; font-size: 20px; } + +/* Recording in progress: alert red; the module is also the stop button. */ +#custom-recording.recording { + color: @red; +} diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc index deca81c..56a4317 100644 --- a/themes/summer-night/waybar.jsonc +++ b/themes/summer-night/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/nomarchy", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], + "modules-right": ["custom/recording", "idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -86,6 +86,14 @@ "on-click": "nomarchy-powerprofile-cycle" }, + "custom/recording": { + "return-type": "json", + "interval": 10, + "signal": 8, + "exec": "nomarchy-record status", + "on-click": "nomarchy-record stop" + }, + "custom/vpn": { "return-type": "json", "interval": 5,