feat(desktop): add screen recording (task 0029) #21

Merged
alexion merged 2 commits from task-0029-screen-recording into main 2026-07-22 23:15:33 -04:00
4 changed files with 99 additions and 5 deletions
Showing only changes of commit 0e92de7eea - Show all commits

View File

@@ -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-<date>.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.

View File

@@ -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;

View File

@@ -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}"
];
};
};
}

View File

@@ -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;
};
};
};
};