diff --git a/.claude/tasks/0029-screen-recording.md b/.claude/tasks/0029-screen-recording.md index 8c9cfc8..cdaff2d 100644 --- a/.claude/tasks/0029-screen-recording.md +++ b/.claude/tasks/0029-screen-recording.md @@ -13,8 +13,30 @@ 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. + +- **Output paths follow XDG user-dirs.** + A new `modules.desktop.userdirs` declares the XDG user directories (home-manager `xdg.userDirs`), and the recorder resolves its base with `xdg-user-dir VIDEOS`, writing timestamped `recording-.mp4` under `/Recordings` (created on first capture). + The screenshot module (task 0028) was aligned to the same convention (`xdg-user-dir PICTURES` → `/Screenshots`), so relocating a directory is a one-line change to `xdg.userDirs` rather than an edit in each tool. + +- **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..b5ffa70 100644 --- a/modules/desktop/desktop.nix +++ b/modules/desktop/desktop.nix @@ -15,9 +15,11 @@ 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; + modules.desktop.userdirs.enable = lib.mkDefault true; modules.desktop.theming.enable = lib.mkDefault true; modules.desktop.waybar.enable = lib.mkDefault true; modules.desktop.audio.enable = lib.mkDefault true; diff --git a/modules/desktop/recording.nix b/modules/desktop/recording.nix new file mode 100644 index 0000000..0493836 --- /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"; + xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir"; + + # 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 + dir="$(${xdgUserDir} VIDEOS)/Recordings" + ${mkdir} -p "$dir" + file="$dir/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/screenshot.nix b/modules/desktop/screenshot.nix index b8ba43f..e996919 100644 --- a/modules/desktop/screenshot.nix +++ b/modules/desktop/screenshot.nix @@ -13,8 +13,7 @@ let grimblast = "${pkgs.grimblast}/bin/grimblast"; satty = "${pkgs.satty}/bin/satty"; wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy"; - - shotDir = "$HOME/Pictures/Screenshots"; + xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir"; # satty is the annotation step, and its copy action is set to save as well, # so a single keystroke through it lands the shot in both the clipboard and a @@ -22,10 +21,11 @@ let capture = target: pkgs.writeShellScript "screenshot-${target}" '' - mkdir -p ${shotDir} + dir="$(${xdgUserDir} PICTURES)/Screenshots" + mkdir -p "$dir" ${grimblast} save ${target} - \ | ${satty} --filename - \ - --output-filename "${shotDir}/screenshot-%Y%m%d-%H%M%S.png" \ + --output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \ --copy-command ${wl-copy} \ --save-after-copy \ --early-exit \ diff --git a/modules/desktop/userdirs.nix b/modules/desktop/userdirs.nix new file mode 100644 index 0000000..89f4aee --- /dev/null +++ b/modules/desktop/userdirs.nix @@ -0,0 +1,16 @@ +{ config, lib, ... }: +# Declares the XDG user directories, so a tool that queries them lands in a +# well-known folder a host can relocate from one place. +let + cfg = config.modules.desktop.userdirs; + user = config.user.name; +in +{ + options.modules.desktop.userdirs.enable = lib.mkEnableOption "XDG user directories"; + + config = lib.mkIf cfg.enable { + # enable writes ~/.config/user-dirs.dirs from the option defaults, which + # xdg-user-dir then reads. + home-manager.users.${user}.xdg.userDirs.enable = true; + }; +} 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; + }; }; }; };