Record and replay macOS mouse/keyboard interactions as human-readable Python scripts
Project description
screenplay-scripter
Record and replay macOS mouse/keyboard interactions as human-readable Python scripts. Recordings become plain Python DSL files you can hand-edit, then replay with smooth, interpolated mouse movement for polished tutorial screen recordings.
Why
Screen-recording a tutorial by hand is fiddly: jittery cursor paths, mistimed
clicks, and no way to redo a take cleanly. scripter captures your interactions
into an editable script, then replays them deterministically with eased mouse
motion so every take looks intentional.
Requirements
- macOS
cliclick5.1+
Install
Homebrew (recommended)
brew tap software-engineer-vinokurov/tap
brew install screenplay-scripter
uv
uv tool install screenplay-scripter
Or add to an existing project / run from a clone:
uv add screenplay-scripter # as a project dependency
uv sync && uv run scripter --help # from a clone
macOS Accessibility permission
scripter uses pynput to listen for global input events and cliclick /
Quartz to synthesize them. macOS gates both behind Accessibility permission.
- Open System Settings -> Privacy & Security -> Accessibility.
- Add and enable the app that runs
scripter— this is your terminal (Terminal.app, iTerm2, etc.), notscripteritself. - If you run through an IDE terminal, grant the IDE the permission.
- Restart the terminal after granting so the entitlement takes effect.
Without this permission, recording captures nothing and playback silently fails.
Usage
Record
scripter record demo.py
A menu bar indicator appears while recording:
- Title blinks
● REC/○ RECwhile active,○ recwhile paused. - Click the indicator to open the menu:
- Stop & Edit — save the script and open it in
$EDITORinside iTerm (or Terminal.app as fallback). - Quit (discard) — exit without saving.
- Stop & Edit — save the script and open it in
- Ctrl+C — stop recording, save, and open the editor.
Controls while recording:
- Ctrl+Opt — toggle recording ON/OFF (the gate). The chord itself is never written to the script.
- Ctrl+Opt+Shift — insert a
move(x, y)call for the current cursor position without clicking. Use this to record an explicit cursor path between two clicks. The chord itself is never written to the script.
The recorder starts PAUSED. Press Ctrl+Opt to arm it, perform your actions, press Ctrl+Opt again to pause. Section comments are inserted automatically:
# --- recording started ---
click(760, 540)
# --- paused ---
# --- resumed ---
type_text('hello')
To disable the menu bar and use terminal-only mode:
scripter record demo.py --no-menubar
# Ctrl+C ends the session, writes the script, and opens $EDITOR
Suppress automatic sleep() insertion between events:
scripter record demo.py --no-timing
Play
scripter play demo.py
The terminal shows a live status line while the script runs:
▶ Playing — Ctrl+Opt: stop
Press Ctrl+Opt at any time to abort; the line updates in place:
⏹ Stopped — Ctrl+Opt: stop
Show a menu bar progress indicator and enable pause/resume:
scripter play demo.py --review
The terminal status line now reads:
▶ Playing demo.py — Ctrl+Opt: pause/resume
Pressing Ctrl+Opt toggles it between playing and paused:
⏸ Paused demo.py — Ctrl+Opt: pause/resume
The --review menu bar shows ▶ 12/87 (current source line / total lines) and
includes a Previous Pauses submenu. As sleep() calls are executed, they
appear in the submenu (newest first, up to 10). Clicking an entry toggles it
between active and commented-out directly in the script file — no editor needed:
Previous Pauses
7.38 ← click to comment out: becomes # 7.38
# 1.00 ← click again to uncomment
0.25
Commented-out sleeps from a previous session are remembered and appear in the submenu as soon as the player walks past their line in the script, so you can toggle them without restarting from scratch.
Click the indicator itself for a Stop menu item.
Preview the exact cliclick argv without moving the mouse:
scripter play demo.py --dry-run
Override the easing calibration factor (default 555 — see Easing):
scripter play demo.py --easing 300
Environment equivalents (CLI flags take precedence):
SCRIPTER_DRY_RUN=1SCRIPTER_EASING=300
Easing
Mouse movement speed is kept constant across all distances using inverse proportional easing:
easing = factor × 1000 / distance
The --easing flag (default 555) is the cliclick easing coefficient applied
at exactly 1000 px. Shorter moves get proportionally higher easing so the cursor
always travels at the same apparent speed. Pass a smaller factor for faster
overall motion, a larger one for slower.
DSL reference
Every generated script begins with from scripter import *. Available calls:
| Function | Description |
|---|---|
click(x, y) |
Move to (x, y) with easing, then left-click. |
double_click(x, y) |
Move to (x, y) with easing, then double-click. |
right_click(x, y) |
Move to (x, y) with easing, then right-click. |
drag(start, end) |
Press at start, drag along eased path, release at end. |
scroll(x, y, amount) |
Position at (x, y), scroll amount lines (Quartz wheel). |
key(*keys) |
Key combo. Modifiers (cmd, shift, ctrl, alt, fn) held around the terminal key, e.g. key('cmd', 'c'), key('return'). |
type_text(text) |
Type a literal string (including spaces). |
sleep(seconds) |
Pause for seconds. |
move(x, y) |
Move the cursor with easing (rarely needed; click auto-moves). |
Named keys accepted by key(): return, space, tab, esc,
arrow-up/down/left/right, f1–f16, page-up, page-down, home, end,
and more (see cliclick's kp: list). Any single character not in that set is
typed with t:.
Double-clicks are detected automatically during recording: two left-clicks
within 0.5 s and 10 px of each other are collapsed into a single
double_click() call.
Capital letters typed while Shift is held are captured directly into
type_text(...) rather than emitted as key('shift', 'X') combos, so a
sentence like "Nice descent" records as one type_text('Nice descent') call.
Space typed in a text field is captured as part of type_text(...), not as
a separate key('space'). Space combined with a modifier (e.g. Cmd+Space for
Spotlight) is recorded as key('cmd', 'space').
Editing scripts
Recorded scripts are ordinary Python. Open and tweak them freely:
- Adjust coordinates, reorder actions, or delete takes you do not want.
- Tune
sleep()values to change pacing. - Add an explicit
move()for a deliberate cursor path between clicks.
Playback runs a warn-only AST scan first: non-DSL imports or unexpected
function calls are reported to stderr but do not block execution, so you can add
light control flow (for, range, print) when you need it.
Example hand-edited script:
from scripter import *
# --- recording started ---
click(760, 540)
sleep(0.50)
type_text('hello world')
key('return')
for i in range(3):
scroll(760, 540, -3)
sleep(0.30)
ffmpeg screen-recording wrapper
Capture the screen with ffmpeg while scripter drives the UI, so the whole
take is hands-off. Save as record-take.sh:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT="${1:?usage: record-take.sh SCRIPT.py [OUTPUT.mp4]}"
OUTPUT="${2:-take.mp4}"
# List devices to find your screen index:
# ffmpeg -f avfoundation -list_devices true -i ""
SCREEN_INDEX="${SCREEN_INDEX:-1}"
# Start the screen capture in the background.
ffmpeg -y -f avfoundation -capture_cursor 1 -framerate 30 \
-i "${SCREEN_INDEX}:none" "${OUTPUT}" &
FFMPEG_PID=$!
# Give the capture a moment to spin up, then play the script.
sleep 1
scripter play "${SCRIPT}"
# Stop ffmpeg cleanly.
sleep 1
kill -INT "${FFMPEG_PID}"
wait "${FFMPEG_PID}" 2>/dev/null || true
echo "Wrote ${OUTPUT}"
Run it:
chmod +x record-take.sh
./record-take.sh demo.py my-tutorial.mp4
Known limitations
- B1 indicator / unfocused terminal: If the terminal window is not focused,
macOS may show a
B1mouse-button state indicator and some synthesized events can be dropped. Keep the target app in focus during playback; drive from a background capture rather than clicking into the terminal mid-take. type_textcaveat: Typing is synthesized viacliclick t:. Characters requiring dead keys, IME composition, or non-US layouts may not render as expected; prefer ASCII, and usekey()for special keys.- Trackpad scroll fidelity: Recorded scroll is coarse line-based (
dycoalesced to integer lines). High-resolution/inertial trackpad scrolling is approximated and will not reproduce momentum exactly. - Horizontal scroll discarded: Only vertical scroll (
dy) is captured; the horizontal component (dx) is dropped during recording.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file screenplay_scripter-0.5.1.tar.gz.
File metadata
- Download URL: screenplay_scripter-0.5.1.tar.gz
- Upload date:
- Size: 32.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de016154d7157b8c5c082b6d8f429fd7065d0faf83e8e12cac12e887618c5eee
|
|
| MD5 |
c5fd8ed351753628d03f907192effa10
|
|
| BLAKE2b-256 |
0654efcdce3139f1967fb6b779ca7ca85618ac67782a041573383bf48a76309c
|
File details
Details for the file screenplay_scripter-0.5.1-py3-none-any.whl.
File metadata
- Download URL: screenplay_scripter-0.5.1-py3-none-any.whl
- Upload date:
- Size: 21.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd59d7c4eac75299f53828b27325cb8e832ea3c5121e91a8935745b8c340066f
|
|
| MD5 |
01289a46a803d548f9d05e09942966b0
|
|
| BLAKE2b-256 |
d353bf05e3585904c794be97eeada3a9b44c2ec70946c9fb62f12520c1471dbb
|