# Introduction
URL: https://tuios.gaurav.zip/docs
> TUIOS is a terminal multiplexer and window manager with workspaces, tiling, persistent sessions and a vim-style modal interface.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
TUIOS (Terminal UI Operating System) runs many shells inside one terminal. Each shell gets its own window. You arrange windows with tiling or by hand, group them into nine workspaces, and detach and come back later without losing anything.
## Install
```bash
brew tap Gaurav-Gosain/tap
brew install tuios
```
```bash
yay -S tuios-bin
```
```bash
curl -fsSL https://raw.githubusercontent.com/Gaurav-Gosain/tuios/main/install.sh | bash
```
```bash
go install github.com/Gaurav-Gosain/tuios/cmd/tuios@latest
```
Nix, Docker, release binaries and building from source are covered in [Getting Started](/docs/getting-started#install).
## Run it
```bash
tuios
```
Press n to open a window, i to type in it, and Alt+Esc to get back to window management. Ctrl+P opens the command palette, which lists every command. [Getting Started](/docs/getting-started) walks through the rest.
`tuios` starts a background daemon and attaches to a session in it. Close the terminal and the session keeps running. Run `tuios` again to get it back.
## What you get
**Windows and layout**
* Two modes, modelled on vim: window management mode for arranging windows, terminal mode for typing into one.
* BSP tiling by default, with explicit splits, preselection, swapping and resizing. Master-stack and a niri-style scrolling layout are one palette command away.
* Nine workspaces, zoom, minimize, layout templates and optional shared borders.
* A command palette (Ctrl+P) and an app launcher (Alt+Space).
**Sessions**
* A daemon owns the shells, so detaching costs nothing and several terminals can attach to one session at once.
* Window layout and working directories come back after a daemon restart or a reboot. Running programs and scrollback do not. See [Sessions](/docs/sessions).
* Sessions and single panes can live on other machines, reached over ssh.
**Scripting**
* A JSON [control protocol](/docs/control-protocol) on the daemon socket, with blocking waits and an event stream.
* CLI commands for the same things: `send-text`, `capture-pane`, `wait-for`, `new-window` and more.
* [Tape scripts](/docs/tape-scripting) to record and replay workflows, and [hooks](/docs/hooks) that run shell commands on window, session and agent events.
* Panes that run coding agents report whether the agent is working, idle or waiting for you.
**Terminal**
* Vim-style copy mode over 10,000 lines of scrollback per window, plus mouse wheel scrolling and selection.
* Kitty graphics (including `mpv --vo=kitty` video), sixel passthrough, the kitty keyboard protocol and synchronized output.
* 342 built-in themes with a live picker, and an in-app settings page.
**Remote access**
* `tuios ssh` serves TUIOS over SSH with public key authentication.
* `tuios-web`, a separate binary, serves it to a browser. See [Web Terminal](/docs/web).
## Where to go next
## Community
* Source, issues and discussions: [Gaurav-Gosain/tuios](https://github.com/Gaurav-Gosain/tuios) on GitHub.
* License: MIT.
# Getting Started
URL: https://tuios.gaurav.zip/docs/getting-started
> Install TUIOS, open your first windows, and learn the keys you need on day one.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
This page takes you from nothing to a working session in about five minutes.
You need a terminal with true color support. A [Nerd Font](https://www.nerdfonts.com) makes the icons render; without one, start TUIOS with `--ascii-only`.
## Install
macOS and Linux:
```bash
brew tap Gaurav-Gosain/tap
brew install tuios
```
```bash
yay -S tuios-bin
```
Linux and macOS. Installs the `tuios` binary into `/usr/local/bin`, `~/.local/bin` or `~/bin`, whichever it can write to.
```bash
curl -fsSL https://raw.githubusercontent.com/Gaurav-Gosain/tuios/main/install.sh | bash
```
```bash
nix run github:Gaurav-Gosain/tuios#tuios
```
The flake builds for `x86_64-linux`, `aarch64-linux` and `aarch64-darwin`. On an Intel Mac, use a release binary.
Needs Go 1.26 or newer.
```bash
go install github.com/Gaurav-Gosain/tuios/cmd/tuios@latest
```
```bash
docker run -it --rm ghcr.io/gaurav-gosain/tuios:latest
```
Pre-built binaries for Linux, macOS, Windows and FreeBSD are on the [releases page](https://github.com/Gaurav-Gosain/tuios/releases).
Check the install:
```bash
tuios --version
```
The first line names the version and the terminal emulator backend the binary was built with, for example `tuios version 0.7.0 [pure-Go backend]`.
If you installed with the script or a release binary, `tuios update` installs the newest release over the binary (`tuios update --check` only reports). For Homebrew, AUR, Nix and Go installs, use that tool to update. `tuios update` detects them and prints the right command instead.
The browser terminal is a separate binary, `tuios-web`. See [Web Terminal](/docs/web) to install it.
### Build from source
```bash
git clone https://github.com/Gaurav-Gosain/tuios.git
cd tuios
go build -o tuios ./cmd/tuios
```
`./scripts/install.sh` builds and installs into `~/.local/bin`. It defaults to the libghostty-vt emulator backend, which needs `zig`. Run `./scripts/install.sh pure` for the pure Go emulator, which needs nothing beyond Go.
## Your first session
### Start TUIOS
```bash
tuios
```
The first run creates a session called `session-0` and shows a welcome screen with four keys on it:
You are in **window management mode**. Keys here arrange windows. Nothing is sent to a shell yet.
### Open a window
Press n. A window opens with your shell in it.
### Type in it
Press i (or Enter) to enter **terminal mode**. Your keys now go to the shell. Run a command:
```bash
ls
```
### Go back to window management
Press Alt+Esc (Opt+Esc on macOS), or Ctrl+B then Esc.
A bare Esc stays in terminal mode and goes to the program, so vim and other full-screen programs keep working.
### Open two more windows
Press n twice. Tiling is on by default, so each new window takes half of the focused one and all three stay visible.
Move between them with Tab and Shift+Tab, the number keys 1 to 9, or Alt+arrow keys to move to the neighbour in that direction. You can also click a window.
### Try another workspace
Press Alt+2 (Opt+2 on macOS) to switch to workspace 2. It is empty. Open a window there, then press Alt+1 to go back.
### Detach, and come back
Press Ctrl+B then d. TUIOS exits, but the session and its shells keep running in the background daemon. Run `tuios` again and everything is where you left it.
Press Ctrl+P in either mode to open the command palette and search every command by name. Press Ctrl+B and wait half a second: a panel lists every key the prefix accepts next. Ctrl+B then ? opens the full help.
## The two modes
| Mode | What your keys do | How to get there |
| ----------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
| Window management | Create, close, move, tile and switch windows | Alt+Esc, or Ctrl+BEsc |
| Terminal | Go to the program in the focused window | i or Enter |
Ctrl+B is the **prefix** key. It works in both modes: press it, release it, then press the next key. You can change it with `keybindings.leader_key` in the [configuration](/docs/configuration).
## Keys for day one
On macOS, read Alt as Opt. If an Opt chord types a strange character instead, see the [macOS notes](/docs/keybindings#macos).
**Windows**
| Key | Action |
| --------------------------------------------------------- | ------------------------------------------------------------------ |
| n | New window (window mode) |
| w or x | Close the focused window (window mode) |
| r | Rename the focused window (window mode) |
| Tab / Shift+Tab | Next / previous window (window mode) |
| Alt+N / Alt+P | Next / previous window (both modes) |
| Alt+arrow | Focus the window in that direction (both modes) |
| 1 to 9 | Focus window by number (window mode) |
| z | Zoom the focused window to fill the screen, and back (window mode) |
| m | Minimize to the dock (window mode). M restores all. |
The prefix versions work in both modes: Ctrl+B then c (new), x (close), r (rename), n/p (next/previous), an arrow key (focus that way), 0 to 9 (focus by number), z (zoom).
**Tiling**
| Key | Action |
| ------------------------------------------------------------- | ----------------------------------------------------------------------- |
| t or Ctrl+BSpace | Turn tiling on or off |
| - / \| | Split the focused window: new window below / to the right (window mode) |
| HJKL | Swap with the neighbour left / down / up / right (window mode) |
| = | Make all splits equal (window mode) |
[BSP Tiling](/docs/bsp-tiling) covers preselection, resizing and the other layouts.
**Workspaces**
| Key | Action |
| ------------------------------------------------------------ | -------------------------------------------- |
| Alt+1 to 9 | Switch to workspace 1 to 9 |
| Alt+Shift+1 to 9 | Move the focused window there, and follow it |
| Ctrl+Bw, then a digit | Switch workspace from the prefix menu |
Ctrl+B followed directly by a digit focuses a **window**, not a workspace.
**Scrollback and copying**
| Key | Action |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Mouse wheel | Scroll the window's history. Typing, or scrolling back to the bottom, returns to live output. |
| Ctrl+B\[ | Enter copy mode: move with vim keys, / to search, v to select, y to copy, Esc to leave |
| Drag with the mouse | Select text. It is copied when you release. |
**Everything else**
| Key | Action |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Ctrl+P | Command palette |
| Alt+Space or Ctrl+Ba | App launcher: start any program on your `$PATH` in a new window |
| Ctrl+BS | Session switcher |
| Ctrl+B, | Settings page (, in window mode) |
| Ctrl+B? | Help (? in window mode) |
| Ctrl+Bd | Detach, leaving the session running |
| Ctrl+Bq | Quit menu: detach, switch session, or kill this session |
The full list is on the [Keybindings](/docs/keybindings) page, and `tuios keybinds list` prints the bindings in effect on your machine.
## Sessions
`tuios` on its own attaches to your most recent session, and creates one if there is none. To manage several:
```bash
tuios new work # create a session called "work" and attach to it
tuios ls # list sessions
tuios attach work # attach to "work"
tuios kill-session work # end "work" and every window in it
```
Inside TUIOS, Ctrl+BS switches between sessions and N (window mode) creates one.
If you want a session that lives and dies with this terminal and starts no daemon, run `tuios --standalone`. [Sessions](/docs/sessions) explains both kinds and exactly what survives a restart.
## Make it yours
```bash
tuios config edit # open the config file in $EDITOR
tuios config path # print where it is
tuios --list-themes # list the built-in themes
tuios --theme dracula # start with a theme
```
Most settings can also be changed live on the settings page, Ctrl+B,. See [Configuration](/docs/configuration) for every option.
## Shell completions
```bash
# Bash
tuios completion bash > /etc/bash_completion.d/tuios
# Zsh
tuios completion zsh > "${fpath[1]}/_tuios"
# Fish
tuios completion fish > ~/.config/fish/completions/tuios.fish
# PowerShell
tuios completion powershell > tuios.ps1
```
## Next steps
# Keybindings
URL: https://tuios.gaurav.zip/docs/keybindings
> Every default key in TUIOS for window mode, terminal mode, prefix commands, copy mode and the mouse, grouped by the config section that owns it.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
TUIOS has two modes. **Window mode** is for managing panes. **Terminal mode** sends your keys to the focused pane. A new session starts in window mode.
The leader key is Ctrl+B. Press it, release it, then press a command key. The leader works in both modes.
Every table on this page names the config section that holds the binding and the action name to use in it. Every action is rebindable. See [Customizing](#customizing) at the end.
This page lists the defaults. `tuios keybinds list`, `tuios keybinds explain ` and the keybind manager (Ctrl+Bk) show what is bound on your machine.
## Modes
Section: `[keybindings.mode_control]`. These keys work in window mode.
| Key | Action | Does |
| ------------------------------ | --------------------- | ----------------------------------------------------- |
| i, Enter | `enter_terminal_mode` | Enter terminal mode |
| Esc | `enter_window_mode` | Enter window mode |
| ? | `toggle_help` | Toggle the help overlay |
| , | `open_settings` | Open the settings page |
| q | `quit` | Quit |
| none | `hold_window_mode` | Window mode while the key is held. Unbound by default |
To leave terminal mode, press Ctrl+BEsc, or the direct key from [Terminal mode](#terminal-mode).
`hold_window_mode` needs a terminal that speaks the Kitty keyboard protocol (Ghostty, kitty, WezTerm, foot, Alacritty), because only that protocol reports key releases. Bind it to an ordinary spare key, such as an unused function key from `f1` to `f12`. Binding a modifier such as `leftalt` also works, but then every keystroke in the session arrives as an escape code.
## Window mode
### Windows
Section: `[keybindings.window_management]`.
| Key | Action | Does |
| -------------------------------------------- | -------------------------------------- | ----------------------------------------------------------- |
| n | `new_window` | New window |
| N | `new_session` | New session. Asks which machine when there is more than one |
| w, x | `close_window` | Close the focused window |
| r | `rename_window` | Rename the focused window |
| m | `minimize_window` | Minimize the focused window |
| M | `restore_all` | Restore all minimized windows |
| z | `toggle_zoom` | Zoom the focused window, or unzoom it |
| Tab | `next_window` | Focus the next window |
| Shift+Tab | `prev_window` | Focus the previous window |
| 1 to 9 | `select_window_1` to `select_window_9` | Focus a window by number |
| c | `copy_selection` | Copy the mouse selection |
| s | `focus_sidebar` | Give the keyboard to the session rail |
| S | `start_screensaver` | Start the screen saver now |
| Alt+Shift+N | `next_session` | Next session |
| Alt+Shift+P | `prev_session` | Previous session |
Section `[keybindings.restore_minimized]` restores one minimized window by position:
| Key | Action |
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| Shift+1 to Shift+9 (also ! to () | `restore_minimized_1` to `restore_minimized_9` |
Section `[keybindings.system]` has one binding: b is `toggle_spotlight`, which lights one part of the screen and dims the rest. See the `[spotlight]` options in [Configuration](/docs/configuration#the-spotlight-table).
### Workspaces
Section: `[keybindings.workspaces]`. There are nine workspaces. The modifier is Option on macOS and Alt elsewhere.
| Key | Action | Does |
| -------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------- |
| Alt+1 to Alt+9 | `switch_workspace_1` to `switch_workspace_9` | Show a workspace |
| Alt+Shift+1 to Alt+Shift+9 | `move_and_follow_1` to `move_and_follow_9` | Move the focused window there and follow it |
### Layout and snapping
Section: `[keybindings.layout]`.
| Key | Action | Does |
| ------------------------------------------ | --------------------------- | ------------------------------------------------------------- |
| t | `toggle_tiling` | Turn tiling on or off |
| h | `snap_left` | Snap to the left half. In tiling, focus left |
| l | `snap_right` | Snap to the right half. In tiling, focus right |
| f | `snap_fullscreen` | Fill the screen |
| u | `unsnap` | Restore the size before the snap |
| H, Ctrl+← | `swap_left` | Swap with the window to the left |
| L, Ctrl+→ | `swap_right` | Swap with the window to the right |
| K, Ctrl+↑ | `swap_up` | Swap with the window above |
| J, Ctrl+↓ | `swap_down` | Swap with the window below |
| \< | `resize_master_shrink` | Narrow the master area |
| > | `resize_master_grow` | Widen the master area |
| \{ | `resize_height_shrink` | Shorten the focused window from the bottom edge |
| } | `resize_height_grow` | Lengthen the focused window from the bottom edge |
| . | `resize_master_grow_left` | Widen the master area from the left edge |
| \[ | `resize_height_shrink_top` | Shorten the focused window from the top edge |
| ] | `resize_height_grow_top` | Lengthen the focused window from the top edge |
| none | `resize_master_shrink_left` | Narrow the master area from the left edge. Unbound by default |
`resize_master_shrink_left` has no default because , opens settings. Bind it to a free key if you want it.
Corner snapping is on the [layout prefix](#layout-prefix), not on the bare digits.
### BSP splits
Also in `[keybindings.layout]`. See [BSP Tiling](/docs/bsp-tiling).
| Key | Action | Does |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------------------------- |
| - | `split_horizontal` | Split the focused pane top and bottom |
| \|, \\ | `split_vertical` | Split the focused pane left and right |
| R | `rotate_split` | Flip the split direction |
| = | `equalize_splits` | Reset every split ratio |
| Alt+h / j / k / l | `preselect_left`, `preselect_down`, `preselect_up`, `preselect_right` | Choose where the next window opens |
The preselect keys use Option on macOS. The prefix menu has its own copies of the four split actions, and rebinding one set does not change the other.
## Terminal mode
Section: `[keybindings.terminal_mode]`. These keys work in terminal mode with no prefix. Everything else goes to the pane.
| Key | Action | Does |
| ---------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------- |
| Alt+n | `terminal_next_window` | Focus the next window |
| Alt+p | `terminal_prev_window` | Focus the previous window |
| Alt+Esc | `terminal_exit_mode` | Go to window mode |
| Alt+arrows | `terminal_focus_left`, `_right`, `_up`, `_down` | Focus the pane in that direction |
| Shift+↑ | `terminal_scroll_up` | Scroll into the pane's scrollback |
| Shift+↓ | `terminal_scroll_down` | Scroll back toward live output |
| Ctrl+Shift+V, Super+V | `terminal_paste_host` | Paste from the host clipboard |
| Key | Action | Does |
| ------------------------------------------------------------------------------ | ----------------------------------------------- | --------------------------------- |
| Option+Tab, Alt+n | `terminal_next_window` | Focus the next window |
| Option+Shift+Tab, Alt+p | `terminal_prev_window` | Focus the previous window |
| Option+Esc | `terminal_exit_mode` | Go to window mode |
| Alt+arrows | `terminal_focus_left`, `_right`, `_up`, `_down` | Focus the pane in that direction |
| Shift+↑ | `terminal_scroll_up` | Scroll into the pane's scrollback |
| Shift+↓ | `terminal_scroll_down` | Scroll back toward live output |
| Ctrl+Shift+V, Super+V | `terminal_paste_host` | Paste from the host clipboard |
Readline, fish and zsh use Alt+← and Alt+→ to move a word at a time. To give them back, run `tuios keybinds free alt+left` and `tuios keybinds free alt+right`. Ctrl+B then an arrow still moves focus.
Plain Ctrl+V is not bound. It reaches the pane, so vim's visual block mode works.
## Global keys
Section: `[keybindings.global]`. These keys work in window mode and terminal mode.
| Key | Action | Does |
| ------------------------------- | ----------------- | ------------------------ |
| Ctrl+P | `command_palette` | Open the command palette |
| Alt+Space | `launcher` | Open the app launcher |
The **command palette** searches commands, sessions and windows. Start the query with `@` to filter panes by agent state (`@n` for needs input), or with `#` to search actions to rebind.
The **app launcher** lists every executable on `$PATH`, ranked by use. Enter runs the selection in a new pane with no shell in between. Tab types the command into the focused shell instead, so you can add arguments.
These two keys take Ctrl+P from fish and vim and Alt+Space from readline. Set an action to `[]` to give its key back. On macOS, Option+Space types a non-breaking space, so use Ctrl+Ba for the launcher.
## Prefix commands
Press Ctrl+B, then the key. Section: `[keybindings.prefix_mode]`.
| Key | Action | Does |
| --------------------------------------------- | ----------------------------------------------- | ------------------------------------------------ |
| c | `prefix_new_window` | New window |
| x | `prefix_close_window` | Close the focused window |
| r | `prefix_rename_window` | Rename the focused window |
| n, Tab | `prefix_next_window` | Next window |
| p, Shift+Tab | `prefix_prev_window` | Previous window |
| 0 to 9 | `prefix_select_0` to `prefix_select_9` | Jump to a window |
| arrows | `terminal_focus_left`, `_right`, `_up`, `_down` | Focus the pane in that direction |
| ) | `next_session` | Next session |
| ( | `prev_session` | Previous session |
| Space | `prefix_toggle_tiling` | Turn tiling on or off |
| z | `prefix_fullscreen` | Zoom the focused window |
| - | `prefix_split_horizontal` | Split top and bottom |
| \|, \\ | `prefix_split_vertical` | Split left and right |
| R | `prefix_rotate_split` | Flip the split direction |
| = | `prefix_equalize_splits` | Reset every split ratio |
| \[ | `prefix_selection` | Enter [copy mode](#copy-mode) |
| s | `prefix_scrollback` | Open the scrollback browser |
| C | `prefix_screenshot` | Take a screenshot |
| P | `prefix_command_palette` | Open the command palette |
| a | `launcher` | Open the app launcher |
| , | `prefix_settings` | Open the settings page |
| k | `prefix_keybinds` | Open the keybind manager |
| b | `prefix_toggle_sidebar` | Show or hide the session rail |
| e | `prefix_explore` | Move the keyboard to the rail, or back |
| S | `prefix_session_switcher` | Open the session switcher |
| W | `prefix_workspace_switcher` | Open the workspace switcher |
| j | `prefix_jump_notif` | Jump to the pane of the newest message |
| M | `prefix_mail` | Open the agent mailbox |
| t | `prefix_window` | [Window prefix](#window-prefix) |
| m | `prefix_minimize` | [Minimize prefix](#minimize-prefix) |
| w | `prefix_workspace` | [Workspace prefix](#workspace-prefix) |
| L | `prefix_layout` | [Layout prefix](#layout-prefix) |
| T | `prefix_tape` | [Tape prefix](#tape-prefix) |
| D | `prefix_debug` | [Debug prefix](#debug-prefix) |
| d | `prefix_detach` | Detach. The session keeps running |
| X | `prefix_close_session` | Close the session and its panes |
| Esc | `prefix_exit_mode` | Leave terminal mode. Never detaches |
| q | `prefix_quit` | Quit. In a daemon session this kills the session |
| ? | `prefix_help` | Toggle help |
In terminal mode, pressing the leader twice sends one literal Ctrl+B to the pane.
After a command worth repeating, such as an arrow, the prefix stays armed for 500 ms. Ctrl+B then ←←← moves three panes. Change this with `appearance.prefix_repeat_time`. `0` turns it off.
### Window prefix
Ctrl+Bt, then the key. Section: `[keybindings.window_prefix]`.
| Key | Action | Does |
| ------------------------------- | ---------------------- | ------------------------- |
| n | `window_prefix_new` | New window |
| x | `window_prefix_close` | Close the focused window |
| r | `window_prefix_rename` | Rename the focused window |
| Tab | `window_prefix_next` | Next window |
| Shift+Tab | `window_prefix_prev` | Previous window |
| t | `window_prefix_tiling` | Turn tiling on or off |
| Esc | `window_prefix_cancel` | Cancel |
### Minimize prefix
Ctrl+Bm, then the key. Section: `[keybindings.minimize_prefix]`.
| Key | Action | Does |
| ---------------------------- | ---------------------------------------------------------- | -------------------------------------- |
| m | `minimize_prefix_focused` | Minimize the focused window |
| 1 to 9 | `minimize_prefix_restore_1` to `minimize_prefix_restore_9` | Restore a minimized window by position |
| M | `minimize_prefix_restore_all` | Restore all minimized windows |
| Esc | `minimize_prefix_cancel` | Cancel |
### Workspace prefix
Ctrl+Bw, then the key. Section: `[keybindings.workspace_prefix]`.
| Key | Action | Does |
| --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------- |
| 1 to 9 | `workspace_prefix_switch_1` to `workspace_prefix_switch_9` | Show a workspace |
| !@#$%^&\*( | `workspace_prefix_move_1` to `workspace_prefix_move_9` | Move the focused window there and follow it |
| r | `workspace_prefix_rename` | Rename the current workspace |
| Esc | `workspace_prefix_cancel` | Cancel |
### Layout prefix
Ctrl+BL, then the key. Section: `[keybindings.layout_prefix]`.
| Key | Action | Does |
| -------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| l | `layout_prefix_load` | Open the layout picker |
| s | `layout_prefix_save` | Save the current layout as a template |
| 1 to 4 | `snap_corner_1` to `snap_corner_4` | Snap to the top-left, top-right, bottom-left or bottom-right quarter. Floating mode only |
| 5 to 9 | `resize_width_50` to `resize_width_90` | Set the focused window's width to 50% to 90% |
| Shift+5 to Shift+9 | `resize_height_50` to `resize_height_90` | Set its height to 50% to 90% |
| Esc | `layout_prefix_cancel` | Cancel |
Actions `resize_width_10` to `resize_width_40` and `resize_height_10` to `resize_height_40` also exist, with no default key.
Loading a layout moves the existing windows into place. It does not close any. Extra windows are minimized. `tuios layout` manages saved templates from the shell.
### Tape prefix
Ctrl+BT, then the key. Section: `[keybindings.tape_prefix]`. See [Tape Recording](/docs/tape-recording).
| Key | Action | Does |
| -------------- | --------------------- | ----------------------------------------------- |
| m | `tape_prefix_manager` | Open the tape manager |
| r | `tape_prefix_record` | Start recording |
| s | `tape_prefix_stop` | Stop recording |
| t | `tape_prefix_review` | Review the project tape found in this directory |
| Esc | `tape_prefix_cancel` | Cancel |
While a tape plays, section `[keybindings.script]` is active: Ctrl+P is `script_pause` and pauses or resumes playback.
### Debug prefix
Ctrl+BD, then the key. Section: `[keybindings.debug_prefix]`.
| Key | Action | Does |
| -------------- | ------------------------- | --------------------------------------------- |
| l | `debug_prefix_logs` | Toggle the log viewer |
| c | `debug_prefix_cache` | Toggle style cache statistics |
| k | `debug_prefix_showkeys` | Toggle the [showkeys](/docs/showkeys) overlay |
| a | `debug_prefix_animations` | Toggle animations |
| Esc | `debug_prefix_cancel` | Cancel |
## Session rail
Section: `[keybindings.sidebar]`. Active while the rail has the keyboard: press s in window mode, or Ctrl+Be.
| Key | Action | Does |
| ----------------------------------------------- | -------------------- | ---------------------------------------------- |
| j, ↓ | `cursor_down` | Move down |
| k, ↑ | `cursor_up` | Move up |
| g, Home | `first` | First row |
| G, End | `last` | Last row |
| l, → | `expand` | Expand the row |
| h, ← | `collapse` | Collapse the row |
| Enter | `activate` | Open the row |
| J, Shift+↓ | `reorder_down` | Move the session or machine down |
| K, Shift+↑ | `reorder_up` | Move the session or machine up |
| Tab, Shift+Tab | `section` | Go to the next section |
| 1 to 9 | `jump_1` to `jump_9` | Jump to a row by number |
| n | `new_session` | New session |
| t | `new_window` | New window |
| r | `rename` | Rename the pane or session |
| c | `accent` | Set the session's accent colour |
| x | `kill` | Open the row's close menu |
| m | `menu` | Open the row's menu |
| i | `mail` | Open the mailbox for the pane under the cursor |
| f | `agents_filter` | Filter the agents section |
| o | `agents_sort` | Sort the agents section |
| / | `palette` | Open the command palette |
| \< | `narrow` | Narrow the rail |
| > | `widen` | Widen the rail |
| ? | `help` | Show the rail's help |
| Esc, s | `exit` | Give the keyboard back |
When the cursor is on a row in the files section, section `[keybindings.sidebar_files]` answers first:
| Key | Action | Does |
| ---------------- | --------------------- | -------------------------------------------- |
| Enter | `file_open` | Open the folder, or copy the file's path |
| a | `file_create` | New file. End the name with `/` for a folder |
| r | `file_rename` | Rename |
| d | `file_delete` | Delete to the trash |
| D | `file_delete_forever` | Delete with no trash |
| y | `file_copy` | Copy |
| x | `file_cut` | Cut |
| p | `file_paste` | Paste into this folder |
See [The files section](/docs/configuration#the-files-section).
## Copy mode
Press Ctrl+B\[ to enter copy mode. It moves a cursor through the scrollback with vim keys. These keys are fixed and are not in the config. Put a count before a motion to repeat it: `10j`, `5w`.
| Keys | Does |
| ---------------------------------------------------------------------------------------------- | -------------------------------------------- |
| hjkl, arrows | Move the cursor |
| wbe | Next word, previous word, end of word |
| WBE | The same, for space-separated words |
| 0^$ | Line start, first non-blank, line end |
| HML | Top, middle and bottom of the screen |
| gg, G | Top of the scrollback, bottom (live output) |
| `{n}G` | Go to line `n` |
| \{} | Previous and next paragraph |
| % | Matching bracket |
| Ctrl+U / Ctrl+D | Half page up, down |
| Ctrl+B / Ctrl+F, PgUp / PgDn | Full page up, down |
| fFtT then a character | Find a character on the line |
| ;, | Repeat the character search, forward or back |
| /? | Search forward, backward |
| nN | Next match, previous match |
| Ctrl+L | Clear search highlights |
| vV | Start a character or line selection |
| y, c | Copy the selection to the clipboard |
| i | Leave copy mode and go to terminal mode |
| q, Esc | Leave copy mode |
The scrollback browser (Ctrl+Bs) is a different tool. It lists past commands by their prompt marks, for shells that emit OSC 133.
## Mouse
| Gesture | Does |
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
| Click a pane | Focus it. In window mode it also starts typing, per `appearance.click_to_type` |
| Drag a title bar | Move the window. In tiling, swap it with the pane you drop it on |
| Alt + drag | Move the window from anywhere inside it (`appearance.alt_drag`) |
| Right-drag | Resize the window |
| Right-click | In window mode, open the menu for the pane, the dock or the desktop |
| Shift or Ctrl + right-click | Open the pane menu even when the program in the pane uses the mouse |
| Title bar buttons | Minimize, zoom or close |
| Click a dock item | Restore a minimized window |
| Wheel up | Enter copy mode and scroll, when the program is not using the mouse |
| Drag the right border | Scroll with the scrollbar |
| Shift + click a link | Open it. `appearance.links` decides what counts as a link |
In floating mode, drop a window on a screen edge to snap it: the left or right edge for a half, a corner for a quarter, the top center for the full screen.
In copy mode, a click moves the cursor and a drag selects. Dragging past the top or bottom of the pane scrolls.
## Customizing
Bindings live in 19 sections under `[keybindings]` in `config.toml`. Each action takes a list of keys.
```toml
[keybindings]
leader_key = "ctrl+a"
[keybindings.window_management]
new_window = ["n", "ctrl+t"]
close_window = [] # unbind: the key goes back to the pane
```
An empty list and a missing line are different. A missing action gets its default back at the next load. An empty list stays empty. Key syntax and every section are on the [Configuration](/docs/configuration#keybindings) page.
Inspect and change bindings from the shell:
```bash
tuios keybinds list # the main tables
tuios keybinds doctor # conflicts, and keys taken from the pane
tuios keybinds explain alt+left # everything one key does
tuios keybinds unbind close_window w # take one key off one action
tuios keybinds free alt+left # take a key off every action
```
`doctor` tags each finding with its evidence: `certain` comes from TUIOS routing, `observed` from the live pane, `reference` from a list of common program defaults. `doctor` and `explain` take `--json` and `--guest `.
The **keybind manager** (Ctrl+Bk) shows the same analysis in four tabs: Bindings, Conflicts, Guests and Record. Record captures the next key you press. On a binding, Ctrl+D removes it and Ctrl+X takes its key off every action.
The config file reloads on save, but only `leader_key` among the keybindings. After editing other bindings, detach and attach again, or restart TUIOS. Changes made with the keybind manager apply at once.
`tuios keybinds list-custom` compares 9 of the 19 sections: `window_management`, `workspaces`, `layout`, `mode_control`, `system`, `prefix_mode`, `window_prefix`, `minimize_prefix` and `workspace_prefix`. It does not report `leader_key`. Use `tuios keybinds doctor` or read the file for the rest.
## macOS
macOS treats Option as a compose key unless the terminal says otherwise. An Option chord then arrives as a character. TUIOS maps most of those characters back to the chord, which is why Option+1 to Option+9 work with no setup. Two kinds cannot be recovered:
* **Dead keys.** Option+e, i, n, u and \` send nothing until a second key. Alt+n (next window in terminal mode) takes two presses.
* **Rewritten chords.** Many terminals send Option+← and Option+→ as readline word motions, with no sign that an arrow was pressed.
To fix both, turn on your terminal's Option-as-Alt setting:
| Terminal | Setting |
| ------------ | ------------------------------------------------------- |
| Ghostty | `macos-option-as-alt = true` |
| Terminal.app | Settings > Profiles > Keyboard > Use Option as Meta Key |
| iTerm2 | Settings > Profiles > Keys > Left Option key: Esc+ |
| kitty | `macos_option_as_alt yes` |
| WezTerm | `send_composed_key_when_left_alt_is_pressed = false` |
| Alacritty | `option_as_alt = "Both"` under `[window]` |
| VS Code | `terminal.integrated.macOptionIsMeta` |
Ghostty also needs `keybind = alt+left=unbind` and `keybind = alt+right=unbind`, because its own keybinds rewrite those arrows first.
The prefix works on every terminal with no setup. Ctrl+B then an arrow, n, p, (, ), a or a digit covers every navigation command.
Command chords never reach a terminal program. macOS sends them to the menu bar.
## Related
# TUIOS vs Others
URL: https://tuios.gaurav.zip/docs/comparison
> How TUIOS compares with tmux, zellij, herdr, GNU screen and the splits in WezTerm, kitty and Ghostty, and when to pick one of them instead.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
TUIOS is a terminal multiplexer that works like a window manager. Shells run in windows that float or tile. A daemon keeps them running when you detach. TUIOS also tracks the coding agents running in its panes. It runs inside any terminal, over SSH, or in a browser.
TUIOS is not a terminal emulator: it does not draw fonts or open GUI windows. It is also young. The first commit is from September 2025, and the last tagged release is v0.7.0 (March 2026). This page describes the main branch.
Each fact about another tool comes from that project's own docs, changelog, manual or source, as of September 2026. The [Sources](#sources) list links them. The versions checked are tmux 3.7c (3.8 is in release candidate), zellij 0.45.1, herdr 0.9.1, GNU screen 5.0.2, WezTerm 20240203, kitty 0.49.0 and Ghostty 1.3.1. The TUIOS facts are checked against the TUIOS source and a current build. There are no speed numbers here, because no side-by-side benchmark has been run.
## At a glance
This table covers the tools that run inside a terminal. Terminals with their own splits, such as WezTerm, kitty and Ghostty, are a different kind of tool and have [their own section](#terminals-with-built-in-splits).
| | TUIOS | tmux | zellij | herdr | GNU screen |
| ------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------- |
| Language, licence | Go, MIT | C, ISC | Rust, MIT | Rust, Apache 2.0 | C, GPL 3 |
| Binary, macOS arm64 | 23 MB, plus `tuios-web` | 1 MB, with shared libraries | 41 MB | 22 MB | Not measured |
| Layout | Floating windows, or tiled: BSP, master-stack, scrolling columns | Tiled panes with preset layouts. Floating panes since 3.7 | Tiled, floating and stacked panes | Tiled splits, with zoom | Split regions |
| Grouping | 9 workspaces per session | Windows in a session | Tabs in a session | Workspaces, then tabs | Windows in a session |
| Mouse | Always on. Drag, resize, focus | Off by default in 3.7, on in 3.8 | On by default | On by default. Click, drag, menus | Focus a region (`mousetrack`) |
| Detach and reattach | Yes, a daemon | Yes, a server | Yes, a server | Yes, a server | Yes |
| After a restart | Layout and working directories | Nothing built in. The tmux-resurrect plugin | Layout and commands. Optional scrollback | Layout, working directories and supported agent conversations. Optional screen history | Nothing |
| Several clients | Yes, all can type. Sized to the smallest client | Yes. Read-only clients and per-user access | Yes. A cursor per user, read-only attach | Yes. Each client can view its own tab | Yes. Per-user access lists, read-only possible |
| Remote machines | Built-in SSH server. Sessions and panes on other hosts in your own client | ssh in and attach | ssh in, or attach to a zellij web server over HTTPS | ssh in, `--remote`, or several saved machines in one window | ssh in. Also serial and telnet windows |
| Browser client | `tuios-web`, a separate binary. No login, read-only flag | None | Built in. Login tokens, read-only tokens | None | None |
| Scripting | CLI verbs, JSON protocol, blocking waits, event stream, tapes, hooks | tmux commands, control mode, hooks, formats | `zellij action`, `subscribe`, JSON pane lists, plugin pipes | CLI and JSON socket API, output and agent waits, event subscriptions | `screen -X` and `-Q` |
| Coding agents | States, alerts, waits, messages between agents, worktrees, fan-out | None | None | States, alerts, prompt and wait, worktrees, conversation restore | None |
| Images | Kitty graphics and sixel, passed through. Experimental | Sixel when built with it. Others by passthrough | Sixel and Kitty graphics | Kitty graphics | None |
| Config | TOML, live `set-config`, settings page | `tmux.conf`, written as tmux commands | KDL | TOML | `.screenrc` |
| Extending | Hooks, dock components, control protocol. No plugins | No plugin system. TPM, a third-party manager | WebAssembly plugins | Plugins: any executable with a manifest | None |
| Platforms | Linux, macOS, Windows, FreeBSD, OpenBSD | Linux, macOS, the BSDs, Solaris | Linux, macOS, Windows | Linux, macOS, Windows | Unix-like systems |
Binary sizes are for macOS on Apple silicon: tmux 3.7c from Homebrew, the zellij 0.45.1 and herdr 0.9.1 release binaries, and TUIOS built from main with `-ldflags "-s -w"`.
## tmux
tmux is the default choice, and for good reason. It is packaged almost everywhere and is often already installed. The binary is about 1 MB. Its control mode (`tmux -C`) lets terminals such as iTerm2 show tmux windows as native tabs and windows.
**Where tmux is stronger**
* It runs on servers you do not control, with nothing to install.
* Read-only clients (`attach -r`) and per-user access (`server-access`) are built in. TUIOS has neither in its terminal client.
* It is mature and widely documented, with a large body of shared configs and scripts.
* 3.8 is moving fast: floating panes that move and resize with commands, modal panes, JSON layout strings, and `wait-for -E` to wait on hook events.
**Where TUIOS is stronger**
* Floating windows are the core model, not an add-on. In 3.7 tmux floating panes move and resize only with the mouse. TUIOS also has three tiling modes and zoom.
* Layout and working directories come back after a reboot with no plugin.
* Programs get a JSON protocol with `list-verbs` for discovery, blocking `wait-for` on output, exit, idle or agent state, and an event stream.
* Agent state, alerts and messages between agents. tmux has nothing for agents.
* A browser client and a built-in SSH server.
**Pick tmux** if you work on many machines you do not own, need read-only sharing, or use a terminal built around control mode.
## zellij
zellij is the modern, friendly multiplexer. It shows the available keys on screen, extends through WebAssembly plugins, and ships a web client.
**Where zellij is stronger**
* It teaches itself: the status bar shows the keys for the current mode.
* Plugins are WebAssembly. Rust is the officially supported language, and community projects cover others.
* The web client (`zellij web`) has login tokens, read-only tokens, and requires HTTPS on any address other than `127.0.0.1`. `tuios-web` has no login and needs an authenticating proxy in front.
* Resurrection brings back the commands in each pane, behind a "Press ENTER to run" prompt, and can save the viewport and scrollback. TUIOS brings back a fresh shell with no screen contents.
* Read-only attach and a separate cursor per user.
* It supports sixel, and Kitty graphics as of 0.45. TUIOS passes images through, and its support is experimental.
**Where TUIOS is stronger**
* Overlapping, draggable windows and three tiling modes. zellij floating panes sit above a tiled layout.
* Agent tracking, and waits on agent state.
* Blocking waits from the CLI on a pattern in the output, on idle and on exit. zellij can block until a pane's command exits, and `zellij subscribe` streams a pane's output for a script to watch.
* Sessions on other machines in your own client, without a web server.
**Pick zellij** if you want discoverable keys, plugins, or browser access with its own login.
## herdr
herdr is the closest tool to TUIOS. It is a multiplexer built for coding agents, released in March 2026. It is more widely used, with about 40k [GitHub stars](https://github.com/herdrdev/herdr) against 3.7k for TUIOS, and it releases more often: 0.9.0 and 0.9.1 both shipped in September 2026. Several of the agent screen rules in TUIOS are adapted from herdr's manifests, which are Apache 2.0.
**Where herdr is stronger**
* After a server restart it can resume supported agents' conversations, and can replay recent screen history if you turn that on. TUIOS restores the layout and a fresh shell.
* Several agents, such as Pi, OpenCode and Kimi, report their state through lifecycle hooks or plugins. New agents are added often: 0.9.1 added Letta Code and 0.9.0 added Muse.
* Plugins: a directory with a `herdr-plugin.toml` manifest and commands in any language, plus a marketplace.
* One window holds local work and several saved SSH machines, with one combined agent list.
* Each attached client can look at a different tab at its own size. TUIOS draws a session at the size of the smallest client.
* `herdr update --handoff` can update the server while panes keep running. This is experimental.
**Where TUIOS is stronger**
* Floating windows, and BSP, master-stack and scrolling-column layouts. herdr has splits and zoom.
* A browser client and a built-in SSH server. herdr uses an SSH client on phones instead.
* Tape scripts to drive the UI, and tape recording.
* Sixel images as well as Kitty graphics.
* Mailboxes and questions between agents, and a stash for files agents share.
**Pick herdr** if agents are most of what you run, splits are enough, and you want conversations to survive restarts.
### tmux-based agent managers
Tools such as [claude-squad](https://github.com/smtg-ai/claude-squad) (1.0.20) are not multiplexers. claude-squad runs each agent in its own tmux session and git worktree, and gives you a list of tasks to review, check out and push. It needs tmux and `gh`. Pick it if you want a task list over tmux. Pick TUIOS or herdr if you want to see the agents' panes side by side.
## GNU screen
screen is the oldest of these, and is still maintained. 5.0 added truecolor and rewrote authentication, and 5.0.2 shipped in July 2026.
**Where screen is stronger**
* It is on many old systems where nothing else is.
* A window can open a serial line (`screen /dev/ttyUSB0 115200`) or a telnet connection directly.
* Multiuser mode has per-user access lists, including read-only access.
**Where TUIOS is stronger**: almost everything else on this page. screen splits into regions, but has no floating windows, no restore after a restart, no JSON interface and no image support.
**Pick screen** for serial consoles and old machines.
## Terminals with built-in splits
WezTerm, kitty and Ghostty are terminal emulators. They draw their own windows, and their splits and tabs are part of that window. TUIOS, tmux, zellij and herdr run inside a terminal instead, so they work over SSH and in any terminal. You can use both: TUIOS inside kitty, WezTerm or Ghostty gets Kitty graphics from the host.
| | WezTerm | kitty | Ghostty |
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------- | ----------------------- |
| Version checked | 20240203 (latest stable, nightly builds since) | 0.49.0 | 1.3.1 |
| Language | Rust | C, Python and Go | Zig |
| Splits | Panes, tabs, windows, workspaces | Layouts: splits, tall, fat, grid, stack, horizontal, vertical | Native tabs and splits |
| Detach | Yes, through a mux server | No | No |
| Remote | SSH and TLS domains to a WezTerm mux on the remote host | `kitten ssh`. No remote persistence | No |
| Scripting | `wezterm cli`, Lua events | `kitten @` remote control, JSON protocol | AppleScript, macOS only |
| Images | iTerm2, Kitty graphics, sixel | Kitty graphics (kitty created it) | Kitty graphics |
| Config | Lua | `kitty.conf` | Plain text config file |
| Platforms | Linux, macOS, Windows, FreeBSD, NetBSD | Linux, macOS, BSDs | Linux, macOS |
### WezTerm
WezTerm is the one terminal here that is also a real multiplexer. A unix domain runs a mux server that keeps panes alive after the GUI closes. An SSH or TLS domain connects to a WezTerm mux running on another machine, and draws its panes as native tabs. `wezterm cli` splits panes, sends text, reads pane text and lists panes as JSON. The config is a Lua program.
* **Stronger than TUIOS**: native GUI rendering, fonts and ligatures, and remote panes with native scrollback and mouse.
* **TUIOS is stronger**: it runs in any terminal and over plain SSH, has floating windows and agent tracking, and needs nothing but `tuios` on the remote side. WezTerm needs a matching WezTerm on the remote host. The last stable WezTerm release is from February 2024.
* **Pick WezTerm** if you want one program for terminal and multiplexer and you control both ends.
### kitty
kitty tiles its own windows in seven layouts and has a remote control protocol (`kitten @`) that can open windows, send text and read their contents. Session files set up tabs, windows and layouts, and since 0.43 kitty can save the current layout as a session file. kitty's FAQ says the one thing it lacks from tmux is remote persistence: close kitty and its shells end.
* **Stronger than TUIOS**: GPU rendering, native windows, and it created the Kitty graphics and keyboard protocols that other terminals now use.
* **TUIOS is stronger**: detach and reattach, sessions that survive closing the terminal, remote hosts, floating windows and agent tracking.
* **Pick kitty** if you work on one machine and do not need sessions to outlive the terminal.
### Ghostty
Ghostty has native tabs and splits, and on macOS an AppleScript dictionary for windows, tabs and input. It has no detach and no multiplexer of its own. Pick it as a fast native terminal, and run TUIOS or tmux inside it when you need sessions that persist.
## Coming from tmux
TUIOS uses the same leader key as tmux, and several prefix keys match:
| Action | tmux | TUIOS |
| -------------------- | ------------------------------------------ | ------------------------------------------ |
| New window | Ctrl+Bc | Ctrl+Bc |
| Detach | Ctrl+Bd | Ctrl+Bd |
| Copy mode | Ctrl+B\[ | Ctrl+B\[ |
| Zoom | Ctrl+Bz | Ctrl+Bz |
| Split left and right | Ctrl+B% | Ctrl+B\| |
| Split top and bottom | Ctrl+B" | Ctrl+B- |
Many CLI verbs share tmux's names too: `new-window`, `split-window`, `send-keys`, `capture-pane`, `list-windows`, `kill-session`.
The difference to learn is the two modes. Keys typed in window management mode move windows. Keys typed in terminal mode go to the shell. i enters terminal mode and Alt+Esc leaves it. The prefix key works in both. [Keybindings](/docs/keybindings) lists everything, and every key can be rebound.
## Sources
**tmux**
* [tmux 3.7c release](https://github.com/tmux/tmux/releases/tag/3.7c) and [3.8-rc](https://github.com/tmux/tmux/releases/tag/3.8-rc)
* [CHANGES](https://github.com/tmux/tmux/blob/master/CHANGES): floating panes in 3.7 and 3.8, mouse on by default in 3.8, `wait-for -E`, sixel fixes
* [Manual page source](https://github.com/tmux/tmux/blob/master/tmux.1): `attach -r`, `server-access`, `mouse`, `allow-passthrough`, control mode
* [README](https://github.com/tmux/tmux/blob/master/README): supported platforms
* [configure.ac](https://github.com/tmux/tmux/blob/master/configure.ac): sixel is off unless built with `--enable-sixel`
* [tmux-resurrect](https://github.com/tmux-plugins/tmux-resurrect) and [TPM](https://github.com/tmux-plugins/tpm), both third party
* [iTerm2 tmux integration](https://iterm2.com/documentation-tmux-integration.html): tmux windows as native tabs and windows
**zellij**
* [CHANGELOG](https://github.com/zellij-org/zellij/blob/main/CHANGELOG.md): web client (0.43.0), native Windows, read-only attach, `subscribe` and `list-panes` (0.44.0), Kitty graphics (0.45.0)
* [Session resurrection](https://zellij.dev/documentation/session-resurrection.html)
* [Web client](https://zellij.dev/documentation/web-client.html)
* [CLI actions](https://zellij.dev/documentation/cli-actions.html)
* [Plugins](https://zellij.dev/documentation/plugins.html)
* [0.45.1 release](https://github.com/zellij-org/zellij/releases/tag/v0.45.1)
**herdr**
* [Concepts](https://herdr.dev/docs/concepts/): workspaces, tabs, panes, mouse, clients
* [Session state and restore](https://herdr.dev/docs/session-state/): restart, screen history, agent restore, live handoff
* [Supported agents](https://herdr.dev/docs/agents/) and [agent automation](https://herdr.dev/docs/agent-automation/)
* [Plugins](https://herdr.dev/docs/plugins/), [configuration](https://herdr.dev/docs/configuration/) (TOML, Kitty graphics), [connecting machines](https://herdr.dev/docs/connecting-machines/), [install](https://herdr.dev/docs/install/)
* [CHANGELOG](https://github.com/herdrdev/herdr/blob/master/CHANGELOG.md) and [0.9.1 release](https://github.com/herdrdev/herdr/releases/tag/v0.9.1)
* [claude-squad](https://github.com/smtg-ai/claude-squad)
**GNU screen**
* [Manual](https://www.gnu.org/software/screen/manual/screen.html): `-X`, `-Q`, `split`, `mousetrack`, `aclchg`, serial and telnet windows
* [Release tarballs](https://ftp.gnu.org/gnu/screen/): 5.0.2, July 2026
* [5.0.0 announcement](https://lists.gnu.org/archive/html/info-gnu/2024-08/msg00004.html)
**Terminals**
* WezTerm: [multiplexing](https://wezterm.org/multiplexing.html), [features](https://wezterm.org/features.html), [CLI](https://wezterm.org/cli/cli/index.html), [releases](https://github.com/wezterm/wezterm/releases)
* kitty: [layouts](https://sw.kovidgoyal.net/kitty/layouts/), [remote control](https://sw.kovidgoyal.net/kitty/remote-control/), [sessions](https://sw.kovidgoyal.net/kitty/sessions/), [FAQ](https://sw.kovidgoyal.net/kitty/faq/), [changelog](https://sw.kovidgoyal.net/kitty/changelog/)
* Ghostty: [features](https://ghostty.org/docs/features), [1.3.1 release notes](https://ghostty.org/docs/install/release-notes/1-3-1)
## Related
# Layout Modes
URL: https://tuios.gaurav.zip/docs/layout-modes
> Floating, BSP, master-stack, scrolling columns and zoom, and how to switch between them.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
Tiling is a switch: on or off. When it is on, the panes follow one of three layouts. When it is off, the panes float and you place them yourself. Zoom sits on top of all of them and shows one pane large for a moment.
| Mode | What it does |
| ------------ | ------------------------------------------------------------------------------------------------------------------ |
| BSP | A tree of splits. Any number of panes, each resizable on its own. The default. See [BSP Tiling](/docs/bsp-tiling). |
| Master-stack | Fixed arrangements by pane count, with one master pane on the left. |
| Scrolling | Niri-style columns on a strip wider than the screen. The screen scrolls instead of every pane shrinking. |
| Floating | Tiling off. Panes can overlap, and you move, snap and resize them. |
## Switching modes
Turn tiling on or off:
| Key | Where |
| --------------------------------------------- | ---------------------- |
| t | Window management mode |
| Ctrl+BSpace | Any mode |
Pick the layout from the command palette (Ctrl+P):
* `Layout: BSP tiling`
* `Layout: master-stack`
* `Layout: scrolling (niri-style)`
* `Layout: disable tiling`
Picking a layout also turns tiling on. There is no default key for a layout. Bind one yourself if you switch often.
The layout mode belongs to the session. It is kept in the daemon's session state, so a scrolling session is still scrolling after you detach and reattach, and turning tiling off does not forget which layout you had. The layout itself (the BSP tree, the scrolling columns) is kept per workspace, so each of the nine workspaces has its own.
`startup.layout` sets the mode a **new** session starts in: `bsp`, `master-stack` or `scrolling`. `startup.tiled` (default `true`) decides whether a new session starts tiled at all. A session that already exists keeps its own mode.
```toml
[startup]
layout = "scrolling"
tiled = true
```
From a script, `tuios set-layout` turns tiling on or off and tidies the splits:
```bash
tuios set-layout --tiling true
tuios set-layout --equalize
tuios set-layout --rotate
```
It needs an attached client.
## Master-stack
The arrangement depends on how many panes the workspace has:
| Panes | Arrangement |
| --------- | -------------------------------------------------------------------------------------------- |
| 1 | Full screen |
| 2 | Side by side, or stacked when the screen is taller than it is wide as drawn |
| 3 | A master pane on the left, two panes stacked on the right |
| 4 or more | A grid: two columns up to six panes, three columns beyond. A short last row shares its width |
`appearance.master_ratio` is the master pane's share of the screen, as a percent from 30 to 70 (default `50`). \< and > in window management mode move it for the workspace you are on. A workspace nobody has changed starts at the configured value.
Master-stack recomputes every rectangle from `master_ratio` each time it retiles. A resize to a percentage (below) is dropped on the next retile. BSP keeps the split ratio, so the resize survives there.
## Scrolling
The scrolling layout is modeled on the [niri](https://github.com/YaLTeR/niri) window manager. Each pane is a **column** on a horizontal strip, and the screen is a window onto that strip. A new pane gets a new column right after the focused one, and the strip scrolls to show it. The other columns keep their width.
A column holds one pane by default. It can hold several stacked vertically, which split its height evenly. Closing the last pane in a column removes the column and moves focus to the column on its left.
### Moving around
| Input | Action |
| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Alt+Left / Alt+Right (terminal mode) | Focus the column to the left or right |
| Alt+P / Alt+N | Focus the column to the left or right |
| H / L, or Ctrl+Left / Ctrl+Right (window mode) | Move the focused column left or right along the strip |
| \< / > (window mode) | Shrink or grow the focused column by four cells |
| Alt or Shift + mouse wheel | Scroll the strip sideways |
Keyboard focus centers the focused column, so its neighbours peek in at the edges. Clicking a column that is partly off screen brings all of it on screen (`appearance.niri_click_reveals`). With `focus_follows_mouse` on, hovering does the same (`appearance.niri_hover_reveals`).
The plain mouse wheel belongs to the pane under the pointer. The strip only scrolls while Alt or Shift is held. This stops a trackpad's sideways drift from sliding the strip while you scroll a pane's history.
### Column commands
These have palette entries and no default key. The action names can be bound in `[keybindings]`.
| Palette entry | Action | What it does |
| ------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `Scroll: cycle column width` | `scroll_cycle_width` | Cycle the focused column through 33%, 50%, 55%, 67% and 90% of the screen |
| `Scroll: move window into the column below` | `scroll_consume` | Pull the pane from the next column into the focused one, stacked below |
| `Scroll: move window out to its own column` | `scroll_expel` | Move the focused pane out of its column into a new column to the right. Focus follows it, so expel undoes a consume |
| none | `scroll_focus_left`, `scroll_focus_right` | Focus the column to the left or right |
| none | `scroll_move_left`, `scroll_move_right` | Move the focused column left or right |
A column's width is a share of the screen until you resize it with \< or >, which pins it to a cell count. Cycling the width unpins it again.
### Settings
| Key | Default | Meaning |
| -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `appearance.scroll_column_width` | `55` | Width of a new column, as a percent of the screen (20 to 100) |
| `appearance.scroll_column_max` | `90` | The widest a column may be set, as a percent. `100` lets a column fill the screen, and the next column no longer peeks in |
| `appearance.niri_scroll_cells` | `8` | Cells the strip moves per wheel event (1 to 200) |
| `appearance.niri_reverse_scroll` | `false` | Reverse the wheel direction on the strip |
| `appearance.niri_click_reveals` | `true` | Clicking a partly hidden column scrolls it fully into view |
| `appearance.niri_hover_reveals` | `true` | With focus-follows-mouse, hovering does the same |
The default column width of 55% is over half on purpose: two columns never quite fit, so the strip reads as something you scroll.
The scroll offset and the columns are part of the session. Every client attached to the session sees the same columns at the same offset, and the columns come back after a session switch or a reattach. [A session switch forgot my column widths](/blog/a-session-switch-forgot-my-column-widths) tells how the columns became session state.
**Limits:** shared borders draw no dividers in the scrolling layout, since each column keeps its own border. The strip always animates when it slides, even with animations off, because a jump is hard to follow.
## Floating
With tiling off, panes float. Drag a pane by its title bar to move it, or Alt+drag anywhere on it. Right-drag resizes it from the nearest corner. In window management mode:
| Key | Action |
| ------------------------------------------------------------------- | ------------------------------ |
| h / l | Snap to the left or right half |
| f | Snap to full screen |
| u | Unsnap |
| Ctrl+BL1-4 | Snap to a corner |
Dragging a pane to a screen edge or corner snaps it there too. See [Keybindings](/docs/keybindings).
The palette entry `Toggle floating` lifts only the focused pane out of the tiling and lets it float over the others. Run it again to put the pane back.
For a short-lived floating pane that closes when its program exits, use a popup:
```bash
tuios popup --width 90% --height 80% -- htop
```
## Zoom
Zoom shows the focused pane over the whole layout. The layout underneath is not changed.
| Key | Where |
| ----------------------------------------- | ---------------------- |
| z | Window management mode |
| Ctrl+Bz | Any mode |
On a tiled pane, the third title bar control toggles zoom too (`appearance.window_button_zoom`).
| Key | Default | Meaning |
| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `appearance.zoom_size` | `100` | Share of the screen a zoomed pane takes, as a percent (50 to 100). Below 100 the layout stays visible around it |
| `appearance.zoom_max_width` | `0` | Width cap in cells. `0` fills the screen. Useful on an ultrawide display |
| `appearance.zoom_follows_focus` | `true` | Moving focus while zoomed zooms the pane you moved to |
| `appearance.zoom_animation` | `true` | Slide between the tile and the zoomed box instead of switching in one frame |
## Gaps and sizing
`appearance.gap` keeps empty cells between neighbouring panes, in every tiling mode (0 to 8, default `0`). It is an inner gap only: there is no margin at the screen edge.
The layout prefix sizes the focused pane to a percentage:
| Key | Action |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| Ctrl+BL5-9 | Width to 50%, 60%, 70%, 80% or 90% of the content area |
| Ctrl+BLShift+5-9 | Height to 50% through 90% |
Each percentage is its own action (`resize_width_50` to `resize_width_90`, `resize_height_50` to `resize_height_90`), so any of them can be rebound in `[keybindings.layout_prefix]`. The layout still applies its minimum pane size and gaps. In the scrolling layout only the width actions do anything.
`gap`, `master_ratio`, `scroll_column_width` and `shared_borders` decide how many cells each pane gets, so they are shared across the session: change one on any client and every attached client follows. Purely visual settings such as the theme, border style and glyphs stay per client.
## Finding a pane
The palette entry `All windows` opens a searchable list of every pane on every workspace, with a short preview of each. It has no default key.
| Key | Action |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Type | Filter by title, workspace number, preview text or directory |
| Up / Down, Ctrl+P / Ctrl+N | Move the selection |
| Enter | Go to the pane: switch workspace, restore it if minimized, focus it |
| Ctrl+U | Clear the query |
| Esc | Close |
The preview is the first three non-empty lines of the pane, taken when the list opens. The directory is only read on Linux.
## Typing into several panes
Multifocus sends your typing to several panes at once, like tmux's `synchronize-panes`.
| Input | Action |
| ------------------------------------------------ | ------------------------------ |
| Ctrl+Shift+click on a pane | Add or remove it from the set |
| Palette: `Toggle multifocus` | Add or remove the focused pane |
| Palette: `Clear multifocus` | Empty the set |
Panes in the set get a distinct border colour. While the set is not empty and you are in terminal mode, every key that goes to the focused pane also goes to each pane in the set. Keys TUIOS handles itself, such as the leader key, are not copied.
The set belongs to your client. It is not shared with other clients, it does not survive a detach, and switching sessions clears it.
## Related
# BSP Tiling
URL: https://tuios.gaurav.zip/docs/bsp-tiling
> Tile windows with binary space partitioning, and switch to the master-stack or scrolling layouts.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
When tiling is on, TUIOS arranges windows for you so they never overlap. The default layout is **BSP** (binary space partitioning): every new window splits an existing one in two, so the screen becomes a tree of splits you can reshape with the keyboard or the mouse.
All keys on this page are for window management mode unless they start with Ctrl+B, which works in both modes. On macOS, read Alt as Opt.
## Turning tiling on and off
New sessions start tiled. To switch it:
| Key | Where |
| ------------------------------------------------------ | ---------------------- |
| t | Window management mode |
| Ctrl+BSpace | Any mode |
| Ctrl+Btt | The window prefix menu |
Tiling is on or off for the whole session. When it is off, windows float: you move and resize them freely, and h, l and f snap the focused window to the left half, the right half or the full screen.
While tiling is on, the dock shows the tiling icon followed by `V` or `H`: the direction the **next** split will take.
`startup.tiled = false` in the [configuration](/docs/configuration) makes new sessions start floating.
## Layout modes
Tiling uses one of three layouts:
| Layout | How windows are arranged |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **BSP** (default) | A tree of splits. Any number of windows, each resizable on its own. |
| **Master-stack** | One window: full screen. Two: side by side, or stacked on a tall screen. Three: a master window on the left and two stacked on the right. Four or more: a grid, two columns wide up to six windows and three beyond. |
| **Scrolling** | Niri-style columns on a strip wider than the screen. New windows add a column and the view scrolls, instead of every window shrinking. |
There is no default key for switching layouts. Open the command palette (Ctrl+P) and run one of:
* `Layout: BSP tiling`
* `Layout: master-stack`
* `Layout: scrolling (niri-style)`
* `Layout: disable tiling`
Choosing a layout turns tiling on if it was off. The layout belongs to the session and survives detaching; turning tiling off and on again brings the same layout back. `startup.layout` sets the layout new sessions start with: `bsp`, `master-stack` or `scrolling`.
`Next layout` and `Previous layout` in the palette do something else: they cycle through your saved [layout templates](#layout-templates).
The rest of this page is about BSP. The scrolling layout's keys and commands are described in the repository's [LAYOUT\_MODES.md](https://github.com/Gaurav-Gosain/tuios/blob/main/docs/LAYOUT_MODES.md).
## How new windows are placed
A new window splits the focused one. Which way it splits depends on the tree's **insertion scheme**:
| Scheme | Rule |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `spiral` (default) | Alternate vertical and horizontal by the depth of the window being split, like bspwm. Repeatedly splitting the newest window spirals inward. |
| `longest_side` | Split across the longer side of the window. |
| `alternate` | Alternate vertical and horizontal by the total number of splits in the tree. |
| `smart_split` | Split vertically when the window is more than twice as wide as it is tall, horizontally when it is taller than wide, and otherwise by depth. |
A new workspace always starts with `spiral`. There is no config option or key for the scheme. It is saved in [layout templates](#layout-templates) (`tiling_scheme`), so loading a template that carries one is how you change it.
`Smart split` in the command palette splits the focused window once with the `smart_split` rule, whatever the tree's scheme. Its action name is `smart_split` if you want to bind it.
## Splitting
A split opens a new window beside the focused one.
| Key | Prefix form | Action |
| ------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------- |
| - | Ctrl+B- | Split horizontally: the new window goes below |
| \| or \\ | Ctrl+B\| | Split vertically: the new window goes to the right |
| R | Ctrl+BR | Rotate the split holding the focused window, so side by side becomes stacked and back |
| = | Ctrl+B= | Equalize: set every split back to half and half |
A split creates the window at once. There is no empty slot waiting to be filled, so pressing n after a split adds a third window.
## Preselection
Preselection chooses where the **next** window goes, relative to the focused one, without creating it yet.
| Key | Next window appears |
| --------------------------- | ------------------- |
| Alt+h | Left |
| Alt+j | Below |
| Alt+k | Above |
| Alt+l | Right |
It only applies while tiling is on, and the next new window uses it up. There is no key to cancel a preselection. It stays until a window is created, or until a split key replaces it with its own direction.
## Moving focus and swapping
| Key | Action |
| ------------------------------------------------------------------- | -------------------------------------------------- |
| h / l | Focus the window to the left / right |
| Alt+arrow, or Ctrl+B then an arrow | Focus the window in that direction, in either mode |
| H or Ctrl+← | Swap with the window to the left |
| J or Ctrl+↓ | Swap with the window below |
| K or Ctrl+↑ | Swap with the window above |
| L or Ctrl+→ | Swap with the window to the right |
A swap exchanges two windows without changing the shape of the tree. If there is no window in that direction, nothing happens. Dragging a window onto another with the mouse also swaps them.
## Resizing
Each key moves one edge of the focused window. Every window that shares that edge moves with it.
| Key | Edge | Step |
| ---------------- | ------------------- | --------- |
| > | Right edge, outward | 4 columns |
| \< | Right edge, inward | 4 columns |
| . | Left edge, outward | 4 columns |
| {'}'} | Bottom edge, down | 2 rows |
| {'{'} | Bottom edge, up | 2 rows |
| ] | Top edge, up | 2 rows |
| \[ | Top edge, down | 2 rows |
A window cannot shrink below 20 columns or 5 rows, and an edge already at the screen border cannot move outward. If a key does nothing, try the opposite edge: . instead of > for a window on the right side of the screen.
There is no default key to move the left edge inward. , would be the natural one, but it opens the settings page. Bind the `resize_master_shrink_left` action to a free key if you want it.
To resize with the mouse, drag a window's border. With [shared borders](#shared-borders) on, drag the divider line between two windows.
In master-stack, the same keys move the edge by 4 columns, as in any tiled layout. The master window's default share of the screen is set with `appearance.master_ratio` (30 to 70, default 50) or on the settings page.
### Resizing to a percentage
The layout prefix sizes the focused window to a share of the screen:
| Keys | Action |
| -------------------------------------------------------------------------------------------- | -------------------- |
| Ctrl+BL then 5 to 9 | Width to 50% to 90% |
| Ctrl+BL then Shift+5 to 9 | Height to 50% to 90% |
The layout still keeps every window at its minimum size, so you get the requested share wherever the neighbours allow it.
### How a resize is stored
A resize moves the window rectangles first, then recomputes every split ratio in the tree from the result. Ratios are what the tree keeps, so a resize survives later retiling, detaching and reattaching. Split ratios in BSP have no fixed range; only master-stack's `appearance.master_ratio` setting is limited to 30% to 70%.
## Shared borders
By default each window draws its own border, so two tiled windows show two lines between them. Shared borders draw a single line instead, and leave more room for content.
| | |
| ---------- | -------------------------------------------------------------------- |
| Config | `appearance.shared_borders = true` |
| Default | off |
| Flag | `--shared-borders` |
| In the app | the settings page, or `Toggle shared borders` in the command palette |
```toml
[appearance]
shared_borders = true
```
Shared borders apply to BSP and master-stack. The scrolling layout always draws a border per window.
With shared borders on, the pointer changes shape over a divider, and dragging it moves the divider.
The divider takes one cell, and it comes out of the right or bottom window. With a 160-column screen split in half, the left window is 80 columns wide and the right one 79. This is expected, and = does not change it.
`appearance.gap` adds empty cells between neighbouring windows, in every layout.
## Layout templates
A template saves the current arrangement so you can restore it later.
| Key | Action |
| ------------------------------------------------------ | ---------------------------------------- |
| Ctrl+BLs | Save the layout, after asking for a name |
| Ctrl+BLl | Load a layout from a searchable list |
The same two are in the command palette as `Save layout` and `Load layout`, and `Next layout` and `Previous layout` cycle through saved templates. A template records window positions, workspaces, working directories, startup commands and the insertion scheme.
From the shell:
```bash
tuios layout list # list saved templates
tuios layout dir # print the templates directory
tuios layout export mysetup # print a template as a tape script
tuios layout delete mysetup # delete a template
```
With tiling off, Ctrl+BL then 1 to 4 snaps the focused window to a corner: top left, top right, bottom left, bottom right.
## Persistence
Each workspace has its own BSP tree. The trees are part of the session, so they survive detaching, and the daemon saves them to disk so they come back after a restart. See [Sessions](/docs/sessions#session-resurrection) for what else a restart keeps.
If a tree goes stale, for example because it still names a window that has closed, TUIOS rebuilds it from the current windows and keeps their proportions.
## Troubleshooting
### Windows are not tiling
Check the dock for the tiling icon with `V` or `H`. If it is not there, press t.
If windows are arranged but not as BSP would arrange them, you are in master-stack or scrolling. Run `Layout: BSP tiling` from the command palette.
### A resize key does nothing
The edge is at the screen border, or moving it would shrink a window below 20 columns or 5 rows. Use the key for the opposite edge. , opens settings and never resizes.
### The layout looks lopsided
Press = to set every split back to half and half.
### A new window appeared in the wrong place
A preselection was still set. It lasts until a window uses it, and a split key replaces it: pressing - after Alt+l puts the window below, not to the right.
## Related
# Session Rail
URL: https://tuios.gaurav.zip/docs/session-rail
> The sidebar that lists sessions, machines, panes, files, agents and git state.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
The session rail is a sidebar down one edge of the screen. It lists your sessions, the panes in the current session, the files in the focused pane's directory, the agents at work, and the git state of the focused pane. Everything on it can be reached with the mouse or the keyboard.
## Turning it on
The rail is off by default.
| How | What it does |
| ----------------------------------------- | ---------------------- |
| Ctrl+Bb | Show or hide the rail |
| Palette: `Toggle sidebar` | The same |
| `appearance.sidebar.enabled = true` | Show it on every start |
```bash
tuios set-config appearance.sidebar.enabled true
tuios set-config appearance.sidebar.position right
```
`position` is `left` (default), `right` or `hidden`. `width` is the preferred width in columns on a wide screen (default `28`). On a narrow screen, or after \<, the rail collapses to a one-column strip of marks. Hover the strip for a label (`tooltips`).
## The sections
| Section | What it lists |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessions` | Every session, grouped by machine when you have [remote hosts](/docs/remote-hosts). Worktree sessions sit under their repository, labelled by branch. A `global` group appears once a second machine is reachable |
| `terminals` | The panes of the current session |
| `files` | The focused pane's directory. See [the files section](/docs/configuration#the-files-section) |
| `agents` | Every pane running a coding agent, with its state. See [Agents](/docs/agents) |
| `git` | The focused pane's repository, branch, and how far it is ahead of or behind its upstream |
`appearance.sidebar.sections` is one string that says which sections the rail draws, in what order, and the most each may take:
```toml
[appearance.sidebar]
sections = "sessions:25,terminals,files:25,agents:34"
```
A name left out is a section the rail does not draw. The percent is a ceiling, not a reservation. `spacer` is an empty block you can repeat to add a gap or push the next section to the bottom. The `git` section is not in the default layout, so add it by name:
```toml
[appearance.sidebar]
sections = "sessions:25,terminals,git,files:25,agents:34"
```
The settings page (, in window mode) has an editor for this under Sidebar, Sections. The full syntax is on the [configuration page](/docs/configuration#sections).
The last section in the layout is pinned to the bottom of the rail. A divider row sits above it. Drag the divider, or put the cursor on it and press \< or >, to move the split. A double click on it, or Enter, restores the layout's own share.
`appearance.git_dirty` (default `true`) adds counts of staged, changed and untracked paths to the git section. It is the only part of the section that walks the working tree, so turn it off on a very large repository.
## Using it with the keyboard
Move the keyboard into the rail with s in window management mode, or Ctrl+Be from anywhere. The same keys take you back to the panes. Esc also leaves.
While the rail has the keyboard, these keys from `[keybindings.sidebar]` apply:
| Key | Action |
| --------------------------------------------------------------- | ------------------------------------------------------------------- |
| j / k, Down / Up | Move the cursor |
| g / G, Home / End | First or last row |
| l / h, Right / Left | Next or previous section |
| Tab / Shift+Tab | Cycle sections |
| Enter | Activate the row: attach the session, or focus the pane |
| 1-9 | Jump to a session by its position |
| J / K | Move a session or machine down or up the rail |
| n | New session. Asks which machine when there is more than one |
| t | New terminal in the session |
| r | Rename the pane under the cursor |
| c | Recolour the pane under the cursor |
| m | Open the menu for the row under the cursor |
| x | Open that menu on its Close or Kill entry |
| f | Agents: show all sessions, or only this one |
| o | Agents: sort by priority, or by most recent |
| i | Open the mailbox for the pane under the cursor |
| / | Search every pane in every session. `@state` filters by agent state |
| \< / > | Collapse or expand the rail. On the divider, move the split |
| ? | Show the rail's keys |
On a row of the files section, `[keybindings.sidebar_files]` answers first: a create, r rename, d delete to the trash, D delete permanently, y copy, x cut, p paste.
Every key can be rebound. See [Configuration](/docs/configuration#keybindings).
## Using it with the mouse
* Click a session to switch to it, or a pane to focus it.
* Click the `+` on a section header to make a session or a terminal. The `+` beside a machine makes a session on that machine.
* Right-click a row for its menu.
* Hover a row whose title is too long and it scrolls (`marquee`).
* Click a folder in the files section to walk into it (`folder_click`).
## Agent rows
Each row in the agents section shows the pane's agent state glyph, its name, and how long it has been in that state. When the rail has room, a second line carries the agent's harness and the note it reported.
`[appearance.sidebar.agent_row]` picks which tokens a row shows, in what order, and how each is coloured. It is a table, so set it in `config.toml` rather than with `set-config`:
```toml
[appearance.sidebar.agent_row]
# harness, name, state, elapsed, message, session, host
tokens = ["session", "harness", "name", "elapsed", "message"]
[[appearance.sidebar.agent_row.elapsed.rule]]
gt = 30
fg = "warning"
```
A rule has exactly one test (`equals`, `contains`, `starts_with`, `gt` or `lt`) and sets `fg`, `bold` or both. For `elapsed`, `gt` and `lt` compare minutes. The first matching rule wins, with at most eight per token.
## Other options
| Key | Default | Meaning |
| ------------------ | ------------ | -------------------------------------------------------- |
| `show_glyphs` | `true` | Agent state glyph on each row |
| `show_counts` | `true` | Pane count on each session row |
| `marquee` | `true` | Scroll a hovered row's long title |
| `tooltips` | `true` | Label the collapsed strip on hover |
| `file_icons` | `true` | A Nerd Font icon per file type |
| `file_icon_colors` | `true` | Draw each file icon in its type's colour |
| `folder_click` | `"navigate"` | `navigate`, `cd` (send the focused pane there) or `both` |
| `file_actions` | `true` | Allow create, rename, delete, copy, cut and paste |
| `file_delete` | `"trash"` | `trash` or `permanent` |
All of these live under `[appearance.sidebar]`. `appearance.session_colors` gives each session its own colour on the rail, and `appearance.session_border` carries that colour onto every pane border.
The flat `sidebar_*` keys and the `show_windows` and `show_agents` booleans still load. They are folded into `[appearance.sidebar]` and `sections` when the file is read. Use the new keys in new configs.
## Related
# Themes and Appearance
URL: https://tuios.gaurav.zip/docs/themes
> Colour themes, custom and imported themes, chrome colours, glyph sets, spacing, dimming and the spotlight.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
Four things decide what TUIOS looks like:
| Part | What it controls | How to set it |
| ------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Colour | The terminal colours, the accents, the borders | `appearance.theme` |
| Shape | The characters the chrome is drawn with: borders, controls, rules, rail marks | `appearance.glyphs` |
| Spacing | Empty ground between panes, padding inside dialogs | `appearance.gap`, `appearance.panel_padding` |
| Text | What a window title, a workspace tab and the clock show | `window_title_format`, `dock_workspace_tab_format`, `clock_format` |
Every option on this page can be changed while TUIOS runs, from the settings page (, in window mode), with `tuios set-config`, or by editing `config.toml`, which is reloaded when saved.
## Choosing a theme
TUIOS ships over 300 themes. A theme sets the 16 ANSI colours plus foreground, background and cursor, and TUIOS derives its own interface colours from them.
```toml
[appearance]
theme = "catppuccin_mocha"
```
```bash
# Change it in the running session
tuios set-config appearance.theme catppuccin_mocha
# Start with a theme, overriding the config for this run
tuios --theme dracula
# Find one
tuios list-themes --filter gruvbox
tuios --list-themes | fzf --preview 'tuios --preview-theme {}'
```
Theme ids use underscores: `tokyo_night`, not `tokyo-night`.
In the app, open the picker from the palette (`Theme picker`) or from the Theme row on the settings page. It is searchable, shows a swatch per theme, and applies the selection live as you move. Esc puts back the theme you started with.
An empty `theme` turns theming off. TUIOS then emits plain colour indices and your terminal's own palette decides what they look like.
### Checking a theme
`tuios list-themes ` prints a theme's colours with the contrast each one measures against the theme's background. It tells you whether a palette is readable before you look at it.
```bash
tuios list-themes catppuccin_latte
tuios list-themes catppuccin_latte --json | jq -r '.palette.illegible[]'
tuios list-themes --json | jq -r .active
```
## Importing a theme
`tuios import-theme` converts a kitty, ghostty, alacritty or wezterm colour scheme into a TUIOS theme. The format is read from the file's content, so the extension does not matter.
```bash
tuios import-theme ~/.config/kitty/current-theme.conf --name mine
tuios set-config appearance.theme mine
```
A scheme that sets only some colours imports those, and the rest fall back to the xterm defaults. Wezterm's TOML schemes are read. Its Lua schemes are not.
## Writing a theme
A theme is a JSON file in `~/.config/tuios/themes/` (following `$XDG_CONFIG_HOME`). The file name without `.json`, lowercased, is its id unless the file sets `id`.
```json
{
"id": "my-theme",
"display_name": "My Theme",
"fg": "#e0def4",
"bg": "#191724",
"cursor": "#e0def4",
"black": "#26233a",
"red": "#eb6f92",
"green": "#31748f",
"yellow": "#f6c177",
"blue": "#9ccfd8",
"purple": "#c4a7e7",
"cyan": "#ebbcba",
"white": "#e0def4",
"bright_black": "#6e6a86",
"bright_red": "#eb6f92",
"bright_green": "#31748f",
"bright_yellow": "#f6c177",
"bright_blue": "#9ccfd8",
"bright_purple": "#c4a7e7",
"bright_cyan": "#ebbcba",
"bright_white": "#e0def4"
}
```
* The colour names are `purple` and `bright_purple`, not `magenta`.
* A colour is a hex string or an object: `{"r": 255, "g": 0, "b": 0, "a": 255}`.
* Every field is optional. `fg` defaults to `#e5e5e5`, `bg` to `#000000`, `cursor` to `fg`, the eight normal colours to the xterm defaults, and each `bright_*` to its normal colour. Set the bright colours yourself, or bright text looks the same as normal text.
* The directory is read again when you select a theme or run `list-themes`, so a file you just wrote can be selected at once. Subdirectories are not read.
* A file that does not parse is skipped. `tuios list-themes` reports it under `problems` with the reason.
### Chrome colours
The 16 ANSI colours are what programs in your panes paint with. By default TUIOS also takes its own accents from them: the logo and the selected row from `bright_blue`, the terminal-mode border from `bright_green`, and so on. A `chrome` object sets those accents directly, without changing what programs see:
```json
{
"id": "amber",
"chrome": {
"accent": "#ffb454",
"accent_bright": "#ffd580",
"success": "#aad94c",
"warning": "#ffb454",
"error": "#ff3333",
"info": "#59c2ff",
"surface": "#2b2118"
}
}
```
| Field | What it colours | Taken from when absent |
| ------------------------- | --------------------------------------------------------------------- | ---------------------- |
| `accent` | Logo, selected row, window-mode pill | `bright_blue` |
| `accent_bright` | Secondary accent, focused border in window mode | `bright_cyan` |
| `success` | Terminal-mode pill, focused border in terminal mode, success messages | `bright_green` |
| `warning` | Copy-mode pill, warnings | `yellow` |
| `error` | Error messages | `red` |
| `info` | Info messages | `blue` |
| `surface` | The fill of every dialog: palette, pickers, menus, which-key | A fixed dark grey |
| `canvas`, `panel`, `card` | The darker and lighter steps around `surface` | Derived from `surface` |
Dialogs sit on a fixed grey ramp so they stay readable over any pane content. `surface` moves that ramp, and TUIOS picks text colours for it by contrast, so a light surface gets dark text. Text colours cannot be set directly. A field that is not a valid hex colour is ignored on its own.
### Border colours
`appearance.border_focused_color` and `appearance.border_unfocused_color` override the theme's border colours with a hex value. They live in `config.toml`, not in the theme. Clear one to hand the border back to the theme.
## Glyph sets
A glyph set picks the shapes the chrome is drawn with. Four ship: `default`, `unicode` (no Nerd Font glyphs), `heavy` and `ascii`.
```bash
tuios set-config appearance.glyphs heavy
tuios set-config appearance.border_style glyphs
tuios list-glyphs heavy
```
`border_style = "glyphs"` is what makes the border follow the set. The settings page has a searchable Glyph set picker with a live preview. Writing your own set, the roles it can name and the width rules are covered in [Configuration](/docs/configuration#glyph-sets).
## Spacing and text
```bash
tuios set-config appearance.gap 2 # cells between tiled panes, 0 to 8
tuios set-config appearance.panel_padding 4 # columns inside dialogs, 1 to 6
tuios set-config appearance.clock_format "Mon 3:04PM"
tuios set-config appearance.window_title_format "{index}: {title}"
```
* `window_title_format` accepts `{title}`, `{index}` and `{cwd}`.
* `dock_workspace_tab_format` accepts `{index}` and `{name}`.
* `clock_format` is a Go time layout, such as `15:04` or `Mon 3:04PM`.
## Quieting what you are not using
| Key | Default | Meaning |
| -------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `appearance.dim_unfocused` | `0` | Fade panes you are not in, as a percent from 0 to 90. `0` is off |
| `appearance.zen_mode` | `"disabled"` | Hide pane borders: `disabled`, `always`, or `mouse` (hidden while the mouse is still) |
| `appearance.border_style` | `"rounded"` | `rounded`, `normal`, `thick`, `double`, `block`, `outer-half-block`, `inner-half-block`, `ascii`, `hidden` or `glyphs` |
`dim_unfocused` dims pane content only. Borders, title bars, the rail and the dock stay as they are. Without a theme it can only dim cells a program coloured itself, because a cell in the terminal's default colour has no colour TUIOS knows. Set a theme first.
## Selection and search colours
`[appearance.selection]` holds the colours a pane marks text with. They are settings rather than part of the theme, because TUIOS chooses them and the program in the pane does not.
| Key | Default | Meaning |
| ------------------------- | --------------------- | ---------------------------------------------------------- |
| `bg` / `fg` | `#45475A` / empty | Selected text. An empty `fg` keeps the text's own colour |
| `bold` | `false` | Also draw selected text bold |
| `search_bg` / `search_fg` | `#8A6D2F` / `#F5E7C8` | Every search match |
| `match_bg` / `match_fg` | `#E5A93D` / `#1C1B19` | The match under the cursor |
| `cursor_bg` / `cursor_fg` | `#39C5CF` / `#08222B` | The copy mode cursor |
| `flash` | `true` | Sweep a band of light over text you just copied |
| `flash_ms` | `420` | How long the sweep takes, 80 to 3000 |
| `flash_style` | `"diagonal"` | `diagonal`, `diagonal-reverse`, `horizontal` or `vertical` |
| `flash_color` | empty | The colour of the sweep |
The scrollback browser uses the same search and selection colours.
## Spotlight
The spotlight lights one area of the screen and dims the rest. It is meant for recordings and demos. Press b in window management mode, or run `Toggle spotlight` from the palette.
| Key | Default | Meaning |
| ------------------- | --------- | ------------------------------------------------- |
| `spotlight.enabled` | `false` | Start with the spotlight on |
| `spotlight.follow` | `"mouse"` | Follow the `mouse` or the focused pane's `cursor` |
| `spotlight.radius` | `10` | Half the beam's height, in rows |
| `spotlight.dim` | `75` | Percent of its light an unlit cell loses |
| `spotlight.edge` | `"hard"` | `hard` cuts the beam off, `soft` fades it |
| `spotlight.shake` | `false` | Shake the mouse left and right to toggle the beam |
The spotlight belongs to one client. Another client attached to the same session sees its own screen unchanged. Over SSH or in the browser, `follow = "cursor"` and `edge = "hard"` send far fewer bytes.
## Related
# Configuration
URL: https://tuios.gaurav.zip/docs/configuration
> Every table and option in the TUIOS config.toml, with defaults and how changes apply.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
TUIOS reads one TOML file, `~/.config/tuios/config.toml`. If `XDG_CONFIG_HOME` is set, the file is `$XDG_CONFIG_HOME/tuios/config.toml`. TUIOS writes a commented default file on first run.
```bash
tuios config path # print the path
tuios config edit # open it in $EDITOR
tuios config reset # overwrite it with the defaults, after asking
```
Write only what you want to change. Missing keys and missing tables take their defaults.
```toml
[appearance]
theme = "nord"
[keybindings.window_management]
new_window = ["ctrl+t"]
```
Two things live outside the file: custom themes in `~/.config/tuios/themes/` and custom glyph sets in `~/.config/tuios/glyphs/`.
`tuios list-options` prints every settable option with its type, default and accepted values. It reads the same registry the validator and the settings page use, so it always matches the build you run.
## Tables
| Table | What it holds |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
| [`[appearance]`](#appearance) | Theme, borders, windows, mouse, tiling, dock. Sub-tables `scrollbar`, `selection` and `sidebar` |
| [`[notifications]`](#notifications) | How long messages stay up, and agent alerts |
| [`[keybindings]`](#keybindings) | The leader key and 19 sections of bindings |
| [`[daemon]`](#the-daemon-table) | Daemon log level and agent detection |
| [`[startup]`](#the-startup-table) | How a new session starts |
| [`[tape]`](#the-tape-table) | Project tapes |
| [`[debug]`](#the-debug-table) | The keycast overlay |
| [`[screenshot]`](#the-screenshot-table) | Capture format, frame and fonts |
| [`[screensaver]`](#the-screensaver-table) | The idle animation |
| [`[spotlight]`](#the-spotlight-table) | The beam that dims all but one part of the screen |
| [`[dock]`](#the-dock-table) | Which components the bar draws, and your own |
| [`[hosts]`](#the-hosts-table) | Other machines this daemon links to |
| [`[tailscale]`](#the-tailscale-table) | Which tailnet machines `tuios hosts add` offers |
| [`[hooks]`](#hooks) | Shell commands run on events |
## Applying changes
TUIOS watches the file. A save is applied about 200 ms later, with no restart. The command palette also has a **Reload Config** entry.
A file with an error is not applied. The running settings stay, and the error is shown on screen.
Most of the file applies live. These parts do not:
| Change | When it applies |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Keybindings other than `leader_key` | The next client: detach and attach again, or restart |
| `[hooks]` | The daemon reads it at start. Run `tuios kill-server` after editing a hook the daemon runs |
| `[daemon]` | The daemon reads it at start. Run `tuios kill-server` after editing it. `tuios set-config daemon.log_level` also changes the log level of a running daemon |
| `[startup]` | The next new session |
| `scrollback_lines` | Panes opened afterwards |
| Custom theme files | See [Custom themes](#custom-themes) |
The daemon also follows the file, but only for `[hosts]` and `preferred_shell`. Adding or removing a host opens or closes its link at once.
### Changing a setting at runtime
```bash
tuios list-options # every path, with default and accepted values
tuios list-options --section sidebar # one group
tuios get-config appearance.border_style
tuios set-config appearance.border_style thick
```
`set-config` changes a running session and does not write the file. A path the registry does not know is refused. An `[appearance]` option also answers to its bare name, so `border_style` and `appearance.border_style` are the same path. `set-config` cannot reach `[keybindings]`, `[hooks]`, `[hosts]`, `[tailscale]` or the `[dock]` lists, because those are maps rather than single values. `dock.clock.format` is the one `[dock]` key it can set.
### The in-app settings page
The settings page edits every option in the registry, in the same groups `list-options` prints. Open it with:
* , in window mode (`open_settings`)
* Ctrl+B, (`prefix_settings`)
* **Settings** in the Ctrl+P command palette
Most rows apply as you change them. Colour options open a colour picker. Two rows open editors of their own: the rail's [sections](#sections) and the dock's [components](#the-dock-table).
Each change is saved by writing the full config to `config.toml`. Your comments, key order and formatting are replaced by the generated header and every option with its value. `tuios keybinds unbind` and `tuios keybinds free` write the file the same way. Keep a copy in version control if you edit the file by hand.
### Command-line flags
Interface flags override the file for one run. They are accepted by `tuios`, `tuios new`, `tuios attach`, `tuios ssh` and `tuios tape play`.
| Flag | Option |
| ------------------------------------------ | ----------------------------------------------- |
| `--theme` | `appearance.theme` |
| `--border-style` | `appearance.border_style` |
| `--dockbar-position` | `appearance.dockbar_position` |
| `--window-title-position` | `appearance.window_title_position` |
| `--window-button-style` | `appearance.window_button_style` |
| `--window-button-position` | `appearance.window_button_position` |
| `--hide-window-buttons` | `appearance.hide_window_buttons` |
| `--hide-scrollbar` | `appearance.hide_scrollbar` |
| `--shared-borders` | `appearance.shared_borders` |
| `--scrollback-lines` | `appearance.scrollback_lines` |
| `--zoom-max-width` | `appearance.zoom_max_width` |
| `--no-animations` | `appearance.animations_enabled = false` |
| `--confirm-quit` | `appearance.confirm_quit` |
| `--show-clock`, `--show-cpu`, `--show-ram` | `appearance.show_clock`, `show_cpu`, `show_ram` |
| `--show-keys` | `debug.show_key_events` |
| `--ascii-only` | none. Draws ASCII instead of Nerd Font glyphs |
## Appearance
All keys below go under `[appearance]`.
### Theme and chrome
| Key | Default | Values | What it does |
| ------------------------ | ------------ | ----------------------------- | ---------------------------------------------------------------- |
| `theme` | `""` | a theme id | Colour theme. Empty keeps your terminal's own colours |
| `glyphs` | `"default"` | a glyph set id | Shapes of the chrome. See [Glyph sets](#glyph-sets) |
| `border_style` | `"rounded"` | see [below](#border_style) | Border drawn around every pane |
| `border_focused_color` | `""` | `#RRGGBB` | Override the focused border colour |
| `border_unfocused_color` | `""` | `#RRGGBB` | Override the unfocused border colour |
| `shared_borders` | `false` | bool | Share one border between adjacent tiled panes |
| `gap` | `0` | 0 to 8 | Empty cells between tiled panes |
| `dim_unfocused` | `0` | 0 to 90 | Fade panes you are not in, in percent |
| `session_colors` | `true` | bool | Give each session its own colour on the rail and in the switcher |
| `session_border` | `false` | bool | Also draw that colour on every pane border |
| `zen_mode` | `"disabled"` | `disabled`, `always`, `mouse` | Hide borders never, always, or while the mouse is idle |
| `panel_padding` | `2` | 1 to 6 | Columns of padding inside overlay panels |
| `animations_enabled` | `true` | bool | Animate transitions |
| `max_fps` | `0` | 0, or 10 to 120 | Frame rate cap. `0` means 60 |
#### border\_style
Values: `rounded` (default), `normal`, `thick`, `double`, `block`, `outer-half-block`, `inner-half-block`, `ascii`, `hidden`, `glyphs`.
`glyphs` uses the border of the active [glyph set](#glyph-sets). `hidden` also hides the window buttons and the scrollbar, because both are drawn on the border.
### Windows and titles
| Key | Default | Values | What it does |
| ------------------------ | ---------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `window_title_position` | `"bottom"` | `bottom`, `top`, `hidden` | Where the title is drawn. `hidden` also disables rename |
| `window_title_format` | `""` | template | Title template. See [below](#window_title_format) |
| `window_button_style` | `"dots"` | `dots`, `pill` | macOS-style traffic lights, or a filled pill |
| `window_button_position` | `"left"` | `left`, `right` | Which end of the title bar holds the buttons |
| `hide_window_buttons` | `false` | bool | Hide the minimize, zoom and close buttons |
| `window_button_zoom` | `true` | bool | Show the zoom button on tiled panes |
| `hide_scrollbar` | `false` | bool | Hide the scrollbar on the pane border |
| `confirm_quit` | `false` | bool | Always confirm on quit, not only when processes are running |
| `preferred_shell` | `""` | path | Shell for new panes. Empty picks one for your platform |
| `new_window_inherit_cwd` | `true` | bool | Start a new window in the focused pane's directory |
| `whichkey_enabled` | `true` | bool | Show the which-key popup after the leader key |
| `whichkey_position` | `"bottom-right"` | `bottom-right`, `bottom-left`, `top-right`, `top-left`, `center` | Where the popup opens |
| `prefix_repeat_time` | `500` | 0 to 5000 | Milliseconds the prefix stays armed after a repeatable command. `0` turns it off |
#### window\_title\_format
A template for every title. It expands three placeholders:
* `{title}`: the custom name, or the title the program set
* `{index}`: the window's 1-based position in its workspace
* `{cwd}`: the window's working directory
```toml
[appearance]
window_title_format = "{index}: {title} ({cwd})"
```
Any other `{...}` produces a validation warning and is drawn as written.
### Mouse and input
| Key | Default | Values | What it does |
| ------------------------------ | ---------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `click_to_type` | `"single"` | `single`, `double`, `off` | In window mode, whether a click on a pane starts typing in it |
| `auto_enter_terminal_on_focus` | `"off"` | `off`, `targeted`, `all` | Start typing when a keyboard focus command lands on a pane. `targeted` covers numbered select and arrows, `all` adds Tab |
| `focus_follows_mouse` | `false` | bool | Focus the pane under the pointer |
| `alt_drag` | `true` | bool | Alt + left-drag moves a pane |
| `right_click_opens_menu` | `false` | bool | In terminal mode, a plain right-click opens the pane menu |
| `copy_on_select` | `true` | bool | Copy a mouse selection when you release the button |
| `word_characters` | `"@-./_~?&=%+#"` | string | Punctuation that counts as part of a word on double-click |
| `links` | `"all"` | `all`, `marked`, `off` | Which links Shift+click opens. See [below](#links) |
| `kitty_placeholders` | `"auto"` | `auto`, `on`, `off` | Draw images placed with kitty Unicode placeholders |
#### links
Shift+click on a link opens it. Shift is the terminal's "this click is mine" modifier, so it works even when the program in the pane uses the mouse.
* `marked` finds only OSC 8 hyperlinks, where the program says a run of cells is a link.
* `all` (default) also finds plain `http`, `https` and `file` URLs in the text.
* `off` finds none.
Only `http`, `https`, `mailto`, `ftp` and `ftps` links go to your desktop's opener. Any other scheme is copied to the clipboard instead, with a notification. A `file://` link opens a file in a new pane running your editor, and a directory in the rail's files section. A remote client cannot open links, so it copies the address.
### Scrolling
| Key | Default | Values | What it does |
| ------------------ | ------- | -------------- | ---------------------------------------------------- |
| `scrollback_lines` | `10000` | 100 to 1000000 | Lines each pane keeps. Read when the pane is created |
| `scroll_lines` | `3` | 1 to 50 | Lines per mouse wheel notch |
### Tiling and zoom
| Key | Default | Values | What it does |
| --------------------- | ------- | --------- | --------------------------------------------------------- |
| `master_ratio` | `50` | 30 to 70 | Master pane width in the master-stack layout, in percent |
| `scroll_column_width` | `55` | 20 to 100 | Column width in the scrolling layout, in percent |
| `scroll_column_max` | `90` | 20 to 100 | Widest a scrolling column may be set to |
| `niri_scroll_cells` | `8` | 1 to 200 | Cells the scrolling layout moves per wheel event |
| `niri_reverse_scroll` | `false` | bool | Reverse the wheel in the scrolling layout |
| `niri_click_reveals` | `true` | bool | Clicking a partly hidden column brings it fully on screen |
| `niri_hover_reveals` | `true` | bool | With `focus_follows_mouse`, hovering does the same |
| `zoom_size` | `100` | 50 to 100 | Share of the screen a zoomed pane takes, in percent |
| `zoom_max_width` | `0` | cells | Width cap for a zoomed pane. `0` fills the screen |
| `zoom_animation` | `true` | bool | Slide a pane into and out of zoom |
| `zoom_follows_focus` | `true` | bool | Moving focus while zoomed zooms the pane you move to |
The layout a new session starts in is `startup.layout`. See [The startup table](#the-startup-table).
### Dock
| Key | Default | Values | What it does |
| --------------------------- | ------------ | ------------------------- | ----------------------------------------------------------- |
| `dockbar_position` | `"bottom"` | `bottom`, `top`, `hidden` | Edge the dock sits on |
| `dock_workspace_tabs` | `true` | bool | Show the clickable workspace strip |
| `dock_workspace_tab_format` | `""` | template | Tab label with `{index}` and `{name}`. Empty means `{name}` |
| `dock_workspace_tooltip` | `true` | bool | Show a truncated workspace name in full on hover |
| `dock_pill_caps` | `false` | bool | Powerline caps on the dock's pills instead of flat ends |
| `show_clock` | `false` | bool | Show the clock |
| `clock_format` | `"15:04:05"` | Go time layout | Clock format, for example `15:04` or `Mon 3:04PM` |
| `show_cpu` | `false` | bool | Show a CPU graph |
| `show_ram` | `false` | bool | Show RAM use |
| `hide_clock` | `false` | bool | Deprecated. Use `show_clock` |
To reorder the dock or add your own cells, see [The dock table](#the-dock-table).
### Rail options
Two rail options sit directly in `[appearance]`:
| Key | Default | What it does |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `global_session` | `true` | Offer a session on the rail that holds panes from several machines, once a second machine is reachable |
| `git_dirty` | `true` | Count staged, changed and untracked paths in the rail's git section. This is the only part of that section that walks the working tree |
The rest are in [`[appearance.sidebar]`](#the-session-rail).
### The scrollbar
`[appearance.scrollbar]`:
| Key | Default | Values | What it does |
| ------- | --------- | ---------------------------------------- | ---------------------------------------- |
| `style` | `"thin"` | `thin`, `track` | A hairline thumb, or a full-height track |
| `thumb` | `""` | one-cell glyph | Thumb glyph. Empty uses the style's own |
| `track` | `""` | one-cell glyph, or `none` | Track glyph. Empty uses the style's own |
| `tint` | `"quiet"` | `quiet`, `border`, `muted`, or `#RRGGBB` | Bar colour |
### Selection colours
`[appearance.selection]` sets the colours TUIOS draws over pane content to mark text.
| Key | Default | What it colours |
| ------------------------ | ------------------------ | -------------------------------------------------------- |
| `bg` | `"#45475A"` | Background of selected text |
| `fg` | `""` | Selected text. Empty keeps the text's own colour |
| `bold` | `false` | Also draw selected text bold |
| `search_bg`, `search_fg` | `"#8A6D2F"`, `"#F5E7C8"` | Every search match |
| `match_bg`, `match_fg` | `"#E5A93D"`, `"#1C1B19"` | The match the cursor is on |
| `cursor_bg`, `cursor_fg` | `"#39C5CF"`, `"#08222B"` | The copy mode cursor |
| `flash` | `true` | Sweep a band of light over text you just copied |
| `flash_color` | `""` | Colour of the sweep |
| `flash_ms` | `420` | Length of the sweep, 80 to 3000 ms |
| `flash_style` | `"diagonal"` | `diagonal`, `diagonal-reverse`, `horizontal`, `vertical` |
An empty foreground keeps the colour the program wrote. An empty background falls back to the default.
### The session rail
`[appearance.sidebar]` configures the rail at the side of the screen.
| Key | Default | Values | What it does |
| ------------------ | -------------------------------------------- | ------------------------- | ------------------------------------------------------------- |
| `enabled` | `false` | bool | Show the rail |
| `position` | `"left"` | `left`, `right`, `hidden` | Edge the rail sits on |
| `width` | `28` | columns | Preferred width on a wide screen |
| `sections` | `"sessions:25,terminals,files:25,agents:34"` | layout string | Which sections to draw, in what order. See [below](#sections) |
| `show_glyphs` | `true` | bool | Agent-state glyph on each row |
| `show_counts` | `true` | bool | Window count on each session row |
| `marquee` | `true` | bool | Scroll a hovered row's long title |
| `tooltips` | `true` | bool | Label the collapsed strip on hover |
| `file_icons` | `true` | bool | A Nerd Font icon per file type in the files section |
| `file_icon_colors` | `true` | bool | Draw each icon in its file type's colour |
| `folder_click` | `"navigate"` | `navigate`, `cd`, `both` | What a click on a folder row does |
| `file_actions` | `true` | bool | Allow create, rename, delete, copy, cut and paste |
| `file_delete` | `"trash"` | `trash`, `permanent` | Where d sends a file |
Deprecated keys still load: `show_windows`, `show_agents` and `workspaces` in this table, and the flat `sidebar_*` keys in `[appearance]`. They are folded into the current keys when the file is read.
#### sections
One string sets which sections the rail draws, their order from the top, and the share of the rail each may take.
```toml
[appearance.sidebar]
sections = "sessions:25,terminals,files:25,agents:34"
```
* Separate sections with commas. Add `:N` for a share in percent.
* The section names are `sessions`, `terminals`, `files`, `agents` and `git`. Leave a name out to hide that section.
* A share is a ceiling, not a reservation. A section only takes the lines its rows fill. A section with no share takes what the others leave.
* `spacer` draws nothing and takes lines. It is the only name you can repeat. A spacer with a share keeps that share. A spacer with no share takes the lines nothing else wants, which pushes what follows to the bottom.
```toml
[appearance.sidebar]
# Sessions at the top, files at the bottom
sections = "sessions:30,terminals,spacer,files:40"
```
An unknown name, a bad percent or a repeated section is dropped and reported as a config warning.
The settings page has an editor for this: the **Sections** row under Sidebar. Arrows select, Shift+arrows move, Enter adds or removes a section, ←/→ change the share, r restores the default and u undoes your edits.
#### The files section
The files section lists the focused pane's directory. It is read again when the shell changes directory, when the focus moves and when a client attaches. Nothing polls, so a file written by another program appears on the next read.
`folder_click` decides what a click on a folder does: `navigate` opens it in the listing, `cd` sends the focused pane there, `both` does both. A `cd` is only typed into a pane that is at a shell prompt.
The file keys are in `[keybindings.sidebar_files]` and work while the rail has the keyboard and the cursor is on a file row. See [Keybindings](/docs/keybindings#session-rail). Each action opens a centred dialog. The delete dialog opens on Cancel, so Enter on an untouched dialog deletes nothing.
Delete uses the freedesktop.org trash in your home directory, so your desktop's trash shows the file and can restore it. D deletes permanently. Set `file_delete = "permanent"` to make d permanent too. The per-volume trash is not implemented.
#### Agent rows
`[appearance.sidebar.agent_row]` chooses which tokens an agent row shows and styles each one, with optional rules that match on the token's value:
```toml
[appearance.sidebar.agent_row]
tokens = ["session", "harness", "name", "elapsed", "message"]
[[appearance.sidebar.agent_row.elapsed.rule]]
gt = 30 # minutes in the current state
fg = "warning"
```
Each rule has one test: `equals`, `contains`, `starts_with`, `gt` or `lt`. The first matching rule wins. A mistake in this table is a warning, not an error.
## Glyph sets
A theme sets the chrome's colours. A glyph set sets its shapes: border corners, window buttons, rules, separators and rail marks.
```toml
[appearance]
glyphs = "heavy"
```
```bash
tuios list-glyphs # every set, and every role name
tuios list-glyphs heavy # what this set draws, role by role
tuios set-config appearance.glyphs heavy
```
| Id | What it is |
| --------- | ------------------------------------------------------------ |
| `default` | Rounded frame, Nerd Font powerline caps, `✕` and `□` buttons |
| `unicode` | Box drawing and geometric shapes, with no Nerd Font glyphs |
| `heavy` | One stroke weight heavier throughout |
| `ascii` | 7-bit ASCII only |
A set's border is used only when `border_style = "glyphs"`. This keeps a set from silently overriding a border style you chose. A set that defines only some border runes gets the rounded border for the rest.
### Writing a set
Save `.json` in the glyphs directory. `tuios list-glyphs` prints its path.
```json
{
"display_name": "Mine",
"inherits": "heavy",
"bullet": "◦",
"focus": "▐",
"border": {
"top_left": "╔",
"top_right": "╗",
"bottom_left": "╚",
"bottom_right": "╝"
}
}
```
Every field is optional. The id defaults to the file name. A role you do not set comes from the inherited set, and finally from `default`. Inheritance goes up to eight levels deep.
The roles:
* **Window buttons:** `close`, `maximize`, `minimize`, `dot`, `pill_left`, `pill_right`
* **Rules and separators:** `rule`, `separator`, `arrow_left`, `arrow_right`, `dash_rule`
* **Rail marks:** `focus`, `attention`, `bullet`, `add`, `collapse`, `expand`, `fold_open`, `fold_shut`, `tree_branch`, `tree_last`
* **Files section:** `folder`, `parent`, `file`
* **Scrollbar:** `scrollbar_thumb`, `scrollbar_track`
* **Text:** `ellipsis`, `sigil`
* **Border:** `border.top`, `border.bottom`, `border.left`, `border.right`, the four corners, and the junctions `middle`, `middle_top`, `middle_bottom`, `middle_left`, `middle_right`
Most roles must be exactly one cell wide, because the window buttons' click areas are fixed offsets. A glyph of the wrong width falls back to the default. `separator`, `ellipsis`, `collapse` and `expand` may be any width. To see what was dropped:
```bash
tuios list-glyphs mine --json | jq -r '.problems[]?'
```
The directory is read again on every lookup, so a new file can be selected at once. A file that does not parse is skipped and listed under `problems`.
`--ascii-only` replaces non-ASCII glyphs role by role, so a set keeps every role it already spelled in ASCII. A glyph set never changes colours, and it does not change the dock's icons.
The settings page has a **Glyph set** row under **Theme**. Its picker applies each set as you move through the list.
## Themes
342 themes ship with TUIOS.
```bash
tuios --list-themes # every theme id
tuios --preview-theme dracula # the 16 ANSI colours
tuios list-themes --filter gruvbox # from a running daemon, with --json
tuios list-themes catppuccin_mocha # colours and their contrast
```
```toml
[appearance]
theme = "dracula"
```
With no theme, TUIOS uses your terminal's own colours. A theme only changes ANSI colours 0 to 15. 256-colour and truecolor output is drawn as the program wrote it.
To pick one in the app, press Enter on the **Theme** row of the settings page, or choose **Theme Picker** in the command palette. The picker previews as you move and Esc restores the theme you had. It has no key binding.
### Custom themes
Save one theme per JSON file in `~/.config/tuios/themes/`. Subdirectories are ignored.
```json
{
"id": "my-theme",
"display_name": "My Theme",
"fg": "#e0def4",
"bg": "#191724",
"black": "#26233a",
"red": "#eb6f92",
"green": "#31748f",
"yellow": "#f6c177",
"blue": "#9ccfd8",
"purple": "#c4a7e7",
"cyan": "#ebbcba",
"white": "#e0def4"
}
```
* `id` defaults to the file name, lowercased. `display_name` defaults to `id`.
* The colour names are `black`, `red`, `green`, `yellow`, `blue`, `purple`, `cyan`, `white`, each with a `bright_` variant, plus `fg`, `bg` and `cursor`. Note `purple`, not `magenta`.
* A missing colour gets the xterm default. A missing `bright_*` colour copies its normal colour, so set them if you want bright text to look different.
* An optional `chrome` object sets the UI colours directly (`accent`, `accent_bright`, `success`, `warning`, `error`, `info`, `surface`) without changing the pane palette.
* A file that does not parse is skipped and logged.
`tuios import-theme ` converts a kitty, Ghostty, Alacritty or WezTerm scheme into a file here.
Selecting an id that is not registered yet, with `set-config` or the settings page, reads the directory again first. So does `tuios list-themes`. The picker builds its list when it opens.
## Notifications
`[notifications]` sets how long messages stay on screen. `0` uses the built-in default.
| Key | Default | What it does |
| ------------------ | ------- | ---------------------------------------------------- |
| `duration` | `0` | Seconds an info or success message stays up |
| `warning_duration` | `0` | Seconds a warning stays up |
| `error_duration` | `0` | Seconds an error stays up when `error_sticky` is off |
| `error_sticky` | `true` | Errors stay until you press Esc |
### Agent alerts
`[notifications.agent]` controls what happens when an agent in a pane changes state.
| Key | Default | What it does |
| ------------------------ | --------- | ------------------------------------------------------------------------------ |
| `enabled` | `true` | Turn every agent alert on or off |
| `notify` | `true` | Send a desktop notification through the terminal (OSC 9, so it works over SSH) |
| `dock` | `true` | Show the alert in the dock. Click it to go to the pane |
| `sound` | `false` | Make the alert audible |
| `sound_mode` | `"audio"` | `audio` plays a cue, `bell` sends BEL, `both` does both |
| `sound_cooldown_seconds` | `3` | Shortest gap between two sounds, across all panes |
| `command` | `""` | Shell command to run on an alert |
| `settle_seconds` | `2` | Wait this long, and drop the alert if the pane leaves the state |
| `suppress_focused` | `true` | No alert for the pane you are looking at |
| `quiet_hours` | `""` | `HH:MM-HH:MM` in local time when nothing alerts |
`[notifications.agent.states]` turns alerts on per state: `needs_input`, `errored` and `done` default to `true`. `idle` and `working` default to `false`.
`[notifications.agent.sounds]` replaces the two built-in cues with your own files: `needs_input` (also used for `errored`) and `done` (also used for `idle`). A path that does not exist falls back to the built-in cue.
The `after-agent-state` [hook](/docs/hooks) follows the same rules, including the settle wait.
## Keybindings
### leader\_key
The prefix key. The default is `ctrl+b`.
```toml
[keybindings]
leader_key = "ctrl+a"
```
`alt+space` is already the app launcher.
### Sections
Each section is a table under `[keybindings]`. Each action takes a list of keys. The [Keybindings](/docs/keybindings) page lists every default.
| Section | When it is active |
| ------------------- | ------------------------------------------------------------------ |
| `window_management` | Window mode: windows, sessions, zoom, copy |
| `workspaces` | Window mode: switch and move between workspaces |
| `layout` | Window mode: snapping, tiling, swaps, resizes, splits, preselect |
| `mode_control` | Window mode: enter terminal mode, help, settings, quit |
| `restore_minimized` | Window mode: restore a minimized window by number |
| `system` | Window mode: the spotlight |
| `navigation` | Empty. Kept so older files still parse |
| `terminal_mode` | Terminal mode, no prefix |
| `global` | Both modes: the command palette and the launcher |
| `prefix_mode` | After the leader key |
| `window_prefix` | After leader, t |
| `minimize_prefix` | After leader, m |
| `workspace_prefix` | After leader, w |
| `layout_prefix` | After leader, L |
| `tape_prefix` | After leader, T |
| `debug_prefix` | After leader, D |
| `script` | While a tape plays |
| `sidebar` | While the rail has the keyboard |
| `sidebar_files` | While the rail's cursor is on a file row. Checked before `sidebar` |
### Key syntax
* **Modifiers:** `ctrl+`, `alt+`, `shift+`, `super+`. On macOS, also `opt+` and `option+`, which mean the same as `alt+`.
* **Special keys:** `enter`, `return`, `esc`, `escape`, `tab`, `space`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `home`, `end`, `pgup`, `pageup`, `pgdown`, `pagedown`, `f1` to `f12`.
* **Any single character:** letters, digits, symbols, and accented letters.
* **Held modifiers:** `leftalt`, `rightalt`, `leftctrl`, `leftsuper` and the like, for `hold_window_mode` only. They need the Kitty keyboard protocol.
`super+` only reaches TUIOS in terminals that speak the Kitty keyboard protocol. `cmd+` is not valid. On Linux, `opt+` is an error.
```toml
new_window = ["n", "ctrl+t"] # several keys for one action
close_window = [] # no key: the key goes to the pane
```
An empty list is different from a missing line. A missing action gets its default back at the next load. An empty list stays empty.
`shift+1` and `!` are the same key, and so are `shift+a` and `A`. Binding one spelling binds both. Do not bind the two spellings to different actions.
### Platform defaults
On macOS, the workspace and preselect defaults use `opt+`, and `terminal_mode` adds `opt+tab`, `opt+shift+tab` and `opt+esc`. Writing `alt+` on macOS works but gives an advisory warning. Elsewhere the defaults use `alt+`. See [Keybindings](/docs/keybindings#terminal-mode).
## The daemon table
| Key | Default | What it does |
| ---------------------- | ------- | -------------------------------------------------------------------------- |
| `log_level` | `"off"` | `off`, `errors`, `basic`, `messages`, `verbose` or `trace` |
| `agent_autodetect` | `true` | Detect an agent CLI running in a pane and show its state |
| `agent_detect_seconds` | `0` | Seconds between checks. `0` means 2. A negative number turns detection off |
| `agent_binaries` | `[]` | Extra program names to treat as agents, added to the built-in list |
`tuios set-config daemon.log_level messages` changes the level of a running daemon at once. `verbose` and `trace` also log pane content, window titles and paths. `tuios logs` reads the log.
## The startup table
`[startup]` decides how a new session starts. It never changes a running session.
| Key | Default | What it does |
| ------------------------ | ------- | ------------------------------------------------------------------------- |
| `daemon` | `true` | A bare `tuios` attaches to a daemon session instead of running standalone |
| `tiled` | `true` | Start with tiling on |
| `layout` | `"bsp"` | Tiling layout: `bsp`, `master-stack` or `scrolling` |
| `open_default_window` | `false` | Open one window when a session starts empty |
| `start_in_terminal_mode` | `false` | Start in terminal mode when a window is present |
`--standalone` and `TUIOS_NO_DAEMON=1` override `daemon` for one run or one shell. If the daemon will not start, a bare `tuios` runs standalone and says so.
`daemon` and `tiled` default to `true` in a new install. A config file that already exists and does not mention them reads them as `false`, so an upgrade keeps the floating, standalone start it had. Add the keys to opt in.
A session keeps its own layout once it is running. Attaching never rearranges a session to match your config.
## The tape table
`[tape]` controls project tapes: a `.tuios.tape` file in a directory. See [Tape Scripting](/docs/tape-scripting).
| Key | Default | What it does |
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `autorun` | `"ask"` | `off` ignores project tapes. `ask` shows a badge and runs nothing until you choose. `auto` runs a trusted, unedited tape on entry |
| `auto_review` | `false` | Open the review dialog when a tape is found, instead of only showing the badge |
A tape edited after you trusted it is untrusted again.
## The debug table
| Key | Default | What it does |
| ----------------- | ------- | ------------------------------------- |
| `show_key_events` | `false` | Show the keycast of recent keypresses |
`--show-keys` and Ctrl+BDk toggle the same overlay. See [Showkeys](/docs/showkeys).
## The screenshot table
`[screenshot]` sets the defaults for [`tuios screenshot`](/docs/cli-reference#screenshot) and the in-app capture (Ctrl+BC). Each key has a command flag that overrides it for one capture.
| Key | Default | Values | What it does |
| -------------- | ----------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `format` | `"png"` | `png`, `svg`, `ansi`, `html`, `txt` | Output format |
| `directory` | `"~/Pictures/tuios"` | path | Where captures are saved |
| `copy` | `true` | bool | Try to copy the capture to the clipboard |
| `preview` | `true` | bool | Open the preview panel after a capture |
| `frame` | `"window"` | `window`, `plain`, `none` | A window card, a plain card, or nothing |
| `background` | `"auto"` | `auto`, `none`, `#RRGGBB`, `#RRGGBB..#RRGGBB` | Backdrop. `auto` derives it from the theme. Two colours make a gradient |
| `padding` | `48` | 0 to 128 | Space around the card, in pixels |
| `radius` | `10` | 0 to 32 | Card corner radius, in pixels |
| `shadow` | `true` | bool | Soft shadow under the card |
| `controls` | `"auto"` | `auto`, `macos`, `glyphs`, `none` | Window buttons on the card |
| `title_format` | `"{title}"` | template | Title bar text, with `{title}`, `{index}` and `{cwd}` |
| `font_family` | `"JetBrains Mono, monospace"` | string | Font to use when your terminal does not report its own |
| `font_file` | `""` | path | Font file to draw with. Wins over every other font choice, and is embedded in SVG and HTML output |
| `scale` | `2` | 1 to 4 | PNG size multiplier |
| `cursor` | `false` | bool | Draw the cursor cell |
```toml
[screenshot]
format = "svg"
frame = "plain"
background = "none"
padding = 24
```
`png` and `svg` carry the frame. `ansi` and `txt` are plain output, so the frame keys do not apply.
## The screensaver table
`[screensaver]` animates the screen after a period with no input. It is off by default.
| Key | Default | What it does |
| -------------- | ---------- | ----------------------------------------------------- |
| `enabled` | `false` | Start after `idle_minutes` with no input |
| `idle_minutes` | `10` | Minutes of quiet before it starts, 1 to 240 |
| `effect` | `"random"` | The effect, or `random` for a different one each time |
| `while_busy` | `false` | Start even while a pane runs a command or an agent |
```toml
[screensaver]
enabled = true
idle_minutes = 5
effect = "matrix"
```
There are 36 effects. `tuios list-options screensaver.effect` prints them. The settings page previews each effect on your current screen.
S in window mode starts the screen saver now, whatever `enabled` says.
## The spotlight table
`[spotlight]` lights one part of the screen and dims the rest, for screen sharing. b in window mode toggles it.
| Key | Default | Values | What it does |
| --------- | --------- | ----------------- | ---------------------------------------------------------------------- |
| `enabled` | `false` | bool | Turn the spotlight on |
| `follow` | `"mouse"` | `mouse`, `cursor` | Follow the pointer, or the focused pane's cursor |
| `radius` | `10` | 2 to 200 | Half the beam's height, in rows |
| `dim` | `75` | 10 to 95 | How much an unlit cell is dimmed, in percent |
| `edge` | `"hard"` | `hard`, `soft` | Cut the beam off, or fade it. A fade sends about three times the bytes |
| `shake` | `false` | bool | Shake the mouse left and right to toggle the beam |
`follow = "cursor"` sends fewer bytes to a remote client.
## The dock table
`[dock]` sets what the bar draws: three ordered lists of component names, plus a table for each component that needs one. Edit it in the file, or with the **Dock > Components** editor on the settings page. `set-config` cannot change the lists.
```toml
[dock]
left = ["mode", "workspaces", "trail", "tape"]
center = ["windows"]
right = ["notifications", "copy-help", "cpu", "ram", "clock", "session-controls"]
[dock.clock]
format = "15:04"
[dock.custom.branch]
command = "git branch --show-current"
refresh = "event:after-focus-change"
on-click = "tuios new-window log -- git log --oneline -20"
```
These lists are the default. A missing list uses the default. An empty list (`left = []`) draws nothing on that side.
The built-in components are `mode`, `workspaces`, `trail`, `tape`, `windows`, `notifications`, `copy-help`, `cpu`, `ram`, `clock` and `session-controls`. Listing one lets it draw. It still needs its own condition, so `cpu` still needs `show_cpu`.
`[dock.clock]` takes `format`, a Go time layout. A layout without seconds refreshes once a minute.
### Custom components
`[dock.custom.NAME]` defines a cell. Put `custom/NAME` in a list to place it.
| Key | Default | What it does |
| ----------- | -------- | ------------------------------------------------------------------ |
| `command` | required | Run with `sh -c`. The first line of output is the cell text |
| `refresh` | `"once"` | `once`, a duration such as `"30s"`, `push`, or `event:TYPE[,TYPE]` |
| `on-click` | `""` | Run with `sh -c` when the cell is clicked |
| `max-width` | `24` | Widest the cell may be, in cells |
With `push`, the command keeps running and each line it prints replaces the cell. With `event:`, the command runs again when the daemon reports one of those [hook events](/docs/hooks).
A component whose command fails is hidden. `tuios list-dock-components` shows what each component last did and why a cell is missing. `tuios refresh-dock [name]` runs a component again now.
## The hosts table
`[hosts]` names the other machines this daemon links to. Each host is a table.
```toml
[hosts.build]
addr = "gaurav@buildbox"
[hosts.work]
addr = "workstation"
connect_timeout = 5
```
| Key | Default | What it does |
| ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `addr` | required | Anything ssh accepts, including an `ssh_config` alias. A host with no `addr` is ignored and logged |
| `connect_timeout` | `10` | Seconds one dial may take before the host counts as unreachable |
| `command` | `""` | The `tuios` binary on the host. Empty means the link finds it on the `PATH`, at the usual install paths, or through a login shell |
| `ssh_options` | `[]` | Extra ssh arguments, placed before the address |
You can also manage the table from the shell. These commands edit the file, and the daemon applies the change at once:
```bash
tuios hosts add build gaurav@buildbox
tuios hosts test build
tuios hosts remove build
```
The daemon runs ssh with `BatchMode` on, so a link never asks for a password or about a host key. Run ssh to the host once by hand to accept its key.
Once a host is linked, you can list its sessions and agents (`tuios ls --all-hosts`, `tuios list-agents --host build`), attach to or create sessions on it (`tuios attach --host build api`, `tuios new --host build`), and open a pane whose process runs there (`tuios new-window --host build`). See [`tuios hosts`](/docs/cli-reference#hosts).
### Reaching a host you cannot ssh to directly
A host behind NAT, with no port forward and no jump host, is a network problem to solve under TUIOS. Any tool that pipes stdio to a remote port works as an ssh `ProxyCommand`, and `ssh_options` is where you name it.
Tailscale's [tailcat](https://github.com/tailscale/tailcat) is one such tool. It carries a connection over WireGuard with no Tailscale account and no open port. Run it on the far side against sshd:
```sh
tailcat --serve=22
```
Then name it in the host entry:
```toml
[hosts.laptop]
addr = "gaurav@laptop"
ssh_options = ["-o", "ProxyCommand=tailcat 22"]
```
ssh still decides who may connect: the tunnel only carries bytes to sshd, which asks for your key as usual. A leaked tailcat token gives reachability, not access. TUIOS adds no identity system of its own and opens no listening port.
## The tailscale table
`[tailscale]` changes which tailnet machines `tuios hosts add` and `tuios hosts tailnet` offer as addresses. It never adds a host by itself.
| Key | Default | What it does |
| -------------------- | ------------------------ | --------------------------------------------------------------------- |
| `enabled` | `true` | Offer tailnet machines. Empty on a machine that is not on a tailnet |
| `addr` | `"dns"` | Address form: `dns` (MagicDNS name), `name` (short name) or `ip` |
| `user` | `""` | ssh login to put before every address |
| `users` | none | Per-machine logins, keyed by short name. Wins over `user` |
| `os` | platforms that run TUIOS | Operating systems to offer. An empty list offers every machine |
| `offline` | `false` | Also offer machines that are offline |
| `self` | `false` | Also offer this machine |
| `shared` | `false` | Also offer machines shared into your tailnet |
| `include`, `exclude` | `[]` | Glob patterns on the short name and the MagicDNS name. `exclude` wins |
| `max` | built in | Most machines to offer |
| `socket` | `""` | Path to the tailscaled socket, if it is not in the usual place |
```toml
[tailscale]
user = "ubuntu"
exclude = ["*-pad-*"]
[tailscale.users]
build = "admin"
```
## Hooks
`[hooks]` runs shell commands on window, workspace, session and agent events. Event names use hyphens, such as `after-new-window`. See [Hooks](/docs/hooks) and `tuios list-hooks`.
## Validation
The file is checked when it loads. Problems are errors or warnings.
**Errors** are bindings TUIOS cannot parse, such as `"cmd+t"` or `"ctrl+"`. At startup they print to stderr:
```
Config error in [window_management]: cmd+t - invalid modifier: cmd
```
If the file has an error when TUIOS starts, TUIOS uses the built-in defaults for everything, not just the broken key. The message prints before the screen is taken over, so it is easy to miss. Run `tuios keybinds list` to see it: that command prints `Using default keybindings...` when the file was rejected.
A file saved with an error while TUIOS runs is not applied, and the error is shown on screen.
**Warnings** keep the file. They cover unknown values (which fall back to the default), unknown title placeholders, actions with no key, two actions on one key in the same section, and the macOS `alt+` advice. TUIOS shows them on screen.
## Troubleshooting
**The file is not loading.** Run `tuios config path` to confirm which file TUIOS reads, then `tuios keybinds list` to see parse errors.
**A setting changed nothing.**
1. Check the [table of what applies live](#applying-changes). Keybindings need a new client.
2. Check that the file parsed. Look for an error notification, or run `tuios keybinds list`.
3. Check whether a command-line flag is overriding it.
4. For a hook, check the event name spelling with `tuios list-hooks`.
**A key does nothing.** Run `tuios keybinds explain ` to see every place it is bound, and `tuios keybinds doctor` for conflicts.
## Examples
### Browser-style keys
```toml
[keybindings.window_management]
new_window = ["ctrl+t"]
close_window = ["ctrl+w"]
next_window = ["ctrl+tab"]
prev_window = ["ctrl+shift+tab"]
```
### Screen-style leader
```toml
[keybindings]
leader_key = "ctrl+a"
[keybindings.prefix_mode]
prefix_new_window = ["c"]
prefix_split_horizontal = ["S"]
prefix_split_vertical = ["V"]
prefix_session_switcher = ["s"]
prefix_scrollback = ["/"]
```
In this example, `S` and `s` are taken by the splits and the switcher, so the scrollback browser moves to `/`. Keys inside one section must not collide.
### A quiet, dense layout
```toml
[appearance]
theme = "nord"
border_style = "hidden"
dockbar_position = "hidden"
shared_borders = true
whichkey_enabled = false
window_title_format = "{index}: {title}"
scrollback_lines = 50000
max_fps = 120
```
## Related
# Sessions & Daemon Mode
URL: https://tuios.gaurav.zip/docs/sessions
> Persistent sessions that survive detaching, shared sessions, and what comes back after a restart.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
A session is a set of windows and workspaces. By default a background daemon holds your sessions, so they keep running when you close the terminal, and you can attach to them again later, from the same terminal or several at once.
## Quick start
```bash
tuios # attach to your most recent session, or create one
tuios new work # create a session called "work" and attach to it
# ... work, then press Ctrl+B d to detach ...
tuios ls # list sessions
tuios attach work # attach to "work" again
tuios kill-session work # end "work" and every window in it
```
## Daemon sessions and standalone sessions
TUIOS runs a session in one of two ways.
**Daemon session (the default).** A separate `tuios` daemon process owns the shells and runs a terminal emulator for each one. The TUIOS you see is a client: it draws what the daemon sends and forwards your keys. Closing the client does not stop anything.
**Standalone session.** Everything lives in the one `tuios` process. No daemon starts, no socket is created and nothing is saved. When the process exits, the session is gone.
Ask for a standalone session in any of these ways:
```bash
tuios --standalone # this run only
TUIOS_NO_DAEMON=1 tuios # every tuios started from this shell
```
```toml
[startup]
daemon = false # every tuios, from the config file
```
The flag and the environment variable win over the config file. If the daemon fails to start, `tuios` says so and runs standalone for that run, so you always get a terminal.
The rest of this page is about daemon sessions.
Daemon sessions work on Linux, macOS and Windows. Windows needs build 17063 or later, which added Unix socket support.
## Creating sessions
```bash
tuios # attach to the most recent session, creating one if none exist
tuios new # create a session with a generated name (session-0, session-1, ...)
tuios new work # create a session called "work" and attach to it
tuios attach work -c # attach to "work", creating it if it does not exist
```
Inside TUIOS, press N in window management mode to create a session.
The daemon starts on its own the first time you need it.
A session name cannot contain `/` or `\`, start or end with whitespace, contain control characters, or be `.` or `..`. The name is also the name of the file the session is saved to.
### Headless sessions
`--detach` (short `-d`) creates a session without attaching to it. The session gets one window and is ready for `send-text`, `capture-pane`, `wait-for` and the [control protocol](/docs/control-protocol) straight away. This is the starting point for scripts.
```bash
tuios new build --detach
tuios send-text -s build 'make all
'
tuios wait-for window-output -s build --pattern 'make: \*\*\*|Build complete'
```
A headless session is 80 columns by 24 rows until a client attaches. Anything you capture before then is 80 columns wide.
Appearance flags such as `--theme` and `--show-keys` do nothing with `--detach`. They apply to a client, and a headless session has none. Passing them is not an error.
## Listing sessions
```bash
tuios ls
```
`tuios ls --json` prints the same list for scripts. When no daemon is running, `tuios ls` lists the sessions saved on disk instead, marked `saved`, and exits with status 3.
## Attaching and detaching
| Action | How | What happens |
| ------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Attach | `tuios attach [name]` | Draws the session in this terminal. With no name, attaches to the most recent session. |
| Detach | Ctrl+Bd | Saves this client's view to the daemon and exits. The session and its shells keep running. |
| Quit | Ctrl+Bq, or q in window mode | Opens the quit menu: detach (the default, so qq detaches), switch session, or kill this session. |
When you attach, you get the session exactly as you left it: the same windows, positions and screen contents, because the shells never stopped.
A client that dies without detaching, because its terminal closed or its SSH connection dropped, counts as a detach. The daemon notices the connection close and keeps the session running.
## Switching sessions
Ctrl+BS opens the session switcher.
| Key | Action |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| Type | Filter sessions by name |
| ↑↓, Ctrl+PCtrl+N | Move the selection |
| Enter | Switch to the selected session. If nothing matches, create a session with the name you typed. |
| Ctrl+R | Give the selected session a display name. Scripts keep addressing it by its real name. |
| Ctrl+D | Kill the selected session, after a y/n confirmation. You cannot kill the session you are on this way. |
| Ctrl+U | Clear the filter |
| Esc | Close without switching |
The session you leave keeps running. Ctrl+B( and Ctrl+B) step to the previous and next session without opening the switcher.
## Sharing a session
Several clients can attach to one session at the same time, for example two terminals on one machine, or a terminal and an SSH login.
* Every client sees the same windows, workspaces and output, and changes made in one show up in the others.
* The session is drawn at the size of the smallest attached client.
* Each client keeps its own input mode and copy mode position, so one person entering terminal mode does not change what another is doing.
* Each client shows a short notice when another client joins or leaves.
```bash
# Terminal 1
tuios new pairing
# Terminal 2
tuios attach pairing
```
Every attached client has full control: it sees all output, types into every shell and can close windows. There are no read-only clients. Local clients are limited to your own user by the socket's permissions. SSH clients are limited by `tuios ssh` authentication, and browser clients by whatever you put in front of `tuios-web`.
## Session resurrection
Detaching loses nothing, because the daemon keeps running. Resurrection is what happens when the daemon itself stops: `tuios kill-server`, a crash, or a reboot. It brings back the **shape** of each session, not what was running in it.
### When state is saved
Each session is written to disk within a couple of seconds of any change to its windows or layout, every 30 seconds regardless (which keeps each window's working directory current), and once more on a clean shutdown. The write is atomic, so a crash while saving cannot leave a half-written file.
When the daemon starts, it restores every saved session before it accepts connections.
### What comes back
* Every window, with its position, size, title and custom name
* Workspace assignment, minimize state and which window had focus
* The BSP tree for each workspace and the layout mode
* The working directory of each window
* Session options
### What does not come back
Resurrection is crash recovery, not a snapshot. Your layout comes back and each window starts in the right directory, but whatever was running in it is gone.
* **Processes.** Every window gets a fresh shell. A `vim`, a build or an SSH connection that was running is not restarted.
* **Screen contents and scrollback.** They live only in the daemon's memory and are never written to disk.
* **The last few seconds before a crash.** A window created just before a `SIGKILL` may be missing, and a `cd` made less than 30 seconds before it may not be recorded.
* **Your terminal's environment.** Restored shells inherit the daemon's environment and `$SHELL`, not those of the terminal you later attach from.
* **Working directories on Windows and the BSDs.** The daemon asks the kernel where each shell is, which works on Linux and macOS. Elsewhere, restored windows open in the shell's default directory.
A restored window prints a dimmed line so you do not mistake it for the old shell:
```
-- tuios: session restored, fresh shell in /home/you/project --
```
The `in ` part is left out when the directory is unknown. Restored shells also get `TUIOS_RESTORED=1` in their environment, so your shell profile can react.
The session is marked too. It shows a `restored` tag in `tuios ls`, the sidebar and the session switcher, and `tuios attach` prints a notice before drawing it. The mark clears on the first attach.
### What survives each kind of interruption
| | Detach, client crash, SSH drop | `kill-server`, `SIGTERM` | Daemon crash (`SIGKILL`, OOM) | Reboot |
| ------------------------------------ | ------------------------------ | ------------------------ | --------------------------------------- | --------------------------------------- |
| Session exists afterwards | Yes | Yes, restored | Yes, restored | Yes, restored |
| Window layout | Yes | Yes | As of the last save | As of the last save |
| Working directories | Yes | Yes, on Linux and macOS | As of the last save, on Linux and macOS | As of the last save, on Linux and macOS |
| Shell processes and running programs | Yes | No | No | No |
| Screen contents and scrollback | Yes | No | No | No |
| Copy mode position, input mode | No, these are per client | No | No | No |
### Controlling it
```bash
tuios resurrect # list saved sessions, whether each is live, and when it was saved
tuios resurrect work # restore "work" from saved state and attach to it
tuios daemon --no-restore # start a daemon that restores nothing on start
```
`tuios resurrect ` starts the daemon if needed. If the daemon already restored that session, it simply attaches. `restore` is an alias.
`tuios kill-session` deletes the session's saved state, and so does killing it from the quit menu. A session you killed on purpose never comes back.
A state file that is corrupt, or written by a newer TUIOS, is moved to an archive directory and skipped, so one bad file never stops the daemon from starting.
## Sessions on other machines
The daemon can reach TUIOS on other machines over ssh. Name a machine once:
```bash
tuios hosts add build you@buildbox # any address ssh understands, including an ssh_config alias
tuios hosts test build # check the link
```
Then:
```bash
tuios attach --host build api # draw the session "api" on build, in this terminal
tuios new --host build ci -d # create a session on build without attaching
tuios new-window deploy --host build # a window in this session whose shell runs on build
```
A window whose process runs elsewhere shows the machine in its title, as `build:deploy`. `tuios new NAME --global` creates a session meant to hold windows from several machines. The daemon runs ssh with `BatchMode` on, so ssh to the host once by hand first to accept its key.
See the [CLI Reference](/docs/cli-reference) for `tuios hosts`, including `tuios hosts tailnet` for machines on a Tailscale tailnet.
## Daemon location
| What | Where |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| Socket, Linux and macOS | `$XDG_RUNTIME_DIR/tuios/tuios.sock`, or `/tmp/tuios-/tuios.sock` when `XDG_RUNTIME_DIR` is unset |
| Socket, Windows | `%LOCALAPPDATA%\tuios\tuios.sock` |
| PID file | The socket path with `.pid` appended |
| Saved sessions | `$XDG_STATE_HOME/tuios/sessions/.json`, usually `~/.local/state/tuios/sessions/`, readable only by you |
| Archived bad state | `$XDG_STATE_HOME/tuios/sessions/archive/`, pruned after 14 days |
To run the daemon in the foreground with logging, for debugging:
```bash
tuios daemon --log-level=messages
```
`tuios kill-server` stops the daemon and every session. It waits until every session is saved and the socket is removed, so you can start a new daemon as soon as it returns.
## How it works
The daemon owns the PTYs and keeps an emulator fed for each one, so output is parsed whether or not a client is watching. When a client attaches, it asks the daemon for each window's screen and scrollback and paints them.
## Remote control
Scripts can drive a running session through CLI commands that talk to the daemon. They all take `-s ` (default: the most recently active session) and most take `-w `, a window name or ID (default: the focused window).
### Run commands in a window
```bash
# Type a line and press Enter (the trailing newline submits it)
tuios send-text -w build 'go test ./...
'
# Send keys by name, as if typed at TUIOS
tuios send-keys 'ctrl+b,c'
```
`send-text` writes its argument to the shell exactly as given. `send-keys` parses key names and splits on spaces and commas, which suits chords and control keys. Use `send-text` for anything that contains spaces.
### Open windows
```bash
tuios new-window build # prints the new window's ID and name
tuios new-window htop /usr/bin/htop # run a program instead of a shell
tuios new-window --json logs | jq -r .window_id
```
### Wait and read
```bash
tuios wait-for window-output -w build --pattern 'PASS|FAIL' --timeout 600000
tuios wait-for window-idle -w build --idle 2000
tuios capture-pane -w build --lines 40
tuios capture-pane -w build --scrollback > build.log
```
`wait-for` blocks until the condition matches and exits non-zero on timeout, so you do not need a poll loop.
### Inspect
```bash
tuios list-windows --json # every window, with window_id, display_name, workspace and more
tuios get-window build # one window
tuios session-info # the session
```
### Run any command
`tuios run-command` runs any named command, the same set tape scripts use. `tuios run-command --list` prints them all.
```bash
tuios run-command ToggleTiling
tuios run-command SwitchWorkspace 2
tuios run-command --json NewWindow dev
```
With `--json`, the result fields sit at the top level, so the new window's ID is at `.window_id`.
### Example: a development layout
```bash
#!/bin/sh
tuios new dev --detach
tuios new-window -s dev editor
tuios new-window -s dev server
tuios new-window -s dev logs
tuios send-text -s dev -w editor 'nvim .
'
tuios send-text -s dev -w server 'npm run dev
'
tuios send-text -s dev -w logs 'tail -f app.log
'
tuios attach dev
```
### Example: pick a window with fzf
```bash
#!/bin/sh
WINDOW=$(tuios list-windows --json |
jq -r '.windows[] | "\(.display_name)\t\(.window_id)"' |
fzf --with-nth=1 | cut -f2)
[ -n "$WINDOW" ] && tuios focus-window "$WINDOW"
```
### Beyond the CLI
Each CLI call opens its own connection. For a long-running program, speak the [JSON control protocol](/docs/control-protocol) on the daemon socket directly: structured errors, blocking waits and a live event stream. Run `tuios list-verbs` to see every verb. The [CLI Reference](/docs/cli-reference#remote-control-commands) documents every command and flag.
## Troubleshooting
### "Daemon not running" or a stale socket
```bash
tuios kill-server # stops the daemon, or removes a stale socket if none is running
tuios # starts a fresh daemon
```
This does not give you a clean slate. `kill-server` saves every session on the way out, and the next daemon restores them. To start a daemon with no sessions, run `tuios daemon --no-restore`.
### "Session not found"
The name is wrong or the session was killed. `tuios ls` lists the live ones, and `tuios resurrect` lists the saved ones.
### A session stopped responding
```bash
tuios kill-session stuck
tuios new fresh
```
`tuios logs` shows the daemon log.
## Related
# Remote Hosts
URL: https://tuios.gaurav.zip/docs/remote-hosts
> Open sessions on other machines in your own client, run panes on another machine, and script across hosts.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
TUIOS can reach other machines over ssh. Once a machine is added as a host, you can list its sessions and agents, open one of its sessions in your own client, run a single pane on it inside a local session, and send commands and messages to it.
Each host needs TUIOS installed. Your daemon holds one ssh link to each host and talks to the TUIOS daemon on the other side. There is no new listening port and no second login system: ssh decides who can connect.
## Adding a host
```bash
tuios hosts add build gaurav@buildbox
```
The name (`build`) is what you type later. It takes letters, digits, dot, dash and underscore. The address is anything ssh understands, including an alias from `~/.ssh/config`.
The command writes the `[hosts]` table in your config file. A running daemon follows the file, so no restart is needed. The link is tested at once and the result is printed. A host that does not answer is still added.
```bash
tuios hosts # every host and the state of its link
tuios hosts test build # dial it now and print what ssh said
tuios hosts remove build # drop it
```
`hosts add` also takes:
* `--ssh-option ARG`: one extra ssh argument. Repeat it, for example `--ssh-option -J --ssh-option bastion` for a jump host.
* `--connect-timeout SECONDS`: how long one dial may take (default 10).
* `--command PATH`: the TUIOS binary to run on the host. Without it, the link looks on the `PATH`, then in the usual install locations, then in a login shell.
The daemon runs ssh with `BatchMode` on, so a link never asks for a password or about a host key. Run `ssh` to the host once by hand to accept its key, and use key-based login.
### Link states
| Status | Meaning |
| -------------- | ------------------------------------------------------------------------------------------------ |
| `up` | The link is open and the remote daemon answers |
| `no_daemon` | The machine is up and no TUIOS daemon runs on it |
| `no_tuios` | The machine is up and the link cannot find TUIOS on it. `tuios hosts test` shows where it looked |
| `unreachable` | The last attempt failed. The line under the table says why |
| `reconnecting` | The link dropped and TUIOS is dialling again |
| `incompatible` | The two sides speak different control protocol versions. Update one of them |
| `connecting` | The first attempt has not finished |
### The hosts table
`tuios hosts add` writes this for you. You can also edit it by hand:
```toml
[hosts.build]
addr = "gaurav@buildbox"
[hosts.lab]
addr = "lab-01"
connect_timeout = 5
command = "/opt/tools/tuios"
ssh_options = ["-J", "bastion"]
```
A machine behind NAT is a reachability problem for ssh, not for TUIOS. Any tool that works as an ssh `ProxyCommand` works here too. See [Configuration](/docs/configuration#the-hosts-table) for an example.
## Machines on a tailnet
If this machine is on a [Tailscale](https://tailscale.com) tailnet, TUIOS can list the machines on it:
```bash
tuios hosts tailnet
```
```
arch-btw arch-btw.example.ts.net offline
+ ente ente.example.ts.net
= forgejo forgejo.example.ts.net already the host forgejo
my-phone my-phone.example.ts.net cannot run tuios (iOS)
```
Add one by its name:
```bash
tuios hosts add ente --tailnet
```
Nothing is added on its own. The list only saves you typing the MagicDNS name. A host added this way is still reached over ssh. TUIOS asks the local `tailscaled` for the list, so it needs no root and no auth key.
By default the list leaves out machines that are offline, this machine, machines shared from another tailnet, and phones and tablets. The `[tailscale]` table changes that:
```toml
[tailscale]
addr = "dns" # dns (MagicDNS name), name (short name) or ip (100.x address)
user = "ubuntu" # ssh login put in front of every address
os = ["linux", "macOS"] # an empty list offers every OS
offline = false
include = ["*"]
exclude = ["*-pad-*"]
[tailscale.users]
build = "root" # per-machine login, wins over user
```
`tuios hosts tailnet --json` gives every machine with `offered`, and a `skipped` reason when it is false.
## Opening a session on a host
```bash
tuios attach --host build api # attach the session api on build
tuios new --host build # create a session on build and attach it
tuios new --host build ci --detach # create the session ci on build and return
tuios ls --all-hosts # sessions on every machine
```
The session is drawn in your client, with your theme, config and prefix key. Nothing is nested. The connection goes through your local daemon and its link to the host.
In the rail, sessions are grouped by machine. Press Enter on a session under a host to attach it, or on the `+` beside a host to create a session there. While you are on a host, the rail lists this machine's sessions under `local`. Press Enter on one to come back.
If the link drops, the session keeps running on the host. Your client keeps the pane on screen, the dock says it is reconnecting, and TUIOS dials again on its own. After three minutes it gives up, says why, and returns you to the session you left on this machine.
`--ssh` runs `ssh` to the host and starts the TUIOS there instead. Use it when the host's TUIOS is too old to serve your client. You then see the remote client nested in yours, and you press the prefix key twice to send it to the inner one.
## Panes on another machine
A single pane can run on another machine while it stays in a local session:
```bash
tuios new-window deploy --host build
```
The palette has the same thing as `New window on another machine`. The pane is drawn, laid out and closed here. Only its process runs on `build`. Its title bar reads `build:deploy`, so two panes side by side never look alike when they are on different machines.
The session is still an ordinary session: `tuios ls`, scripting, the mailbox, hooks and resurrection work on it as usual. The host runs only the process and its pty. It keeps no screen or scrollback for the pane, and the pane does not appear in `tuios ls` on the host.
Agent detection works in such a pane: the local daemon asks the host what the pane is running. An agent inside it cannot report its own state or read its mail, though, because nothing on the host can reach your daemon. These panes have `TUIOS_PANE_HOSTED=1`, `TUIOS_HOST` set to the host, and `TUIOS_SESSION_REMOTE` set to the session name here. They do not have `TUIOS_ENV`, `TUIOS_SOCKET`, `TUIOS_PANE_ID` or `TUIOS_SESSION`.
**Limits:**
* The pane ends when the link drops, the same as closing it. It does not come back when the link does.
* A resurrected session brings the pane back as a local shell.
* Both machines need a TUIOS new enough to open remote panes. An older one refuses with a message that says to update.
### Global sessions
A session with panes from several machines belongs to none of them. Once a second machine is reachable, the rail draws a `global` group above the machines for such sessions.
```bash
tuios new deploy --global
```
A global session starts with no panes. Every way of making a pane in it (the key, the rail's `+`, the palette) asks which machine to run on. Turn the group off with `appearance.global_session = false`.
## Scripting across machines
Name the host in the target and a command runs on that host's daemon, through the link:
| Target | Means |
| ------------------------ | ----------------------------------------------------- |
| `-s HOST:SESSION` | A session on a host |
| `-w HOST:SESSION:WINDOW` | A pane in a session on a host |
| `-s local:NAME` | A session on this machine whose name contains a colon |
```bash
tuios list-windows -s build:api
tuios capture-pane -w build:api:0
tuios send-text -s build:api -w 0 'make test'
tuios wait-for window-idle -w build:api:0
tuios list-agents --host build
tuios kill-session build:api
```
The CLI says which host answered, and `--json` adds a `host` field. A host name is matched exactly. An unknown name fails with `unknown_host` and lists the configured names, and a host that is down fails with `host_unreachable`. Nothing is queued for later.
Messages and files cross links too. See [Agent Messaging](/docs/agent-messaging#across-machines).
## Related
# Web Terminal
URL: https://tuios.gaurav.zip/docs/web
> Use TUIOS from a browser with tuios-web, a separate server binary.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
`tuios-web` serves TUIOS to a web browser. The browser draws the terminal with WebGL and connects over WebTransport when it can, or WebSocket when it cannot.
It is a separate binary from `tuios`, so the main binary contains no web server. It is built on [sip](https://github.com/Gaurav-Gosain/sip), a library for serving any Bubble Tea program in a browser.
Anyone who can reach the port gets a shell as the user running `tuios-web`. There is no password or token. Keep it on `localhost`, or put a reverse proxy with authentication in front of it. See [Security](#security).
## Install
```bash
brew tap Gaurav-Gosain/tap
brew install tuios-web
```
```bash
yay -S tuios-web-bin
```
```bash
curl -fsSL https://raw.githubusercontent.com/Gaurav-Gosain/tuios/main/install-web.sh | bash
```
```bash
go install github.com/Gaurav-Gosain/tuios/cmd/tuios-web@latest
```
## Quick start
### Start the server
```bash
tuios-web
```
It listens on `http://localhost:7681`, and for WebTransport on UDP port 7682 of the same host.
### Open it
Go to [http://localhost:7681](http://localhost:7681). The page picks the best transport on its own.
By default every browser attaches to one daemon session called `web`, created the first time it is needed. Close the tab and the session keeps running, like any [daemon session](/docs/sessions). Open a second tab and both show the same session.
## Reaching it from another device
Binding anything other than a loopback address needs TLS, because otherwise every keystroke crosses the network in clear text. `tuios-web` refuses to start without one of these:
```bash
# HTTPS from a self-signed certificate tuios-web generates and keeps
tuios-web --host 0.0.0.0 --auto-tls
# HTTPS from a certificate you already have
tuios-web --host 0.0.0.0 --cert cert.pem --key key.pem
# Plain HTTP, only on a network you trust
tuios-web --host 0.0.0.0 --insecure
```
With `--auto-tls`, each browser shows a certificate warning on its first visit, because the certificate is self-signed. `tuios-web cert info` explains what the warning looks like and how to make it go away. The other `cert` subcommands are `new` (replace the certificate), `path` (print its path) and `rm` (delete it and the key).
An SSH tunnel is another option, and needs no certificate:
```bash
ssh -L 7681:localhost:7681 you@server
```
Then open `http://localhost:7681` on your own machine.
On phones and tablets, `tuios-web` adds a key bar with the prefix key and buttons for common commands (new, close, next, zoom, the command palette and more). It detects touch devices from the browser's request. Use `--touch on` or `--touch off` when it guesses wrong.
## Flags
| Flag | Default | Description |
| ------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `--host` | `localhost` | Address to bind |
| `--port` | `7681` | HTTP port. WebTransport uses the next port up. |
| `--auto-tls` | off | Serve HTTPS from a self-signed certificate that `tuios-web` generates and keeps |
| `--cert`, `--key` | | Serve HTTPS from your own certificate and key, in PEM form |
| `--cert-days` | `365` | Days an `--auto-tls` certificate is valid. Under 14 also keeps WebTransport working in Chrome. |
| `--cert-dir` | sip's directory in your user config dir | Where `--auto-tls` keeps its certificate |
| `--cert-host` | | Extra DNS name or IP for the `--auto-tls` certificate. Repeatable. |
| `--insecure` | off | Allow plain HTTP on a non-loopback address |
| `--read-only` | off | Ignore all input from browsers |
| `--max-connections` | `0` | Maximum concurrent connections. `0` means no limit. |
| `--default-session` | `web` | Session every browser attaches to |
| `--ephemeral` | off | Give each connection its own standalone session, with no daemon. Nothing persists. |
| `--touch` | `auto` | Touch mode: `auto`, `on` or `off` |
| `--debug` | off | Log connections, sessions and errors |
All the appearance flags of `tuios` also work here and apply to every browser session: `--theme`, `--border-style`, `--dockbar-position`, `--show-keys`, `--shared-borders`, `--ascii-only`, `--no-animations` and the rest. `tuios-web --help` lists them. They are layered over your config file the same way `tuios` layers them.
```bash
tuios-web --theme nord --show-keys
```
`tuios-web` starts the daemon if it is not running. If the daemon cannot start, it logs a warning and falls back to ephemeral sessions, which do not persist.
Windows opened from the browser get `TERM=xterm-256color` and `COLORTERM=truecolor`.
## Browser settings
The gear button in the corner opens the settings panel. Settings are saved in the browser's local storage.
| Setting | Options |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Transport | Auto (prefers WebTransport), WebTransport, WebSocket |
| Renderer | Auto (prefers WebGL), WebGL, Canvas, DOM, vtgl (experimental) |
| Font size | 10 to 28 px, default 14 |
| Toggles | Copy on select, blinking cursor, the browser's right-click menu, and capturing Ctrl+W and similar keys (fullscreen only) |
The page ships with JetBrains Mono Nerd Font, so icons render without anything installed on the client.
## Graphics in the browser
Inline images work, with a smaller part of the Kitty graphics protocol than a native terminal supports.
**Works:** direct base64 transmission (`t=d`), placing and deleting images, RGB, RGBA and PNG data, zlib compression, chunked transmission, source-region clipping, and images moving with scrolling. Sixel images work too.
**Does not work:** file transmission (`t=f`), temporary files (`t=t`), shared memory (`t=s`), animation frame edits (`a=f`), and Unicode placeholders (`U=1`).
TUIOS tells programs what the browser can do. A program that asks whether it can send a file path is told no, and falls back to sending the bytes. A program that insists on shared memory gets no image: `mpv --vo=kitty` is the common case. See [Graphics](/docs/architecture#graphics) for how TUIOS handles graphics in a native terminal.
## Security
`tuios-web` has no login of any kind. Whoever reaches the port gets a full shell with your privileges.
Browser connections from other web sites are refused: a WebSocket or WebTransport connection must come from a page served by `tuios-web` itself. That stops a page you happen to visit from opening a shell on `localhost:7681`, but it is not authentication. Programs that are not browsers send no origin and are accepted.
If anyone but you can reach the port:
1. **Put authentication in a reverse proxy.** `tuios-web` has none of its own.
2. **Bind to localhost** with `--host 127.0.0.1` so that only the proxy can reach it.
3. **Use TLS** end to end, or at least between the browser and the proxy.
4. **Limit connections** with `--max-connections`.
5. **Use `--read-only`** for demos, so viewers cannot type.
### WebTransport and certificates
WebTransport always runs over TLS.
* **On localhost without TLS,** `tuios-web` generates a short-lived self-signed certificate for WebTransport alone. It is valid for 10 days, Chrome's limit for certificates pinned by hash, and the page fetches its hash from `/cert-hash`, so no warning appears.
* **With `--cert` or `--auto-tls`,** WebTransport uses the same certificate as HTTPS. For a self-signed `--auto-tls` certificate, Chrome only accepts WebTransport if the certificate is valid for under 14 days, so set `--cert-days 13` if you want it.
* **With `--insecure`** there is no certificate, so there is no WebTransport. Browsers use WebSocket.
## Behind a reverse proxy
Run `tuios-web` on `127.0.0.1` and let the proxy handle TLS and authentication. Most proxies carry only the WebSocket connection, so browsers use WebSocket rather than WebTransport.
The proxy must pass the original `Host` header through. `tuios-web` compares it with the page's origin and refuses the WebSocket connection if they differ.
### Nginx with basic auth
```nginx
server {
listen 443 ssl http2;
server_name tuios.example.com;
ssl_certificate /etc/letsencrypt/live/tuios.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tuios.example.com/privkey.pem;
location / {
auth_basic "TUIOS";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:7681;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}
server {
listen 80;
server_name tuios.example.com;
return 301 https://$server_name$request_uri;
}
```
Create the password file with `htpasswd -c /etc/nginx/.htpasswd yourname`.
### Caddy with basic auth
Caddy gets a certificate for you:
```text
tuios.example.com {
basic_auth {
yourname
}
reverse_proxy localhost:7681
}
```
### Cloudflare Tunnel
A tunnel reaches the server without opening a port. Put [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/applications/) in front of the hostname, or anyone who finds it gets a shell.
### Create the tunnel
```bash
cloudflared tunnel login
cloudflared tunnel create tuios
cloudflared tunnel route dns tuios tuios.example.com
```
### Configure it
`~/.cloudflared/config.yml`:
```yaml
tunnel:
credentials-file: /path/to/credentials.json
ingress:
- hostname: tuios.example.com
service: http://localhost:7681
- service: http_status:404
```
### Run both
```bash
tuios-web --host 127.0.0.1
cloudflared tunnel run tuios
```
### Traefik
```yaml
# docker-compose.yml
services:
tuios:
# Build this image yourself (see Docker below). No published image contains tuios-web.
image: tuios-web:local
labels:
- "traefik.enable=true"
- "traefik.http.routers.tuios.rule=Host(`tuios.example.com`)"
- "traefik.http.routers.tuios.entrypoints=websecure"
- "traefik.http.routers.tuios.tls.certresolver=letsencrypt"
- "traefik.http.routers.tuios.middlewares=tuios-auth"
- "traefik.http.middlewares.tuios-auth.basicauth.usersfile=/etc/traefik/htpasswd"
- "traefik.http.services.tuios.loadbalancer.server.port=7681"
```
The `basicauth` middleware is the only access control here. Without it, this publishes a shell to the internet.
## Running as a service
### Docker
The published `ghcr.io/gaurav-gosain/tuios` image contains only `tuios`. Build your own image for `tuios-web` from a checkout of the repository:
```dockerfile
FROM golang:1.26 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/tuios-web ./cmd/tuios-web
FROM debian:bookworm-slim
COPY --from=build /out/tuios-web /usr/local/bin/tuios-web
EXPOSE 7681
# Plain HTTP inside the container: terminate TLS and authenticate at a proxy in front of it.
ENTRYPOINT ["tuios-web", "--host", "0.0.0.0", "--insecure"]
```
### systemd
```ini
# /etc/systemd/system/tuios-web.service
[Unit]
Description=TUIOS web terminal
After=network.target
[Service]
Type=simple
User=tuios
ExecStart=/usr/local/bin/tuios-web --host 127.0.0.1 --port 7681
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable --now tuios-web
```
## How it works
Each browser connection runs a TUIOS client inside `tuios-web`, which attaches to the daemon like any other client. The first byte of every message says what it is. It is an ASCII digit, not a raw byte value:
| Type | Byte | Direction | Content |
| -------------- | ----- | ----------------- | ---------------------------- |
| Input | `'0'` | browser to server | Keyboard and mouse input |
| Output | `'1'` | server to browser | Terminal output |
| Resize | `'2'` | browser to server | New size in cells and pixels |
| Ping | `'3'` | browser to server | Keep-alive |
| Pong | `'4'` | server to browser | Keep-alive reply |
| Title | `'5'` | server to browser | Window title |
| Options | `'6'` | server to browser | Session settings |
| Close | `'7'` | server to browser | The session ended |
| Kitty keyboard | `'8'` | browser to server | Keyboard protocol flags |
## Troubleshooting
### It refuses to start on `0.0.0.0`
That is the TLS requirement. Add `--auto-tls`, `--cert` and `--key`, or `--insecure`. The error message lists the exact commands.
### WebTransport does not connect
The page falls back to WebSocket, so this only costs latency. Check that UDP port 7682 (or your `--port` plus one) is reachable, that you are not using `--insecure`, and that your browser supports WebTransport. The browser console shows certificate errors. You can force WebSocket in the settings panel.
### The WebSocket connection is refused behind a proxy
The proxy is probably rewriting the `Host` header. Pass it through unchanged, for example `proxy_set_header Host $host;` in nginx.
### Blank terminal
Check the browser console for errors, try another renderer in the settings panel, and run `tuios-web --debug` to see the server's log.
### The tab stays open after quitting
In a daemon session, Ctrl+Bq opens the quit menu, and its default is to detach. Detaching or killing the session from that menu ends the browser connection. If the page does not close, reload it.
## Related
# Agents
URL: https://tuios.gaurav.zip/docs/agents
> Track what coding agents in your panes are doing, get alerted when one needs you, and let agents drive TUIOS.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { Step, Steps } from 'fumadocs-ui/components/steps';
TUIOS keeps an agent state for every pane. When a pane runs a coding agent such as Claude Code, Codex or Gemini CLI, the state says whether it is working, waiting for you, or done. The state is drawn on the pane's title bar and in the rail, it can raise an alert, and scripts can read it and wait on it.
## States
| State | Glyph | ASCII | Meaning |
| ------------- | ----- | ----- | --------------------------------------------------- |
| `none` | | | Not an agent, or not reporting. The default |
| `working` | `●` | `*` | Working on a task |
| `needs_input` | `▲` | `!` | Blocked, waiting for a person |
| `idle` | `○` | `o` | Not working and not blocked |
| `done` | `■` | `#` | Finished its task |
| `errored` | `×` | `x` | Stopped on an error |
| `unknown` | `□` | `?` | An agent is there and nothing says what it is doing |
The glyphs are different shapes, not the same shape in different colours, so they still read in a monochrome screenshot. The ASCII column is what `--ascii-only` draws.
`needs_input` and `errored` are the two states where a person has to act. `get-agent-state` and `list-agents` report that as `needs_you: true`, so a script does not need to know which states mean it.
The state is held by the daemon, per pane. Every attached client sees it, and it survives a detach. It does not survive a daemon restart.
## Where it shows
* **Title bar:** the glyph sits in each pane's title.
* **Rail:** the agents section lists every agent pane with its state and how long it has been in it. Each session row carries the loudest state among its panes. See [Session Rail](/docs/session-rail#agent-rows).
* **Alerts:** a state change can raise a notification, a sound and a dock message. See [Alerts](#alerts).
## How TUIOS learns the state
Several sources can report on a pane. From most to least trusted:
| Source | What it is |
| ------------ | --------------------------------------------------------------------------- |
| `report` | The agent reports for itself with `tuios set-agent-state` |
| `transcript` | The daemon reads the record file the agent's harness writes |
| `osc` | An escape sequence the agent printed: OSC 9;4 progress, or its window title |
| `screen` | A rule matched text on the pane's screen |
| `detect` | The daemon saw an agent CLI in the pane's foreground |
| `stall` | The pane was `working` and then went quiet |
A source can overwrite a claim from its own rank or lower, never from a higher one. So a screen rule cannot overwrite what an agent said about itself. There is one exception: a screen rule that sees a permission prompt can mark a pane `needs_input` over a stale `working` claim, and the old claim comes back as soon as the prompt is gone.
### The agent reports for itself
This is the most accurate source, and the only one that can reliably say `needs_input`.
```bash
tuios set-agent-state working -m "running the test suite"
tuios set-agent-state needs_input -m "waiting for approval to push"
tuios set-agent-state done
tuios set-agent-state none # clear it
```
From inside a pane, name the pane and the harness, since the focused pane may be a different one:
```bash
tuios set-agent-state working -s "$TUIOS_SESSION" -w "$TUIOS_PANE_ID" --harness claude-code
```
A report that loses to a higher-ranked source still exits 0 and prints `Not applied: ...` on stderr.
The usual way to report is to wire the agent's lifecycle hooks to these calls once. The TUIOS repository has a working shim for Claude Code:
Copy the shim somewhere stable:
```bash
mkdir -p ~/.config/tuios/integrations
cp integrations/claude-code/tuios-agent-state.sh ~/.config/tuios/integrations/
chmod +x ~/.config/tuios/integrations/tuios-agent-state.sh
```
In `~/.claude/settings.json`, point the `SessionStart`, `UserPromptSubmit`, `Notification` and `Stop` hooks at it:
```json
{
"hooks": {
"Notification": [
{ "hooks": [{ "type": "command", "command": "~/.config/tuios/integrations/tuios-agent-state.sh" }] }
]
}
}
```
Repeat the same entry for the other three events. The shim reads the event from its input, so one command serves all four.
Run Claude Code in a TUIOS pane. Start and prompt submit report `working`, a notification reports `needs_input` with its message, and stop reports `done`.
The shim needs `tuios` and `python3` on `PATH`. Outside TUIOS it does nothing, so it is safe to leave in place. The same pattern works for any agent that can run a command on its lifecycle events. See `integrations/claude-code/README.md` in the repository.
### Progress sequences
An agent that prints OSC 9;4 progress sequences needs no wiring. Setting a progress bar maps to `working`, clearing it to `idle`, the error state to `errored`, and the warning state to `needs_input`.
### Process detection
With no report at all, the daemon checks each pane's foreground process every two seconds. If it is a known agent CLI, the pane is marked `working`. It reads the process name, its executable, and for an interpreter such as `node` or `python3`, the script it runs. It also looks behind wrappers such as `sh -c`, `timeout`, `npx`, `uvx` and `mise exec`. A directory named after an agent is never counted.
TUIOS ships manifests for 22 agent CLIs, including Claude Code, Codex, Gemini CLI, Cursor Agent, Aider, OpenCode, Crush, Amp and Copilot. Add your own manifest as a TOML file in `~/.config/tuios/harnesses/` (or `$TUIOS_HARNESS_DIR`). It is picked up on the next daemon start.
| Key | Default | Meaning |
| ----------------------------- | ------- | ---------------------------------------------------------------------- |
| `daemon.agent_autodetect` | `true` | Detect agent CLIs in the foreground |
| `daemon.agent_detect_seconds` | `0` | Seconds between checks. `0` uses 2. A negative number turns checks off |
Detection alone can say that an agent is present. It cannot say the agent is waiting for you.
### Screen and title rules
An agent sitting on a permission prompt often prints nothing more: no output, no title, no progress. The only evidence is the text on the screen. A manifest can carry screen rules that match the bottom of the pane and report `needs_input`, and title rules that match the window title. The Codex manifest, for example, reads `Action Required` from the title.
Rules only run on a pane already attributed to a harness, and only when the pane writes, so a quiet pane costs nothing.
### The stall timer
If a pane reported `working` and then prints nothing for 30 seconds, the daemon looks at its screen one last time. If no rule matches, the pane becomes `unknown`. The timer only ever moves a pane out of `working`, so it never overrides `needs_input`, `done` or `errored`.
Set `TUIOS_AGENT_STALL_SECONDS` in the daemon's environment to change the window. `0` or a negative value turns the timer off.
## Reading the state
```bash
# One pane
tuios get-agent-state -w build
tuios get-agent-state -w build --json
# Every agent pane in the session
tuios list-agents
tuios list-agents --all # every pane, including ones not seen as agents
tuios list-agents --all-hosts # every machine in [hosts]
# Block until an agent needs a person
tuios wait-for agent-state -s work --until needs_input
tuios wait-for agent-state -w build --until idle,done --timeout 600000
```
`get-agent-state --json` includes `state`, `message`, `needs_you`, the winning `source`, the `harness_id`, and `identity` and `confidence` saying how the harness was recognised. `list-windows --json` carries each pane's `agent_state` too.
## When a pane is marked wrong
Two commands show exactly what the daemon saw.
```bash
tuios explain-agent-detect -w build
```
This starts with a verdict in plain words, then the process name, arguments and executable the daemon read, any wrapper it looked behind, and what every manifest made of it:
```
This pane runs claude-code behind timeout.
The foreground process timeout is a wrapper, so tuios read the processes behind it.
The process claude matched the manifest claude-code on comm=claude.
A process name is strong evidence.
```
```bash
tuios explain-agent-screen -w build
tuios explain-agent-screen -w build --harness codex --lines 20
```
This prints the pane's screen tail exactly as the rules read it, then each rule, which one fired, and for each rule that did not, which string was the reason. Use it when writing a rule.
## Alerts
A state change can raise a desktop notification, a sound or terminal bell, a dock message you can click to go to the pane, and a shell command. The `[notifications.agent]` table controls all of it:
```toml
[notifications.agent]
enabled = true
notify = true # notification in the terminal you are using
dock = true # clickable message in the dock
sound = false
sound_mode = "audio" # audio, bell or both
suppress_focused = true # nothing for the pane you are looking at
settle_seconds = 2 # drop the alert if the state changes within this time
quiet_hours = "22:00-07:00"
command = "" # shell command to run on an alert
[notifications.agent.states]
needs_input = true
errored = true
done = true
idle = false
working = false
```
The notification is written into the stream your terminal draws, so it reaches the terminal in front of you even when the session runs on another machine. Alerts are raised by an attached client, so a detached session tracks state without announcing it. See [Configuration](/docs/configuration#notifications) and [Hooks](/docs/hooks) for the command contract.
## Environment in a pane
Every daemon pane gets these variables, which is how a shim or an agent finds TUIOS:
| Variable | Value |
| ----------------- | ------------------------------------------------------ |
| `TUIOS_ENV` | `1` inside TUIOS |
| `TUIOS_SOCKET` | The daemon socket |
| `TUIOS_PANE_ID` | The pane's window id |
| `TUIOS_WINDOW_ID` | The same id |
| `TUIOS_SESSION` | The session name |
| `TUIOS_HOST` | The hostname of the machine the pane's process runs on |
| `TUIOS_RESTORED` | `1` in a shell started again after a restore |
A pane whose process runs on another machine has a different set. See [Remote Hosts](/docs/remote-hosts#panes-on-another-machine).
## Letting an agent drive TUIOS
TUIOS ships a skill: a guide written for an agent running inside a pane. It covers addressing panes, reading and writing them, opening panes to run work in, waiting on conditions instead of polling, reporting state, and working with other agents. It is built into the binary, so it always matches the version you run.
```bash
tuios --skill
```
To install it for Claude Code:
```bash
mkdir -p ~/.claude/skills/tuios
tuios --skill > ~/.claude/skills/tuios/SKILL.md
```
Run it again after you update TUIOS.
Everything an agent does goes through the same `tuios` commands and [control protocol](/docs/control-protocol) you can use from a shell. There is no separate agent API.
## Related
# Agent Messaging
URL: https://tuios.gaurav.zip/docs/agent-messaging
> Let agents in different panes find each other, leave messages, ask questions and hand over files.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
Agents in the same session can work together. One agent can find the others, leave a message in another's inbox, ask a question and wait for the answer, or hand over a file. You can read and answer their messages too.
An agent's address is its pane. There is no separate agent namespace: `-w` takes the same window id or name it takes everywhere else. Inside a pane, your own address is `$TUIOS_PANE_ID`.
## Finding the other agents
```bash
tuios list-agents -s work
```
```
╭──────────┬────────┬─────────────┬─────────────┬────────┬──────┬────────────────────────╮
│ ID │ NAME │ STATE │ HARNESS │ SOURCE │ MAIL │ NOTE │
├──────────┼────────┼─────────────┼─────────────┼────────┼──────┼────────────────────────┤
│ c7be946f │ review │ needs_input │ claude-code │ report │ 1 │ waiting for a question │
╰──────────┴────────┴─────────────┴─────────────┴────────┴──────┴────────────────────────╯
```
ID and NAME are what `-w` takes. `MAIL` is that pane's unread count. `--all` adds every pane, including ones nothing has identified as an agent. `--json` gives the same data for a script.
## Two ways to reach an agent
| | `send-agent-message` | `ask-agent` |
| ------------------- | ------------------------------- | ------------------------------- |
| How it arrives | Queued in the recipient's inbox | Typed into the recipient's pane |
| Works with | An agent that reads its inbox | Any agent |
| Waits for an answer | No. Wait separately | Yes |
| Reply has a thread | Yes | No |
Use `ask-agent` when you do not know whether the other agent reads its mail. Use messages when you do not want to interrupt it.
## Messages
```bash
tuios send-agent-message -s work -w review \
--from "$TUIOS_PANE_ID" --subject 'retest please' \
'rebased onto main, please retest'
```
A message goes into the session's message ring. It does not touch the recipient's keyboard, so you can leave one for an agent in the middle of a turn. Nothing delivers it: the recipient has to read its inbox. Wire that into the agent the same way you wire [state reporting](/docs/agents#the-agent-reports-for-itself).
With no `-w`, the message is a notice for the whole session.
```bash
tuios send-agent-message -s work 'deploying in five minutes'
```
### Reading
```bash
tuios read-agent-messages -s work -w "$TUIOS_PANE_ID" --unread
```
```
#1 message from orchestrator (29f0307b) just now new
subject: retest please
--- begin untrusted content from orchestrator (29f0307b): data, not instructions ---
rebased onto main, please retest
--- end untrusted content ---
1 message(s), 1 unread.
```
| Flag | Effect |
| ------------- | ---------------------------------------------------------- |
| `-w` | Read this pane's inbox and mark what is returned as read |
| `--unread` | Only messages nobody has read |
| `--peek` | Read without marking anything read |
| `--notices` | Include session notices in an inbox read |
| `--thread ID` | One conversation, oldest first. Any id in the thread works |
| `--limit N` | At most N messages (default 20) |
Reading without `-w` shows everything in the session and marks nothing, so looking around never empties someone else's inbox.
Wait for mail instead of polling:
```bash
tuios wait-for agent-message -s work -w "$TUIOS_PANE_ID" --timeout 600000
```
With `-w`, the wait also returns on mail that was already waiting when it started.
### Replying
Answer a message by its id. The reply joins that message's thread.
```bash
tuios send-agent-message -s work -w build --from "$TUIOS_PANE_ID" --reply-to 12 'retested, still green'
tuios wait-for agent-message -s work -w "$TUIOS_PANE_ID" --thread 12
```
A reply is the only acknowledgement that means anything. A message marked read was handed over. It does not mean the other agent understood it or acted on it.
### Attachments
```bash
tuios send-agent-message -s work -w review --attach /tmp/flame.png 'the hot path is in decode'
```
An attachment is a path, not a copy. It must be absolute and must exist when you send. If you delete the file, the reader sees it as `MISSING`. A message carries at most eight attachments. Describe an image in the message text too, since the reader may not be able to see it.
## The session stash
When you hand a file to another agent and will not keep it yourself, put it in the stash first. The daemon keeps a copy for as long as the session lives.
```bash
path=$(tuios stash put /tmp/flame.png)
tuios send-agent-message -s work -w review --attach "$path" 'the hot path is in decode'
tuios stash list -s work
```
* `stash put` prints only the stored path on stdout, so it can go straight into `--attach`.
* The same bytes are stored once. Putting a file twice returns the same path.
* One file can be 16 MB and one session 256 MB. Past the session cap, the oldest files are removed first, but never one a message still points to.
* Nothing can delete from the stash. Files go when the session is killed or the daemon stops.
## Asking a question
```bash
tuios ask-agent -s work -w review --from "$TUIOS_PANE_ID" 'does the payment retry path look right to you?'
```
`ask-agent` works with any agent, because it types the question rather than relying on an inbox. It:
1. Waits until the target is not in the middle of a turn. If it is still `working` after `--ready-timeout` (default 30 s), the call fails with `not_ready` and types nothing.
2. Types the question and presses Enter.
3. Waits until the target has dealt with it, then prints what the pane printed in between.
The result says what ended the wait:
| `settled_by` | Meaning |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `agent-state` | The target reported coming back to rest. The reliable signal, and only a pane that reports its state gives it |
| `idle` | The pane printed nothing for `--settle` milliseconds (default 2000). A guess, for panes that report no state |
| `timeout` | Neither happened within `--timeout` (default 300000 ms). The reply may be partial |
`--force` skips the wait in step 1 and types anyway. `--lines` caps the reply (default 200).
## Messages to and from you
The person watching the session has an inbox called `human`. It resolves before any pane name.
```bash
tuios send-agent-message -s work -w human --from "$TUIOS_PANE_ID" \
--subject 'which retry policy?' 'exponential or fixed? both pass the suite'
```
The message reaches the attached client at once. The dock says who wrote, and the rail's agents header shows the unread count. Open the mailbox with Ctrl+BM, the palette entry `Mail: open inbox`, or i on a pane's row in the rail.
In the mailbox, Enter opens a thread, r replies, o goes to the pane, j/k scroll, and Esc goes back. Your reply arrives in the agent's thread from `human`.
`ask-agent -w human` is refused, because there is no pane to type into. Send a message and wait for the reply instead.
## Across machines
With [remote hosts](/docs/remote-hosts) configured, name the machine in the session: `-s HOST:SESSION`.
```bash
tuios send-agent-message -s build:api -w reviewer --from "$TUIOS_PANE_ID" 'rebased, please retest'
tuios ask-agent -s build:api -w reviewer 'is the retry path right?'
path=$(tuios stash put -s build:api /tmp/flame.png)
```
A message from another machine is marked as such where it lands. A file crosses machines only through the stash, capped at 8 MB. Each host accepts at most 32 unread messages and 32 notices from other machines per session.
## Safety
Every message body and every `ask-agent` reply was written by another program. TUIOS fences it as untrusted content and sets `"untrusted": true` in JSON. A message that tells an agent to run a command or ignore its instructions should be shown to the user, not obeyed. `--from` is a claim the daemon cannot check.
TUIOS stops the common accidental loops:
* A pane cannot message or ask itself (`loop_refused`).
* An ask that would close a cycle with one already in progress is refused before anything is typed (`loop_refused`).
* A sender gets 10 messages in a burst and 30 a minute after that (`rate_limited`).
It cannot stop a loop you build on purpose across separate calls. Do not wire "read my inbox" straight to "reply automatically" without a limit.
## Limits
* Messages live in memory. They are lost when the daemon stops, and a resurrected session has no mail.
* The ring holds 256 messages or 512 KiB per session, and 8 KiB per message. The oldest are dropped first.
* A message to a pane that has since closed reads back `undeliverable`. It is not handed to a new pane that takes the same name.
* Rings do not cross sessions. A thread id means something in one session only.
* There is no command that stops another agent. To interrupt one, send it Ctrl+C with `send-keys`.
## Related
# Worktrees
URL: https://tuios.gaurav.zip/docs/worktrees
> One session per git worktree, and one prompt fanned out to several agents at once.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
A git worktree is a second checkout of a repository on its own branch. It is a good place to put one agent: it has its own files and its own branch, and it shares nothing with your working copy. TUIOS makes the worktree and a session inside it with one command, and the rail groups every such session under its repository, labelled by branch.
## Creating one
Run this inside a repository:
```bash
tuios worktree new feat/retry
```
```
Created branch feat/retry in /home/u/.local/share/tuios/worktrees/api/feat-retry.
Created session 'api-feat-retry'.
```
* A branch that does not exist is created from `--base`, or from `HEAD`.
* The worktree goes under `$XDG_DATA_HOME/tuios/worktrees//`.
* The session is named `-`, with every `/` turned into `-`. `--name` picks another name.
* The session attaches at once. `--detach` (`-d`) creates it without attaching.
* `--repo` points at another repository instead of the current directory.
`--agent` starts an agent CLI in the session instead of a shell. Name it the way you type it:
```bash
tuios worktree new feat/retry --base main --agent claude --detach
```
A session you start by hand inside a worktree is recognised too. The daemon reads the directory, so it never runs git to find out.
## Listing and comparing
```bash
tuios worktree ls
tuios worktree ls --repo api
```
Each row shows the session, repository, branch, the agent state inside it, and how many uncommitted changes it holds. A row marked `gone` is a session whose worktree directory was removed. The session is kept, so what the agent printed can still be read.
```bash
tuios worktree diff api-feat-retry
tuios worktree diff api-feat-retry --stat
```
`worktree diff` prints the commits the session made on top of its base, then its uncommitted changes, then its untracked files.
## Removing one
```bash
tuios worktree rm api-feat-retry
```
This runs `git worktree remove` and kills the session. A worktree with uncommitted changes is refused and nothing is removed. Choose what happens to the changes:
| Flag | Effect |
| ---------------- | ------------------------------------------------------------------------------ |
| `--stash` | Move the changes into the repository's stash as `tuios: `, then remove |
| `--force` | Discard the changes. The only option that throws work away |
| `--keep-session` | Remove the worktree and leave the session running |
Every commit made in a worktree stays in the repository. TUIOS never deletes a branch and never runs `git worktree prune`.
## Fan-out: one prompt, several agents
`tuios fan` starts the same prompt in several agents at once, each in its own worktree. When they finish, compare the results and keep the best one.
```bash
tuios fan 3 --agent claude 'Add a retry with backoff to the HTTP client.'
```
```
Started 3 agents on fan/add-retry-backoff-http. Each prompt is sent when its agent is ready.
api-fan-add-retry-backoff-http fan/add-retry-backoff-http ...
api-fan-add-retry-backoff-http-2 fan/add-retry-backoff-http-2 ...
api-fan-add-retry-backoff-http-3 fan/add-retry-backoff-http-3 ...
```
* The count is 1 to 16.
* The branches are a stem, then `stem-2`, `stem-3` and so on. The stem is `fan/` plus the first words of the prompt, or `--name`.
* `--agent` is required: `claude`, `codex`, `gemini`, or any agent TUIOS has a manifest for.
* `--base` sets the ref every branch starts from.
* The prompt is not typed the moment the agent starts. TUIOS waits until the agent is at its prompt, then types it, so it never lands in a start-up screen. If the agent first asks whether to trust the folder, the prompt waits for you to answer.
* `--wait` makes the command return only when every prompt has been sent or given up on.
Follow the run and compare the results:
```bash
tuios worktree ls --group fan/add-retry-backoff-http
tuios worktree diff api-fan-add-retry-backoff-http-2 --stat
```
The `--group` listing shows whether each prompt is `pending`, `sent` or `not_sent`.
Keep one and remove the others:
```bash
tuios fan keep api-fan-add-retry-backoff-http-2 --stash
```
The session you name is untouched. Each sibling is removed the way `worktree rm` removes it. A sibling with uncommitted changes is left in place unless you pass `--stash` or `--force`.
## From a script
The commands are wrappers over the `new-worktree`, `list-worktrees`, `remove-worktree` and `fan` verbs. `--json` on each command gives the result as JSON.
```json
{"id":1,"verb":"fan","params":{"count":3,"agent":"claude","prompt":"Add a retry.","repo":"/src/api"}}
```
See [Control Protocol](/docs/control-protocol) and `tuios list-verbs fan`.
## Related
# Control Protocol
URL: https://tuios.gaurav.zip/docs/control-protocol
> Drive a TUIOS daemon from scripts and programs with line-delimited JSON on its socket.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
The TUIOS daemon accepts JSON requests on its Unix socket. Send one JSON object per line and read one JSON object back per line. No client library is needed: a shell, `nc` and `jq` are enough.
Use it when a script or program needs structured errors, blocking waits, or a live stream of events. For quick one-liners, the [CLI commands](/docs/sessions#remote-control) such as `tuios send-text` and `tuios wait-for` do the same things and are built on this protocol.
## Connecting
The protocol shares the socket the TUIOS client uses. There is no separate port and no handshake to negotiate: connect and write JSON. The daemon tells the two apart by the first byte, since a JSON request starts with `{` and a binary client frame never does.
```bash
if [ -n "$XDG_RUNTIME_DIR" ]; then
SOCK="$XDG_RUNTIME_DIR/tuios/tuios.sock"
else
SOCK="/tmp/tuios-$(id -u)/tuios.sock"
fi
printf '{"id":1,"verb":"list-sessions"}\n' | nc -U "$SOCK" | jq .
```
`socat - UNIX-CONNECT:"$SOCK"` works in place of `nc -U "$SOCK"`. The socket path for Windows is in [Sessions](/docs/sessions#daemon-location).
You can send several requests on one connection. They are answered in order, one response line per request line. To run requests in parallel, open more connections.
The socket is only accessible to your user. There is no token or password on top of that.
## Discovering the protocol
The daemon describes itself, so you do not need this page to use it:
```bash
tuios list-verbs # every verb, its parameters and example requests
tuios list-verbs capture-pane # one verb
tuios list-verbs --json # the same, as JSON
```
The output includes the protocol version, every parameter with its type, default and accepted values, the error codes, and the request and response shapes. The same catalogue is available over the socket as the `list-verbs` verb.
The accepted values come from the same lists the daemon checks requests against, so they cannot disagree with what it will actually take.
## Requests and responses
A request has an optional `id`, a `verb`, and `params` when the verb takes any:
```json
{"id":1,"verb":"send-text","params":{"session":"work","window":"build","text":"make\n"}}
```
The `id` can be a number or a string. It is echoed back on the response, and left out if you left it out.
A successful response carries a `result`. Every result has a `type` field naming its shape:
```json
{"id":1,"result":{"type":"hello","protocol":1,"min_protocol":1,"daemon_version":"0.7.0","pid":4242,"sessions":2}}
```
A failure carries an `error` with a stable `code`, a readable `message`, and usually a `hint`:
```json
{"id":2,"error":{"code":"invalid_params","message":"verb capture-pane has no parameter foo","hint":{"verb":"list-verbs","command":"tuios list-verbs capture-pane","param":"foo","accepted":["session","window","source","styled","scrollback","ansi","resolved","palette","lines","start","end"],"detail":"An unknown parameter is refused rather than silently ignored. Fix the name and retry."}}}
```
The hint says what to do next: the parameter that was wrong, the values it accepts, the closest match to what you typed (`did_you_mean`), the names that do exist (`available`), or a command to run. Every hint field is optional.
A few rules hold for every verb:
* **Unknown parameters are refused**, not ignored. A misspelt parameter fails with `invalid_params` instead of doing something you did not ask for.
* **`session` can usually be left out.** It then means the most recently active session.
* **`window` takes a window ID or name.** Left out, it means the focused window.
* **A request that cannot do what it asked never reports success.**
## Versioning
`hello` reports the protocol version the daemon speaks and the oldest one it accepts. The current version is 1. Calling it first is optional: every other verb works without it.
```json
{"id":1,"verb":"hello","params":{"client":"my-script","version":"1.0.0","protocol":1}}
```
A caller whose version is outside the accepted range gets `protocol_mismatch`. New verbs are added without changing the version; it changes only when an existing verb or the envelope changes incompatibly.
## Verbs
The daemon currently implements 62 verbs. `tuios list-verbs` always reflects the binary you are talking to.
### Discovery
| Verb | Purpose |
| ------------ | ------------------------------------------------------------- |
| `hello` | Report the protocol version and the range the daemon accepts. |
| `list-verbs` | Describe every verb, or one named verb. |
### Sessions
| Verb | Purpose |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list-sessions` | List every session the daemon holds. |
| `new-session` | Create a session, detached, with its first window. Optional `name`, `width`, `height` (default 80x24), `window`, `window_name`, `cwd` and `command`. |
| `session-info` | Details about one session. |
| `kill-session` | End a session and every window in it. |
| `set-session-name` | Set a display name. The session keeps its real name for addressing. |
| `set-session-accent` | Set or clear a session's accent colour. |
### Windows
| Verb | Purpose |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list-windows` | List a session's windows with ID, index, name, workspace, size, focus and agent state. |
| `new-window` | Create a window. Optional `name`, `workspace`, `cwd`, `focus`, `command` (an argv run instead of a shell) and `host` (run it on another machine). |
| `close-window` | Close a window. |
| `focus-window` | Focus a window by `window`, `relative` (`next`, `prev`) or `direction` (`left`, `right`, `up`, `down`). Direction needs an attached client. |
| `move-window` | Move a window to another workspace, optionally following it. |
| `set-window` | Rename a window or change its minimized state. |
| `split-window` | Split a window `horizontal` or `vertical` and open a new one beside it. Needs an attached client with tiling on. |
| `resize` | Resize a window's PTY to `width` by `height`. |
| `popup` | Run a command in a floating window that closes when the command exits. Needs an attached client. |
| `screenshot` | Render a window to a `png`, `svg`, `ansi`, `html` or `txt` file. |
### Workspaces and layout
| Verb | Purpose |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `list-workspaces` | List workspaces with their names, window counts and which one is showing. |
| `select-workspace` | Show a workspace. |
| `set-workspace-name` | Name a workspace. It keeps its number for addressing. |
| `set-workspace-order` | Set the order workspaces are displayed in. |
| `set-layout` | Turn tiling on or off, equalize splits, or rotate the focused split. Needs an attached client. |
| `run-command` | Run any named command, the same set tape scripts and keybindings use. Prefer a dedicated verb where one exists: they report what changed. |
### Input and output
| Verb | Purpose |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| `send-text` | Write text to a window's PTY exactly as given. A trailing `\n` presses Enter. |
| `send-keys` | Send key names such as `"ctrl+b,n"`. Keys are split on spaces and commas, so use `send-text` for ordinary text. |
| `capture-pane` | Read a window's contents. See [Capturing windows](#capturing-windows). |
### Options, themes and the dock
| Verb | Purpose |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `list-options` | Every settable option path with its type, default, accepted values and description. |
| `get-option` | Read the value in effect, and whether it comes from the session or the defaults. |
| `set-option` | Set an option by path, for example `appearance.dockbar_position`. The path and value are checked first. An attached client applies it live. |
| `list-themes` | List the themes, built-in and imported. Name one to get its colours and their contrast. |
| `list-glyphs` | List the glyph sets, and describe one. |
| `list-hooks` | List the hooks and what each last did. |
| `list-dock-components` | List the dock's components and what each last did. |
| `refresh-dock` | Re-run one dock component, or all of them. |
### Agents and messages
| Verb | Purpose |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `list-agents` | List the windows running coding agents, with their state and unread mail. |
| `get-agent-state` | Read a window's agent state. |
| `set-agent-state` | Report an agent's state: `working`, `needs_input`, `idle`, `done`, `errored`, `unknown`, or `none` to clear. |
| `explain-agent-detect` | Explain why a window was or was not detected as running an agent. |
| `explain-agent-screen` | Show what the screen-reading rules make of a window. |
| `send-agent-message` | Leave a message for another window, or a notice for the whole session. |
| `read-agent-messages` | Read a session's messages, or one window's inbox. |
| `ask-agent` | Type a question into another agent's window and wait for its answer. |
### Files and worktrees
| Verb | Purpose |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `stash-put` | Copy a file into the session's own store and return the stored path. |
| `stash-get` | Read a stored file back. |
| `stash-list` | List the files in a session's store. |
| `new-worktree` | Create a git worktree and a session in it. |
| `list-worktrees` | List the sessions whose directory is a git worktree. |
| `remove-worktree` | Remove a worktree session's worktree and kill the session. Refuses uncommitted changes unless told to stash or discard them. |
| `fan` | Start one prompt in several agents, each in its own worktree and session. |
### Other machines
| Verb | Purpose |
| ---------------------- | --------------------------------------------------------------------------- |
| `list-hosts` | List the machines in the `[hosts]` config table and the state of each link. |
| `list-host-sessions` | List sessions here and on every configured host. |
| `list-host-agents` | List agent windows here and on every configured host. |
| `open-host-connection` | Turn this connection into a connection to the daemon on a host. |
Five more verbs, `open-pane`, `pane-agent`, `pane-cwd`, `resize-pane` and `read-dir`, are what one daemon calls on another to run a window's process on a different machine. You do not normally call them yourself.
### Events and waiting
| Verb | Purpose |
| ------------- | -------------------------------------------------------- |
| `subscribe` | Turn this connection into a stream of events. |
| `unsubscribe` | Stop this connection's event stream. |
| `wait-for` | Block until a condition matches, or fail with `timeout`. |
## Capturing windows
`capture-pane` reads one of two buffers, chosen by `source`:
* `visible` (default): the screen as it is now.
* `recent`: the screen plus the scrollback above it.
Set `styled` to keep ANSI colours and attributes, and `resolved` to turn the 16 theme colours into 24-bit RGB. Limit the output with `lines` (the last N lines) or with `start` and `end` (a 1-based inclusive range, which overrides `lines`).
```json
{"id":1,"verb":"capture-pane","params":{"session":"work","window":"build","source":"recent","lines":50}}
```
Long lines come back split at the window's width. The emulator does not record which rows were soft-wrapped, so there is no unwrapped capture.
## Waiting for conditions
`wait-for` replaces poll loops. It blocks until the condition matches, then returns a `wait_result`. If the timeout passes first, it fails with the `timeout` error code.
| Condition | Matches when |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `session-exists` | A session named `session` exists. |
| `window-output` | Text matching the regular expression `pattern` is in the window. Checked at once, then on every new output. |
| `window-exit` | The window's process exits. |
| `window-idle` | The window prints nothing for `idle` milliseconds (default 500). |
| `agent-state` | A window's agent state becomes one of the states in `until` (comma-separated). Without `window`, any window in the session counts. |
| `agent-message` | A message arrives. With `window`, in that window's inbox. `thread` narrows it to one thread. |
`timeout` defaults to 30000 milliseconds. For `window-output`, `source` defaults to `recent`, so output that has already scrolled off the screen still matches.
```json
{"id":1,"verb":"wait-for","params":{"condition":"window-output","session":"work","window":"build","pattern":"build (ok|failed)","timeout":120000}}
```
## Event stream
`subscribe` turns the connection into a stream. The first line back is an acknowledgement carrying the current sequence number, and every line after it is an event:
```json
{"id":1,"verb":"subscribe","params":{"session":"work","types":["window-created","window-closed","bell"]}}
```
```json
{"id":1,"result":{"type":"subscribed","seq":42}}
{"seq":43,"type":"window-created","session":"work","window":"1f3c...","pty_id":"9ab2...","title":"build","time":1790102604545781000}
```
Filter with `session`, `window` and `types`. Leave `types` out to receive everything.
There is no backfill: events start at the moment you subscribe. Call `list-windows` first to learn the current state, then follow the stream.
The event types are `window-created`, `window-closed`, `window-exit`, `window-retitled`, `window-focused`, `window-moved`, `window-minimized`, `window-restored`, `workspace-switched`, `agent-state`, `agent-message`, `output`, `bell`, `mode-changed`, `session-created`, `session-closed` and `gap`.
Events fire the same way whether a change came from a verb or from a person using the TUI. Window geometry changes do not produce events.
If you read too slowly, the daemon drops events rather than wait for you, and sends a `gap` event with the number dropped before the next one it delivers. `queue` sets how many events buffer before that happens (default 256).
## Verbs that need an attached client
Some verbs change what is drawn, which needs a client to draw it: `split-window`, `set-layout`, `popup`, and `focus-window` with a `direction`. On a headless session, such as one made with `tuios new --detach`, they fail with `needs_client` instead of pretending to succeed. Attach a client first if you need them.
## Error codes
Codes are stable, so it is safe to branch on them.
| Code | Meaning |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `invalid_request` | The line is not a valid request, or the connection is in the wrong state for the verb. |
| `unknown_verb` | No verb has that name. The hint has the closest match and the full list. |
| `invalid_params` | A parameter is missing, malformed, unknown, or outside its accepted values. The hint names it. |
| `session_not_found` | No session has that name. The hint lists the sessions that exist. |
| `session_exists` | `new-session` was given a name that is already taken. |
| `window_not_found` | The window target matched nothing. The hint lists the windows. |
| `no_windows` | The session has no windows to act on. |
| `pty_not_found` | The window's process has already exited. |
| `needs_client` | The verb needs an attached client and none is attached. |
| `option_not_found` | No option has that path. The hint has the closest match. |
| `command_failed` | The attached client ran the request and it failed. |
| `timeout` | A `wait-for` condition did not match in time. |
| `not_ready` | The target agent is busy, so the call did not type into it. |
| `loop_refused` | The call would loop, for example a window messaging itself. |
| `rate_limited` | The sender is over the message rate limit. |
| `no_keyboard` | The target is the person's inbox, which has no window to type into. |
| `protocol_mismatch` | The caller's protocol version is outside the range the daemon accepts. |
| `unknown_host` | No host by that name is configured. |
| `host_unreachable` | The host is configured but not answering. |
| `host_refused` | The link to the host is up but cannot take another connection. |
| `unknown_pane` | The daemon is not running a pane with that ID. |
| `not_worktree` | The session is not in a git worktree. |
| `worktree_dirty` | The worktree has uncommitted changes and neither stash nor force was passed. |
| `git_failed` | A git command failed. The message is git's own. |
| `internal` | An unexpected failure inside the daemon. |
## Examples
```bash
# Create a headless session and run a build in it
printf '{"verb":"new-session","params":{"name":"ci"}}\n' | nc -U "$SOCK"
printf '{"verb":"send-text","params":{"session":"ci","text":"make test\\n"}}\n' | nc -U "$SOCK"
# Block until it finishes, then read the last 20 lines
printf '{"verb":"wait-for","params":{"condition":"window-output","session":"ci","pattern":"(PASS|FAIL)","timeout":600000}}\n' | nc -U "$SOCK"
printf '{"verb":"capture-pane","params":{"session":"ci","source":"recent","lines":20}}\n' \
| nc -U "$SOCK" | jq -r .result.content
# Watch for bells and exits in every session
printf '{"verb":"subscribe","params":{"types":["bell","window-exit"]}}\n' | nc -U "$SOCK" | jq -c .
```
## Related
# Hooks
URL: https://tuios.gaurav.zip/docs/hooks
> Run shell commands when windows, workspaces, sessions and agents change.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
A hook is a shell command TUIOS runs when something happens: a window opens, focus moves, you switch workspace, you attach or detach, an agent finishes. Hooks live in the `[hooks]` table of your [config file](/docs/configuration).
```toml
[hooks]
after-new-window = "notify-send 'TUIOS' \"opened $TUIOS_WINDOW_NAME\""
after-workspace-switch = "echo \"$TUIOS_PREV_WORKSPACE -> $TUIOS_WORKSPACE\" >> ~/.tuios-ws.log"
```
Other config keys use `snake_case`. Hook event names use hyphens and are case-sensitive: `after-new-window`, not `after_new_window`. An unknown name is not an error. TUIOS logs one line and the hook never runs. `tuios list-hooks` shows what was actually loaded.
## Events
| Event | Fires when | Runs in |
| ------------------------ | -------------------------------------------------------------------------------- | ------- |
| `after-new-window` | A window is created | daemon |
| `after-close-window` | A window closes, by keybinding or because its process exited | daemon |
| `after-focus-change` | Focus moves to a different window | daemon |
| `after-workspace-switch` | The session switches to a different workspace | daemon |
| `after-agent-state` | An agent in a pane changes state, filtered by the `[notifications.agent]` policy | daemon |
| `after-attach` | A client attaches to a session, including a switch between sessions | client |
| `after-detach` | A client detaches from a session | client |
| `after-layout-change` | The tiling layout changes | client |
| `after-resize` | A window is resized, by keyboard or by dragging its border | client |
A hook runs on the side that owns the fact it reports:
* **Daemon events** describe the session. The daemon runs them, so they fire when nobody is attached, and they fire once however many clients are attached.
* **Client events** describe one client. Each attached client runs them from its own config. Three clients attaching is three `after-attach` firings.
* A standalone TUIOS (`tuios --standalone`) has no daemon and runs every hook itself.
`after-agent-state` follows the same `[notifications.agent]` settings as the other agent alerts, including the settle delay, so it does not fire on every brief change. `suppress_focused` only applies while a client is attached. A command in `[notifications.agent].command` is registered as one more `after-agent-state` hook. See [Notifications](/docs/configuration#notifications).
## One command or several
A value is one string, or an array of strings:
```toml
[hooks]
after-attach = "my-greeting"
after-new-window = [
"logger -t tuios \"new window $TUIOS_WINDOW_ID\"",
"touch ~/.cache/tuios/last-window",
]
```
The commands in an array run at the same time, not in order. If order matters, put the steps in one string joined with `&&`.
## What a hook receives
TUIOS runs the string with `sh -c`. There are no arguments and no stdin. Everything arrives as environment variables.
Every variable is set for every event. A value that does not apply is empty for text and `0` for numbers, so a script can read any of them without checking first.
| Variable | Meaning |
| ----------------------------- | ------------------------------------------------ |
| `TUIOS_EVENT` | The event name |
| `TUIOS_WINDOW_ID` | Window ID |
| `TUIOS_WINDOW_NAME` | Window name or title |
| `TUIOS_WORKSPACE` | Workspace number |
| `TUIOS_SESSION_ID` | Session name. Empty in a standalone TUIOS |
| `TUIOS_PREV_WORKSPACE` | The workspace you came from |
| `TUIOS_LAYOUT` | `bsp`, `master-stack`, `scrolling` or `floating` |
| `TUIOS_WIDTH`, `TUIOS_HEIGHT` | The window's new size in cells |
| `TUIOS_AGENT_STATE` | The agent's new state |
| `TUIOS_AGENT_PREV_STATE` | The state it left |
| `TUIOS_AGENT_HARNESS` | The detected harness, for example `claude-code` |
| `TUIOS_AGENT_MESSAGE` | The last message the agent reported |
Which variables carry data for each event:
| Event | Variables |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `after-new-window`, `after-close-window`, `after-focus-change` | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID` |
| `after-workspace-switch` | `TUIOS_WORKSPACE`, `TUIOS_PREV_WORKSPACE`, `TUIOS_SESSION_ID` |
| `after-attach`, `after-detach` | `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID` |
| `after-layout-change` | `TUIOS_LAYOUT`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID` |
| `after-resize` | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_WIDTH`, `TUIOS_HEIGHT`, `TUIOS_WORKSPACE`, `TUIOS_SESSION_ID` |
| `after-agent-state` | `TUIOS_WINDOW_ID`, `TUIOS_WINDOW_NAME`, `TUIOS_AGENT_STATE`, `TUIOS_AGENT_PREV_STATE`, `TUIOS_AGENT_HARNESS`, `TUIOS_AGENT_MESSAGE`, `TUIOS_SESSION_ID` |
A hook the daemon runs gets the daemon's environment, not your current shell's. A daemon started by `tuios new` keeps the environment of the shell that started it. Use full paths when in doubt.
## When a hook does not fire
```bash
tuios list-hooks
```
This lists every registered command with the side that runs it, how many times it ran, its last exit code, when it last ran and its last error. The error is the tail of the command's stderr.
* **No row**: the hook was never loaded. Check the event name.
* **0 runs**: the command is registered and the event has not happened.
* **Non-zero exit**: the command ran and failed. The error says why.
`tuios list-hooks --event after-close-window` shows one event, and `--json` gives machine-readable output. Client hooks are listed only while a client is attached.
The daemon also logs a warning for every failing hook. Run `tuios daemon --log-level=basic` to log one line per firing as well.
## Limits
* **Hooks load at startup.** Editing `[hooks]` does not reload them. Restart the client for client hooks, and run `tuios kill-server` so the daemon restarts for daemon hooks. This stops every session and the processes in them. The next `tuios` restores the saved layouts, but not the running programs.
* **No timeout.** A hook that never exits keeps running. On detach, and when the daemon shuts down, TUIOS waits at most 2 seconds for running hooks.
* **Stdout is discarded.** Only the tail of stderr (1 KiB) and the exit code are kept, for `list-hooks`. Redirect to a file if you need the output.
* **No ordering between hooks.** Each command runs in its own goroutine, so do not rely on the order of two events that happen together.
* **Restored windows count as new.** When the daemon restores a saved session, `after-new-window` fires for each window it brings back.
* **`after-detach` needs a daemon session.** A standalone TUIOS has nothing to detach from.
* **Wrong types are ignored.** A number or a boolean where a string or array belongs is dropped without a message.
## Examples
Log every event, to see what fires and when:
```toml
[hooks]
after-new-window = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_ID $TUIOS_WINDOW_NAME\" >> ~/.tuios-hooks.log"
after-close-window = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_ID\" >> ~/.tuios-hooks.log"
after-focus-change = "echo \"$TUIOS_EVENT $TUIOS_WINDOW_NAME\" >> ~/.tuios-hooks.log"
after-workspace-switch = "echo \"$TUIOS_EVENT $TUIOS_PREV_WORKSPACE->$TUIOS_WORKSPACE\" >> ~/.tuios-hooks.log"
after-layout-change = "echo \"$TUIOS_EVENT $TUIOS_LAYOUT\" >> ~/.tuios-hooks.log"
after-resize = "echo \"$TUIOS_EVENT ${TUIOS_WIDTH}x${TUIOS_HEIGHT}\" >> ~/.tuios-hooks.log"
```
Show attach state in a status bar:
```toml
[hooks]
after-attach = "echo attached > ~/.cache/tuios-state"
after-detach = "echo detached > ~/.cache/tuios-state"
```
Send a desktop notification when an agent needs you:
```toml
[hooks]
after-agent-state = "[ \"$TUIOS_AGENT_STATE\" = needs_input ] && notify-send \"$TUIOS_WINDOW_NAME needs input\""
```
## Related
# Tape Scripting
URL: https://tuios.gaurav.zip/docs/tape-scripting
> Script TUIOS with .tape files, and build project layouts with .tuios.tape.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
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](/docs/tape-recording).
```bash
# demo.tape
EnableTiling
NewWindow
Type "htop"
Enter
SmartSplit
Type "git log --oneline"
Enter
```
## Running a tape
| Command | What it does |
| ---------------------------- | ---------------------------------------------------------------- |
| `tuios tape play ` | Starts a standalone TUIOS and plays the tape in it |
| `tuios tape validate ` | Parses the tape and lists its commands, without running anything |
| `tuios tape exec ` | Plays the tape in a running session that has a client attached |
`tuios tape list`, `show`, `delete` and `dir` manage recorded tapes. See [Tape Recording](/docs/tape-recording#managing-recordings).
### 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`.
* Playback runs one command at a time and waits for running animations to finish before the next one.
* Ctrl+P pauses and resumes playback.
* A progress indicator shows the current command and the total.
* A command that fails shows an error notification, and playback continues with the next line.
* TUIOS stays open when the tape ends. Quit it as usual, with Ctrl+Bq.
### tape validate
```bash
tuios tape validate demo.tape
```
Prints each parsed command, or the parse errors with line numbers. It exits non-zero if the tape does not parse. 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
```
`exec` sends the tape to the daemon, which hands it to the TUI client attached to that session. The client runs it and shows its progress on screen. The command returns when the tape has finished.
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](#scripting-a-detached-session).
`exec` differs from `play` in three ways:
* **`Wait` and `WaitUntilRegex` are skipped** without a warning. Only `Sleep` pauses an `exec` run.
* **It does not wait for a new pane.** In a daemon session, `NewWindow` and `SmartSplit` ask the daemon for a pane, and the pane arrives a moment later. Put a `Sleep` after them, or the next `Type` goes to the old pane.
* **Commands run 50 ms apart**, with no wait for animations.
| | `tape play` | `tape exec` |
| ---------------------------------- | ---------------------- | --------------------------- |
| Needs a running session | No | Yes, with a client attached |
| Runs in | A new standalone TUIOS | The attached client |
| `Sleep` | Yes | Yes |
| `Wait`, `WaitUntilRegex` | Yes | Skipped |
| Waits for a new pane before typing | Yes | No |
### 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](/docs/sessions) and the [Control Protocol](/docs/control-protocol) for the full set of commands.
A single tape command can also be sent with `tuios run-command`. It takes a few commands a tape cannot, such as `Split horizontal`, `SetTheme` and `ShowNotification`. Run `tuios run-command --list` to see them.
## 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.
### 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 parse a trailing number but do not repeat. `ToggleTiling 3` toggles once, and `NewWindow 3` opens one window named `3`.
### The @ delay
The parser accepts a delay between a command and its count, as in `Down@100ms 3` or `Type@50ms "text"`. The delay is stored and never used: `Down@100ms 3` sends three Down keys at once. Use `Sleep` lines for pacing. `Sleep@200ms` is a parse error.
## Commands
### Modes
| Command | Effect |
| ---------------------- | -------------------------------- |
| `WindowManagementMode` | Switch to window management mode |
| `TerminalMode` | Switch to terminal mode |
Key commands such as `Type` and `Enter` 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` | Open a window |
| `CloseWindow` | Close the focused window |
| `NextWindow`, `PrevWindow` | Move focus |
| `FocusWindow ` | Focus a window by name or ID. The argument is a bare word or number, not a quoted string |
| `RenameWindow "name"` | Name the focused window |
| `MinimizeWindow` | Minimize the focused window |
| `RestoreWindow` | Restore the focused window, if it is minimized |
```bash
NewWindow
RenameWindow "server"
Type "npm run dev"
Enter
```
`NewWindow "name"`, `CloseWindow "name"`, `MinimizeWindow "name"` and `RestoreWindow "name"` validate, but the tape parser drops the string. The command acts on the focused window, and `NewWindow "name"` opens an unnamed window. Use `RenameWindow` after `NewWindow`, or `tuios run-command NewWindow "name"`.
Minimizing moves focus to another window, so a bare `RestoreWindow` right after `MinimizeWindow` does not bring the minimized one back.
### Tiling and layout
| Command | Effect |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `EnableTiling`, `DisableTiling`, `ToggleTiling` | Turn tiling on or off |
| `SmartSplit` | Split the focused pane and open a new pane in the space. TUIOS picks the direction. Needs tiling on |
| `RotateSplit` | Rotate the direction of the focused split. Needs tiling on |
| `EqualizeSplits` | Reset every split to an even ratio. Needs tiling on |
| `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. Currently snaps to the top-left quarter, not the full screen |
| `SaveLayout "name"` | Save the current layout under a name |
| `LoadLayout "name"` | Apply a saved layout |
See [BSP Tiling](/docs/bsp-tiling) for how splits work, and `tuios layout list` for saved layouts.
`Split horizontal` validates, but the parser drops the direction, so it always fails at run time with `Split needs horizontal or vertical`. Use `SmartSplit` in a tape, or `tuios run-command Split horizontal` from the shell.
### Workspaces
| Command | Effect |
| ---------------------------- | ------------------------------------------ |
| `SwitchWorkspace ` | Show workspace `n` |
| `MoveToWorkspace ` | Move the focused window to workspace `n` |
| `MoveAndFollowWorkspace ` | Move the focused window and switch with it |
`n` runs from 1 to the number of workspaces, 9 by default.
### Keys and text
| Command | Sends |
| ------------------------------------------------------------------------- | --------------------- |
| `Type "text"` | The text, all at once |
| `Enter`, `Space`, `Tab`, `Backspace`, `Delete`, `Escape` | That key |
| `Up`, `Down`, `Left`, `Right`, `Home`, `End` | That key |
| `Ctrl+c`, `Alt+x`, `Ctrl+Alt+t`, `Shift+Left`, `Ctrl+Enter`, `Ctrl+Space` | A key with modifiers |
These all 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.
After a modifier, the parser accepts a letter or word, a number, an arrow key, `Home`, `End`, `Enter` or `Space`. `Tab`, `Escape`, `Backspace` and `Delete` are not accepted, so `Shift+Tab` and `Ctrl+Backspace` are parse errors (`expected key after modifier, got Tab`). To send one of those, use `tuios send-keys "shift+tab"` while the pane is in terminal mode.
### Timing
| Command | Effect |
| ------------------------------- | -------------------------------------------------------------------- |
| `Sleep ` | Pause |
| `Wait ` | Same as `Sleep`. A duration is required |
| `WaitUntilRegex "pattern" [ms]` | Pause until the focused pane's screen matches the regular expression |
```bash
Type "make build"
Enter
WaitUntilRegex "BUILD (OK|FAILED)" 60000
```
`WaitUntilRegex` checks the visible screen of the focused pane. The timeout is in milliseconds and defaults to 5000. On timeout it shows a warning and playback continues, so it does not stop a failing tape. Both `Wait` and `WaitUntilRegex` are skipped by `tuios tape exec`.
### Animations and other commands
| Command | Effect |
| ----------------------------------------------------------- | ---------------------------------------------------------- |
| `EnableAnimations`, `DisableAnimations`, `ToggleAnimations` | Turn animations on or off |
| `CommandPalette` | Open the command palette |
| `Screenshot` | Save the focused pane as an image, like `tuios screenshot` |
### Parsed but ignored
These keywords parse and validate, and then do nothing:
| Command | Note |
| ------------------- | ------------------------------------------------------ |
| `Set ` | No effect. To change a setting, use `tuios set-config` |
| `Output ` | No effect |
| `Source ` | Does not include another tape |
| `Focus ` | No effect. Use `FocusWindow` |
## Examples
### Three panes
```bash
DisableAnimations
EnableTiling
NewWindow
RenameWindow "editor"
Type "nvim ."
Enter
SmartSplit
RenameWindow "server"
Type "npm run dev"
Enter
WaitUntilRegex "ready" 30000
SmartSplit
RenameWindow "tests"
Type "npm test -- --watch"
Enter
FocusWindow editor
```
### Two workspaces
```bash
NewWindow
Type "cd ~/project"
Enter
SwitchWorkspace 2
NewWindow
Type "htop"
Enter
SwitchWorkspace 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.
### 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+BTt, 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 ` | 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 ` | 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](/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
# Project Tapes
URL: https://tuios.gaurav.zip/docs/project-tapes
> A .tuios.tape file that builds a project's session and layout when you cd into it, after you review and trust it.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
A project tape is a `.tuios.tape` file in a project directory. When the focused shell `cd`s into that directory, TUIOS offers to build the project's session and layout from it: an editor in one pane, a dev server in another, a shell in a third. It works like `.envrc` does for direnv.
A tape can type any command, so it is treated as code you must approve. **An untrusted tape never runs.** TUIOS only reads it, hashes it and shows it to you. Nothing happens until you review it and choose to run it.
## Quick start
Put a `.tuios.tape` in your project:
```
# .tuios.tape
Session "myproject"
Require "pnpm"
RenameWindow "edit"
Type "nvim ." Enter
Split vertical
RenameWindow "serve"
Run "pnpm dev"
Split horizontal
RenameWindow "sh"
Focus "edit"
```
Then, in a TUIOS pane, `cd` into the project:
1. A banner appears and the dock shows a `tape ?` badge. Nothing has run.
2. Press Ctrl+BTt, or run `Tape: review the project tape` from the palette.
3. The review dialog shows the tape's path, its trust status, what it will build, and its full content. Scroll it with the arrow keys.
4. Choose:
* r **Run once**: run it now and ask again next time.
* t **Trust and run**: remember this exact file and run it.
* n **Never**: never offer this path again.
* Esc **Not now**: close the dialog. The badge stays.
Next time the badge reads `tape ✓`, and one key takes you to the project session.
## What running a tape does
With the default `Scope session`, TUIOS:
1. Picks a session name: the `Session` directive, or the project directory's name.
2. If that session exists, switches to it and builds nothing. Entering the project again never duplicates panes.
3. Otherwise creates the session with one pane at the project root, runs the tape body in it, and switches you there.
Because the tape always starts from a fresh session with one pane, the same keystrokes give the same layout every time. Session scope needs the daemon. In a standalone TUIOS, the tape runs in the current session instead, and TUIOS says so.
`Scope current` runs the tape in the current session, starting from the focused pane. It adds to whatever is already there, so keep such tapes small.
## The header
Optional directives at the very top of the file, before any other command:
| Directive | Meaning | Default |
| ---------------------------------- | -------------------------------------------------------------- | ---------------------------- |
| `Session "name"` | Session to build or switch to | The project directory's name |
| `Scope session` or `Scope current` | Where the tape runs | `session` |
| `Workspace N` | Workspace to build in | None |
| `Require "command"` | Skip the tape, with a notice, if this program is not on `PATH` | None |
A directive that comes after a body command is read as part of the body.
## The body
The body is a small layout language, one command per line. Keywords are not case sensitive.
| Command | Effect |
| -------------------------------------- | -------------------------------------------------------------------------- |
| `Type "text"` | Type text into the focused pane. Add `Enter` on the same line to submit it |
| `Run "cmd"` | Short for `Type "cmd" Enter` |
| `Enter` | Press Enter in the focused pane |
| `Split vertical` or `Split horizontal` | Split the focused pane (`v` and `h` also work) |
| `NewWindow "name"` | Open a new tiled pane. The name is optional |
| `RenameWindow "name"` | Name the focused pane. `Rename` also works |
| `Focus "name"` | Focus a pane by name |
| `Sleep 500ms` | Pause |
| `EnableTiling`, `DisableTiling` | Turn tiling on or off |
Blank lines and lines starting with `#` are ignored, and so is any command not in this table. TUIOS waits briefly after each `Split` and `NewWindow`, so the new pane is ready before the next line types into it.
This is not the full [tape scripting](/docs/tape-scripting) language. It is a smaller set made for building a layout.
## Trust
Trust is granted to a pair: the file's real path and the SHA-256 hash of its content.
* **Any edit makes the tape untrusted again.** If a `git pull` changes a trusted tape, you see it as "changed since you trusted it" and review it again. It never runs silently.
* **What you approve is what runs.** TUIOS reads the file once, and shows and runs that same copy. Replacing the file after you approve it changes nothing.
* **Never** is recorded by path only, so editing the file does not bring the prompt back. A denied path shows no banner, no badge and no dialog.
Decisions are stored in `$XDG_DATA_HOME/tuios/tape-trust.toml`, readable only by you. They belong to this machine and do not travel with your dotfiles.
A tape is not offered at all, and the dialog only lets you dismiss it, if it:
* is not a regular file owned by you,
* is writable by group or others,
* sits under a world-writable directory without the sticky bit, or
* is larger than 64 KiB.
TUIOS also ignores shells running over ssh, since it cannot read or verify a file on another machine. While a tape runs, detection is paused, so a tape that `cd`s elsewhere cannot start another one.
## Configuration
```toml
[tape]
autorun = "ask" # off, ask or auto
auto_review = false # open the review dialog as soon as a tape is found
```
| `autorun` | Behaviour |
| --------- | ---------------------------------------------------------------------------------------------------------------- |
| `off` | No scanning. The feature is invisible |
| `ask` | The default. Every tape gets a banner and a badge, and nothing runs until you choose Run |
| `auto` | A trusted, unchanged tape runs as soon as you enter the directory. Untrusted or changed tapes behave as in `ask` |
No mode runs a tape you have not reviewed.
`auto_review = true` opens the review dialog on its own, at most once per directory per session, instead of waiting for you to press the key. You still have to choose Run.
`TUIOS_TAPE_AUTORUN=off` overrides the config for one run, for example in CI or when you look at code you do not trust:
```bash
TUIOS_TAPE_AUTORUN=off tuios
```
## Related
# Tape Recording
URL: https://tuios.gaurav.zip/docs/tape-recording
> Record what you do in TUIOS into a tape you can edit and replay.
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Card, Cards } from 'fumadocs-ui/components/card';
TUIOS can record what you do into a `.tape` file. A recording holds actions, such as "open a window" and "type this text", not terminal output. The result is a plain text [tape](/docs/tape-scripting) that you can read, edit and replay.
## Record a tape
### Start
Press Ctrl+BTr. Type a name, or keep the suggested `recording__
### Work as usual
Open windows, type commands, switch workspaces. Each action is added to the recording.
### Stop
Press Ctrl+BTs. TUIOS writes the tape to disk and shows the file name.
### Replay
```bash
tuios tape play "$(tuios tape dir)/my-workflow.tape"
```
## Keys
The tape prefix is Ctrl+BT, followed by:
| Key | Action |
| -------------- | -------------------------------------------------------------------- |
| r | Start recording (asks for a name) |
| s | Stop recording and save |
| m | Open the tape manager |
| t | Review a detected [project tape](/docs/tape-scripting#project-tapes) |
| Esc | Cancel the prefix |
The tape manager lists your recordings. Use j and k or the arrow keys to select, Enter to play in the current session, r to record and d to delete.
## What a recording looks like
```bash
# my-workflow
# Recorded: 2026-09-22T10:15:04+04:00
# Disable animations for consistent playback
DisableAnimations
EnableTiling
WindowManagementMode
NewWindow
Sleep 1.204113s
TerminalMode
Type "ls -la"
Sleep 612.40875ms
Enter
# Re-enable animations
EnableAnimations
```
Every recording has the same frame:
* A header comment with the name and the start time.
* `DisableAnimations` at the start and `EnableAnimations` at the end, so playback does not depend on your animation setting.
* The state at the moment you started: `SwitchWorkspace` if you were not on workspace 1, then `EnableTiling` or `DisableTiling`, then the mode.
Between actions, a pause longer than 100 ms becomes a `Sleep` with the exact measured time. Round the values by hand if you want a tidier file.
Printable keys you type into a pane are joined into one `Type` command. The text is written out when the next non-text key, action, mode switch or workspace switch happens, or when you stop recording.
## What is recorded
| Recorded | As |
| ------------------------------------------------ | ------------------------------------------------------ |
| New, close, next and previous window | `NewWindow`, `CloseWindow`, `NextWindow`, `PrevWindow` |
| Minimize | `MinimizeWindow` |
| Restore all minimized | `RestoreWindow` |
| Toggle tiling | `ToggleTiling` |
| Snap left, right, fullscreen | `SnapLeft`, `SnapRight`, `SnapFullscreen` |
| Select window by number | `FocusWindow ` |
| Workspace switch | `SwitchWorkspace ` |
| Mode switch | `TerminalMode`, `WindowManagementMode` |
| Text typed into a pane | `Type "..."` |
| Special keys and key combinations sent to a pane | `Enter`, `Tab`, `Up`, `ctrl+c` and so on |
Not recorded:
* Mouse actions: clicks, drags and resizes.
* Splits, zoom and other BSP actions. A session built from splits replays as if you never split.
* Renames, and moving a window to another workspace.
* Copy mode, overlays and anything else not in the table above.
Some recorded commands do not replay the way you performed them:
* `RestoreWindow` restores only the focused window, not every minimized one.
* `FocusWindow ` looks for a window named or with the ID `n`, not the window at position `n`, so it usually does nothing.
* `SnapFullscreen` snaps to the top-left quarter.
Fix these by hand after recording. `SmartSplit`, `RenameWindow "name"` and `FocusWindow ` all work on playback. See [Tape Scripting](/docs/tape-scripting#commands) for every command.
## Managing recordings
```bash
tuios tape list # name, size and date of each recording
tuios tape show my-workflow # print a recording
tuios tape delete my-workflow
tuios tape dir # print the recordings directory
```
`show` and `delete` take the name with or without `.tape`. `delete` asks for confirmation.
Recordings are saved in the `tuios` folder of your data directory, which `tuios tape dir` prints:
| Platform | Default location |
| ------------- | -------------------------------------- |
| Linux and BSD | `~/.local/share/tuios/` |
| macOS | `~/Library/Application Support/tuios/` |
Setting `XDG_DATA_HOME` moves it on every platform. Only files ending in `.tape` are listed.
## Editing a recording
A recording is a normal tape, so you can:
* add comments,
* replace the exact `Sleep` values with round ones,
* add `WaitUntilRegex` so playback waits for real output,
* add the splits and renames the recorder missed,
* delete mistakes.
For example, a recording that starts a dev server can wait for it before opening a second pane:
```bash
EnableTiling
NewWindow
RenameWindow "server"
Type "npm start"
Enter
WaitUntilRegex "listening on" 15000
SmartSplit
RenameWindow "shell"
```
Run `tuios tape validate` on the file after editing.
`Wait` and `WaitUntilRegex` only work under `tuios tape play`. `tuios tape exec` skips them, so a tape meant for both should pace itself with `Sleep`.
## Tips
* Turn tiling on before you record. Tiled layouts replay the same way at any terminal size. Floating window positions are not recorded.
* Keep one task per tape. Short tapes are easier to fix than a whole afternoon.
* Start from a known state. `tape play` starts with no windows, so a recording made in a busy session will not replay the same way.
* Recording writes the file once, when you stop. Nothing is written per keystroke.
## Related
# Screenshots
URL: https://tuios.gaurav.zip/docs/screenshots
> Capture a pane, a region or the whole screen as a styled PNG, SVG, HTML or text file.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
TUIOS renders screenshots from the pane's own cells, not from pixels on your screen. Colours, bold, italics and links are exact, and it works over SSH, in the browser, and on a detached session. A PNG or SVG gets a frame: padding, a background derived from the theme, rounded corners, a shadow and a title bar.
## In the app
| How | What it captures |
| -------------------------------------------- | -------------------------------------------------- |
| Ctrl+BC | Enter capture mode, then pick a pane |
| Palette: `Screenshot this window` | The focused pane, at once |
| Palette: `Screenshot a region` | Enter capture mode and drag over the area you want |
| Palette: `Screenshot the screen` | The whole screen: panes, rail and dock |
| Right-click a pane: `Screenshot this window` | That pane |
In capture mode:
| Key | Action |
| ------------------------------------------------------------------------ | ---------------------------- |
| Tab, arrows, hjkl | Move between panes |
| Enter or Space | Capture the highlighted pane |
| Mouse drag | Capture a region |
| f | Capture the full screen |
| Esc, q | Cancel |
After a capture a preview panel opens (`screenshot.preview`). On a terminal with kitty graphics it shows the framed image. Elsewhere it shows the cells.
| Key | Action |
| -------------------------------------------------------- | ------------------------------- |
| Enter, q | Keep the file and close |
| Esc | Delete the file and close |
| c | Copy the image to the clipboard |
| o | Open the file |
| r | Retake |
| Arrows, PgUp/PgDn, Home | Scroll the preview |
Files are saved under `screenshot.directory`, which defaults to `~/Pictures/tuios`.
The action names `screenshot_window` and `screenshot_screen` have no default key. Bind them in `[keybindings]` if you want one-key captures.
## From the command line
`tuios screenshot` asks the daemon to render a pane, so nobody needs to be attached.
```bash
# The focused pane, as a PNG in screenshot.directory
tuios screenshot
# A named pane in a named session
tuios screenshot -s work -w build
# Include the last 200 lines of history above the screen
tuios screenshot --scrollback --lines 200
# An SVG for a README
tuios screenshot --format svg --out demo.svg
# Render in a different theme
tuios screenshot --theme catppuccin_mocha
```
| Flag | Meaning |
| --------------------- | ----------------------------------------------------------------------------------------- |
| `-s`, `--session` | Target session |
| `-w`, `--window` | Target pane, by name or id |
| `-f`, `--format` | `png`, `svg`, `html`, `ansi` or `txt` |
| `--frame` | `window`, `plain` or `none` |
| `--theme` | Render in this theme instead of the session's |
| `-o`, `--out` | Output path instead of a generated name |
| `-S`, `--scrollback` | Put the pane's history above the screen |
| `--lines` | Limit the history to the last N rows |
| `--cursor` | Draw the cursor |
| `--copy`, `--no-copy` | Copy to the clipboard, or do not |
| `--json` | Print the result as JSON: `path`, `format`, `cols`, `rows`, `bytes`, `copied`, `warnings` |
A tape can take a screenshot too, with the `Screenshot` command. It renders the focused pane.
## Formats
| Format | What you get |
| ------ | ------------------------------------------------------------------ |
| `png` | An image with the frame. `screenshot.scale` sets the pixel density |
| `svg` | A vector image with the frame. Text stays text |
| `html` | A page with the styled cells |
| `ansi` | The pane as text with colour escape codes, no frame |
| `txt` | Plain text, no frame |
With no theme set, TUIOS does not know your terminal's palette, so the 16 basic colours fall back to the xterm defaults and the result says so. Set `appearance.theme`, or pass `--theme`, to render in a known palette.
## The frame
Every part of the frame is a `[screenshot]` option. `--frame`, `--format`, `--cursor` and `--copy`/`--no-copy` override the matching option for one capture.
```toml
[screenshot]
format = "png"
directory = "~/Pictures/tuios"
frame = "window" # window, plain or none
background = "auto" # auto, none, #rrggbb, or #rrggbb..#rrggbb for a gradient
padding = 48 # pixels, 0 to 128
radius = 10 # corner radius, 0 to 32
shadow = true
controls = "auto" # auto, macos, glyphs or none
title_format = "{title}" # {title}, {index}, {cwd}
scale = 2 # PNG size multiplier, 1 to 4
copy = true
preview = true
```
To draw text in a particular font, set `font_family`, or `font_file` to a font file. `font_file` is used for PNG and embedded in SVG and HTML. The full table is in [Configuration](/docs/configuration#the-screenshot-table).
## Related
# Showkeys Overlay
URL: https://tuios.gaurav.zip/docs/showkeys
> Show the keys you press on screen, for demos, screencasts and teaching.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
Showkeys draws the keys you press in the bottom-right corner of the screen. Viewers of a screencast or a live demo can see which shortcuts you used.
## Turn it on
At startup, with the `--show-keys` flag:
```bash
tuios --show-keys
tuios new demo --show-keys
tuios attach demo --show-keys
tuios tape play demo.tape --show-keys
tuios ssh --show-keys
tuios-web --show-keys
```
While TUIOS runs, toggle it in any of three ways:
* Ctrl+BDk (the debug prefix)
* **Toggle show keys** in the command palette
* **Show keys** on the [settings page](/docs/configuration#the-in-app-settings-page)
A toggle is saved to your config file, so the choice survives a restart. You can also set it by hand:
```toml
[debug]
show_key_events = true
```
Showkeys belongs to each client. Two clients attached to the same session each have their own setting and only show their own keys.
## What it shows
Each key is drawn as a pill. Up to 5 keys are on screen at once:
```
Ctrl + b t j ×3 Esc
```
* **Modifiers** come before the key: `Ctrl + c`, `Alt + 1`, `Ctrl+Alt + t`.
* **Shift is hidden on single letters**, since the capital letter already shows it. Shift still shows on other keys, as in `Shift + Tab`.
* **Repeats collapse.** Pressing the same key again adds a count, as in `j ×3`, instead of a new pill.
* **Special keys** have short names: `Enter`, `Esc`, `Tab`, `Space`, `Backspace`, `Delete`, `Home`, `End`, `PgUp`, `PgDn`, and arrows as `↑` `↓` `←` `→`.
* **The leader key is highlighted** in cyan, so the prefix stands out in a recording.
* **Keys expire.** Each pill disappears 3 seconds after its last press, and when a sixth key arrives the oldest one is dropped.
The overlay sits above the dock when the dock is at the bottom, and draws over windows and notifications. On a narrow screen it drops the oldest pills until the rest fit. The pill colors are fixed and do not follow the theme.
Showkeys shows keys, not what they do. It captures every key in both window management mode and terminal mode, before any binding handles it.
Showkeys draws every key, including a password typed into a pane. There is no filter. Toggle it off with Ctrl+BDk before you type a secret, and back on after.
## Recording a demo
1. Start TUIOS with showkeys and a theme that suits the recording:
```bash
tuios --show-keys --theme nord
```
2. Start your screen recorder. A terminal recorder such as `asciinema` records the overlay along with everything else, because the overlay is part of what TUIOS draws.
3. Run your demo. Viewers see each shortcut in the corner as you press it.
For a demo you want to replay the same way every time, script it as a [tape](/docs/tape-scripting) and play it with `tuios tape play demo.tape --show-keys`. Keys a tape sends go straight to the pane and do not appear in the overlay, so showkeys is most useful when you drive TUIOS by hand.
For a live talk, create the session ahead of time and attach when you start:
```bash
tuios new workshop --detach
tuios attach workshop --show-keys
```
## Troubleshooting
**Nothing appears.** Check that showkeys is on. The toggle shows a notification with the new state. Keys vanish 3 seconds after they are pressed, so the overlay is empty while you are not typing.
**A modifier is missing.** Some terminals do not report every combination. `Ctrl+Shift+letter` often arrives as `Ctrl+letter`. A terminal with the kitty keyboard protocol, such as kitty, WezTerm, Ghostty or foot, reports more of them.
**The overlay covers output.** It sits in the bottom-right corner, over whatever is there. Toggle it off for a moment, or stop typing for 3 seconds and it clears.
## Related
# CLI Reference
URL: https://tuios.gaurav.zip/docs/cli-reference
> Every tuios command and flag, with examples. Sessions, windows, input and output, agents, remote hosts, tapes, environment variables and exit codes.
import { Callout } from 'fumadocs-ui/components/callout';
import { Card, Cards } from 'fumadocs-ui/components/card';
```bash
tuios [flags] # start or attach to TUIOS
tuios [flags] # run a command
```
Every command accepts `--help`, and its output always matches your build. `tuios help ` does the same.
## Conventions
Most commands that act on a running session share these flags:
* `-s, --session `: the target session. The default is the most recently active one.
* `-w, --window `: the target window. The default is the focused one.
* `--json`: print JSON instead of a table.
A window target is matched in this order, and the first match wins:
1. The full window ID
2. The index `list-windows` prints
3. A unique ID prefix, such as the 8 characters `list-windows` shows
4. The custom name, then the window title
An index or ID therefore beats a window that happens to be named `2`.
Commands that talk to the daemon exit `3` when no daemon is running. See [Exit codes](#exit-codes).
## Global flags
Three flags work on every command:
* `--debug`: enable debug logging.
* `--cpuprofile `: write a CPU profile.
* `--pprof `: serve `net/http/pprof` on this address, for example `localhost:6060`. The server has no authentication, so bind it to localhost.
These work only on a bare `tuios`:
* `--standalone`: run a session without the daemon, for this run. `TUIOS_NO_DAEMON=1` does the same for a whole shell.
* `--skill`: print the agent skill for driving TUIOS from a pane, then exit.
* `--list-themes`: print every theme id, then exit.
* `--preview-theme `: print a theme's 16 ANSI colours, then exit.
* `-v, --version`: print the version, the VT backend (`pure-Go` or `ghostty`), commit, build date and builder. There is no `version` subcommand.
```bash
tuios --theme $(tuios --list-themes | fzf --preview 'tuios --preview-theme {}')
tuios --skill > ~/.claude/skills/tuios/SKILL.md
```
A bare `tuios` attaches to a daemon session, because `startup.daemon` defaults to `true`. See [The startup table](/docs/configuration#the-startup-table).
### Interface flags
These flags change how the TUI looks for one run. They are accepted by `tuios`, `new`, `attach`, `ssh` and `tape play`. Any other command rejects them. Each one overrides a [config option](/docs/configuration#command-line-flags).
| Flag | Values | Default |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `--theme ` | a theme id | none: your terminal's colours |
| `--border-style