# Tape Scripting

URL: https://tuios.dev/docs/tape-scripting

> Script TUIOS with .tape files, and build project layouts with .tuios.tape.

A tape is a plain text file of TUIOS commands, one per line: open a window, type into it, switch workspace, wait for output. You can write tapes by hand or [record them](https://tuios.dev/docs/tape-recording).

**Try it live:** the [Automation track](https://tuios.dev/learn?t=automation) plays a tape in your browser.

```bash
# demo.tape
EnableTiling
NewWindow
Type "htop"
Enter
SmartSplit
Type "git log --oneline"
Enter
```

## Running a tape

| Command                      | What it does                                                                            |
| ---------------------------- | --------------------------------------------------------------------------------------- |
| `tuios tape play <file>`     | Starts a standalone TUIOS and plays the tape in it                                      |
| `tuios tape validate <file>` | Parses the tape and lists its commands, without running anything                        |
| `tuios tape exec <file>`     | Plays the tape in a running session that has a client attached, and waits for it to end |

`tuios tape list`, `show`, `delete` and `dir` manage recorded tapes. See [Tape Recording](https://tuios.dev/docs/tape-recording#managing-recordings).

All three run the tape the same way:

- Commands run one at a time. Each command waits for animations to finish, and for the pane the command before it asked for.
- `Wait`, `WaitUntilRegex` and `WaitFor` hold playback.
- The first command that fails stops the tape. The rest of the tape does not run. TUIOS shows the line and the reason, and the progress indicator says `FAILED`.

### tape play

```bash
tuios tape play demo.tape
```

`play` starts its own TUIOS, not attached to a daemon session, and runs the tape in it. It reads your config file, and `--theme` and `--show-keys` work as they do for `tuios`.

- Animations start off, so playback is the same on every machine. A tape can turn them back on with `EnableAnimations`.
- Ctrl+P pauses and resumes playback.
- A progress indicator shows the current command and the total.
- TUIOS stays open when the tape ends. Quit it as usual, with Ctrl+B q. If the tape failed, `tape play` then exits with an error that names the line.

### tape validate

```bash
tuios tape validate demo.tape
```

Prints each parsed command, or the errors with their line and column:

```
✗ demo.tape: line 4, column 7: Split: "diagonal" is not one of horizontal, vertical, h, v
```

It exits non-zero if the tape does not parse. It checks every argument: a word left over after a command, an action name that does not exist, and a pattern that is not a regular expression are all errors. A tape that validates can still fail at run time, for example `LoadLayout` with a name that does not exist.

### tape exec

```bash
tuios tape exec demo.tape                 # the most recently active session
tuios tape exec -s work demo.tape         # a named session
tuios tape exec --timeout 1h long.tape    # wait longer than 30 minutes
```

`exec` sends the tape to the daemon, which hands it to the TUI client attached to that session. The client plays it and shows its progress on screen. The command returns when the tape ends:

- It exits 0 and prints `Tape finished` when every command ran.
- It exits non-zero when the tape has an error or a command fails. The message gives the file, line and column, also for a line in a file the tape includes with `Source`.
- It gives up after `--timeout`, 30 minutes by default. The tape may still be playing then.

> **exec needs an attached client**
>
> A detached session has no renderer to run a tape. `tuios tape exec` against one fails with `tape scripts need an attached client`. For a detached session, use the control commands directly. See [Scripting a detached session](https://tuios.dev/docs/tape-scripting#scripting-a-detached-session).

### Scripting a detached session

For CI or a script that drives a headless session, skip tapes and use the control commands. They work with no client attached, and `wait-for` blocks on real output instead of a fixed sleep:

```bash
#!/bin/sh
set -eu

tuios new ci --detach
tuios new-window -s ci tests
tuios send-text -s ci -w tests 'npm test
'
tuios wait-for window-output -s ci -w tests --pattern 'Tests passed' --timeout 120000
tuios kill-session ci
```

`wait-for` exits non-zero on timeout, so the script fails if the tests never print the marker. See [Sessions](https://tuios.dev/docs/sessions) and the [Control Protocol](https://tuios.dev/docs/control-protocol) for the full set of commands.

A single tape command can also be sent with `tuios run-command`, and so can any keybinding action by its name:

```bash
tuios run-command -s work Split horizontal
tuios run-command -s work toggle_spotlight
tuios run-command -s work Press "ctrl+b ?"
```

Run `tuios run-command --list` for the commands, and `tuios keybinds list` for the action names. An action and `Press` need a client attached.

## Syntax

### Lines and comments

One command per line. `#` starts a comment, on its own line or after a command:

```bash
# Build the editor pane
NewWindow  # comments can follow a command
```

Keywords are not case-sensitive: `NewWindow`, `newwindow` and `NEWWINDOW` are the same command. Arguments keep their case.

### Arguments

An argument is a quoted string, a word, a number or a duration. A word can hold dots and dashes, so `appearance.border_style`, `lib.tape` and `even-horizontal` are each one word. Quote an argument that has spaces.

Every command checks its arguments. A missing argument, a value the command does not take, and a word left over at the end of the line are errors that `tape validate` reports.

### Strings

Double quotes, single quotes and backticks all work:

```bash
Type "hello world"
Type 'hello world'
Type `hello world`
```

### Durations

A number followed by a unit, as Go writes durations: `ms`, `s`, `m`, `h`. Decimals are allowed.

```bash
Sleep 500ms
Sleep 2s
Sleep 1.5s
```

### Repeat counts

A key command takes an optional count and sends the key that many times:

```bash
Down 5
Backspace 10
Enter 2
```

Counts work on `Enter`, `Space`, `Tab`, `Backspace`, `Delete`, `Escape`, `Up`, `Down`, `Left`, `Right`, `Home` and `End`. Other commands do not take a count: `ToggleTiling 3` is an error.

### The @ delay

An `@` and a duration after `Type` or a key command set the pause between the keys it sends:

```bash
Type@50ms "hello"   # one character every 50 ms
Down@100ms 3        # three Down keys, 100 ms apart
```

Other commands do not take an `@` delay. Use a `Sleep` line between them.

## Commands

### Modes

| Command                | Effect                           |
| ---------------------- | -------------------------------- |
| `WindowManagementMode` | Switch to window management mode |
| `TerminalMode`         | Switch to terminal mode          |

`Type`, `Run` and the key commands write to the focused pane in either mode. A mode command changes what TUIOS shows and how it treats the keys you press. Recorded tapes include them so the replay ends in the same state.

### Windows

| Command                    | Effect                                                                  |
| -------------------------- | ----------------------------------------------------------------------- |
| `NewWindow ["name"]`       | Open a window, with a name if you give one                              |
| `CloseWindow ["name"]`     | Close the focused window, or every window with that name                |
| `NextWindow`, `PrevWindow` | Move focus                                                              |
| `FocusWindow <name>`       | Focus a window by name or ID. `Focus` is the same command               |
| `RenameWindow "name"`      | Name the focused window. `RenameWindow "old" "new"` renames another one |
| `MinimizeWindow ["name"]`  | Minimize the focused or the named window                                |
| `RestoreWindow ["name"]`   | Restore the focused or the named window, if it is minimized             |

```bash
NewWindow "server"
Run "npm run dev"
```

Minimizing moves focus to another window, so a bare `RestoreWindow` right after `MinimizeWindow` does not bring the minimized one back. Name the window, or use `Action restore_all`.

### Tiling and layout

| Command                                         | Effect                                                                                             |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `EnableTiling`, `DisableTiling`, `ToggleTiling` | Turn tiling on or off                                                                              |
| `Split horizontal`, `Split vertical`            | Split the focused pane. `h` and `v` also work. Needs tiling on                                     |
| `SmartSplit`                                    | Split the focused pane. TUIOS picks the direction. Needs tiling on                                 |
| `Preselect left`                                | Put the next split on that side: `left`, `right`, `up` or `down`                                   |
| `RotateSplit`                                   | Rotate the direction of the focused split                                                          |
| `EqualizeSplits`                                | Reset every split to an even ratio                                                                 |
| `ArrangePanes tiled`                            | Lay the panes out again: `tiled`, `even-horizontal` or `even-vertical`, then optional window names |
| `FocusDirection left`                           | Focus the pane on that side                                                                        |
| `ToggleZoom`                                    | Zoom the focused pane, or unzoom it                                                                |
| `SnapLeft`, `SnapRight`                         | Floating: snap the focused window to half the screen. Tiled: focus the neighbour on that side      |
| `SnapFullscreen`                                | Floating only. Snap the focused window to the full screen                                          |
| `SetMultifocus [names...]`                      | Make the multifocus set exactly these windows. No names clears it                                  |
| `SaveLayout "name"`, `LoadLayout "name"`        | Save the current layout under a name, or apply a saved one                                         |

The master-stack layout has its own commands: `SetMasterPosition left` (`left`, `right`, `top`, `bottom` or `center`), `SetMasterCount 2`, `CycleMasterPosition`, `AddMaster`, `RemoveMaster`, `SwapWithMaster` and `FocusMaster`. See [Layout Modes](https://tuios.dev/docs/layout-modes).

### Workspaces

| Command                      | Effect                                     |
| ---------------------------- | ------------------------------------------ |
| `SwitchWorkspace <n>`        | Show workspace `n`                         |
| `MoveToWorkspace <n>`        | Move the focused window to workspace `n`   |
| `MoveAndFollowWorkspace <n>` | Move the focused window and switch with it |

`n` runs from 1 to the number of workspaces, 9 by default.

### Any action

`Action <name>` runs a keybinding action by the name your config binds a key to. It does what the key does, so every feature with a key is scriptable:

```bash
Action toggle_scratch      # the scratch pane
Action toggle_spotlight    # spotlight
Action prefix_inbox        # the Inbox
Action hints               # link and path hints
Action restore_all         # restore every minimized window
Action command:build       # a [[keybindings.command]] entry named build
```

Run `tuios keybinds list` for the names. A name that is not an action is an error in `tape validate`. A few keys answer only inside their own view: the Inbox and mail keys, and the rail's agent keys. `Action` refuses those and says so. Open the view and use `Press`.

### Keys and text

| Command                                                  | Sends                                           |
| -------------------------------------------------------- | ----------------------------------------------- |
| `Type "text"`                                            | The text, to the focused pane                   |
| `Run "command"`                                          | The command line and Enter, to the focused pane |
| `Enter`, `Space`, `Tab`, `Backspace`, `Delete`, `Escape` | That key, to the focused pane                   |
| `Up`, `Down`, `Left`, `Right`, `Home`, `End`             | That key, to the focused pane                   |
| `Ctrl+c`, `Alt+x`, `Shift+Tab`, `Ctrl+Backspace`         | A key with modifiers, to the focused pane       |
| `Press "keys"`                                           | Keys to TUIOS itself, as if you pressed them    |

The first five rows write bytes to the focused pane. They do not go through TUIOS keybindings: `Ctrl+b` sends byte `0x02` to the pane and does not start the TUIOS prefix. The one exception is `Alt+1` to `Alt+9`, which switch workspace.

`Press` sends keys through the TUIOS key handling instead. The leader opens the prefix, copy mode gets its motions, and an open dialog gets its keys. Separate keys with spaces or commas:

```bash
Press "ctrl+b ?"        # open the key help
Press "esc"             # close it
Press "ctrl+b ["        # enter copy mode
Press "k k k v"         # move up three lines and start a selection
```

### Waiting and checking

| Command                         | Effect                                                                                       |
| ------------------------------- | -------------------------------------------------------------------------------------------- |
| `WaitFor <condition> [timeout]` | Hold playback until the condition holds. Fail the tape at the timeout, 10 seconds by default |
| `Expect <condition>`            | Fail the tape if the condition does not hold now                                             |
| `Sleep <duration>`              | Pause. `Wait <duration>` is the same                                                         |
| `WaitUntilRegex "pattern" [ms]` | The older text wait. It shows a warning at the timeout and does not stop the tape            |

The conditions:

| Condition                      | Holds when                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------- |
| `text "pattern" [in "pane"]`   | The pane's screen matches the regular expression                                |
| `pane "name"`                  | A pane with this name or ID exists                                              |
| `gone "name"`                  | No pane has this name or ID                                                     |
| `focus "name"`                 | The focused pane has this name or ID                                            |
| `panes <n>`                    | The current workspace has `n` panes                                             |
| `agent "state" [in "pane"]`    | The pane's agent reports this state, such as `working`, `needs_input` or `done` |
| `workspace <n>`                | Workspace `n` is on screen                                                      |
| `mode terminal`, `mode window` | TUIOS is in this mode                                                           |

Without `in`, `text` and `agent` read the focused pane. The timeout is a duration, as in `WaitFor text "ready" 30s`.

```bash
Run "make build"
WaitFor text "BUILD (OK|FAILED)" 2m
Expect text "BUILD OK"

Split vertical
WaitFor panes 2
```

Prefer `WaitFor` to `Sleep`. A sleep that is long enough on your machine is too short on a slow one. A wait holds exactly as long as it needs.

> **Match the output, not the command**
>
> The command you type is on the screen too. `Run "echo ready"` followed by `WaitFor text "ready"` matches the typed line at once. Wait for something only the output has, such as `Run "echo ready-$((40+2))"` and `WaitFor text "ready-42"`.

### Including another tape

`Source "file.tape"` runs the commands of another tape at that point. The path is relative to the tape that names it. A tape can include a tape that includes another, up to 16 deep. A file that includes itself is an error. Errors in an included file name that file and its line.

`Source` works in `tape play`, `tape validate`, `tape exec` and the tape manager. It does not work in a project tape.

### Settings and appearance

| Command                                                     | Effect                                                                                       |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `Set <path> <value>`                                        | Change a setting, like `tuios set-config`. `SetConfig` is the same                           |
| `SetTheme <name>`                                           | Change the color theme                                                                       |
| `SetBorderStyle <style>`                                    | Change the window border style                                                               |
| `SetDockbarPosition <position>`                             | `top`, `bottom` or `hidden`                                                                  |
| `Notify "message" [type]`                                   | Show a notification: `info`, `success`, `warning` or `error`. `ShowNotification` is the same |
| `EnableAnimations`, `DisableAnimations`, `ToggleAnimations` | Turn animations on or off                                                                    |
| `CommandPalette`                                            | Open the command palette                                                                     |
| `Screenshot`                                                | Save the focused pane as an image, like `tuios screenshot`                                   |

`Output` is an error: TUIOS does not render a tape to a file. Use `Screenshot`.

## Examples

### Two panes, checked

```bash
DisableAnimations
EnableTiling

RenameWindow "editor"
Run "echo ready-$((20+22))"
WaitFor text "ready-42" 15s

Split vertical
WaitFor panes 2
RenameWindow "server"
Run "npm run dev"
WaitFor text "listening on" in "server" 60s

Action equalize_splits
FocusWindow editor
Expect focus "editor"
```

The [examples folder](https://github.com/Gaurav-Gosain/tuios/tree/main/examples) has more, and `actions_and_waits.tape` uses every command above.

### Two workspaces

```bash
NewWindow
Run "cd ~/project"

SwitchWorkspace 2
NewWindow "monitor"
Run "htop"

SwitchWorkspace 1
Expect workspace 1
```

## Project tapes

A project tape is a file named `.tuios.tape` in a project directory. When your shell inside TUIOS enters that directory, TUIOS offers to build a session for the project from it, much like `direnv` offers to load an `.envrc`.

A tape can type any command into a shell, so an untrusted project tape never runs on its own. TUIOS reads it once, hashes it and shows it to you. It runs only after you review it and choose to run it.

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/docs/tape-scripting)*

### Reviewing and trusting

1. `cd` into a directory with a `.tuios.tape`. A notification appears and the dock shows a `tape ?` badge. Nothing has run.
2. Press Ctrl+B T t, or choose **Tape: review the project tape** in the command palette. The dialog shows the path, the trust status, what the tape builds and its full content.
3. Choose an action:

| Key | Action                                     |
| --- | ------------------------------------------ |
| r   | Run once, without remembering the decision |
| t   | Trust this exact file and run it           |
| n   | Never ask about this path again            |
| Esc | Not now. The badge stays                   |

Trust is stored per path and SHA-256 content hash in `tuios/tape-trust.toml` under your data directory. Any edit to the file makes it untrusted again, so a `git pull` that changes the tape brings the review back. TUIOS runs the same bytes it showed you and does not read the file again.

A tape is ineligible, and can only be dismissed, if it is not a regular file you own, is group- or world-writable, or is larger than 64 KiB. A pane whose shell is connected to another machine over SSH is ignored, because TUIOS cannot read or verify the remote file.

### Writing a project tape

```bash
# .tuios.tape
Session "myproject"
Require "pnpm"

RenameWindow "edit"
Type "nvim ." Enter

Split vertical
RenameWindow "serve"
Run "pnpm dev"

Split horizontal
RenameWindow "sh"

Focus "edit"
```

The optional header comes first:

| Directive                          | Meaning                                                           | Default            |
| ---------------------------------- | ----------------------------------------------------------------- | ------------------ |
| `Session "name"`                   | Name of the session to build                                      | The directory name |
| `Scope session` or `Scope current` | Build a new session, or apply the tape to the current one         | `session`          |
| `Workspace <n>`                    | Workspace to build in                                             | None               |
| `Require "cmd"`                    | Skip the tape with a notice if `cmd` is not on `PATH`. Can repeat | None               |

The body uses a smaller language than a normal tape, made for building layouts:

| Command                                | Effect                                               |
| -------------------------------------- | ---------------------------------------------------- |
| `Type "text" [Enter]`                  | Type into the focused pane, and press Enter if given |
| `Run "cmd"`                            | Same as `Type "cmd" Enter`                           |
| `Enter`                                | Press Enter                                          |
| `Split vertical` or `Split horizontal` | Split the focused pane (`v` and `h` also work)       |
| `NewWindow ["name"]`                   | Open a new pane                                      |
| `RenameWindow "name"`                  | Name the focused pane. `Rename` also works           |
| `Focus "name"`                         | Focus a pane by name                                 |
| `Sleep <duration>`                     | Pause                                                |
| `EnableTiling`, `DisableTiling`        | Turn tiling on or off                                |

Unknown lines are skipped. TUIOS waits after each `Split` and `NewWindow` so the new pane is ready before the next line types into it.

With `Scope session`, running the tape creates the session, opens a pane at the project root, builds the layout and switches you to it. If a session with that name already exists, TUIOS switches to it and does not build it again. Session scope needs a daemon-backed TUIOS; without one, the tape runs in the current session.

### Autorun

The `[tape]` table in the config controls detection. See [the tape table](https://tuios.dev/docs/configuration#the-tape-table).

```toml
[tape]
autorun = "ask"       # off, ask or auto
auto_review = false   # open the review dialog on detection
```

- `off`: no detection at all.
- `ask` (the default): show the notification and badge, and run nothing until you choose.
- `auto`: a trusted, unchanged tape runs when you enter the directory. An untrusted or edited tape behaves as in `ask`.

`TUIOS_TAPE_AUTORUN=off` overrides the setting for one run. A path you marked Never stays silent in every mode.

## Related

*[An interactive figure goes here. Open the page to use it.](https://tuios.dev/docs/tape-scripting)*
