Untitled.mov
A macOS CLI and TUI for managing iOS simulators. A Swift replacement for ad-hoc
xcrun simctl wrappers, built as a single SwiftPM binary with no external
runtime dependencies beyond swift-argument-parser.
holodeck # full-screen TUI (default)
holodeck list # scripting subcommands for CI / shell composition
holodeck boot "iPhone 17 Pro"
holodeck record "iPhone 17 Pro" -o demo.mp4brew install otaviocc/apps/holodeckBuilds from source on your machine — no signing or notarization involved. Needs Xcode 26+ with Swift 6.2+.
Mint also works:
mint install otaviocc/HolodeckThis pulls a tagged release, builds it once, and drops holodeck on your
PATH.
git clone https://github.com/otaviocc/Holodeck.git
cd Holodeck
swift build -c release
cp .build/release/holodeck /usr/local/bin/ # or anywhere on PATHRequirements: macOS with Xcode installed (simctl lives there) and Swift 6.2+
— either via swiftly or the Xcode
toolchain (xcrun swift … works).
For development, swift build, swift test, and swift run holodeck … all
work from the repo root.
Run holodeck with no arguments to launch the TUI. The list refreshes every
two seconds; arrow keys move the selection; Enter boots or shuts down;
? opens an overlay listing every keybinding; q quits.
For CLI use, every subcommand accepts --help:
holodeck --help # all subcommands
holodeck record --help # one subcommand's optionsThe TUI polls simctl list --json every two seconds (polling pauses while
recording). User config is read once at launch from
~/.config/holodeck/config.json — see Configuration.
| Key | Action |
|---|---|
↑ ↓ / j k |
Navigate the simulator list |
Enter / Space |
Boot or shut down the selection |
R |
Force refresh |
r |
Start / stop recording (banner while active) |
p |
Screenshot |
a |
Appearance submenu (l light / d dark / Esc) |
n |
New simulator wizard (device type → runtime) |
f |
Focus Simulator.app on the selection |
e |
Erase (shut-down sims only; y/n confirm) |
d |
Delete (y/n confirm) |
P |
Privacy wizard (app → action → permission) |
? |
Help overlay |
q / Esc |
Quit (or cancel the active modal) |
The terminal enters the alt-screen and raw mode on launch; SIGINT, SIGTERM, and SIGHUP handlers restore it before exit so a crash never leaves the shell broken.
Every command resolves a simulator by full UDID or by name (substring-fuzzy).
Ambiguous names error out with the matching candidates. Pass --help to any
command for the authoritative usage.
List installed simulators, grouped by runtime. --json emits a machine-readable
array suitable for piping to jq; --platform filters by OS family.
holodeck list [--platform ios|watchos|tvos|visionos] [--json]
holodeck list --platform ios --json | jq '.[].name'List apps installed on a booted simulator. Default filters to user apps;
--system includes Apple's preinstalled bundles. --json emits an array of
{bundleID, name, version, isUserApp} records:
holodeck apps list <name-or-udid> [--system] [--json]
holodeck apps list "iPhone 17 Pro" --json | jq '.[].bundleID'Boot a shut-down simulator:
holodeck boot <name-or-udid>
holodeck boot "iPhone 17 Pro"Shut down a booted simulator:
holodeck shutdown <name-or-udid>
holodeck shutdown "iPhone 17 Pro"Create a new simulator. --device and --runtime accept substrings matched
against the live simctl device-type and runtime catalogs:
holodeck create <name> --device <substr> --runtime <substr>
holodeck create "Demo iPhone" --device "iPhone 17 Pro" --runtime "iOS 18"Erase a single shut-down simulator (or all of them with --all). Prompts
before erasing; -y skips the prompt:
holodeck erase <name-or-udid> [-y]
holodeck erase --all [-y]
holodeck erase "Demo iPhone" -yDelete a simulator (or every simulator whose runtime is no longer available
with --unavailable). Prompts unless -y:
holodeck delete <name-or-udid> [-y]
holodeck delete --unavailable [-y]
holodeck delete --unavailable -yRecord video from a booted simulator. Press Ctrl-C to stop — SIGINT is
forwarded to simctl io so the MP4 finalizes cleanly (SIGKILL would corrupt
it):
holodeck record <name-or-udid> [-o path] [--codec h264|hevc]
holodeck record "iPhone 17 Pro" --codec hevc -o ~/Desktop/demo.mp4Capture a screenshot from a booted simulator. Prints the saved path on success:
holodeck screenshot <name-or-udid> [-o path] [--type png|jpeg|tiff|bmp]
holodeck screenshot "iPhone 17 Pro" --type pngSet light or dark appearance (booted simulators only):
holodeck appearance <name-or-udid> light|dark
holodeck appearance "iPhone 17 Pro" darkOverride one or more status-bar fields. Overrides reset when the simulator shuts down:
holodeck statusbar override <name-or-udid> \
[--time <hh:mm>] [--battery-state charging|charged|discharging] \
[--battery-level 0-100] [--wifi-bars 0-3] [--cellular-bars 0-4] \
[--operator-name <string>]
holodeck statusbar override "iPhone 17 Pro" --time 9:41 --battery-level 100Clear status-bar overrides:
holodeck statusbar clear <name-or-udid>
holodeck statusbar clear "iPhone 17 Pro"Set the simulator's locale and language (BCP-47). Writes both AppleLanguages
and AppleLocale; reboot the simulator to apply:
holodeck locale <name-or-udid> <bcp47>
holodeck locale "iPhone 17 Pro" pt-BROverride the simulated GPS location (booted simulators only):
holodeck location set <name-or-udid> <latitude> <longitude>
holodeck location set "iPhone 17 Pro" 37.7749 -122.4194Clear the simulated location:
holodeck location clear <name-or-udid>
holodeck location clear "iPhone 17 Pro"Grant, revoke, or reset a privacy permission. The bundle ID is required for
grant and revoke; for reset it's optional (omit it to reset every app's
prompt state for that permission).
Permissions: all, calendar, contacts, contacts-limited, location,
location-always, photos, photos-add, media-library, microphone,
motion, reminders, siri.
holodeck privacy <name-or-udid> grant|revoke|reset <permission> [bundle-id]
holodeck privacy "iPhone 17 Pro" grant photos com.example.MyApp
holodeck privacy "iPhone 17 Pro" reset allWipe the simulator's keychain without erasing the whole device (handy for clearing test credentials between runs):
holodeck keychain reset <name-or-udid>
holodeck keychain reset "iPhone 17 Pro"Bring Simulator.app to the front, focused on the selected device:
holodeck focus <name-or-udid>
holodeck focus "iPhone 17 Pro"Note. Under the hood this runs
open -a Simulator --args -CurrentDeviceUDID <udid>, which Simulator.app persists into its preferences. If you later quit Simulator.app and relaunch it (from anywhere), it will start with that device focused. This is Simulator.app's own behavior, not something holodeck tracks.
holodeck reads ~/.config/holodeck/config.json (honoring $XDG_CONFIG_HOME)
at launch. A missing file uses the defaults below; a malformed file errors
out. All fields are optional — include only the ones you want to override.
{
"defaultPlatform": "iOS",
"screenshotsDirectory": "~/Captures",
"videoCodec": "hevc",
"screenshotType": "png",
"pollIntervalSeconds": 2.0
}| Field | Default | Affects |
|---|---|---|
defaultPlatform |
null |
list --platform default |
screenshotsDirectory |
~/Desktop |
Output dir for record and screenshot |
videoCodec |
h264 |
record --codec default |
screenshotType |
png |
screenshot --type default |
pollIntervalSeconds |
2.0 |
TUI refresh cadence |
CLI flags always win over config; config wins over hard-coded defaults.
┌─────────────────────────────────────────┐
│ Presentation (HolodeckTUI + holodeck) │ ← argument-parser subcommands, raw-mode TUI
├─────────────────────────────────────────┤
│ HolodeckServices │ ← SimulatorService, RecordingService, etc.
├─────────────────────────────────────────┤
│ HolodeckCore │ ← models, SimctlClient, ProcessRunner
└─────────────────────────────────────────┘
SimctlClientshells out toxcrun simctland decodes--jsonoutput withCodable— no regex parsing of the human-readable form.ProcessRunneris an injectable protocol so services can be unit-tested with stubs.- The TUI is pure-data-driven:
Reducer.reducereturns the nextAppStateplus a list ofSideEffectvalues;HolodeckAppdispatches each effect on a detachedTaskand feeds responses back into the event stream. - Recording uses
Process.interrupt()(SIGINT) to stopsimctl io recordVideoso the MP4 finalizes cleanly. SIGKILL would corrupt the file.
swift test # run the suite (Swift Testing, ~140 tests)
swiftformat Sources Tests # apply formatting (.swiftformat)
swiftlint --quiet # apply lint (.swiftlint.yml)Both format and lint should run clean on a fresh checkout. Tests cover JSON
decoding, input parsing, the full reducer (navigation, recording, lifecycle
modals, wizard), terminal-mode basics, the config loader, and Recorder's
interrupt-and-wait semantics.
MIT — see file headers.