omarchy-studio
Screen recorder · Linux · Hyprland

Record the pixels.
Decide everything else later.

A non-destructive screen recorder in the Screen Studio mould, built for Hyprland. The camera, the cursor, the clicks and the window's position are all captured as separate, editable data — so the zoom, the framing and the crop are decisions you make after the take, not before it.

One rule explains most of the design: recording captures pixels, editing captures intent, and export is the only place the two meet. Nothing you do in the editor touches what was recorded, and nothing about how you recorded limits what you can decide afterwards.

01How it works

The model, and what it buys you.

A recording is a directory, not a file

screenrecording-2026-09-05_13-23-43/
├── capture.json     what the hardware did        IMMUTABLE
├── edit.json        what you decided             MUTABLE
├── media/
│   ├── screen.mp4     the master, at capture resolution
│   ├── screen.mp4.ts  first-frame timestamp sidecar
│   ├── cam.mp4        the camera, recorded separately
│   └── cam.tsv        camera frame timestamps
├── events/
│   ├── input.jsonl    clicks, with timestamps
│   ├── cursor.bin     pointer positions, 120 Hz
│   └── chrome.json    rectangles that are UI, not content
├── proxy/             fast draft copies, rebuilt on demand
└── assets/            images you added

capture.json is written once and holds only facts: resolution, frame rate, the monitor and its scale, the clock anchor that ties events to frames. It is never edited, because it is the record of what actually happened. If it were mutable, every later question — where was the cursor at 4.2s? — would have two possible answers.

edit.json is everything you chose. It is small, it is JSON, and it is the only thing an edit writes. Delete it and you have the raw recording back, exactly as captured.

The camera is a separate stream, never burned in. That is what lets you move the bubble, resize it, cut it out of part of the take, or drop it entirely — after the fact. A recorder that composites the camera into the frame has thrown that away before you ever see it.

Four stages

1 · Setup — decide what to record

A bar appears at the bottom of the screen, and an overlay on each display shows its name, resolution and frame rate with a Start button in the middle. The button sits on the target itself, which doubles as proof you picked the right monitor.

Pressing Start begins a countdown — and capture initialisation runs during it rather than after, so the two waits overlap. Every setup surface is destroyed before the countdown ends, so no frame of the UI can reach the recording.

The setup bar at the bottom of the screen, with mode, microphone, camera and system audio controls, and a Start overlay on the display itself.
Mode, mic, camera and system audio on the bar — and Start on the display you are about to record.

2 · Record — pixels and intent, in parallel

Three things run at once: the screen through one of three capture backends, the camera to its own file with its own timestamps, and an input daemon sampling the pointer at 120 Hz while logging every click.

That third one is why automatic zoom is possible at all. Clicks are recorded as data, not as an effect — so the zoom they imply can be tuned, deleted, or switched off long after the take is over.

3 · Edit — non-destructively

The editor plays a proxy: a fast, lower-resolution copy of the master, built in the background so scrubbing stays responsive on a 5K recording. Everything you see is your edit applied live over that proxy. Nothing is rendered until you export.

4 · Export — one ffmpeg process

The whole edit compiles to a single filtergraph. No intermediate file, no round trip, no generation loss. The graph is written to a file rather than passed as arguments, because a long project's graph exceeds the kernel's argument limit.

Why the stage order is what it is

cut cursor zoom backdrop layers

The order is not arbitrary; each position was measured.

  • Cut first. Everything downstream then runs over fewer frames — 2.20s against 2.83s on the same project. Cut-first with remapped layer timings produces bit-identical frames to cut-last with verbatim ones, so the reordering is provably free.
  • Cursor before the zoom, so the zoom magnifies the pointer along with the pixels underneath it. Drawn after, the pointer would need the zoom's per-frame viewport inverted into its own position maths — and any disagreement between the two copies shows up as the pointer sliding across the thing it is pointing at.
  • Zoom second, while the frame is still planar YUV. The same warp costs 8.4 ms/frame on yuv420p and 14.9 ms/frame once the backdrop has converted the stream to RGBA. Identical pixels, nearly double the price.
  • Backdrop third, and it fixes the output length. The background is an infinite source and the real video reaches it through shortest=1. The other way round, the background's timebase governs the output — a 6.9s clip once came out 208 seconds long.
  • Layers last. Their cost scales with how many exist, not how long each is visible (2.75s against 2.70s for a layer visible 1s of 20 versus throughout).

02Engines

What does the work, and why each choice was made.

Capture: three backends, not one

Wayland does not let an application record whatever it likes, and the three ways in have different powers. The differences leak into what the product can offer, so they are worth knowing.

KMSPortalToplevel
ReadsDRM scanout, below the compositorxdg-desktop-portal streamone window's surface tree
Frame rate60 fps30 fpsdisplay rate
Sub-rectangleyesno — whole monitorn/a
Can hide a windownoyesn/a
Used formonitor, areaanything needing a hidden self-viewtrue single-window

KMS is the default and the fastest. It reads the scanout below the compositor, which is exactly why it cannot honour no_screen_share: the compositor's decision to hide a window has already been applied to a layer this path never sees.

Portal runs at half the rate but the compositor is in the loop, so windows can be excluded. This is the backend that makes a live camera self-view possible — the bubble is on screen for you and absent from the recording.

Toplevel (hyprland_toplevel_export_v1) renders one window's surface tree alone — the Linux equivalent of ScreenCaptureKit's window capture. This is what "just this window" means when it means it literally: anything stacked on top is not in the take, because those surfaces are never composited into the frame.

A region capture that also wants a self-view takes the whole monitor through the portal and crops back at export — because only KMS can capture a sub-rectangle directly, and only the portal can hide the bubble. macOS never faces this; it excludes windows on the same fast path whatever the capture is.

Zoom is derived, not authored

Zoom moves are computed from the click track at render time rather than stored as keyframes. Clicks land on the output timeline, clicks inside a cut are discarded, and nearby clicks cluster into one move — without clustering, a burst of clicks in one dialog produces one zoom per click, each easing out before the next eases in.

Deleting one move stores the source frames of its clicks, not the move's index or its time range — both of those shift the moment any other edit lands. The click track is part of the capture and never changes, so it is the one stable name.

Gating the warp to frames that actually zoom 510-frame export, two zooms
Warp running on every frame56.2 s
Warp gated with enable=31.2 s
Frames that were paying for an identity warp434 of 510
Difference in outputbit-identical

Render: one graph, one process

Everything below the zoom is composited at the size the export will be, not at the master's. The background is a band-limited gradient, the shadow a Gaussian whose sigma is proportional to its radius, the corner mask analytic coverage — all resolution-independent by construction. Building them at four times the pixels and discarding three quarters buys nothing.

This is also better quality, not a trade: the backdrop scales the video down to make room for its own padding, so the old arrangement resampled the picture twice. Now the zoomed master reaches its final size in one lanczos step.

Export time 17 s take · 5120×2880 master · 1440p out
Decode + one downscale + encode — the floor4.0 s
Original88.0 s
Compositing at the delivered size54.3 s
…plus gating the zoom and the cursor chroma18.6 s

The encode runs on the CPU, which is the counter-intuitive part. VAAPI measured slower — 18.11s against 19.82s at 1080p, 49.43 against 52.80 at 4K — because every filter in the graph is CPU-only, so hwupload only adds a copy. VAAPI offloads the encode, which was never the bottleneck. (The native window recorder does use VAAPI, because there the encode is all there is.)

Audio

afade cut declick loudnorm 48 kHz

Every step is placed deliberately. The start-pop fade sits before the cut, so it belongs to the capture's start rather than the edited timeline's — cut the head away and the fade goes with it, instead of silencing the first 450 ms of whatever now begins the video. Declick sits before loudnorm, because a key clack is the loudest thing in a narration track and normalising first would set the gain from the noise and leave the voice quiet.

Removing keyboard clicks

RNNoise with a vendored model, followed by afftdn. Chosen by measurement on a real take, scored by the gap between how much each candidate attenuates the clacks and how much it attenuates speech — the gap is what matters, since loudnorm follows and makes absolute level irrelevant.

Clack suppression vs speech damage higher gap is better
adeclick — the original choice0.4 dB
afftdn alone6.0 dB
arnndn alone6.2 dB
arnndn + afftdn12.1 dB

Tone breaks the tie. Measured as high-band minus low-band energy, the source sits at −13.4 dB and this chain lands at −15.0 dB — 1.6 dB from the source, while taking 18 dB off the clacks. More aggressive alternatives scored better on the gap and left a dulled, underwater voice.

Both denoisers buffer, and nothing in ffmpeg compensates it: arnndn delays 9.94 ms and afftdn 25.00 ms. Without a trim, turning the option on pushed the entire audio track ~35 ms behind the picture — a lip-sync error nobody would attribute to a denoiser. The chain trims its own latency off the front.

Redaction

Blur and pixelate are applied at export, on a shared preset ladder so switching method never silently changes how much is recoverable. The QML preview and the ffmpeg export compute strength from the same function — Qt's MultiEffect and ffmpeg's gblur are different kernels, so the preview value is the export sigma mapped back through a measured ratio. Calibrated to match, not chosen to look right.

Redactions are never partially transparent and never faded. A translucent blur box leaks the pixels it exists to hide.

03Using it

Three surfaces: the setup bar, the HUD, and the editor.

Before recording

bin/omarchy-capture-screenrecording   # opens the setup bar; run again to stop

Worth binding to a key. There is no PrintScreen on every keyboard, and the stock Omarchy capture bindings assume there is:

o.bind("SUPER + SHIFT + code:13", "Screenrecording (studio)",
       "/path/to/omarchy-studio/bin/omarchy-capture-screenrecording")

Two options are worth understanding before you press Start:

  • Just this window captures only that window's own surfaces — anything stacked on top is not in the take. Needs the native window recorder built; without it you get the window's rectangle, and whatever is over it.
  • Follow window records the whole display and carries your selection as a crop, so a window that moves mid-recording can be followed afterwards. It puts the whole screen on disk, which is a privacy consideration: everything beside the window is in the bundle where an export could reach it.

While recording — the HUD

A floating pill with elapsed time, a live microphone level, and three buttons: Pause, Discard and Stop.

It is kept out of the recording by the compositor, and it refuses to appear at all rather than be recorded — if there is nowhere to put it outside the capture and the compositor cannot hide it, it closes itself and tells you the keyboard shortcut instead.

Clicking Stop does not end up in your video. The HUD's own rectangle is recorded as UI, and clicks inside it are dropped from the click track — otherwise every take would end with the zoom lunging at a button the viewer cannot see.

The editor

The omarchy-studio editor: canvas with the recording on a backdrop, a layer list on the left, settings on the right, and the timeline underneath.
Canvas, layer list, settings panel, and the timeline underneath.

The timeline

RowShows
screenthe take, as uniform film cells
layersone sub-row per layer, front-most first, each spanning its time range
zoomthe zoom moves — the row you actually work in, so its label is accented
clicksevery recorded click; the ones that produced a zoom read brighter
audiothe audio track

Cuts are a fold, not a hole. A cut span collapses to a 16-pixel seam that spans every row at once — the x-axis itself folds, so one cut is one object rather than a gap per row. Click a seam to expand it and see what is inside.

Making a cut

Press C to drop a mark at the playhead, move to the other end, and press C again to commit. Backspace removes a cut — but only one you have expanded, so a stray press with nothing open cannot take time out of the movie.

Zoom

Zoom is automatic, derived from your clicks, with amount, hold, ease and merge gap in the panel. To remove a single unwanted move — the click on a menu you would rather not zoom at — select it on the zoom row and press Delete. It stays deleted through every later edit. When any are deleted, the panel grows a restore button, because a deleted move leaves no gap on the timeline to click.

Layers and the camera

Text, images, captions, shapes and redactions, each with its own inspector plus a shared when section for its time range. Draw tools live on the canvas — select, text, blur, pixelate; Esc disarms back to select.

Because the camera is its own stream, the bubble can be moved, resized, reshaped or mirrored at any time. Shadow lifts it off the picture, on all three shapes, and scales with the bubble so shrinking the camera shrinks its shadow. S splits the camera segment at the playhead — deleting the right-hand half is how a head that is on for the whole take becomes one that goes away partway.

Export

Pick 1080p, 1440p, 4k or native. A preset is a ceiling, never a target: asking for 1440p from a 1080p capture gives you 1080p, because inventing pixels costs time and file size to make the picture no better. When the render finishes, your file manager opens at the result.

Keyboard shortcuts

KeyDoes
Spaceplay / pause
step one frame
Cmark a cut, then commit it
Backspaceremove the expanded cut
Deletedelete the selected layer, or the selected zoom
Ssplit the camera segment at the playhead
Ppreview mode — hide every editing handle
Lshow or hide the layer list
Escdisarm the draw tool, clear a selection, or leave preview mode
Ctrl+Ssave
Ctrl+Zundo
Ctrl+Shift+Zredo

Every letter shortcut is suppressed while a text field has focus, so typing a layer's name cannot fire an edit.

Worth knowing

  • Undo is a whole-project snapshot, not a per-operation inverse, and drags coalesce into one step — so one Ctrl+Z undoes one gesture, not sixty frames of a drag.
  • Saving is atomic. A crash mid-save cannot leave an unparseable edit.json.
  • A take survives being stopped by something else. If the recorder is killed from outside — the stock bar indicator, a pkill, a crash — the bundle is finalised anyway, either by a watcher or the next time the dispatch runs.