Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Full history. The sitemap's <lastmod> is read from `git log` per
# file, and the default depth-1 clone gives every file the deployed
# commit's date — all sixteen dated URLs carried the same day.
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
Expand All @@ -53,6 +57,9 @@ jobs:
- name: Type-check
working-directory: website
run: npm run typecheck
- name: Unit tests
working-directory: website
run: npm test
# The walkthrough's clips and stills are committed to this repo, which has
# no LFS filter — their size is permanent, so a budget that lives only in a
# design note drifts on the first re-cut. This also fails any clip encoded
Expand All @@ -70,9 +77,28 @@ jobs:
- name: Check recreation data
working-directory: website
run: npm run check:recreation
# The config reads the star count and the latest release from the GitHub
# API once per build. Unauthenticated, that is the runner IP's 60-an-hour
# quota; the workflow token lifts it, with the read-only contents
# permission this job already has.
- name: Build
working-directory: website
env:
GITHUB_TOKEN: ${{ github.token }}
run: npm run build
# The rspack-minimizers plugin in docusaurus.config.ts edits options that
# Rspack keeps privately, so an Rspack update can make it a silent no-op:
# the license files disappear and range media queries come back, and the
# build still passes. `if` rather than `! grep`, which `bash -e` ignores
# unless it is the last command.
- name: Check minifier output
working-directory: website/build
run: |
ls assets/js/main.*.js.LICENSE.txt
if grep -qE '\((width|height)[<>]' assets/css/*.css */assets/css/*.css; then
echo "Range media syntax in CSS: the rspack-minimizers plugin no longer applies."
exit 1
fi
- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ openscreen record --duration 20 --project demo.openscreen --json
openscreen export demo.openscreen -o demo.mp4 --json
```

See [docs/cli.md](./docs/cli.md).
See the [CLI reference](https://getopenscreen.com/docs/cli/).

## Installation

Expand Down
6 changes: 5 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
},
"formatter": {
"enabled": true,
"includes": ["**", "!website/src/components/Recreation/generated.ts"],
"includes": [
"**",
"!website/src/components/Recreation/generated.ts",
"!website/i18n/**/*.json"
],
"indentStyle": "tab",
"formatWithErrors": true,
"lineEnding": "lf",
Expand Down
268 changes: 2 additions & 266 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,269 +1,5 @@
# OpenScreen CLI

Headless command-line interface for recording the screen and exporting `.openscreen`
projects — no visible windows, machine-readable output. Designed so scripts, CI
pipelines, and AI coding agents can produce polished product demos automatically:
The CLI reference moved to the documentation site: **https://getopenscreen.com/docs/cli/**

```text
record → edit the project JSON programmatically → export → MP4/GIF
```

## Running

Development — build once per checkout:

```bash
npm run build-vite # renderer + main
npm run build:native:mac # capture helpers (recording)
npm run fetch:ffmpeg:mac && npm run build:native:compositor:mac # Rust compositor (export)
bash scripts/build-whisper-stt.sh # STT server (captions; model downloads on first run)
```

Then:

```bash
npm run cli -- <command> [options]
# or directly:
./node_modules/.bin/electron . <command> [options]
```

Packaged app (the CLI ships inside the normal binary):

```bash
# macOS
/Applications/Openscreen.app/Contents/MacOS/Openscreen export demo.openscreen -o demo.mp4
# Windows
"C:\Program Files\Openscreen\Openscreen.exe" export demo.openscreen -o demo.mp4
```

CLI runs skip the single-instance lock, so they work while the GUI app is open.

## Commands

### `openscreen record`

Records headlessly through the same pipeline as the GUI: the native
ScreenCaptureKit helper on macOS, the WGC helper on Windows (browser capture as
fallback). Recordings land in the app's recordings directory
(`<userData>/recordings/`), exactly like GUI recordings — including the
`.cursor.json` cursor-telemetry sidecar used for editable cursors and auto-zoom.

```bash
openscreen record --duration 30 --project demo.openscreen --json
openscreen record --window "My App" --mic --system-audio
openscreen record --display 1 --cursor system
```

| Option | Meaning |
|---|---|
| `--display <n>` | Screen index to record (default 0) |
| `--window <title>` | Record the first window whose title contains `<title>` |
| `--mic` / `--mic-device <name>` | Capture microphone (optionally by device-label substring) |
| `--system-audio` | Capture system audio |
| `--cursor <editable-overlay\|system>` | Hide the system cursor and record telemetry (default), or bake it into the video |
| `--duration <seconds>` | Stop automatically |
| `--project <out.openscreen>` | Write a ready-to-export project file when done |
| `--json` | NDJSON events on stdout |

Stopping without `--duration`: send SIGINT/SIGTERM to the process, or type
`stop` + Enter on its stdin.

Platform notes:

- **macOS**: requires the Swift helper (`npm run build:native:mac`, needs Xcode)
and the Screen Recording permission for whatever binary hosts Electron
(your terminal during development). Webcam capture is not available in CLI
recording on macOS (same limitation as the native helper).
- **Windows**: requires the WGC helper (`npm run build:native:win`). SIGTERM
does not exist on Windows — stop recordings with Ctrl+C, stdin `stop`, or
`--duration` (a hard `taskkill` loses the recording). Microphone access is
gated by Settings → Privacy → Microphone; there is no programmatic prompt.
- **Linux**: uses the browser capture pipeline; cursor options are limited,
matching the GUI. On Wayland, capture goes through the PipeWire portal,
which may show a system picker dialog and requires a desktop session
(headless/SSH sessions without a portal cannot record).

### `openscreen sources`

Lists capturable displays, windows, and microphones — the same enumeration the
GUI picker uses — so scripts and agents can choose `--display`, `--window`, and
`--mic-device` values without guesswork.

```bash
openscreen sources # human-readable
openscreen sources --json
openscreen sources -o sources.json # straight to a file
```

`--json` on stdout is the normal path and works: Chromium's own diagnostics go
to stderr, so a pipe carries only the CLI's output.

What the CLI cannot control is the wrapper around it. Ubuntu's `xvfb-run` — the
usual way to run a GUI binary on a headless machine — merges the command's
stderr into its stdout, and under it Chromium's startup complaints about D-Bus
and OpenGL arrive ahead of the JSON, so `openscreen sources --json | jq` fails.
Other launchers and log collectors do the same.

`-o <file>` writes the result somewhere no wrapper can redirect. It also avoids
shell quoting and encoding differences, which is worth more on Windows than on
POSIX. The file is written only on a successful run; a file left by an earlier
run is not touched when a later one fails, so check the exit code rather than the
file's presence.

**The two channels carry different shapes.** `--json` on stdout wraps the payload
in the NDJSON `done` envelope, because it is one event in a stream. `-o` writes
the payload on its own, because a file is not a stream:

```bash
openscreen sources --json | jq '.sources.displays' # stdout: inside the envelope
openscreen sources -o s.json && jq '.displays' s.json # file: the payload itself
```

`--json` emits the payload on the final `done` event:

```json
{
"event": "done",
"success": true,
"sources": {
"displays": [{ "index": 0, "id": "screen:1:0", "name": "Entire screen" }],
"windows": [{ "id": "window:210:0", "name": "My App" }],
"microphones": [{ "label": "MacBook Pro Microphone (Built-in)" }],
"microphoneLabelsUnavailable": false
}
}
```

### `openscreen export`

Renders a project to MP4 or GIF using the app's real export pipeline (WebCodecs +
PixiJS, faster than realtime) in a hidden window. Falls back to SwiftShader when
no GPU is available (CI), and applies everything the editor would: zooms, trims,
speed regions, wallpaper/padding, annotations, cursor rendering, webcam layouts.

```bash
openscreen export demo.openscreen # format/quality from the project
openscreen export demo.openscreen -o out.mp4 --quality source
openscreen export demo.openscreen -o out.gif --gif-fps 20 --gif-size large
openscreen export demo.openscreen --json | while read line; do ...; done
```

| Option | Meaning |
|---|---|
| `-o, --out <path>` | Output file; extension picks the format. Default: next to the project |
| `--format <mp4\|gif>` | Override the project's stored format |
| `--quality <medium\|good\|source>` | MP4 quality |
| `--gif-fps <15\|20\|25\|30>`, `--gif-size <medium\|large\|original>` | GIF settings |
| `--auto-zoom` | Add automatic zooms from cursor telemetry before rendering — the same dwell-detection engine as the editor's magic wand. Existing zoom regions are kept; suggestions never overlap them |
| `--audio <file>` | Mix a voiceover file into the MP4 (mp3/wav/m4a — anything Chromium can decode; AIFF is not supported) |
| `--audio-mode <mix\|replace>` | Layer the voiceover over the recording's audio (default `mix`) or replace it |
| `--audio-offset <seconds>` | Delay before the voiceover starts (default 0) |
| `--json` | NDJSON progress + result on stdout |

`--audio` mixes after the native render: the exported file is read back, video
packets are copied untouched, and the audio is mixed offline
(OfflineAudioContext) and re-encoded to AAC before overwriting the output.
MP4 only.
In `mix` mode the original audio is ducked to 40% under the voiceover so the
sum cannot clip; use `replace` to drop the original entirely.

**Media path rule**: for safety, a project's referenced media is only auto-approved
when it lives in the app's recordings directory or **next to the project file**.
Keep `.openscreen` files beside their media (or record via the CLI, which uses
the recordings directory).

**No cancel**: the native compositor has no abort mechanism — killing the CLI
mid-export stops output but the render worker runs until process exit.

### `openscreen pack`

Copies a project and everything it references (screen/webcam video, cursor
telemetry sidecar) into one portable folder and rewrites the project's media
paths:

```bash
openscreen pack demo.openscreen --out bundle/
```

The folder survives being moved or shipped as a CI artifact: when the stored
absolute paths go stale, the loader falls back to files with the same basename
next to the project file.

### `openscreen captions`

Transcribes the project's audio with the app's on-device Whisper model (no
upload; language auto-detected) and writes the resulting caption annotations
into the project. Re-running replaces earlier auto-captions; manual annotations
are preserved.

```bash
openscreen captions demo.openscreen --min-words 2 --max-words 7
openscreen export demo.openscreen -o demo.mp4 # subtitles are burned in
```

Requires an audio track in the project's video (e.g. `record --mic`, or a
voiceover mixed in with a re-recorded source). Transcription runs on the
native whisper.cpp engine (ggml-small); the model downloads automatically on
first use.

### `openscreen info`

Prints a project summary (referenced media and whether it exists, format,
region counts). Exits non-zero if the referenced video is missing.

```bash
openscreen info demo.openscreen --json
```

## Machine-readable output (`--json`)

One JSON object per line on stdout (NDJSON). stderr carries diagnostics only.

```jsonl
{"event":"started","command":"export"}
{"event":"progress","percentage":42,"currentFrame":50,"totalFrames":120,"estimatedTimeRemaining":3}
{"event":"done","success":true,"outputPath":"/path/out.mp4","format":"mp4","width":1920,"height":1080}
```

Record emits `log` events (`Recording started`, …), `stopping`, and a final
`done` carrying `screenVideoPath`, `cursorDataPath`, `durationMs`, and
`projectPath` when `--project` was used. Exit code is 0 on success, 1 on
failure, 2 on bad arguments.

## Example: automated product demo (for scripts/agents)

```bash
# 1. Record 20 seconds of the running app
openscreen record --window "MyProduct" --duration 20 --project demo.openscreen --json

# 2. Edit the project: add a zoom and a caption (plain JSON)
node -e '
const fs = require("fs");
const p = JSON.parse(fs.readFileSync("demo.openscreen", "utf8"));
p.editor.zoomRegions.push({ id: "z1", startMs: 2000, endMs: 6000, depth: 3,
focus: { cx: 0.5, cy: 0.4 }, focusMode: "manual", source: "manual" });
p.editor.annotationRegions.push({ id: "a1", startMs: 500, endMs: 4000,
type: "text", content: "One-click setup", textContent: "One-click setup",
position: { x: 8, y: 6 }, size: { width: 40, height: 12 },
style: { fontSize: 24, color: "#fff" }, zIndex: 1 });
fs.writeFileSync("demo.openscreen", JSON.stringify(p, null, 2));
'

# 3. Narrate with any TTS (macOS `say` shown; any engine producing mp3/wav/m4a works)
say -o voice.m4a --file-format=m4af "Welcome to my product. Here's a quick tour."

# 4. Render with auto-zooms; the voiceover replaces the recording's own audio
# (drop --audio-mode replace to duck the original under the narration instead)
openscreen export demo.openscreen -o demo.mp4 --auto-zoom --audio voice.m4a --audio-mode replace --json
```

## Architecture

- `electron/cli/args.ts` — pure argv parser (unit-tested in `args.test.ts`).
- `electron/cli/cliMain.ts` — headless boot: no HUD/tray/menu/dock, stdio
protocol, signal handling, exit codes. Registers the same IPC surface as the
GUI (`registerIpcHandlers`) with inert window callbacks.
- `src/cli/CliExportRunner.tsx` / `src/cli/CliRecordRunner.tsx` — hidden-window
runners (`?windowType=cli-export|cli-record`) that drive the existing
exporter classes and `useScreenRecorder` hook.
- Contracts shared between main and renderer: `src/lib/cliContracts.ts`.
Its source is [`website/docs/cli.md`](../website/docs/cli.md) in this repository. Edit that file, not this one.
2 changes: 1 addition & 1 deletion electron/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type CliCommand = (
* A named file is a channel no wrapper can redirect into. It also sidesteps
* shell quoting and encoding, which matters more on Windows than on POSIX.
*
* Note the shapes differ, and docs/cli.md says so: stdout carries the payload
* Note the shapes differ, and website/docs/cli.md says so: stdout carries the payload
* inside the NDJSON `done` envelope because it is one event in a stream, while
* the file carries the bare payload because a file is not a stream. Written on
* success only.
Expand Down
4 changes: 2 additions & 2 deletions electron/cli/cliMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async function writeProjectFile(projectOut: string, projectData: unknown): Promi

function setupRecordStopSignals(stop: (reason: string) => void): void {
// SIGINT covers Ctrl+C everywhere; SIGTERM never fires on Windows, where
// stdin "stop" or --duration are the graceful alternatives (see docs/cli.md).
// stdin "stop" or --duration are the graceful alternatives (see website/docs/cli.md).
process.on("SIGINT", () => stop("SIGINT"));
process.on("SIGTERM", () => stop("SIGTERM"));
try {
Expand Down Expand Up @@ -447,7 +447,7 @@ export function runCli(command: CliCommand): void {
await fs.mkdir(path.dirname(command.jsonOutPath), { recursive: true });
// Write beside the target and rename over it. writeFile truncates
// first, so a failure part-way through -- a full disk is the easy
// case -- would leave a half-written file where docs/cli.md promises
// case -- would leave a half-written file where website/docs/cli.md promises
// an earlier run's result is untouched by a later failure. rename is
// atomic within a directory, so the reader sees the old file or the
// new one and never a torn one.
Expand Down
6 changes: 3 additions & 3 deletions website/blog/2026-06-15-picking-up-openscreen.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ image: /img/og-image.png

The last commit on [siddharthvaddem/openscreen](https://github.com/siddharthvaddem/openscreen) landed on June 6, 2026. It bumped the Nix package to v1.5.0. Then the repo went read-only, like the README had been warning it would for a while. 39k stars, and v1.5.0 as the final release.

I picked it up on June 15, with the original author's approval. Same name, same MIT license, new URL. This post starts a journal of what happens next.
I picked it up on June 15, with [the original author's approval](https://github.com/siddharthvaddem/openscreen#readme). Same name, same MIT license, new URL. This post starts a journal of what happens next.

<!-- truncate -->

## Where it lives now

[github.com/getopenscreen/openscreen](https://github.com/getopenscreen/openscreen). It started as a personal fork and moved under the `getopenscreen` org in the first week. It stays there.
[github.com/getopenscreen/openscreen](https://github.com/getopenscreen/openscreen). It started as a personal fork and moved under the `getopenscreen` org in the first week. It stays there. Installers are on the [download page](/download/), and the [docs](/docs/intro/) cover installing, recording, editing and export.

The archived original is still online and still read-only. Every commit of it is in this repo's history.

Expand All @@ -28,7 +28,7 @@ Forking a popular archived project is a good way to quietly turn it into somethi
- Stability before features. The recorder has to work on macOS, Windows and Linux. Bugs from real users go first.
- It's not production-grade, and I'll keep saying so. Expect rough edges and breaking changes, including to the project format.

The [roadmap](https://github.com/getopenscreen/openscreen/blob/main/ROADMAP.md) is public: record, edit, export, plus an optional AI editing layer that's off by default and never required. There's a [Discord](https://getopenscreen.com/discord) with a roadmap channel if you want to argue about any of it.
The [roadmap](https://github.com/getopenscreen/openscreen/blob/main/ROADMAP.md) is public: record, edit, export, plus an optional AI editing layer that's off by default and never required. There's a [Discord](https://getopenscreen.com/discord/) with a roadmap channel if you want to argue about any of it.

## It already had contributors

Expand Down
Loading
Loading