feat(desktop): add screen recording (task 0029)
Add a wf-recorder module, enabled by the desktop aggregator. Super+Shift+R picks a region with slurp and toggles a video-only capture (no audio), stopped with SIGINT so the file finalises, saved to ~/Videos/Recordings. Start and saved notifications fire via notify-send. A Waybar custom/recording widget samples the wf-recorder process once a second and shows a video glyph while a capture runs.
This commit is contained in:
@@ -13,8 +13,28 @@ No audio capture and no full-screen recording variant.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] A wf-recorder module exists in the desktop group and is enabled by the aggregator.
|
- [x] 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`.
|
- [x] 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.
|
- [x] 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.
|
- [x] No audio is captured and no full-screen variant is provided.
|
||||||
- [ ] neogaia builds green under `nix flake check`.
|
- [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.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ in
|
|||||||
modules.desktop.hypridle.enable = lib.mkDefault true;
|
modules.desktop.hypridle.enable = lib.mkDefault true;
|
||||||
modules.desktop.login.enable = lib.mkDefault true;
|
modules.desktop.login.enable = lib.mkDefault true;
|
||||||
modules.desktop.mako.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.rofi.enable = lib.mkDefault true;
|
||||||
modules.desktop.screenshot.enable = lib.mkDefault true;
|
modules.desktop.screenshot.enable = lib.mkDefault true;
|
||||||
modules.desktop.terminal.enable = lib.mkDefault true;
|
modules.desktop.terminal.enable = lib.mkDefault true;
|
||||||
|
|||||||
54
modules/desktop/recording.nix
Normal file
54
modules/desktop/recording.nix
Normal 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}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -28,6 +28,16 @@ let
|
|||||||
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
|
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
|
||||||
${pkgs.procps}/bin/pkill -RTMIN+8 waybar
|
${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
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||||
@@ -52,6 +62,7 @@ in
|
|||||||
modules-left = [ "hyprland/workspaces" ];
|
modules-left = [ "hyprland/workspaces" ];
|
||||||
modules-center = [ "clock" ];
|
modules-center = [ "clock" ];
|
||||||
modules-right = [
|
modules-right = [
|
||||||
|
"custom/recording"
|
||||||
"mpris"
|
"mpris"
|
||||||
"wireplumber"
|
"wireplumber"
|
||||||
"network"
|
"network"
|
||||||
@@ -134,6 +145,14 @@ in
|
|||||||
interval = "once";
|
interval = "once";
|
||||||
signal = 8;
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user