diff --git a/.claude/tasks/0029-screen-recording.md b/.claude/tasks/0029-screen-recording.md index 8c9cfc8..6a09f9e 100644 --- a/.claude/tasks/0029-screen-recording.md +++ b/.claude/tasks/0029-screen-recording.md @@ -13,8 +13,28 @@ No audio capture and no full-screen recording variant. ## Acceptance criteria -- [ ] A wf-recorder module exists in the desktop group and is enabled by the aggregator. -- [ ] The recorder selects a region first, then toggles video-only recording on `Super+Shift+R`. -- [ ] A recording indicator appears in the bar, and notifications fire on start and stop. -- [ ] No audio is captured and no full-screen variant is provided. -- [ ] neogaia builds green under `nix flake check`. +- [x] A wf-recorder module exists in the desktop group and is enabled by the aggregator. +- [x] The recorder selects a region first, then toggles video-only recording on `Super+Shift+R`. +- [x] A recording indicator appears in the bar, and notifications fire on start and stop. +- [x] No audio is captured and no full-screen variant is provided. +- [x] neogaia builds green under `nix flake check`. + +## Implementation Notes + +- **Toggle design.** + One key both starts and stops. + A running capture is stopped with SIGINT so wf-recorder finalises the file; otherwise slurp picks a region and wf-recorder runs in the foreground for the whole recording, so the same invocation fires the "saved" notification once the file is written. + Region-first and video-only (no `-a`, so no audio) satisfy the spec directly, and no full-screen variant is offered. + +- **Bar indicator polls rather than signals.** + The Waybar `custom/recording` widget samples the wf-recorder process with `pgrep` on a one-second interval, showing a video glyph while a capture runs and collapsing to nothing when idle. + An earlier draft signalled Waybar (`pkill -RTMIN+9`) from the toggle, but the start path raised the signal before wf-recorder had launched, so `pgrep` saw nothing and the indicator never lit during a recording — caught in review. + Polling is race-free, removes the signal number shared across two files, and is adequate for a status glyph. + +- **No waybar `window-rewrite` icon.** + wf-recorder is headless and slurp is a transient selection overlay, so neither owns a workspace window and the per-application icon convention does not apply. + +- **Recordings land in `~/Videos/Recordings`**, timestamped `recording-.mp4`, with the directory created on first capture. + +- **Live capture is the irreducible manual step.** + The build is green, the config parses under `Hyprland --verify-config`, and the indicator's idle/recording transitions are verified against a stand-in process; exercising a real slurp selection and wf-recorder capture needs a running session. diff --git a/modules/desktop/desktop.nix b/modules/desktop/desktop.nix index c46578f..3f87938 100644 --- a/modules/desktop/desktop.nix +++ b/modules/desktop/desktop.nix @@ -15,6 +15,7 @@ in modules.desktop.hypridle.enable = lib.mkDefault true; modules.desktop.login.enable = lib.mkDefault true; modules.desktop.mako.enable = lib.mkDefault true; + modules.desktop.recording.enable = lib.mkDefault true; modules.desktop.rofi.enable = lib.mkDefault true; modules.desktop.screenshot.enable = lib.mkDefault true; modules.desktop.terminal.enable = lib.mkDefault true; diff --git a/modules/desktop/recording.nix b/modules/desktop/recording.nix new file mode 100644 index 0000000..19a3eca --- /dev/null +++ b/modules/desktop/recording.nix @@ -0,0 +1,54 @@ +{ + config, + lib, + pkgs, + ... +}: +# Keybound screen recording: pick a region, then toggle a video-only capture. +let + cfg = config.modules.desktop.recording; + user = config.user.name; + + slurp = "${pkgs.slurp}/bin/slurp"; + wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder"; + notify-send = "${pkgs.libnotify}/bin/notify-send"; + pgrep = "${pkgs.procps}/bin/pgrep"; + pkill = "${pkgs.procps}/bin/pkill"; + date = "${pkgs.coreutils}/bin/date"; + mkdir = "${pkgs.coreutils}/bin/mkdir"; + + recDir = "$HOME/Videos/Recordings"; + + # A single key both starts and stops. + # A running wf-recorder is stopped with SIGINT so it finalises the file. + # Otherwise slurp picks a region and wf-recorder runs in the foreground until + # that stop arrives, so this instance lives for the whole recording and then + # reports it saved. + # No -a means no audio is captured. + toggle = pkgs.writeShellScript "screen-record-toggle" '' + if ${pgrep} -x wf-recorder >/dev/null; then + ${pkill} -INT -x wf-recorder + exit 0 + fi + + region=$(${slurp}) || exit 0 + ${mkdir} -p "${recDir}" + file="${recDir}/recording-$(${date} +%Y%m%d-%H%M%S).mp4" + + ${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio." + ${wf-recorder} -g "$region" -f "$file" + ${notify-send} -a "Screen recording" "Recording saved" "$file" + ''; +in +{ + options.modules.desktop.recording.enable = lib.mkEnableOption "wf-recorder screen recording"; + + config = lib.mkIf cfg.enable { + home-manager.users.${user} = { + # $mod is defined by the compositor config these binds share. + wayland.windowManager.hyprland.settings.bind = [ + "$mod SHIFT, R, exec, ${toggle}" + ]; + }; + }; +} diff --git a/modules/desktop/waybar.nix b/modules/desktop/waybar.nix index 233787b..fc29cf1 100644 --- a/modules/desktop/waybar.nix +++ b/modules/desktop/waybar.nix @@ -28,6 +28,16 @@ let ${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1 ${pkgs.procps}/bin/pkill -RTMIN+8 waybar ''; + + # Lit while a wf-recorder capture is running. + # Empty text collapses the widget when idle, so it shows nothing until then. + recStatus = pkgs.writeShellScript "waybar-recording-status" '' + if ${pkgs.procps}/bin/pgrep -x wf-recorder >/dev/null; then + printf '{"text":"${g "f03d"}","tooltip":"Recording","class":"recording"}\n' + else + printf '{"text":"","class":"idle"}\n' + fi + ''; in { options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar"; @@ -52,6 +62,7 @@ in modules-left = [ "hyprland/workspaces" ]; modules-center = [ "clock" ]; modules-right = [ + "custom/recording" "mpris" "wireplumber" "network" @@ -134,6 +145,14 @@ in interval = "once"; signal = 8; }; + + # Recording state has no event to hook, so the widget samples the + # wf-recorder process once a second. + "custom/recording" = { + return-type = "json"; + exec = "${recStatus}"; + interval = 1; + }; }; }; };