Skip to content

Repository files navigation

ScreenRight

A macOS-only screen recorder for product demos. Agent-driven by default, with a menu-bar app for human-driven follow-mode recording. Captures real clicks, real scrolling, and real window state — then composites them into a cinematic, presentation-ready MP4 with spring-eased camera motion, a synthetic cursor, motion blur, and a clean desktop stage.

Zero LLM usage. Every frame is produced locally; no network calls are made during capture or render.

Quickstart (human, no agent)

Install packaged app (recommended)

Download ScreenRight_<version>_universal.dmg from latest release, open it, then drag ScreenRight.app onto Applications. Launch it from Launchpad or /Applications; signed GUI bundle does not open Terminal.

Starting your first recording or running Run doctor prompts for Accessibility and Screen Recording. Approve ScreenRight and its signed helper in System Settings → Privacy & Security.

Build from source (contributors)

git clone https://github.com/Orthic-Labs/screenright
cd screenright
pnpm install
pnpm build:native
bin/$(node -p 'process.arch')/ScreenRight

Last command is developer executable running in current Terminal. Normal users should install universal .app from DMG. Release maintainers use pnpm release:build:mac; RightKit builds, signs, notarizes, verifies & seals DMG.

Once running, the menu-bar icon gives you:

  • Window / Display — records the previously frontmost app window or primary display.
  • Quality — 1080p or 4K delivery at 30 or 60 fps. 4K exports at 3840×2160 for 16:9; sourceNative4K / upscaled in the result says whether source pixels were natively large enough.
  • Cursor — Polished, System, or Hidden. Settings adds cursor size, click feedback, and idle behavior.
  • Audio — Off, System audio, or Microphone.
  • Display controls — optionally include Menu Bar, Dock, or notifications. Menu Bar and Dock apply only to Display recording.
  • Save To / Settings — chooses output folder and 16:9, 9:16, or 1:1 aspect.
  • Open Last Video / Reveal in Finder — opens the verified saved MP4.
  • Run doctor / Check for Updates — inspects local prerequisites or fetches a signed update manifest on request.

Ending a recording

Reopen the red ScreenRight menu-bar icon and click Stop recording, or press Escape while recording. Both paths stop capture cooperatively, then continue automatic direction, render, verification, and save. The tray changes to Processing… while this finishes; do not force-quit it.

When notification permission is enabled, a desktop notification fires on start, finish, and failure.

Headless from the installed bundle

Same CLI shipped inside .app works from any folder:

/Applications/ScreenRight.app/Contents/Resources/runtime/bin/node \
  /Applications/ScreenRight.app/Contents/Resources/app/bin/screenright.mjs doctor

Quickstart (agent)

node bin/screenright.mjs doctor                 # JSON host preflight
node bin/screenright.mjs inspect \
  --scenario <scenario.json> \
  --foreground-ok                        # dry-walk the script
node bin/screenright.mjs run \
  --scenario <scenario.json> \
  --foreground-ok --desktop-ok           # capture + render
node bin/screenright.mjs render \
  --session .cache/demos/<session> \
  --timeline .cache/demos/<session>/render-plan.json      # retry render

Tray lifecycle diagnostics are redacted NDJSON at ~/Library/Application Support/ScreenRight/diagnostics/runtime.ndjson. Each recording logs start plus terminal success/failure with stable scenario identity. See docs/AX-AGENT-EXPERIENCE.md for stable AX identifiers, lifecycle semantics, and installed agent acceptance. ScreenRight exposes native macOS AX controls plus the JSON CLI; it does not ship an MCP server or hosted control hub.

The recorder writes an automatic, retryable session bundle under .cache/demos/<name>-<stamp>/:

  • scenario.json — the input that drove the take
  • events.ndjson — append-only event timeline (clicks, scrolls, focus, …)
  • timeline.json — derived camera/cursor keyframes
  • style.json — resolved style contract for the renderer
  • clean.mov — privacy-scrubbed raw capture
  • final.mp4 — the presentation deliverable

Retry rendering from the immutable session without taking the app again.

Follow mode — you drive, it records

A scripted scenario has to name every control it clicks, and a wrong name does not fail loudly: the click still fires, the step still reports ok: true, and the take is quietly wrong. Follow mode removes the targeting problem by letting a human drive.

node bin/screenright.mjs follow --scenario <file> --seconds 90 \
  --foreground-ok --desktop-ok

It prepares the clean desktop, starts capture, then stays out of the way for --seconds. Press Escape to end early. Real clicks are read from a CGEventTap and emitted in the same shape a scripted run produces, so the camera work is identical — spring zoom onto each click, pans between them, wide when idle.

Two differences from a scripted run, both deliberate:

  • The real cursor is kept unless you ask for a synthetic one (capture.cursor.style: "synthetic"). The tapped path is de-jittered with a One Euro filter (Casiez 2012) before it reaches the renderer, because a human hand shakes in a way a generated path does not.
  • Clicks are thinned, not rejected. A human may click any number of times, so the focus list is evenly reduced to the camera cap (first and last always kept) rather than failing at compositing.

Smart motion (Screen Studio-class polish, baked in)

Three treatments are on by default:

  • Spring camera. Every pan and zoom uses a physical spring: zoom is critically damped (scale never overshoots), pans carry capture.camera.bounce (default 0.15, Apple-style settle). capture.camera.easing: "cosine" restores the old curve.
  • Synthetic cursor (capture.cursor). The real cursor is hidden at capture (ScreenCaptureKit showsCursor=false) while the driver still physically moves and clicks it. The native renderer redraws the exported macOS arrow from the event timeline, hotspot-anchored with a press pulse and scale (default 1.5×) for presentation size.
  • Motion blur. Activation ramps continuously with pan and zoom velocity; the shutter window and sample count (1–8) both grow with velocity, so fast pans read as cinematic shutter drag instead of a fixed four-sample blur snapping on at a threshold. Disable with capture.camera.motionBlur: false.

Window scope additionally gets framing (capture.frame, default on): the app is composited at exact fitted dimensions with rounded corners (radius, default 14 logical px, rendered via a single-frame antialiased mask) and a soft drop shadow (shadow: false to disable) over the blurred backdrop. Display scope rejects capture.frame — the full desktop is its own frame.

Capture scope and quality

capture.scope controls framing:

  • "window" is the safe default and records only the target app window.
  • "display" records the full primary desktop for a more cinematic app-in-context look. It requires the separate --desktop-ok acknowledgement because wallpaper, menu-bar items, notifications, and transient system overlays can appear.

capture.quality controls delivery:

  • "high" is the 1920×1080 H.264 default.
  • "maximum" preserves the Retina ScreenCaptureKit source, renders 3840×2160 at 60 fps with Lanczos scaling, and encodes H.264 with the slow preset at CRF 10. This is the preferred presentation master; use a separate ProRes transcode only when a downstream editor explicitly requires it.

For "display", clean-desktop preparation is automatic and mandatory. Before capture, the runner hides every currently visible regular app except the allowlisted target, brings the target window forward, and records the exact hidden process IDs. It restores those same apps after capture, including when recording or app control fails. Some apps cannot be hidden: the runner moves their visible windows beyond the primary display first, falls back to minimizing only when movement is unavailable, and restores the exact window state afterward. It aborts rather than record if any unrelated visible window cannot be cleared.

The privacy-sensitive raw capture is deleted automatically after the scrub stage, including on a scrub or render failure. It is never written into the session bundle: it lives in a screenright-raw directory under the system temp dir, and every run sweeps that directory before capturing. A finally cannot run when the process is killed — keeping the file out of the bundle and sweeping on start is what makes the rule hold under SIGKILL.

Display and window recording use Apple's ScreenCaptureKit through the native helper at the target display's reported backing-pixel scale, rather than downscaling to logical points. Source dimensions therefore vary by display. The helper reports readiness before scripted actions begin, records the real cursor with click circles disabled, and adds one-pixel/no-click start and end anchors for display scope so static pre-roll and post-roll retain their wall-clock duration. The runner probes both source and final durations and rejects truncated files.

The native helper accepts input only from a validated scenario file, allowlists one bundle identifier per run, refuses input when the target app loses focus, and constrains every pointer action to the resolved target window.

Camera actions

The native per-frame renderer evaluates camera and cursor state directly instead of encoding motion as nested FFmpeg expressions. Add a camera-only focus action when the visible result appears somewhere other than the clicked control. Configure the movement under capture.camera; set mode to static when a shot should remain wide.

{
  "action": "focus",
  "target": { "fallback": { "x": 0.33, "y": 0.38 } },
  "zoom": 1.22,
  "holdMs": 1400
}

focus resolves the same semantic identifier, label, role, or bounded window-relative fallback as a click, but it sends no mouse or keyboard input. zoom is optional; when omitted, the compositor fits the resolved element rectangle with margin and caps it at capture.camera.zoom.

Hard-won constraints

Each of these cost a full recording before it was understood. Do not "optimise" them back.

  • The old 20-focus and 96-keyframe ceilings are retired. They were measured limits of the nested FFmpeg expression compositor, not motion-quality rules. The native renderer has a generous 60-move validation bound; the director coalesces and paces dense action streams.
  • Escape aborts, via an event tap — never by polling key state. CGEventSource.keyState reports whether a key is held at that instant, so a tap is already released by the next poll, and synthesized presses never register at all. Both were verified failing.
  • Label matching is a case-insensitive contains. "ViewRight" matched 13 elements including the window title viewscreenright.md, so a click aimed at the H1 landed in the title bar. Prefer long, unique body text.
  • AXHeading rects span the full column width; the glyphs do not. Clicking the centre lands in empty space, which a rich-text editor reads as a block selection rather than a caret. Target the AXStaticText inside it.
  • The accessibility tree populates asynchronously. It can hold 2 elements one moment and 43 the next. Resolution retries before falling back, and a named target that cannot be resolved now aborts the run instead of guessing at a coordinate.
  • A raw display capture is deleted even when compositing fails. That is a privacy rule, not a bug — so validate anything that can fail at compositing before the take instead.

Limitations

  • macOS only. There is no Windows or iOS backend. The native driver refuses to build or run anywhere else.
  • Audio can be Off, System, or Microphone. Microphone mode requires the macOS permission surfaced by tray preflight.
  • Display scope is primary-display only. Window scope uses ScreenCaptureKit's independent-window filter at the target display's backing scale and supports secondary displays and negative bounds.
  • Accessibility and Screen Recording must be granted once, by hand. The driver is signed with the shared Developer ID identity, which keeps its designated requirement stable across rebuilds, so a grant survives future builds. On a machine without that certificate the build falls back to ad-hoc signing, where every rebuild changes the binary's identity and silently revokes both permissions. bin/screenright.mjs doctor reports the current state and prints the exact re-grant step in nextSteps.
  • No detection of an active human. The recorder takes over the pointer and hides applications without checking whether someone is mid-task. Interacting during a take changes window state underneath the preparation snapshot and can make restore partially fail. Stop working before starting a run.

Repository layout

src/           # recorder pipeline, director, scenarios, motion, audio, render
test/          # node --test suite (unit + contract)
native/        # Swift native helper (CGEvent tap, ScreenCaptureKit, AX)
menu/          # Swift menu-bar controller (NSStatusItem)
schemas/       # v2 JSON schemas for scenario / timeline / style / session / etc
scripts/       # build-native.mjs, render-fixtures.mjs, build-app.mjs, build-dmg.mjs, notarize.mjs
fixtures/      # baseline outputs + schema validity fixtures
bin/           # built binaries (gitignored); bin/screenright.mjs ships in the .app
dist/          # built .app bundle and DMG (gitignored)
package.json   # @damned/screenright

Icons

The build pulls icon assets from project-owned sources so local, CI, and release builds use identical bytes:

  • App bundle icon: assets/ScreenRight.iconset/ (iconutil-compatible directory of standard macOS sizes). build-app.mjs converts it to AppIcon.icns and drops it into Contents/Resources/. The CFBundleIconFile + CFBundleIconName keys in Info.plist reference it.
  • Menu-bar template glyph: assets/record-template-pack/black/ (22/44/66 PNGs). build-app.mjs copies them into Contents/Resources/record-template{,.png,@2x.png,@3x.png}; the Swift binary loads via NSImage(named:) and sets isTemplate = true so macOS recolors for light/dark menu bars.

Local development builds can fall back when either path is missing. RightKit release packaging requires every icon input and binds its bytes into release input receipts.

Releasing

Production releases use shared RightKit release contract on provisioned Mac. Build signs, notarizes, verifies, hardens & seals one immutable installer; it never uploads. Upload accepts only exact sealed release ID & publishes finished artifacts through GitHub Releases. Hosted CI runs no signing or release work.

pnpm release:doctor
pnpm release:build:mac
pnpm release:upload:patch:mac -- --release <sealed-id>

release:build:mac resolves approved arm64 & x86_64 Node, FFmpeg, FFprobe, minisign & libsodium paths from generated release input catalogue. Corresponding SCREENRIGHT_*_ARM64 & SCREENRIGHT_*_X86_64 variables can override those paths. Node must be version 22. RightKit binds source and tool inputs to the release, runs the complete test suite, invokes scripts/release-package.mjs, verifies Developer ID signatures, notarization ticket, Gatekeeper, disk-image integrity, embedded-app identity, and hardening, then seals one ScreenRight_<version>_universal.dmg under .right-release/.

Apple credentials resolve in standard RightKit order:

  1. APPLE_API_KEY_PATH / APPLE_API_KEY / APPLE_API_ISSUER (App Store Connect API key)
  2. --keychain-profile apple-dev-notary (login keychain profile)

Missing or partial credentials fail release packaging. The app is submitted inside its signed DMG; successful notarization is stapled and validated before RightKit can seal it. One universal updater payload serves both darwin-aarch64 and darwin-x86_64 manifest rows.

RightKit consumer checks are part of release preflight:

pnpm legal:check
pnpm cargo:check
pnpm deps:check

ScreenRight is an AppKit/Swift plus bundled Node application. Its src-tauri crate is a release-metadata shim, not runtime code, so it intentionally carries no unused RightKit Rust dependency.

The DMG mounts to a ScreenRight <version> volume that contains the .app, a /Applications symlink for drag-to-install, and a short README.

License

MIT — see LICENSE.

Third-party runtime and asset terms, exact Node/FFmpeg input paths and hashes, FFmpeg LGPL source/build note, and receipt policy are generated in legal/THIRD_PARTY_NOTICES.md. pnpm legal:check fails when this notice drifts from src/license-notices.mjs.

Repository truth docs

  • Product overview — what this is and does (generated, code-grounded)
  • Architecture — components, flows, interfaces (generated, code-grounded)

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages