Skip to content

🎥 Record a region to WebM, stopped by the same key - #85

Merged
shubshub merged 4 commits into
mainfrom
fix/issue-78
Sep 17, 2026
Merged

shubshub merged 4 commits into
mainfrom
fix/issue-78

Conversation

@shubshub

Copy link
Copy Markdown
Owner

Summary

  • --record opens the overlay with the region tool only; Capture (or Enter) starts recording instead of taking a shot. The command exits at once and prints the path it is filling, so $(programmers-screenshot --record) does not block for the length of the recording.
  • Stopping is the same command again — the running ffmpeg's pid lives in state.json and gets SIGINT, which finalises the file rather than truncating it. One hotkey does both ends. The notification that sits there while it records carries a Stop button taking the same path.
  • A stale pid (agent killed outright) is checked against /proc/<pid>/comm before anything is signalled, so a recycled pid can never be aimed at an unrelated process.
  • The file is created at 0600 before ffmpeg opens it: ffmpeg truncates the name it is handed, so a file it created itself would sit at the umask's mode for the whole recording — holding a video of somebody's screen.
  • --gif converts afterwards through a palette built from the recording's own frames, and removes the WebM.
  • X11 only: under Wayland the only route is the portal's screencast, which cannot begin without somebody choosing a screen in a dialog, so a hotkey cannot start one. It refuses with a message rather than half working.
  • No new Python dependency. ffmpeg joins Recommends, and its absence is one clear error naming the package.
  • The naming rule a PNG already used moved to output.destination(), so a recording lands the same way: -o, then -d, then a timestamp beside the screenshots.

Validation

  • python3 tests/run.py — 25 suites, 0 failures.
  • ruff check src tests (0.16.6, the CI pin) — passed.
  • ./build.sh — package builds.
  • New tests/test_recording.py: the x11grab command line (size, offset, $DISPLAY, framerate, realtime VP9), the logical→physical scale, odd sizes rounded down to even (yuv420p refuses an odd width), where the file lands, 0600 on a fresh and a pre-existing file, the refusals, and the toggle driven against a real child process — pid found, SIGINT sent, process gone, state cleared, and a foreign pid left untouched.
  • tests/test_interaction.py: the region-only overlay hands back the rectangle rather than a picture, and Escape starts nothing.

Manual check (the recording itself), on X11, ffmpeg 6.1.1

  • 320×240 region, stopped with SIGINT: playable vp9/matroska,webm, 16.88s, -rw-------.
  • Full toggle through the CLI with an isolated XDG_CONFIG_HOME: --record while recording exited 0, state.json went back to {"recording": null}, the file was a 400×300 VP9 WebM of 9.16s at 0600, and no ffmpeg was left behind.
  • --gif: 240×160 GIF, 227 frames, 0600, with the intermediate WebM removed.

Not in scope

Audio, a webcam overlay, trimming, annotating a recording, any upload — as the issue says. A recipe still cannot record: --record is for the person at the keyboard, and --recipe-help and the installed skill now say so.

Closes #78

Shubshub added 4 commits September 17, 2026 11:07
--record marks out an area with the overlay -- the region tool only, since
nothing drawn on a frozen frame could survive into a video of the live
screen -- and hands it to ffmpeg. The command exits at once, printing the
path it is filling, so a shell pipeline is not held open for the length of
a recording and one hotkey can do both ends of it.

Stopping is the same command again: the running ffmpeg is remembered in
state.json and sent SIGINT, which is how it is asked to finalise a file
rather than leave it truncated. The notification carries a Stop button that
takes the same path. A pid left behind by a killed agent is checked against
/proc before anything is signalled, so a recycled number cannot be aimed at
somebody else's process.

The file is created at 0600 before ffmpeg opens it: ffmpeg truncates the
name it is given, and a file it made itself would sit at the umask's mode
for the whole recording, holding a video of somebody's screen. --gif
converts afterwards, through a palette built from the recording's own
colours, and removes the WebM.

X11 only. Under Wayland the one route is the portal's screencast, which
cannot begin without somebody choosing a screen in a dialog, so a hotkey
cannot start one; it refuses with a message instead of half working.

ffmpeg joins Recommends: without it, one clear error naming the package.
The naming rule a PNG already used moves to output.destination(), so a
recording lands the same way -- -o, then -d, then the timestamp.
A red record dot beside Capture, on the bar and on the floating palette
both -- they are meant to be the same controls in two shapes, so putting it
on one and not the other would be the odd thing. Mark out an area, press
the dot instead of Capture, and the screenshot session becomes a recording
without having to start again from a different command.

It is laid out only where recording could actually work: with ffmpeg
installed, under X11. A button that cannot do anything is worse than no
button, and cli already works out the answer before the overlay is built.

Marks on the scene are left behind. A recording is of the live screen, and
they were made on a frozen frame of it -- they would be a still picture
pasted over moving video, describing a moment that has already gone.

The overlay now answers with an Outcome: a picture to deliver, or an area
to record. --record and the button meet at the same two lines of cli, so
there is one way a recording starts rather than two.
A recording's only control was a button on a notification, and GNOME
collapses notifications that carry buttons -- so the one way to stop a
recording could be behind an expander arrow in a tray nobody has open.
That is a poor place for the only control a running thing has.

So while a recording runs there is a red dot in the status area, beside
the volume and the battery, carrying one item: Stop recording. That is
where somebody looks for something that is currently happening, and it is
where the desktop's own recorder puts it.

The agent that owns ffmpeg already holds a main loop for the length of the
recording, so it owns the indicator too and the dot goes away with it.

gir1.2-ayatanaappindicator3-0.1 joins Recommends rather than Depends: the
typelib may not be installed, and the desktop may have nothing listening
for status notifier items at all (on GNOME that is the AppIndicator
extension, which Ubuntu ships switched on). Either way indicator() returns
None, the notification's Stop and running the command again both still
work, and nothing refuses to record over a missing banner.
The app-indicator typelib was a wrapper round a D-Bus interface and one
more thing to have installed -- and a Recommends that somebody skips would
mean the only control a running recording has quietly does not appear. Gio
is already a hard dependency, so the item and its menu are served from
here: seven properties, a menu that never changes, and a click.

The menu is not decoration. GNOME's AppIndicator extension opens the item's
menu on a single click and only calls Activate on a double one
(indicatorStatusIcon.js), so an item with no menu looks broken to anybody
who clicks it once. Activate and the middle click stop the recording too,
for a desktop that reads them that way.

Registering with the watcher is asynchronous on purpose. The watcher reads
the item's properties back before it answers, and a blocking call cannot
serve them: the reply waits on the thread that is already waiting, and it
deadlocks until the timeout with nothing on the bar. Sent asynchronously it
is answered from the loop the recording already runs under.

Checked against the running shell: it registers, serves what the panel
reads, and a dbusmenu click -- the same call the panel makes -- stops the
recording. The suite covers the answers themselves, no session bus needed.
@shubshub
shubshub merged commit 008026b into main Sep 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record a region to WebM, with the same picker and one key to stop

1 participant