feat(capture): screen recording — menu start, bar-click stop (item 12)
All checks were successful
Check / eval (push) Successful in 2m56s
All checks were successful
Check / eval (push) Successful in 2m56s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
99
modules/home/recording.nix
Normal file
99
modules/home/recording.nix
Normal file
@@ -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 <recorder> -> 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
|
||||
'')
|
||||
];
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user