Settings, Sidebar, File editor sets appearance.sidebar.editor, the terminal
editor command used by Shift+Enter on a text file. For example:
[appearance.sidebar]
editor = "micro"Other terminal editors and arguments work too, such as nano or vim -f.
Quote a path or an argument that contains spaces. tuios splits the command into
arguments and runs no shell. It adds the full path of the file at the end. If
the value is empty, tuios uses $EDITOR, then $VISUAL, then vi. File links
and hints open files with the same command. tuios reads the first 512 bytes of
a file to check that it is text. It refuses binary files and special files.
The configuration reference lives on the docs site: https://tuios.dev/docs/configuration
It covers the whole config.toml: the [appearance] table and its sidebar, scrollbar, dock, and window-button options, [notifications.agent], [notifications.mail] (see AGENT_STATE.md), all 23 [keybindings] sections, [daemon], [startup], [tape], [screenshot], [hooks], and [debug], along with what hot-reloads and what needs a restart.
[notifications] sets how many seconds a message stays up. duration is for info and success messages, and its default is 2. warning_duration (default 8) and error_duration (default 15) are for warnings and errors. Any value from 1 to 3600 is valid, and 0 uses the default. tuios warns about a negative value and uses the default. tuios warns about a value above 3600 and uses 3600. style sets how a message looks. full (the default) shows a coloured bar on the left of the message and a line above it that counts down. plain shows the icon and the text only. The icon keeps its colour, so an error still looks different from an info message. level sets the least severe message the dock shows: info (the default) shows every message, warning only warnings and errors, and error only errors. A message the dock does not show still goes to the log viewer (Ctrl+B D l), so the notice for a key that did nothing can be read there. Set log_hidden = false to keep those messages out of the log as well. Warnings and errors always go to the log. A message from a pane, an agent or mail does not use level. [notifications.agent] and [notifications.mail] control those messages.
[notifications.mail] has enabled, notify, dock, sound and between_agents. Each key except between_agents follows the same key in [notifications.agent] while it is unset, so a config without the table alerts as before. between_agents (default false) also alerts on a message from one agent to another. Sound mode, cooldown, cue files and quiet hours always come from [notifications.agent]. tuios set-config notifications.mail.KEY "" clears a key, and tuios get-config then prints (follows notifications.agent.KEY).
tuios list-options describes every settable path with its type, default, and accepted values, straight from the registry the validator uses. The in-app settings page (Ctrl+B ,) edits and persists the same options, and its rows are derived from that same registry: an option an agent can set is an option a person can reach, and a test fails the build if one is not.
When you put the pointer on a session row in the sidebar, the terminals
section shows the panes of that session. When you move the rail cursor onto a
session row, the same thing occurs. This is the peek. To turn it off, set
appearance.sidebar.peek to false:
[appearance.sidebar]
peek = false # true is the defaultWith the peek off, the terminals section shows the panes of the attached
session. It changes only when you attach to a different session. The setting
also shows in Settings, Sidebar, and tuios set-config appearance.sidebar.peek false changes it while tuios runs.
You can keep part of the config in other files. Use this to share one config.toml between machines and keep the settings for one machine, such as its hosts, in a separate file. A Nix or home-manager setup can also put its settings in a file that it manages, and leave config.toml to tuios.
There are two ways to add files. Use one or both.
-
Put an
includelist at the top of config.toml, before the first table:include = ["hosts.toml", "~/.config/tuios/local.toml", "?work.toml"]
A relative path is relative to the file that has the
includeline.~is your home directory. A name that starts with?is optional: when the file is not there, tuios says nothing. An included file can have its ownincludelist.On Windows, write an include path with forward slashes, or put it in a single-quoted literal string. In a double-quoted TOML string, a backslash starts an escape:
include = ["C:/Users/me/tuios/local.toml", 'C:\Users\me\tuios\work.toml']
-
Put
*.tomlfiles in aconfig.ddirectory next to config.toml. tuios reads them in name order, so10-theme.tomlcomes before50-hosts.toml.
tuios reads the files in this order. A later file wins over an earlier file.
- The files in the
includelist, in list order. An included file's own includes come before it. - The files in
config.d, in name order. - config.toml.
config.toml wins, and it holds only what you changed. On the first start,
tuios writes a config.toml with comments and no settings. The one exception
is [startup]: tuios writes tiled = true and daemon = true there, unless
another file sets them. Each change from tuios adds or changes only the key
you changed. So an included file applies everywhere except where you chose a
value of your own.
A config.toml from an older tuios sets every key, and so it hides every other
file. Run tuios config prune to remove the keys that have their default
value. When another file sets one of those keys, its value then applies. The
command lists those keys and asks before it changes the file. Use
tuios config prune --dry-run to see the keys and --yes to skip the
question. A run with no terminal needs --yes.
The files merge with these rules:
-
Tables merge key by key, at every depth.
[hosts.NAME]tables merge by name, so each file can add its own hosts. -
For a single value, the later file wins.
-
An array of values replaces the earlier array. It does not add to it.
-
An array of tables, such as
[[keybindings.command]], merges entry by entry. It is never replaced whole. A later entry matches an earlier entry bynamewhen both have a name, and bykeywhen they do not. A matched entry merges key by key. An entry that matches nothing goes at the end. -
To remove an entry that an earlier file sets, add an entry with the same
nameorkeyanddisabled = true:[[keybindings.command]] name = "deploy" disabled = true
A relative file path in an included file, such as a [plugins] dirs entry or
a token_file, is relative to that file.
These are not errors:
- An include that names a file that does not exist. tuios shows a warning and skips it, so one machine can include a file that only it has.
- An include cycle, or a file that includes itself. tuios shows a warning and reads each file once.
- An optional include or a
config.dfile that tuios cannot read, such as a link that points to itself. tuios shows a warning and skips it. A required include that tuios cannot read stops the load. - An
includekey below a table header. TOML puts it in that table, so it includes nothing. tuios shows a warning.
An include that names a directory stops the load. Put a directory of files in
config.d. A file that exists and has a TOML error also stops the load, the
same as an error in config.toml.
tuios config files # every file, in merge order
tuios config origin # every key and the file that sets it
tuios config origin appearance.theme # one keytuios config origin also names the earlier files that set the same key. Their
values lose.
tuios watches every file it reads: config.toml, each included file, each file
in config.d, and the config.d directory itself. When a file is a link,
tuios also watches the file that the link points to. A save to any of them
takes effect at once. An included file that was missing applies when you make
it, if its directory exists.
tuios config apply makes the daemon and every tuios client on this machine
apply the files as they are now. Use it when a save does not reach a client,
for example when you make an included file in a new directory.
The settings page, tuios set-config, the keybind manager,
tuios keybinds unbind, tuios hosts add and tuios plugins write the config.
They write only the keys you changed, and never copy the other files into
config.toml. A change made while a file changed on disk keeps the new values
of that file. Each change goes to one file:
- A key that a file sets goes to the last file that sets it. The look file,
config.d/look.toml, is not a target: a change to a key that it sets goes to config.toml. See Looks. - A new entry in a table of entries, such as a new
[hosts.NAME], goes to the last writable file that has entries in that table. - Any other new key goes to config.toml.
- A removed key is removed from each file that sets it.
tuios changes only the lines of the key, so the comments, the blank lines and the line endings of the file stay. A comment above a table stays with that table. tuios never writes a file you wrote again from its values. When it cannot express a change in the lines of an included file, it writes the change to config.toml and tells you. When it cannot remove a key that way, the save fails and the message names the file. Only config.toml itself is written again whole in that case, without its comments.
tuios never writes a read-only file. This includes a file that Nix or
home-manager links in from the store. When a read-only file sets the key,
tuios writes the change to config.toml and tells you. When config.toml is
read-only too, tuios writes the change to the last writable file that config.toml
includes. The change must go to a file that comes after every file that sets
the key. If there is no such file, the save fails and the message names the
file to change. tuios cannot remove a key from a read-only file. It removes an
array entry with a disabled = true entry in a writable file. When an entry
that tuios writes must not take keys back from an earlier file's entry, tuios
writes a disabled = true entry first and the new entry after it.
tuios config reset writes the first-start config.toml again, with your
include list, and lists the other files that still apply. When config.toml
has an error, the reset still keeps an include list that it can read.
You can share one config.toml between a Mac and a Linux machine, for example with a dotfiles repo. tuios reads opt+ and option+ as alt+ on every platform. On Linux, a key such as opt+1 is alt+1.
The [keybindings.terminal_mode] table also accepts middle for the middle
mouse button, with ctrl, alt or shift, such as ctrl+middle. A program
that uses the mouse gets a plain middle press.
tuios ignores a key that it cannot read and loads the rest of the file. The action gets its default key when it has no other key. If another action already has that default key, the action has no key. tuios shows a config problem when it starts, and the log viewer (leader D l) names each key and its file. tuios keybinds doctor and tuios config apply list these keys too.
[keybindings]
keyboard_layout = "us" # us, other
option_glyphs = "auto" # auto, alt, bind, type| Key | Default | What it does |
|---|---|---|
keyboard_layout |
us |
us lets a binding on opt+shift+7 also match opt+&, as on a US keyboard. Set other for AZERTY, QWERTZ and other layouts. A key that the terminal reports through the Kitty keyboard protocol uses that report. |
option_glyphs |
auto |
What a character that macOS Option typed does. auto reads the keyboard layout of the Mac when tuios starts. It reads it again when such a character arrives and the last read is more than 30 seconds old. On a US or ABC layout it runs the Option binding for the character. On other layouts the character goes to the pane. A keyboard_layout that you set overrides the layout of the Mac. alt runs the binding only when the terminal also reports Alt. bind always runs it. type always sends the character to the pane. A tuios that serves SSH or web clients uses the layout of the Mac it runs on. |
See Shifted digits and AZERTY and One Option key for typing.
[keybindings]
lock = true| Key | Default | What it does |
|---|---|---|
lock |
false |
true gives every key to the pane in terminal mode, except the leader. The leader unlocks for one action, and then tuios locks again. A repeatable action keeps its menu open until you press esc. That menu takes only repeatable keys, and any other key goes to the pane. The zellij preset sets true. |
See Lock mode.
[keybindings]
nest_exit_key = "f12"
[keybindings.prefix_mode]
nest = ["enter"]| Key | Default | What it does |
|---|---|---|
nest_exit_key |
"f12" |
The key that ends nest mode. A key that cannot be read goes back to "f12", and a key programs type is reported as a problem. The nest action (the leader, then enter) turns nest mode on: every key goes to the pane, the leader too, until this key. |
See Nest mode.
tuios finds two kinds of link in a pane. An OSC 8 hyperlink is a link that a
program marked, such as ls --hyperlink, gcc, delta, gh or an agent
CLI. Its text and its target can be different. A bare URL is plain text that
starts with http://, https://, ftp://, ftps://, file://, ssh:// or
git://. Hints mode and the pointer use the same URL detector.
[appearance]
links = "all" # off, marked (OSC 8 only), all
link_click = "both" # both, ctrl, shift, off
link_hover = "safe" # safe, always
link_opener = "" # for example "firefox --new-tab" or "open -a Safari %s"| Key | Default | What it does |
|---|---|---|
links |
all |
The links that tuios finds. marked finds only OSC 8 links. |
link_click |
both |
The click that opens a link. both is Ctrl+click and Shift+click. Ctrl+click also works on macOS, in Ghostty, kitty, WezTerm and Terminal.app. Cmd+click does not open a link there. |
link_hover |
safe |
The hover underline and label. safe skips a pane whose program tracks the mouse. always draws them in every pane. |
link_opener |
empty | The command that opens a web link. tuios puts the URL where %s is, or at the end. If this is empty, tuios uses $BROWSER, then the system opener. |
The system opener is open on macOS, rundll32 on Windows, wslview under
WSL, and xdg-open on other systems. tuios runs no shell. It gives the URL to
the opener as one argument. If the opener fails, tuios shows a message and
puts the URL on your clipboard.
- A local client opens the link on your machine.
- Under
ssh, tuios does not use the system opener, because it would open the browser on the remote machine. tuios puts the URL on your clipboard. Set$BROWSERorlink_openerto a command that forwards the URL, if you have one. - A remote client (
tuios ssh, the web client) puts the URL on your clipboard. - A Linux machine with no
DISPLAYand noWAYLAND_DISPLAYhas no desktop. tuios puts the URL on your clipboard.
In all these cases, tuios also sends each link to your terminal as OSC 8. Use the link click of your terminal to open it on your machine.
- Only
http,https,mailto,ftpandftpslinks go to the opener. tuios copies other links, such asjavascript:,data:or the scheme of an application. - A
file://link opens in an editor pane only when the file is on this machine. Afile://link with the name of another host does not open. - tuios refuses an address with control characters or spaces.
- The hover label shows the target. After a click, the message names the host. tuios does not ask before it opens a link. The label and the scheme list give the protection, and a prompt on each click teaches people to accept it.
tuios draws each pane into its own frame, so your terminal sees the output of
tuios and not the output of the program. tuios writes every link in the
focused pane as OSC 8. An OSC 8 link keeps its URI and its id=. A bare URL
gets an id= from tuios, so a URL that wraps to the next row is one link.
Your terminal can then show and open the real target. In a pane that is not
focused, tuios writes the OSC 8 links of the program. Your terminal finds the
bare URLs itself.
A terminal sends a click to tuios only when the terminal does not use the
click itself. Most terminals keep Shift+click for their own selection when
a program reads the mouse. That is why Ctrl+click is the main click.
| Terminal | Shift+click gets to tuios |
Ctrl+click gets to tuios |
The terminal opens OSC 8 links |
|---|---|---|---|
| kitty | No. kitty opens the URL under the pointer. | Yes | Yes, with Shift+click or Ctrl+Shift+click |
| Ghostty | No. Ghostty extends its selection. | Yes. On a link, Ghostty opens it. | Yes, with Ctrl+click (Linux) or Cmd+click (macOS) |
| WezTerm | No. Shift bypasses mouse reporting. |
Yes | Yes |
| Alacritty | No. Alacritty opens a hinted URL. | Yes | Yes, with Shift+click |
| foot | No. Shift bypasses mouse reporting. |
Yes | Yes, in URL mode (Ctrl+Shift+O) |
| iTerm2 | Yes | Yes | Yes, with Cmd+click |
| Windows Terminal | No. Shift bypasses mouse mode. |
Yes | Yes, with Ctrl+click when it gets the click |
| xterm | No. Shift bypasses mouse reporting. |
Yes | No |
Cmd is not part of the mouse protocol, so tuios never sees Cmd+click. On
macOS, Cmd+click opens a link through the terminal, from the OSC 8 that
tuios writes. In Ghostty, a program can ask for Shift+click with
XTSHIFTESCAPE, or you can set mouse-shift-capture = true. tuios does not
ask, because that takes Shift selection away from you.
appearance.focus_follows_mouse = true focuses the pane under the pointer as
the mouse moves. Set appearance.focus_follows_mouse_pause to hold a key that
pauses it:
[appearance]
focus_follows_mouse = true
focus_follows_mouse_pause = "ctrl" # ctrl, alt, or off (the default)- Hold the key and move the mouse. The focus stays on the current pane.
- Release the key. The next mouse move focuses the pane under the pointer.
- The key still goes to the pane. A program that reads the mouse gets the
motion with the key on it.
Ctrl+drag andAlt+drag work as before.
tuios reads the key from the mouse reports that your terminal sends. Most
terminals send Ctrl and Alt. Shift is not a choice, because most terminals
keep Shift with the mouse for their own selection. On macOS, some terminals
use Option with the mouse for their own actions. If alt does not work in
your terminal, use ctrl.
appearance.nvim_navigation is false by default. Set it to true to enable
pane navigation through the optional
tuios-nvim-navigator plugin:
[appearance]
nvim_navigation = trueSee KEYBINDINGS.md for the matching focus-key mappings and protocol behaviour.
appearance.focus_wrap is false by default. Set it to true to wrap
directional focus at the edge of a tiled layout, as tmux select-pane does:
[appearance]
focus_wrap = trueWhen no pane is in that direction, focus goes to the pane on the far side that faces the focused pane. If more than one pane is on the far side, tuios picks by the rule of a normal focus move. A pane that fills the layout in that direction keeps focus.
The option applies to every directional focus move in a tiled layout:
- The focus keys in window mode, in terminal mode and after the prefix.
- The Neovim navigator hand-back, when
nvim_navigationis on. tuios focus-window --directionand theFocusDirectiontape command.
In the scrolling layout, up and down wrap inside the column. Left and right move along the strip and do not wrap. With tiling off, focus does not wrap. The tmux shim always wraps, as tmux does, and does not read this option.
persist_scrollback in [daemon] (default true) saves each pane's history with its session. After a daemon restart or a reboot, the restored pane shows that history above a dim divider, and the new shell starts under it. persist_scrollback_lines (default 1000) and persist_scrollback_kb (default 2048) set the most lines and the most KiB one pane saves. The files hold what your panes printed, secrets included. Set persist_scrollback = false to stop this. The next time the daemon starts, it deletes the history it saved before. See SESSIONS.md.
window_size in [daemon] sets the size of a session that has more than one client. The names are the names of the tmux window-size option. See SESSIONS.md.
| Value | Size of the session |
|---|---|
latest (default) |
The client that last had input. A smaller client shows a part of the session. |
smallest |
The smallest client. Every client shows the full session. |
largest |
The largest client. A smaller client shows a part of the session. |
A client that is larger than the session fills the space the session does not use with dots. A line in that space names the client that sets the size. The rail and the dock stay at the edges of each client's screen.
To change the value of one session while it runs, use tuios set-config daemon.window_size latest -s NAME. The value is not saved to the config file. A change in the config file applies when the daemon starts again.
single_client in [daemon] (default false) keeps one client for each session. When a client attaches, every other client of that session detaches. The newest attach wins. Each detached client exits with status 0 and prints "Another client attached to this session." The session continues to run. This is tuios attach -d on every attach. Two attaches do not detach the other clients: a view-only client, such as tuios-web --read-only, and the attach that tuios makes on its own to get a session back after a host link drops. A change in the config file applies to the next attach.
[daemon]
single_client = truessh_agent in [daemon] (default "off") can be "follow". Then each session has a stable link to the ssh agent socket of the client that attached to the session or used it last. When you ssh to the machine with agent forwarding and run tuios attach, your panes use the agent of that ssh connection.
[daemon]
ssh_agent = "follow"- The link is
agent-<session id>.sockin the folder of the daemon socket, for example$XDG_RUNTIME_DIR/tuios/agent-3f2a9c1d-0b7e-4d0a-9c4e-5a1f2b3c4d5e.sock.tuios ssh-agent-pathprints it. - Each new pane of the session gets
SSH_AUTH_SOCKset to the link. - A shell that started before you set the option keeps its old value. To use the link in that shell and in every shell, add this line to your shell rc:
p=$(tuios ssh-agent-path 2>/dev/null) && export SSH_AUTH_SOCK="$p" - When the client that the link points at detaches, the link moves to the socket of the client before it. When no attached client has a socket, the link points at the
SSH_AUTH_SOCKof the daemon, if the daemon has one that passes the checks below. Otherwise tuios removes the link. - The daemon removes its links when it stops and when you set the option to
"off". At start, it removes the links that a stopped daemon left. - tuios uses the
SSH_AUTH_SOCKoftuios attachandtuios newonly. A client that runs inside a pane does not count. tuios finds such a client by its process, so a process that leaves its pane on purpose (for example, withsetsidand a clean environment) can still move the link. This check stops mistakes and agents, not a local process that wants to get past it. - tuios resolves a symbolic link once, so
~/.ssh/agent.sockand the 1Password agent work. The link then points at the real socket. The real socket must be a Unix socket that you own. Every folder above it must be owned by you or by root, and no other user can write to it, unless it is sticky like/tmp. tuios refuses other sockets. - A client of the tuios SSH server or of
tuios-webhas no agent socket, so it does not move the link. To follow your agent, ssh to the machine withssh -Aand runtuios attach. - The agent can follow you to a remote host too. Turn on
ssh_agent = "follow"on both machines, and turn on agent forwarding for the link to that host. UseForwardAgent yesfor the host in~/.ssh/config, orssh_options = ["-A"]under[hosts.NAME]. tuios does not forward the agent by default. The daemon keeps one link for each such host,agent-link-<host>-<hash>.sock. The host name is in lower case and cut to 32 characters, and the hash is of the exact name, so two hosts never share a link. It points at the agent of the person on this machine who attached a session on that host, or typed in one, last. A session of this machine does not move it. The daemon starts the link ssh to that host withSSH_AUTH_SOCKset to that link. On the host, the panes of a session you attach through the link use the forwarded agent. - tuios decides that a host forwards from what
ssh -Gprints for its address with itsssh_options. That is how ssh itself reads the options and~/.ssh/config. tuios asks again when the config changes, and whenssh -Gfailed the last time. A host that leaves the config takes its link with it. - Before any client attaches a session on a forwarding host, its link points at the daemon's own
SSH_AUTH_SOCK. If the daemon has none, or its socket fails the checks above, the panes on that host have no agent until a client attaches. The link ssh to a host that does not forward starts with the environment of the daemon, unchanged. Without forwarding, the panes on the host use theSSH_AUTH_SOCKthat their own login there sets: the agent of the daemon on the host, or one from keychain or gnome-keyring through PAM, or none. - If the link to a host shares an ssh master connection, ssh forwards the agent of that master.
- Forward the agent only to a host you trust. Root on the host can use a forwarded agent to sign in to other machines as you while the link is up. If the host pins the hub with
restrictinauthorized_keys, addagent-forwardingto the options of that key.
A change in the config file applies to the next attach. New panes get the link while the option is on.
display in [daemon] (default "follow") can be "off". A daemon that started without a display has no WAYLAND_DISPLAY or DISPLAY. This happens when a host link starts the daemon over ssh. With "follow", the panes of such a daemon get the display of the local client that attached to the session or used it last. Then wl-copy, wl-paste, xclip and image paste work in the panes when you attach from the desktop.
[daemon]
display = "follow"- A daemon that has a
WAYLAND_DISPLAYor aDISPLAYof its own keeps it. Its panes get the daemon's values, and the option does nothing. - Each session has a link to the Wayland socket of the client:
wayland-<session id>.sockin the folder of the daemon socket. A new pane getsWAYLAND_DISPLAYset to the full path of the link. - The link moves to the socket of the newest client. A pane that started under one desktop client still reaches the display after that client detaches and another attaches.
- A new pane also gets
DISPLAYandXAUTHORITYof the client.DISPLAYis a name, not a path, so a pane keeps the value it started with. - A shell that started before a desktop client attached has no display. To get the display in that shell, run this line. To get it in every shell, add it to your shell rc:
eval "$(tuios display-env 2>/dev/null)" - When the client detaches, the session uses the display of the client before it. When no attached client has a display, tuios removes the link, and new panes get no display.
- tuios uses the display of
tuios attachandtuios newonly. The client must run on this machine, as your user, and outside every pane. tuios ignores a client that comes through a host link, a client of the tuios SSH server and a client oftuios-web. WAYLAND_DISPLAYcan be a full path or a name likewayland-1. tuios finds a name in theXDG_RUNTIME_DIRof the client, then in the one of the daemon, then in/run/user/<uid>. The client does not needXDG_RUNTIME_DIR.- The Wayland socket must be a Unix socket that you own. Every folder above it must be owned by you or by root, and no other user can write to it, unless it is sticky like
/tmp. DISPLAYmust be a display on this machine,:Nor:N.S. Its socket/tmp/.X11-unix/XNmust be owned by you or by root. tuios ignores a forwarded display such aslocalhost:10.0.XAUTHORITYmust be a file that you own, in folders that pass the same checks.- The daemon removes its links when it stops and when you set the option to
"off". At start, it removes the links that a stopped daemon left.
[hints] sets what hints mode (Ctrl+B F) labels. See HINTS.md.
[panes] sets label_keys, the keys that the pane labels (Ctrl+B Q) use. The default is 1234567890. Letters a to z and digits are allowed. See KEYBINDINGS.md.
[panes] also sets navigator_layout, the layout the pane navigator (Ctrl+B /) opens in: tree, flat or cards. The default is tree. Press v in the navigator to change the layout. navigator_search = true opens it ready to type a search, and esc with nothing typed closes it. See KEYBINDINGS.md.
[panes] drag_and_drop = true turns on drag and drop between panes over the kitty drag and drop protocol (OSC 72). A program that speaks it, such as yazi 26.9 or later, can drag files from its pane: hold the left button on a file in terminal mode and carry it to another pane. A pane whose program speaks the protocol takes the drop itself. Any other pane gets the paths as a paste, by the rules of dragging a file into a pane below: quoted for the shell in the pane, and refused, with the reason in the label, when that pane could not use a path on this machine. The same rules hold the pane the drag starts from. Dropped text, as against paths, goes only into a pane whose program turned bracketed paste on. tuios reads no file for either side and refuses a drop back into the pane the drag came from. The default is false. The protocol is kitty's and still settling, so the switch is off until it is asked for.
[panes] drag_and_drop_desktop = true, with drag_and_drop, bridges drags to and from the desktop when tuios runs in a terminal that speaks the same protocol itself: kitty 0.47 or later. tuios asks its terminal once at startup and, when it answers, registers as a program that takes drops and offers drags. A file dragged from a file manager or a browser onto the tuios window then lands on the pane under the pointer, by the rules above: a program that speaks the protocol takes it itself, any other pane gets the path as a paste. A file a program in a pane drags, from yazi say, becomes the desktop's drag and can be dropped on any other window; the drag image the desktop shows is the file names and the pane they come from. While the bridge is up the terminal recognises the drag gesture and tuios answers only one whose press went to a pane that offers drags, so moving a pane, selecting text and dragging a row of the rail are as they were. The bridge is for a terminal on this machine: a client over SSH, through tuios-web or attached to another machine's daemon does not register. Both switches take effect without a restart: turning the bridge off takes the registration back, turning it on asks the terminal again. The default is false.
[path_picker] sets show_hidden. With true, the path picker (Alt+;) shows the names that start with a dot when it opens. The default is false. Alt+H in the picker shows or hides them. See KEYBINDINGS.md.
appearance.sidebar.file_drag lets you drag a row of the files section onto a
pane. The pane gets the path, quoted for the shell in the pane. It is on by
default. I on a row does the same for the focused pane, with this option on
or off. See
KEYBINDINGS.md.
[appearance.sidebar]
file_drag = false[scratch] sets the size of the box in which Ctrl+B g shows the scratch terminal. The old session key is no longer used. See SESSIONS.md.
[pip] sets the size (width, height, border included, default 40x12) and the first corner (default bottom-right) of the picture-in-picture view that p pins. See SESSIONS.md.
[[keybindings.command]] binds a key to a command that you write: a scratch terminal, a popup, a pane or a command with no window. A mistake in an entry is a warning, and tuios ignores that entry. See KEYBINDINGS.md.
appearance.animation_speed is the percent window animations run at: minimize
and restore, snap, swap and zoom. 100 is the speed tuios ships with. 200 runs
them in half the time, and 50 in twice the time.
[appearance]
animation_speed = 200 # a number from 25 to 400
# animation_speed = 0 # 100, the defaultA value outside 25 to 400 moves to the nearest end. appearance.motion = "none"
still turns the animations off, whatever the speed. Overlay fades and the copy
sweep keep their own timing. The copy sweep uses appearance.selection.flash_ms.
appearance.calm turns calm mode on. Nothing moves or blinks, and only the
messages that need you show in the dock. Ctrl+B Z (toggle_calm) turns it
on and off and saves the choice here.
[appearance]
calm = true # default: falseCalm mode and appearance.motion = "none" also stop the enter, exit and
skeleton effects of TML surfaces. A surface then shows each part at once.
Calm mode sits over appearance.motion. When you turn calm mode off, the
motion level you set comes back. See
KEYBINDINGS.md for what calm mode changes.
appearance.theme_transition sets how tuios shows a change of theme. The
change can come from the theme picker, tuios set-config, a config reload or
the Omarchy theme.
nonechanges the colors in one frame. This is the default.circlegrows the new theme from the middle of the screen, over the old one.fadechanges every cell from the old colors to the new colors.
[appearance]
theme_transition = "circle"
theme_transition_duration = 450 # milliseconds, from 100 to 2000appearance.animation_speed also applies to the duration. A value outside 100
to 2000 moves to the nearest end. appearance.motion = "none" turns the
transition off. The fade transition needs a truecolor terminal. On a terminal
with fewer colors, tuios changes the colors in one frame. While the transition
runs, the screen outside the circle does not change.
appearance.max_fps is the highest frame rate tuios draws at. Your terminal
and your monitor can show fewer frames. A value above what they can show does
not make tuios smoother.
[appearance]
max_fps = 144 # a number from 10 to 240
# max_fps = "auto" # the refresh rate of your display
# max_fps = 0 # 60, the default| Value | Frame rate |
|---|---|
0 |
60. This is the default. |
10 to 240 |
That number. A value outside the range moves to the nearest end. |
"auto" |
The refresh rate of your display, from 10 to 240. If tuios cannot find the rate, it uses 60. |
The settings page offers Auto, 30, 60, 90, 120, 144, 165 and 240. With Auto
selected, the row shows the rate in use, for example Auto (144). A change
applies at once, with no restart.
What limits the frames you see. tuios sends a frame to your terminal. The terminal then draws the frame on its own schedule. kitty and ghostty draw at most once for each refresh of the monitor, so a 60 Hz monitor shows at most 60 frames a second, whatever max_fps is. Over SSH, the network and the remote terminal set the limit too.
What it costs. tuios draws only when something on the screen changes. The terminal library under it still wakes at max_fps while tuios is idle. An idle tuios at 240 wakes about three times as often as at 60. See perf.md for the measured numbers.
How auto finds the rate. tuios looks once, in the background, when it starts. It looks again when the config file is reloaded. It never delays the first frame. Until the answer arrives, tuios draws at 60.
- Linux on Wayland: tuios asks
hyprctlon Hyprland,nirion niri, andwlr-randron other wlroots compositors. It reads the current mode of each output that is on. - Linux on X11, or on a Wayland compositor that none of these tools speaks
for: tuios asks
xrandr --current. Under XWayland the answer comes from the compositor and can be less exact. - macOS: tuios asks
system_profiler SPDisplaysDataType. Some built-in displays do not report a rate. tuios then uses 60. - Windows: tuios does not look, and uses 60.
A terminal window cannot tell which display it is on. With several displays, auto uses the highest rate among them.
Auto looks only when tuios runs on the same machine as your desktop. Over SSH, in tuios-web and in the SSH server, auto uses 60, because the displays on the machine are not the ones you see.
A program in a pane can draw a picture with sixel. Your terminal shows the picture when it supports sixel or kitty graphics. Some terminals support neither: kmscon, the Linux console, and many SSH clients. On these, tuios draws the picture with block characters. Each character cell shows two colours.
appearance.image_symbols sets the characters.
[appearance]
image_symbols = "auto" # the default
# image_symbols = "octant"
# image_symbols = "off" # show a box instead of the picture| Value | Characters | Detail in one cell |
|---|---|---|
"auto" |
Chosen from TERM. See the next table. |
|
"octant" |
Octants, Unicode 16 (U+1CD00 and on) | 2 by 4 |
"sextant" |
Sextants, Unicode 13 (U+1FB00 and on) | 2 by 3 |
"quadrant" |
Quadrants (▖ ▗ ▘ ▝ and others) | 2 by 2 |
"half" |
Half blocks (▀ ▄) | 1 by 2 |
"off" |
None. tuios shows a box with "image" in it. |
A finer set shows more detail, but your font must have the characters. A
terminal cannot tell tuios which characters its font has. So auto reads
TERM:
TERM |
auto uses |
Why |
|---|---|---|
kmscon |
Octants | kmscon draws with its built-in Unifont, which has the octants. |
linux |
Half blocks | The Linux console uses fonts of 256 or 512 characters. These fonts have the CP437 block characters and nothing finer. |
| Anything else | Quadrants | Quadrants are in the Basic Multilingual Plane. Every font with block characters has them. |
If your font has octants, set "octant" for the best picture.
Colours. tuios uses the colours your terminal shows.
- 24-bit colour: each cell gets the two colours that fit the picture best.
- 256 colours: tuios mixes palette colours in a dither pattern.
- 16 colours, for example the Linux console: tuios always uses half blocks. It mixes the 16 colours in a dither pattern. It uses only the 8 dark colours as a background, because the Linux console uses bright backgrounds for blink. If a picture keeps too little of its shape in 16 colours, tuios shows the box instead. On the pictures measured, this did not happen. See KMSCON-GRAPHICS.md.
What the program sees. With block characters on, tuios tells the
program in the pane that sixel works. Programs such as chafa, timg and yazi
then send sixel. With "off", tuios tells the program that sixel does not
work, and the program uses its own text output.
A terminal with sixel or kitty graphics always gets the real picture. It never gets block characters.
tuios does not draw kitty graphics images as block characters yet. On a terminal without graphics, a program that only uses kitty graphics uses its own text output.
tuios mermaid builds its colours from your tuios theme. [mermaid] theme
picks one of the named diagram themes instead. The --theme flag wins over it.
[mermaid] background is theme (the default) or none. With none, a kitty
or png picture has no background, so the diagram is on the background of your
terminal. The --background flag wins over it.
[mermaid]
theme = "flexoki" # tuios (the default), pele, flexoki, flexoki-dark, nord,
# everforest, gruvbox, rose-pine, catppuccin-mocha,
# catppuccin-latte, tokyo-night, dracula, solarized-light, kanagawa
background = "none" # theme (the default) or nonetuios has two built-in themes made from CharmTone, the colour system of Charm.
| Theme | Ground | Text | Accent |
|---|---|---|---|
charmtone |
Pepper #201F26 |
Sash #ECEBF0 |
Charple #6B50FF |
charmtone-light |
Salt #F7F6FB |
Char #3A3943 |
Charple #6B50FF |
[appearance]
theme = "charmtone"
background = "theme" # paint the Pepper ground behind the panesThe dark theme uses the ANSI colours of Crush, the coding agent from Charm.
Bright black is Squid, so that shell suggestions and comments are easy to read.
The light theme uses a darker shade of each colour.
CharmTone has no dark yellow, so yellow is Cumin, a light brown.
Run tuios list-themes charmtone-light to see the contrast of each colour.
On Omarchy, tuios uses the Omarchy theme when you set no theme. The panes, the chrome, the rail and the dock take the colours of the applied theme. The answers to OSC 10, 11 and 4 in a pane are the theme's colours too.
When you switch the Omarchy theme, tuios changes its colours at the same moment as your terminal. You do not restart tuios and you do not press a key. A program in a pane that turned on mode 2031 gets a light or dark report each time the theme changes. tuios sends no SIGWINCH. See Programs That Follow the Theme. On a machine without Omarchy, tuios watches nothing.
appearance.theme |
On a machine with Omarchy | On a machine without Omarchy |
|---|---|---|
| empty (the default) | The Omarchy theme | The colours of your terminal |
omarchy |
The Omarchy theme | The colours of your terminal, and a warning |
none |
The colours of your terminal | The colours of your terminal |
| a theme id | That theme | That theme |
[appearance]
theme = "omarchy" # use the Omarchy theme, and warn where Omarchy is missingappearance.theme_light names a second theme for a light terminal. theme is
then the dark theme. See
THEMES.md.
tuios reads ~/.local/state/omarchy/current/theme/colors.toml (Omarchy 4),
then ~/.config/omarchy/current/theme/colors.toml (Omarchy 3). The pane
colours are the ones Omarchy gives kitty, ghostty and foot. The chrome takes:
| Chrome | Omarchy key |
|---|---|
| Accent | accent |
| Focused border in window mode | the first colour of hyprland_active_border, else accent |
| Terminal mode, copy mode, errors, notices | green, yellow, red, blue |
| Dialogs | lighter_background, with dark_background and darker_background below it |
Each installed Omarchy theme is also in the theme list as omarchy/NAME, such
as omarchy/gruvbox. Such an id is fixed. It does not change with the Omarchy
theme.
tuios list-themes shows the Omarchy theme it found, and whether tuios uses
it:
active: omarchy (default)
omarchy: Tokyo Night, from /home/you/.local/state/omarchy/current. tuios follows it.
SWATCH_OMARCHY_DIR names another current directory to read.
Every colour key in tuios takes the same values: the backgrounds, the border colours, overlay_border_color, scrollbar.tint, sidebar.cursor_background, [appearance.selection], the dock's fg and bg keys, [dock.tabs], the agent row's fg, and a theme file's chrome object.
| Value | Example | What it is |
|---|---|---|
| Hex | "#7dcfff", "#7cf" |
That colour on every theme. |
| ANSI number | "5", "208" |
0 to 15 are the sixteen slots below. 16 to 255 are the 256-colour palette. |
| Slot name | "magenta", "bright_magenta", "fg", "bg" |
One of the theme's sixteen colours, or its foreground or background. purple is the same slot as magenta. |
| Role name | "accent", "warning", "surface" |
The colour the chrome uses for that role. See the list below. |
| CharmTone name | "charple", "guac" |
A colour from Charm's CharmTone palette. |
The role names are accent, accent_bright, success, warning,
error, info, surface, canvas, panel and card, the roles a
theme's chrome object sets (see THEMES.md). The roles for one
piece of chrome have a dotted name: border.unfocused, border.copy,
border.multifocus, rule, status.attention, status.busy,
dots.close, dots.minimize, dots.zoom, dock.ground and rail.ground.
text, dim and muted are the three text inks. You can use them, but a
theme cannot set them.
A slot name or a role name follows the theme. Change the theme and the colour changes with it. At 16 colours, a slot name is the terminal's own slot, so your terminal's palette paints it. A hex colour stays the same on every theme.
[appearance]
border_focused_color = "accent" # the theme's accent, on every theme
dock_background = "surface"
[appearance.selection]
bg = "bright_black"
[dock.tabs]
active_bg = "accent"
inactive_fg = "muted"Case does not matter, and bright-magenta is the same as
bright_magenta. A key that has its own words keeps them: off and
theme for a background, and quiet, border and muted for the
scrollbar tint. There muted is the tint word, not the text ink. tuios
reads each value when the config or the theme changes, not on each frame.
A value that is not a colour is ignored, and the validator says what to
write instead.
A cell that has no background of its own is transparent, so your terminal's own background shows through it. That is the default everywhere: pane content a program left on the default background, the space between panes, the borders, the dock and the rail. The background options paint those cells instead, one surface at a time or all at once.
| Option | Surface | Values | Default |
|---|---|---|---|
appearance.background |
Every surface below that is not set on its own | off, theme, a colour |
off |
appearance.pane_background |
Pane content, the thin scrollbar over it and the scrollback browser | off, theme, a colour, or empty |
empty (follows background) |
appearance.desktop_background |
Behind and between panes: gaps between tiled panes, the space around floating ones, an empty workspace and its welcome splash | off, theme, a colour, or empty |
empty (follows background) |
appearance.window_chrome_background |
Pane borders and title bars, the lines between shared-border panes, the capture marquee | off, theme, a colour, or empty |
empty (follows background) |
appearance.dock_background |
The dock | off, theme, a colour, or empty |
empty (follows background) |
appearance.sidebar.background |
The rail | off, theme, a colour, or empty |
empty (follows background) |
A theme can name a ground for the dock and the rail (chrome.dock.ground and
chrome.rail.ground, see THEMES.md).
The theme's ground shows when dock_background or sidebar.background is
empty and background is off. A value in either key wins.
What each value paints:
| Value | What is painted |
|---|---|
off |
Nothing. The cell is transparent and your terminal's own background shows through. |
theme |
The active theme's background, with the theme's foreground on text left in the default colour. With no theme set there is no theme background, so it behaves as off. |
| A colour | That colour. With a theme set, default-coloured text takes the theme's foreground, lifted until it reads on the colour; with none, it keeps your terminal's own. |
Precedence. A surface's own option wins whenever it holds a value, off
included. Left empty, it follows appearance.background. So one line paints
everything, and a second line changes or clears one surface:
[appearance]
background = "theme" # every surface on the theme's background
dock_background = "#11111b" # the dock a shade darker
pane_background = "off" # panes keep the terminal's own background
[appearance.sidebar]
background = "#181825"A value that is not a keyword or a colour resolves to off for that
surface (it does not fall back to background), and the validator warns about
it, as it does about theme with no theme set.
What is kept. Only cells with no background of their own are painted. A background a program chose for a cell always wins, and so do the marks tuios paints over a pane (the selection, search matches, the copy mode cursor) and every colour the chrome sets itself: a border's ink stays its focus colour, the title bar's buttons keep theirs, the dock's pills and the rail's highlighted rows keep their fills. The overlay panels (the palette, settings, which-key, the Inbox, every picker, the tooltips and badges) are not on the list because they have no transparent cells: each fills its whole rectangle with its own surface colour.
Programs that ask. A program can ask the terminal for its background with OSC 11 and its default text colour with OSC 10, and some pick a dark or light palette from the answer. While the pane background paints a colour, a pane's OSC 11 is answered with that colour and OSC 10 with the text colour tuios gives default text there, so the program sees what it is drawn on. A program that set its own colours with OSC 10 or 11 gets its own back. With a theme on and the pane background off, the answers are the theme's colours. With no theme and the pane background off, the answers are the host terminal's own: tuios asks the terminal it runs in for its background, its text colour and its sixteen ANSI colours when it starts or attaches, and tells panes those, so a program on a light terminal picks its light palette. OSC 4 queries for the sixteen are answered the same way. Each client asks its own terminal, so an SSH or browser client on a light terminal and a local one on a dark terminal each get the right answer for the session while they are the one typing. In a daemon session the daemon's emulator answers, and the client tells it what to answer when it syncs; with several clients attached, the last one to sync decides.
With no theme the chrome is drawn for the host's background too: on a light terminal the rail, the dock and the unfocused pane borders use the light chrome ramp a light theme gets, measured against the terminal's own colour.
Following light and dark. A local client turns on mode 2031, and a terminal that supports it (ghostty, kitty, contour and others) then tells tuios when the system appearance switches between light and dark. tuios asks for the colours again, tells panes, and redraws the chrome for the new background. The light and dark verdict has hysteresis: a dark verdict turns light only above an 8-bit luminance of 140, and a light one turns dark only below 110, so a mid grey background does not flip the chrome back and forth. SSH and browser clients ask once when they attach and do not follow the switch, because only the local client turns the mode off again on exit.
Terminals that do not answer. A terminal that answers no colour query,
such as mosh, leaves everything as it was: panes are told the emulator's
defaults (a black background and white text) and the chrome uses its dark ramp.
Set a theme, or pane_background, to give panes a real answer there.
All six hot-reload and are on the Backgrounds tab of the settings page
(Ctrl+B ,, then ]): an All surfaces row and one row per surface, each a
colour row that opens the same picker as the border colours, with off and
theme beside the grid and x to clear a surface back to following All
surfaces. tuios set-config appearance.dock_background <value> sets one from a
script. They reach every client of the session, SSH and browser ones included,
since they draw the same frame. A screenshot of one pane is drawn on the pane's
painted colour, and a screen or region capture carries every painted surface
as it is drawn.
The launcher starts a desktop entry in a new pane. A graphical program opens
its own window, so its pane stays empty. Set launcher.gui_command to start
such an entry with another command. The launcher adds the entry's argv to that
command. Entries with Terminal=true and programs from $PATH still open in
a pane.
[launcher]
gui_command = "tuios-wayland launch --" # or "niri msg action spawn --"Use only a command that takes an argv and runs it as an argv. Do not use a
command that joins its arguments into a shell line, such as swaymsg exec --
or hyprctl dispatch exec. With such a command, the text of a desktop entry
becomes shell code. Any installed package can write a desktop entry.
A program from the launcher closes its pane when it exits. Set
launcher.keep_open to keep the pane. A line under the output shows the exit
status, as with tuios new-window --keep-open. Close the pane when you are
done with it. The option needs a daemon session. With --standalone the pane
closes as before.
[launcher]
keep_open = trueA workspace's number is its position. Moving a workspace with
move_workspace_left or move_workspace_right, or dragging its tab in the
dock, renumbers it. A workspace exists while it holds a pane or is on screen.
When a workspace that held a pane stops existing, because its last pane
closed or moved away and it is not on screen, it loses its name.
The numbers close up only when you change something: close a pane, move a
pane to another workspace, close a workspace or move one. Then every
workspace above an empty place moves down one: closing workspace 2 of 1, 2
and 3 leaves 1 and 2. A pane whose program exits on its own (a command
window with close_on_exit, a tuios xpanes run, an agent finishing)
leaves the numbers alone, so a script or agent that holds a workspace number
still names the same workspace. Its place stays empty until your next close
or move closes it up. Typing exit in a pane's shell is a close. An empty workspace you only looked at goes without
moving anything.
Alt+N with a number past the last workspace that holds a pane opens the
next one: with three workspaces, Alt+5 opens workspace 4. When the
workspace on screen is empty, it is the new one, and Alt+N to an unused
number stays on it. A pane put directly on a higher number, with
tuios new-window --workspace 7 or send_to_workspace_7, keeps that number
until the numbers below it close up.
When the last pane on the workspace on screen closes, tuios shows the
workspace you came from. This happens when the pane's program stops, when you
close the pane, when tuios xpanes -ss closes it, and with
tuios close-workspace or the close_workspace action.
tuios goes back in this order:
- For a workspace that
tuios xpanesopened, the workspace where you rantuios xpanes. - The workspace you showed last that still has panes. After a chain of empty workspaces, tuios goes back past each one.
- The workspace with the lowest number that has panes.
When no workspace has panes, tuios shows the splash screen. The workspace on screen is the same for every client of the session, so every client goes back. Moving a pane to another workspace does not close it, so tuios does not go back.
To stay on the empty workspace, turn the setting off:
[workspaces]
return_when_empty = falseThe daemon reads the setting when it starts and when the file changes.
tuios set-config workspaces.return_when_empty false changes it while tuios
runs.
By default, a workspace with no panes shows the splash screen. To open a pane there when you switch to it, as the new-window key does, turn on this setting:
[workspaces]
new_window_when_empty = trueThe new pane starts in the folder of the pane that had focus on the workspace you came from. The new pane starts in the session's start folder in these cases:
- You came from a workspace with no pane.
- You came from a scratch terminal.
- The pane you came from runs on another machine.
The start folder is the folder that tuios new --cwd gave.
A pane opens only when you switch: with a workspace key, a click on the dock, the workspace switcher, the pane navigator or the command palette. These switches do not open a pane:
move_and_followand other moves that take a pane with them.tuios xpanes,tuios select-workspace,tuios run-commandand tape scripts. These bring their own panes or run from a script.- The switch back from an empty workspace that
return_when_emptymakes. - The start of tuios and an attach to a session. The workspace on screen stays as it is.
When two clients show the same session, the client that switches opens the pane. The other client shows it. A workspace gets one pane, also when two clients switch to it at the same time. When you switch away before the pane opens, you stay where you went, and a switch back does not open a second pane.
With appearance.new_window_follow_ssh = true, a pane that runs ssh gives a
new pane that runs the same ssh, as the new-window key does.
The daemon must be this version of tuios or newer. With an older daemon, a
switch opens no pane. Run tuios kill-server and start tuios again to load
the new daemon.
Each client reads the setting from its config file, and a change applies at
once. The settings page has it under Daemon, and
tuios set-config workspaces.new_window_when_empty true changes it while
tuios runs.
tuios keeps your recent yanks as paste buffers. Ctrl+B ] pastes the newest,
and Ctrl+B # shows them all (see KEYBINDINGS.md).
[paste_buffers]
limit = 20 # how many unnamed buffers to keep. 0 keeps none.
max_kb = 16384 # most KiB all buffers hold together, at most 262144. 0 uses 16384.The rules are the tmux rules. A yank makes a buffer that tuios names
buffer0, buffer1 and so on. limit counts only these buffers, and past it
the oldest of them goes. A buffer that you name with set-buffer -b stays
until you delete it, or until the buffers pass max_kb. Past max_kb the
oldest unnamed buffer goes first, then the oldest named one. tuios does not
keep a yank that is larger than max_kb as a buffer, but the yank still goes
to the clipboard. With limit = 0, a yank goes only to the
clipboard.
The daemon keeps the buffers in memory, so every client and every session shares them. They go when the daemon stops. The daemon reads the setting when it starts and when the file changes. A smaller limit drops the oldest buffers at once.
A buffer is yours or one pane's. Your yanks, and what you set from outside
every pane or from a pane that holds admin, are yours. What a pane without
admin sets is that pane's own. A buffer can hold a secret that you copied,
so a pane without admin sees and pastes only its own buffers: not yours,
and not another pane's, in its session or not. It cannot change or replace a
buffer that you set, and it cannot make a named buffer, so a name you keep a
command under stays yours. From inside a pane, reading its buffers needs the
read grant, changing them needs write, and pasting needs both. See
What a pane may do.
Large requests to the daemon, such as a big set-buffer, a stash put or an
image paste, take memory from a budget. You, outside every pane or in a pane
that holds admin, have a budget of your own. A pane without admin shares
another, so it cannot use up yours.
One thing a pane can still learn: the automatic buffer numbers (buffer0,
buffer1) come from one counter for everybody. A gap between two of a pane's
own buffer numbers tells it that other buffers were made in between, such as
your yanks. It tells the pane how often you copy, not what. tuios accepts this
as a low risk.
These options shape the master-stack layout. Each workspace starts with them. The layout keys change the workspace on screen, and the workspace keeps the change. See LAYOUT_MODES.md.
| Option | What it does | Values | Default |
|---|---|---|---|
appearance.master_position |
The side the master panes take. With center, the stack panes go to the right and the left in turn |
left, right, top, bottom, center |
left |
appearance.master_count |
How many panes are master panes | 1-9 | 1 |
appearance.master_ratio |
The share of the screen the master panes take, as a percent | 10-90 | 50 |
appearance.master_grid |
With one master on the left, show four or more panes as a grid | true, false |
true |
This example puts an editor in the middle with terminals on both sides. It keeps that layout at every pane count:
[startup]
layout = "master-stack"
[appearance]
master_position = "center"
master_ratio = 50With center, the grid is off, so master_grid has no effect. The first pane
of the workspace is the master. To make another pane the master, focus it and
press Ctrl+B L Enter.
Two [startup] options choose where a plain tuios opens. Both are empty by
default.
| Option | What it does | Default |
|---|---|---|
startup.session |
The session that tuios attaches to. tuios makes the session if it is gone, as tuios attach NAME -c does. Empty attaches to the most recent session |
empty |
startup.window |
The pane, by name or id, that tuios focuses in startup.session before it attaches. When no pane has that name, the focus does not change. It needs startup.session |
empty |
[startup]
session = "home"
window = "start"tuios attach, tuios new and every other command use the session you give
them, and ignore these options. startup.session uses the daemon even when
startup.daemon is false. --standalone and TUIOS_NO_DAEMON=1 still run a
local session. Both options are on the settings page, in the Startup section.
See SESSIONS.md.
These options shape the scrolling layout. See LAYOUT_MODES.md.
| Option | What it does | Values | Default |
|---|---|---|---|
appearance.scroll_column_width |
The width of a new column, as a percent of the screen | 20 to scroll_column_max |
55 |
appearance.scroll_column_max |
The largest width a column can have, as a percent of the screen | 20-100 | 90 |
appearance.scroll_column_presets |
The widths that scroll_cycle_width steps through, as percents of the screen |
a list of 1-100 | empty (33, 50, 55, 67, 90 and 100) |
scroll_cycle_width sets the focused column to the next width in the list
that is larger than the current width. After the largest width, it goes back
to the smallest. tuios sorts the list. It skips a width that is larger than
scroll_column_max. It ignores a value that is not from 1 to 100 and shows a
config warning. This example cycles a column through a half, two thirds and the
full screen:
[appearance]
scroll_column_width = 50
scroll_column_max = 100
scroll_column_presets = [50, 67, 100]A change to the list applies when tuios reloads the config. The settings page
does not show this option. Edit it in config.toml.
While you drag a tiled pane in BSP, tuios shows where the pane would land if you let go. See LAYOUT_MODES.md. The preview is a frame in the accent colour, drawn with the same border characters as the panes. With fill, the area inside the frame gets one tint, and the text under it fades into that tint. On a 16-colour terminal the frame is reverse video and the area inside stays as it is. Two options shape that preview:
| Option | What it does | Values | Default |
|---|---|---|---|
appearance.drop_preview |
What the preview draws. fill draws the frame and tints the area inside it, outline draws only the frame, and off draws nothing |
fill, outline, off |
outline |
appearance.drop_preview_delay |
How long the pointer stays on one drop zone before its preview shows. A zone you leave before the delay is never drawn, so carrying a pane across the screen does not flash a zone over every pane on the way | a duration such as 120ms, or 0 to show it at once |
120ms |
[appearance]
drop_preview = "fill"
drop_preview_delay = "250ms"Neither option changes the drop: with off, releasing on an edge still splits that pane, and releasing in the middle still swaps. Both are on the settings page, in the Appearance section, and tuios set-config appearance.drop_preview off applies at once. A value that is not one of the above reads as the default, with a warning when the config loads, and tuios set-config refuses it.
appearance.dock_pill_caps puts rounded caps on every pill in the dock: the
mode chip, the workspace tabs and the minimized windows. It is true by
default. Set it to false for flat pills:
[appearance]
dock_pill_caps = falseUnder ASCII glyphs the workspace tabs have no caps. The option also controls
the ends that [dock.tabs] caps puts on flat tabs. The rail's pills do not
change with this option.
The mode pill at the start of the dock shows an icon for the mode. Three options set the icons:
| Option | Mode |
|---|---|
appearance.dock_mode_icon_window |
Window mode |
appearance.dock_mode_icon_terminal |
Terminal mode |
appearance.dock_mode_icon_tiling |
Window mode and terminal mode while tiling is on |
[appearance]
dock_mode_icon_window = "WM"
dock_mode_icon_terminal = ">_"
dock_mode_icon_tiling = ""- When an option is not set, the pill shows the icon of the glyph set. With
use_ascii_only, that icon isW,Tor#. - An empty string shows no icon. If the mode has no other text, the dock shows no pill in that mode.
- An icon can be at most 8 cells wide. A wide character, such as
終, counts as two cells. The dock does not accept control characters. For an icon that it does not accept, the dock shows the default icon and the config check shows a warning. - The pill adds one space on each side of the icon. You do not need to add spaces.
- While tiling is on, the pill also shows the next split direction,
VorH. Setappearance.dock_mode_split_letter = falseto show the tiling icon alone. With the default icons, the pill then has the same width as the pill of the leader key, so the workspace tabs do not move when you press the leader key. appearance.dock_mode_pillsets what the pill shows while tiling is on.tiling(the default) shows the tiling icon in terminal mode and in window mode.inputshows the terminal icon or the window icon, and the split letter after it shows that tiling is on.
[appearance]
dock_mode_pill = "input"To remove the whole mode pill, remove "mode" from the left list in the
[dock] table. See The dock's components.
You can also set the icons on the settings page, in the Dock section, or with
tuios set-config dock_mode_icon_window WM. The word default puts an icon
back to the icon of the glyph set, in the settings, with set-config and in
config.toml. The change applies at once.
When you press the leader key, the mode pill shows a keyboard icon on the
warning colour until you press the next key. With use_ascii_only, the icon is
P. appearance.dock_prefix_indicator sets where the armed leader shows:
| Value | What it does |
|---|---|
pill |
The mode pill shows the armed leader. This is the default |
badge |
A PREFIX badge shows in the corner across from the dock. The mode pill does not change |
[appearance]
dock_prefix_indicator = "badge"The badge shows for each value when the dock is hidden or has no mode pill. The option is on the settings page, in the Dock section.
appearance.dock_workspace_label_max caps a workspace tab's label in cells,
so one long name cannot push the other pills off the bar. The cap is 12 by
default and counts the tab format's own characters, so "<{name}>" loses two
of them to the brackets. Set it to 0 to draw the whole name and let the
strip scroll instead:
[appearance]
dock_workspace_label_max = 0A dock too narrow for even one pill keeps the current workspace's tab, cut to whatever room is left, so the workspaces always have something to click. You can also set it on the settings page, in the Appearance section. The change applies at once.
appearance.dock_workspace_tooltip shows the workspace name above a pill
when you hover it. The name shows when the pill cuts the name short. It also
shows when appearance.dock_workspace_tab_format has no {name}, for
example with number-only tabs. A workspace with no name shows its number,
but only when the tab format has no {index}. The option is on by default.
[appearance]
dock_workspace_tab_format = "{index}"
dock_workspace_tooltip = trueThe dock has two rows by default: a rule and the row of pills.
appearance.dock_compact removes the rule, so the dock has one row. The panes
get that row. The default is false.
[appearance]
dock_compact = trueThe option works with the dock at the top and at the bottom. It has no effect
when dockbar_position is hidden. You can also set it on the settings page,
in the Dock section, or with tuios set-config dock_compact true. The change
applies at once.
The rule shows how long a dock message stays. A compact dock does not show this.
When clients with different docks share a session, the panes keep the largest dock height. A client with a compact dock shows a blank row in that case.
A pane's title sits on its border. Two options set its shape and its place:
| Option | Values | Default |
|---|---|---|
appearance.window_title_style |
pill: the name on a fill in the border's colour, with rounded caps. tab: the fill with no caps. plain: the name in the border's colour, with no fill |
pill |
appearance.window_title_align |
auto: on the top row, the end away from the window buttons; on the bottom row, the middle. left, center, right: that place in the room the buttons leave |
auto |
[appearance]
window_title_style = "plain"
window_title_align = "center"appearance.window_title_position sets the row: top, bottom or
hidden. The caps of pill come from the glyph set. Both options are on the
settings page, in the Appearance section, and tuios set-config applies them
at once.
appearance.host_title sets the title of the terminal tuios runs in, such as
Ghostty's title bar, from the focused pane. It follows the focus, the
workspace and the session, and the focused pane's own title changes. It is
tmux's set-titles. Empty, the default, sets no title. Emptied while a client
is attached, it clears the title it set, as a client does when it detaches.
| Placeholder | What it holds |
|---|---|
{title} |
the focused pane's name, as its title on the border shows it |
{index} |
the pane's place in its workspace, from 1 |
{cwd} |
the pane's working directory |
{session} |
the session's name as the rail shows it, its display name if it has one, or tuios without a daemon |
{workspace} |
the workspace's name, or its number. With a scratch group open, the workspace it is shown over |
[appearance]
host_title = "{title} · {session}"With no focused pane on the workspace in view, the title is the session's
name. So it is when the template uses {title} and the pane has no name, as a
fresh shell has none, and when the template comes out empty. A title a tool
set with herdr's terminal title set wins while it is set. Each client sets
the title of its own terminal. The option is on the settings page, in the
Appearance section, and tuios set-config applies it at once.
The focused pane's border shows the mode. By default it is the theme's cyan in window mode and its green in terminal mode. Three keys set it.
| Option | Values | Default |
|---|---|---|
appearance.border_focused_color |
a colour. It sets the border in both modes | empty |
appearance.border_focused_color_window |
a colour. It sets the border in window mode | empty |
appearance.border_focused_color_terminal |
a colour. It sets the border in terminal mode | empty |
[appearance]
border_focused_color_window = "accent"
border_focused_color_terminal = "#ff9e64"- A key for one mode wins over
border_focused_colorin that mode. - An empty key uses the next key. After the three keys, the theme sets the colour.
border_focused_coloralso replaces the colours for copy mode and for an agent that waits. The keys for one mode do not.- On many themes the cyan and the green have the same lightness. The default terminal mode colour then moves darker, or lighter when darker is too faint. You can then see the mode without the hue.
- The default unfocused border is darker than both focused borders on a dark background, and lighter on a light one. You can see focus without the hue.
- Each default focused border has at least twice the contrast of the unfocused border against the background.
- With no theme, the default unfocused border is a grey near your terminal background. The focused borders use a saturated cyan and green.
- With
appearance.session_borderon, the focused border in window mode uses the session colour. In terminal mode it uses the terminal mode colour. Terminal mode stands further from the background and is at least as saturated, so it is the more visible of the two. The session colour moves toward the background to make the step. - At 16 colours the default borders use bright cyan and bright green, the slots of your terminal's palette.
- The settings page has a row for each key, in the Appearance section.
appearance.border_focused_gradient draws the focused pane's border in two
or more colours that blend from one to the next.
appearance.border_gradient_angle sets the direction in degrees.
| Option | Values | Default |
|---|---|---|
appearance.border_focused_gradient |
two or more colours, separated by spaces or commas. Empty draws one colour | empty |
appearance.border_gradient_angle |
0 to 359. 0 runs left to right, 90 top to bottom, 45 from the top left corner to the bottom right one |
45 |
[appearance]
border_focused_gradient = "#33ccff #00ff99"
border_gradient_angle = 45- The gradient is static. It changes only when the pane moves, resizes or takes focus.
- The border steps through 8 colours, so the cells of one step share one colour code. This keeps the bytes sent to the terminal small.
- The window buttons and the title keep their own colours.
- A focused border in a colour for its state (copy mode or an agent that waits, see THEMES.md) is drawn in that colour, not the gradient.
- At 16 colours the border uses one colour, as before.
- A colour value can name a slot or a role, such as
"accent accent_bright", so the gradient follows the theme.
The small dialogs, such as rename, confirm and new space, draw their frame in the pane border style, appearance.border_style. The larger panels, such as the command palette, the switchers, the Agents view and the settings page, have no frame unless appearance.frame_panels is on. Then they take the same frame.
The scrollback browser and its help modal draw their frame in the border style too.
With appearance.pane_overlays on, a dialog or a panel looks like a pane window:
- Every panel is framed, as if
frame_panelswere on. - The frame takes the focused pane's border colour, because an open dialog or panel is the focused thing. That is
appearance.border_focused_colorwhen it is set. Otherwise it is the theme's colour for the current mode (copy mode or a waiting agent) or the session's tint. - The frame and everything in it sit on the background of an empty pane cell. That is
appearance.pane_backgroundwhen it is set, else the terminal's own. The title and hints in the frame line share that background, as they do in a pane. - The selected row, the row under the pointer and input fields are steps of that background. On a navy pane, a selected row is a darker navy.
- Pills keep their own colours.
The scrollback browser and the copy-mode search bar follow the option too. The browser's frame takes the focused pane's border colour, and its selected row and key pills are steps of the pane's background. The search bar sits on the pane's background.
The option is off by default. Then a dialog or a panel keeps its own shaded background, and its frame keeps its quiet ink. Some cases keep the shade even with the option on:
- Under
hidden, no frame is drawn, so the shade is a dialog's only edge. - At 16 colours every background is the terminal's own, so the option changes only the frame's colour.
- With no theme, and a terminal that never reports its background, there is no colour to fill the overlay with or to step its rows from.
appearance.overlay_border_color sets a colour for every dialog and panel frame, and for the scrollback browser's, with or without pane_overlays.
| Option | Values | Default |
|---|---|---|
appearance.border_style |
rounded, normal, thick, double, hidden, block, ascii, outer-half-block, inner-half-block, glyphs |
rounded |
appearance.frame_panels |
true frames every panel in the border style, false leaves the panels without a frame |
false |
appearance.pane_overlays |
true draws dialogs and panels like pane windows, false keeps their own shaded background |
false |
appearance.overlay_border_color |
A colour for every dialog and panel frame: #89b4fa, a slot name such as bright_cyan, or a role such as accent. Empty keeps the frame's own colour, or the focused pane's border under pane_overlays |
empty |
[appearance]
border_style = "double"
frame_panels = true
pane_overlays = true
overlay_border_color = "accent"How each border style draws a dialog or a framed panel:
rounded,normal,thick,doubleandasciidraw that style's corners and lines.hiddendraws no frame. The dialog keeps its size, and its title and key hints stay where the frame would carry them. A frame that is the only edge of what it holds is drawn as inroundedinstead: the frame of a panel at 16 colours, the frames round the regions inside a panel, such as an explorer's list and preview, and the frames of the scrollback browser and its help modal, which have no background of their own.block,outer-half-blockandinner-half-blockdraw theroundedframe. A solid frame is too heavy round a dialog of three rows.glyphsdraws the border of the active glyph set. A part of the border the set does not name is drawn as inrounded.- With
--ascii-only, every frame is drawn with+,-and|. - At 16 colours a panel always has a frame, because its background is the terminal's own. The frame takes the border style, except
hiddenas above.
Both options are on the settings page, in the Appearance section. A change applies at once.
z zooms the focused pane. By default the zoomed pane keeps its border, and
appearance.zoom_size sets how much of the screen it takes.
appearance.zoom_borderless shows the zoomed pane on the whole pane region
with no border and no title bar. The program in the pane gets the full size.
The dock and the rail stay on the screen. The default is false.
[appearance]
zoom_borderless = trueWith this option on, zoom_size and zoom_max_width have no effect. The
option also applies to a floating pane. When you zoom out, the pane gets its
border back. You can also set it on the settings page, in the Advanced section,
or with tuios set-config appearance.zoom_borderless true. The change applies
at once.
The actions split_ssh_horizontal, split_ssh_vertical and new_window_ssh
open a pane on the machine that the focused pane is connected to with ssh. See
KEYBINDINGS.md.
appearance.new_window_follow_ssh makes the ordinary actions do the same:
split_horizontal, split_vertical and new_window. When the focused pane
does not run ssh, these actions open an ordinary pane. The default is false.
[appearance]
new_window_follow_ssh = truetuios finds ssh in the foreground of the pane. ssh can run from a shell, or
from sshpass, autossh or mosh. ssh can also run from another program,
such as your own helper script (tt ssh box). tuios then follows only an
interactive login: an ssh line with no remote command, and with none of -s,
-N, -T or -W. tuios reads only the processes that you own in the pane. It runs the ssh or mosh that it finds on its own PATH,
with no shell on this machine. It never runs the program that the pane runs.
The new ssh gets the same environment as an ordinary new pane. If the ssh
that tuios follows has an SSH_AUTH_SOCK that names your own agent socket,
the new ssh gets that value too. An agent that your shell started in the pane
then works in the split. The socket must not be a link, and its folder must
be yours and closed to other users.
The new ssh line keeps only what it needs to reach the same host as the same
user. It always gets -o ControlMaster=no. Your ~/.ssh/config applies to it
as usual, so a setting that you put there still works in the split.
tuios follows an ssh line only when each -o option is on one of two lists.
The new pane keeps these options:
AddressFamily, BatchMode, CertificateFile, CheckHostIP, Ciphers,
Compression, ConnectionAttempts, ConnectTimeout, EscapeChar,
HostKeyAlgorithms, HostName, IdentitiesOnly, IdentityFile,
KbdInteractiveAuthentication, KexAlgorithms, LogLevel, MACs,
PasswordAuthentication, Port, PreferredAuthentications, ProxyJump,
PubkeyAcceptedAlgorithms, PubkeyAuthentication, ServerAliveCountMax,
ServerAliveInterval, StrictHostKeyChecking, TCPKeepAlive and User.
The new pane does not get these options, and tuios still follows the line:
- Forwards and sessions:
LocalForward,RemoteForward,DynamicForward,GatewayPorts,ExitOnForwardFailure,ClearAllForwardings,Tunnel,TunnelDevice,RemoteCommand,SessionType,StdinNull,ForkAfterAuthenticationandRequestTTY. - Files, sockets, environment and forwarding:
ControlMaster,ControlPersist,ControlPath,UserKnownHostsFile,GlobalKnownHostsFile,HostKeyAlias,IdentityAgent,SendEnv,SetEnv,ForwardAgent,ForwardX11andForwardX11Trusted. - The flags
-A,-X,-Y,-S,-L,-R,-D,-W,-w,-N,-f,-n,-T,-t,-Mand-s.
These ssh lines get an ordinary pane:
- An
-ooption that is not on the two lists above.ProxyCommand,LocalCommand,XAuthLocationandMatchare examples. - An option or a value with a control character, such as a line break.
-E,-F,-I,-G,-V,-Qand-O, and an option that tuios does not know.- A
-JorProxyJumphop that is not[user@]host[:port]or thessh://form, made of letters, digits and._:@[]-, or a hop that starts with-. - A mosh destination that starts with
-. - mosh with
--ssh,--clientor--server. - ssh that a file transfer starts:
scp,rsync,git,sftporsshfs. - ssh that another program starts, when the line has a remote command, or
-s,-N,-Tor-W.
When a split asks to follow ssh and gets an ordinary pane, the dock says why.
Put a ProxyCommand or a ProxyJump in ~/.ssh/config to use it with a
split. A split with -J on the command line is followed.
The new pane starts in the remote folder when the remote shell reports its
folder with OSC 7. The host name in the report must be the same as the host
in the ssh line, or as the HostName that ssh -G gives for that line.
reachy-mini matches pollen@reachy-mini. When ~/.ssh/config makes prod
an alias for box.example.com, a report from box.example.com or from box
matches ssh prod. A report from box.other.com does not. When there is no
match, the new pane starts in the remote home folder.
fish sends the report by default. For bash, add this line to ~/.bashrc on
the remote machine:
PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$HOSTNAME" "$PWD"'${PROMPT_COMMAND:+;$PROMPT_COMMAND}For zsh, add these lines to ~/.zshrc on the remote machine:
autoload -Uz add-zsh-hook
_tuios_osc7() { printf '\e]7;file://%s%s\e\\' "$HOST" "$PWD" }
add-zsh-hook precmd _tuios_osc7To go to the folder, tuios adds -t, -o RemoteCommand=none and the
command exec sh -c 'cd "FOLDER" ...' to ssh. This works with every login
shell that can start sh. If the folder is gone, the new pane starts in the
home folder. A folder name with a quote, $, a backslash or ! is not used.
The [dock] table's region lists and custom components are not scalar
options, so list-options does not carry them; of the table it carries only
dock.clock.format. The settings page edits them
through an editor of its own rather than a row: Dock → Components, where the
three regions and what is in them are one list. Shifted arrows move a component
and carry it into the next region off the end of its own, Enter takes one off
the bar or puts it back, u undoes the session's edits and r restores the
defaults. It is three ordered lists of component names, plus a table per custom
component:
[dock]
left = ["mode", "workspaces", "trail", "tape"]
center = ["windows"]
right = ["transfers", "notifications", "copy-help", "cpu", "ram", "clock", "session-controls"]
[dock.clock]
format = "15:04"
[dock.custom.branch]
command = "~/.config/tuios/dock/git-branch.sh"
refresh = "event:after-focus-change"
on-click = "tuios new-window log -- git log --oneline -20"The lists above are the default: omit the whole table and the bar is unchanged.
The clock draws hours and minutes (15:04) unless dock.clock.format or
appearance.clock_format sets another Go time layout. To show seconds, set
format = "15:04:05". A clock with seconds draws a frame every second. A
clock without seconds draws one frame a minute.
When the center list has a clock or a custom cell, the center is at the
middle of the terminal. A notification does not move it or shorten its
minimized pane labels. On a crowded bar, the center moves left to keep the
right-hand cells on the screen. Click a cut notification to read the full
message.
transfers shows the copies between machines. It shows nothing while no copy
exists. One copy is a bar and a percent, [####--] 61%. More copies are a
count and a percent, 3 copies 42%. waiting means a link is down, and
1 failed stays until you open the Copies panel. A click opens the panel. Add
transfers to your own right list to see it there.
session shows the session name, workspace and pane, main 2:1, and is in no
default list. [dock.session] takes format ({session}, {workspace},
{pane}) and the cell style keys fg, bg, bold, cap_left and
cap_right, which [dock.custom.NAME] takes too. Colours take any colour value.
A custom component's first line of stdout becomes its cell, it is hidden when
the command fails, and tuios list-dock-components says which and why.
tuios runs one copy of the command at a time. When an event arrives while the
command runs, tuios runs the command one more time when it ends. An interval
that comes due while the command runs is skipped.
By default the workspace strip draws rounded pills. [dock.tabs] can draw
flat tabs instead, the way a tmux status line draws its window list:
[appearance]
dock_workspace_tab_format = "{name}{zoom} {icon}"
[dock.tabs]
style = "flat" # pill (default) | flat
separator = " • " # drawn between flat tabs
separator_fg = "#4c3743"
active_fg = "#272e33"
active_bg = "#a7c080"
active_bold = true
inactive_fg = "#4f5b58"
number_glyphs = ["I", "II", "III"] # {icon}: the first entry is workspace 1; past the end prints the number
zoom_marker = " [ ]" # {zoom} on the current workspace while a pane is zoomed
show_add = false # keep the trailing "+" tab in the flat style
caps = false # rounded ends on each flat tab
active_underline = false # underline the current tabColours take any colour value. The colours are overrides: a flat strip with none set puts the current tab on the theme's accent, in bold, and draws the others in dim text on the bar's own ground, so it still shows where you are. A value that is not a colour is ignored, with a warning, as if it were unset.
The four tab colours (active_fg, active_bg, inactive_fg, inactive_bg)
and inactive_bold also apply to pills. A pill with no colour set keeps the
theme's look. The current pill is always bold, so active_bold is for flat
tabs only.
active_underline underlines the tab of the current workspace. When it is not
set, a pill is underlined and a flat tab is not.
caps = true gives each flat tab the rounded ends of a pill. Each end has the
colour of its tab's background. A tab with no background gets a blank column
in place of each end, so the tabs keep their width. The ends use the pills'
glyphs: there are none under ASCII glyphs or with
appearance.dock_pill_caps = false. Pills always have ends, so caps with
the pill style gives a warning.
Flat tabs with your own colours and rounded ends:
[dock.tabs]
style = "flat"
caps = true
active_bg = "magenta"
active_fg = "bg"
active_bold = true
inactive_bg = "bright_black"
inactive_fg = "fg"Pills with your own colours and no underline:
[dock.tabs]
active_bg = "magenta"
active_fg = "bg"
inactive_bg = "bright_black"
active_underline = falseBesides {index} and {name}, the tab format takes {icon} and {zoom}.
{index} is the workspace's number, which is its position: dragging a tab to
another place, or moving the workspace with move_workspace_left and
move_workspace_right, renumbers it, and Alt+N goes to it by its new number.
The flat style shows even a lone workspace and drops the + tab unless
show_add is on. With no [dock.tabs] table the strip is unchanged.
Three more keys mark a workspace you are not looking at, in the pill style and the flat style:
[dock.tabs]
attention_fg = "warning" # the tab's ink while a pane on it waits for you
bell_marker = "!" # after the label when a pane on it rang the bell
activity_marker = "#" # after the label when a pane on it printedattention_fgis the ink of the tab while an agent on that workspace asks for input or stopped on an error. It takes any colour value.bell_markerandactivity_markergo after the label, after the label cap. The bell marker comes first.- A mark stays until you look at the workspace.
- The current workspace's tab never carries them.
- With a key empty, tuios draws nothing for it and keeps no record.
- Attached to a daemon, the marks need a daemon of this version or newer. An older daemon does not report a pane on another workspace, so its tab gets no bell or activity mark.
examples/dock/README.md is the full contract and five working recipes.
The existing [appearance.sidebar] settings still configure the original rail.
A table for its current edge overrides only the fields it names; the opposite
edge stays hidden unless explicitly enabled. Each edge can have its own width,
section order and custom command:
[appearance.sidebar]
position = "right"
sections = "sessions,terminals,custom"
[appearance.sidebar.custom]
command = "right-summary"
[appearance.sidebar.left]
enabled = true
width = 28
sections = "sessions,files,custom"
[appearance.sidebar.left.custom]
command = "left-summary"[appearance.sidebar.right] has the same enabled, width, sections and
custom keys. Both edge tables also take the five subagent_* keys. On the original edge, omitted keys retain the legacy settings;
on the other edge, an omitted sections uses the default section order.
Writing an edge table without enabled = true does not enable that edge.
With position = "hidden", explicitly enabled edge tables still draw: if
both edges are enabled, the left one supplies the primary rail settings and
the right one supplies the opposite rail settings. Unset fields on those
explicit edges use the edge defaults; they do not inherit a hidden legacy
width, section order or custom command. The file's hidden legacy position is
not rewritten.
Both rails can show the same built-in sections: they use the same sessions,
focused pane's directory listing and repository reading, but have independent
section scrolling, keyboard focus and pointer hit areas. A custom command on
each edge runs independently: rail/custom belongs to the original rail and
rail/secondary/custom to the other. These edge tables are file-only, not
set-option targets, because custom commands execute outside panes.
On narrow screens, the optional edge contracts before the original rail; both
may hide to leave room for the panes. Resizing either rail by dragging its
inner border changes only that edge for this client. In sidebar keyboard focus,
e moves focus to the other visible edge (and back); Ctrl+B H independently
hides or shows the opposite edge, while Ctrl+B b keeps its existing legacy
rail toggle. The opposite-edge toggle does not opt in an edge whose
enabled = true was never configured.
To use a rail as a terminal, assign a local daemon session to that edge.
Its panes on the session's current workspace stack top to bottom in the
rail, independent of the central session's layout and attachment.
For example, create two sessions, then edit config.toml:
tuios new side-left --detach
tuios new side-right --detach[appearance.sidebar]
enabled = true
position = "right"
width = 36
[appearance.sidebar.right]
session = "side-right"
[appearance.sidebar.left]
enabled = true
width = 36
session = "side-left"Settings → Sidebar exposes separate Left session, Right session,
width and workspace controls. Entering a session name enables that edge.
An empty session name restores that rail's ordinary sections (including
sections and custom). While a daemon session occupies the rail, its
ordinary custom-section command does not run. session_workspace = 0 or an omitted value shows
the rail on every central workspace; set session_workspace = 2 on one
edge to show it only on workspace 2 (or use 1–9). This controls
visibility, not the assigned session's own workspace. Each
edge is independent, so one can follow while the other is pinned.
Click a pane to type in it. With the rail focused, Ctrl+B then - splits
downwards: it creates a normal pane in the assigned daemon session and
stacks it below the others. Ctrl+B then c also creates a pane there;
Ctrl+B then | stacks it vertically rather than subdividing a narrow rail
sideways. Ctrl+B then n or p changes the active pane; clicking any
pane's title bar focuses that pane. Each pane uses its normal title and title
position, not the assigned session ID. Its close and minimize buttons target
that daemon pane; zoom temporarily expands the pane within the rail. The
session assignment remains in Settings. There is no extra row above the rail
pane's normal border. The rail crops the daemon's existing grid; it does not
resize a PTY, suppress leading guest whitespace, or shift cursor and mouse
coordinates. A narrow rail does not change the size seen by another viewer.
All visible panes remain on screen. If
the rail is too short to give every pane at least six rows, it shows a
contiguous group containing
the active pane; cycling focus brings the others into view.
Switching central workspaces, hiding the rail or detaching this TUI client does not end the assigned daemon session or its processes. Reattaching restores the pane's output. A daemon restart is different: it ends running pane programs and restores saved layouts with new shells. Right-clicking the rail currently opens Sidebar Settings, not a pane picker. The assignment can be entered before its daemon session exists: the rail shows an attach error and retries when the session is created. Standalone mode cannot attach sessions.
appearance.sidebar.row_style sets how the rail marks the session you are on
and the pane that has focus:
| Value | What it draws |
|---|---|
bar |
A bar in the gutter, in the session's colour. This is the default |
marker |
A › in the gutter, in the session's colour. > with ASCII glyphs |
underline |
The name underlined, in the session's colour |
pill |
The name on a fill of the session's colour, with the pill caps of the glyph set |
[appearance.sidebar]
row_style = "pill"A row that waits for a person keeps its attention mark in the gutter. The
marker glyph is the marker role of the glyph set (see GLYPHS.md).
The option is on the settings page, in the Sidebar section.
The focused pane's row in the terminals section carries the mark. Sometimes the rail does not show that row. The agents row of the pane then carries the mark in its gutter. This occurs in these cases:
sectionsdoes not includeterminals.- The rail is narrow and lists an agent pane only in the agents section.
- The terminals section has no room for the row.
- The terminals section shows the panes of another session.
The agents row uses the bar or the marker. With underline or pill, it
uses the bar.
The path picker types a path at the cursor of a pane. It lists the same folder
as the files section: the folder that the shell in the pane is in. Press
Alt+; in terminal mode, or Ctrl+B .. Type to filter, Tab to open a
folder, and Enter to type the path. See
KEYBINDINGS.md.
tuios worktree new, New worktree... on the rail and tuios fan make a
git worktree for each new session. By default it goes in
$XDG_DATA_HOME/tuios/worktrees/<repo>/<branch>, outside the repository.
[worktree] path puts it somewhere else, given as a path template:
[worktree]
path = "{{ repo_path }}/.worktrees/{{ branch | slug }}"| Variable | What it holds |
|---|---|
repo_path |
The main checkout's directory |
repo |
Its base name |
branch |
The branch, as given |
| slug turns a value into one folder name: feat/retry becomes
feat-retry. Use it on branch. Without it, {{ branch }} keeps the slash
and makes a folder per part, so the worktree of feat/x lands inside the
worktree of feat, and tuios worktree rm --force on feat deletes both.
A relative path starts at repo_path, so .worktrees/{{ branch | slug }} is
the same as the example above. A leading ~/ is your home folder. The
template has to use branch, or every branch would get the same folder.
The variables are the ones worktrunk's worktree-path uses, and | sanitize
is another name for | slug, so a template copied from a worktrunk config
works as it is. worktrunk's sanitize turns only / and \ into -, while
slug turns every run of characters other than letters, digits, . and _
into one -. A branch with other characters, such as fix--retry or
feat+x, can get a different folder name from each tool.
When the worktree is inside the repository and git does not ignore it, the
main checkout lists it as untracked, and git add -A there would take it in.
tuios then warns after it makes the worktree and names the folder that holds
the worktrees, such as /.claude/worktrees/ for
.claude/worktrees/{{ branch | slug }}, to add to .gitignore or
.git/info/exclude. It never edits those files itself.
The daemon makes every worktree, so it reads the setting, when it starts and
when the file changes. The path is then the same whichever client asked.
Empty is the default. While it is empty, TUIOS_WORKTREE_DIR in the daemon's
environment still moves the default folder (see
CLI_REFERENCE.md).
The review overlay (ctrl+b v) opens with the keys on the diff. tab moves
them to the file list. [review] focus picks the side a review opens on:
[review]
# Which side has the keys when a review opens: "diff" or "files" (default: "diff")
focus = "files"It applies to every review that opens, and to an attempt opened from the
compare view. A review with no changes opens on the diff, which says so. r,
u and b read the open review again and keep the side that has the keys.
The setting is on the settings page, in the Behavior section, and
tuios set-config review.focus files applies from the next review. A value
other than diff or files reads as diff, with a warning when the config
loads, and tuios set-config refuses it. See
KEYBINDINGS.md.
A review shows the changes since the base when it opens. In a plain
repository, the base is the merge base with the upstream branch. Commits you
did not push yet are then part of the review. [review] scope picks what a
review shows when it opens:
[review]
# What a review shows when it opens: "base" or "uncommitted" (default: "base")
scope = "uncommitted"u still switches between the two, and b still sets another base. A diff of
two attempts in the compare view does not use the setting. The setting is on
the settings page, in the Behavior section, and
tuios set-config review.scope uncommitted applies from the next review. A
value other than base or uncommitted reads as base, with a warning when
the config loads, and tuios set-config refuses it.
The sessions section gathers the sessions of a repository into one group. The
main checkout's row is the repository's row, and the worktree rows hang off it.
The main checkout is the session whose directory is the repository itself,
usually on main or master.
tuios main 2 ▾
├─ feat/compact-rail 2
└─ fix/pill-bars 1
- An unnamed session on that row shows the repository's name. A session with a name you chose keeps that name.
- A repository with no main checkout session has a row of its own.
- A repository with only a main checkout session shows a plain row.
- Only worktree rows hang off the repository's row. With several sessions in the
main checkout, the first in the rail's order is the repository's row, and the
others are plain rows in their own places. Folding the group does not hide
them, and the group's
+Ndoes not count them. Several sessions in the main checkout and no worktree session show plain rows and no group.
Each group's row ends in a fold mark. These controls fold and open the group:
- Click the fold mark.
- Move the rail cursor to the fold mark and press
enter. - Right-click the row and select
Fold grouporUnfold group. - Click a repository's own row.
A click on a main checkout's row opens its session. A folded row shows +N for
the sessions that it holds, the loudest agent state of the group, and the mark
of the attached session when that session is in the group.
Drag a group's row to move the whole group. Drag a worktree row to move it inside its group. The reorder keys follow the same rule.
appearance.sidebar.group_main gives each repository a row of its own, with the
main checkout under that row. The option is false by default. An explicit
false gives the same layout as no option. The option is on the settings page,
in the Sidebar section, as Separate repository row.
[appearance.sidebar]
group_main = trueIndependent of the option, the daemon records the main checkout the way it
records a worktree, so tuios worktree ls and the worktree verbs see it with the
worktrees of the repository.
Leave files out of appearance.sidebar.sections to keep the section shut when
you attach:
[appearance.sidebar]
sections = "sessions:25,terminals,agents:34"Click files in the rail's footer to open the section. Click it again to close
it. File search and a click on a folder link also open it.
The section goes after terminals with a share of 25, as in the default layout.
A layout with no terminals has it after sessions, and a layout with neither
has it above agents. The section stays open for this client until you close it
or the client exits. The value of appearance.sidebar.sections does not change.
appearance.sidebar.files_style sets how the rail's files section shows a
folder:
| Value | What it shows |
|---|---|
list |
One folder. .. goes up, and a folder row goes into the folder. This is the default |
tree |
One tree from the folder. A folder opens under its own row, with indent guides and an open or closed mark |
[appearance.sidebar]
files_style = "tree"In the tree, the rail has these keys on a name:
| Key | What it does |
|---|---|
l, right |
Open the folder. On an open folder, go to its first name |
h, left |
Close the open folder. On another name, go to its folder |
enter, a click |
Open or close the folder. On a file, do what the list does |
The file action keys (a, r, d, y, x, p, Y, S) act on the name
under the cursor. A new file or a paste goes into the folder of that name. On
an open folder, it goes into that folder. With folder_click = "cd", a folder
row types a cd into the pane and does not open. With both, a folder row
only opens, because a cd moves the root of the tree to that folder.
In both styles, g and G on a file go to the first or last name of the
list. Press the key again to go to the first or last row of the rail.
tuios reads a folder only when you open it. It remembers the open folders of each session until tuios stops. You can open 64 folders at most. Each open folder shows its first 2000 names. tuios watches each open folder on this computer and shows a change without a key. A folder that is open under a folder on another computer shows a change when you open it again. When tuios sees that an open folder is gone, it closes the folder. A new folder with the same name starts closed.
When the rail has the keyboard, the row under the cursor has a lighter
background, and its text is bold and underlined in the accent colour.
appearance.sidebar.cursor_background sets the background of that row.
[appearance.sidebar]
cursor_background = "#3b4261" # or a slot name, such as "blue", or a role, such as "accent"The value is a colour. Empty, the default, keeps the theme's background for the cursor row. The colour does not change the text colour, so choose a colour that your text is readable on. The option sets only the row under the cursor while the rail has the keyboard. The rows under the mouse pointer and the cursor of an unfocused rail keep their own background. At 16 colours, the option has no effect: the cursor row is in reverse video. The option is on the settings page, in the Sidebar section.
appearance.sidebar.header_style sets how a section header is drawn:
| Value | What it draws |
|---|---|
auto |
The label, with a rule after it when header_case is uppercase. This is the default |
plain |
The label with no rule |
rule |
The label and a rule to the rail's edge |
pill |
The label on a quiet fill, with the pill caps of the glyph set |
icon |
The section's icon, then the label |
[appearance.sidebar]
header_style = "icon"The icons are glyph roles: section_sessions, section_terminals,
section_agents, section_files and section_git (see
GLYPHS.md). With Nerd Fonts on, the default icons are Nerd Font
icons. The unicode glyph set has its own. With no icon, the header is plain.
header_case still sets the case of the label. On a narrow rail, a pill or
an icon can leave no room for a header's +.
appearance.sidebar.density sets the space between the rail's sections.
normal (the default) stacks them as the rail always has. roomy puts a
blank row in front of each section header after the first. The block pinned
to the bottom keeps its own line, which holds the divider.
[appearance.sidebar]
density = "roomy"The panes do not move. Only the rail's rows change.
When the rail has the keyboard, the cursor row has a lighter background. The
text of the row is also bold and underlined in the accent colour. This mark
does not need the background, so you can see the cursor on a see-through
terminal or with background = "off". The gutter keeps its own marks. At 16
colours the cursor row is in reverse video.
A section with more rows than the lines it is given shows as many as fit. It ends in … +N for the rest. The wheel over a section scrolls it. The keyboard cursor scrolls it to keep itself in view.
While rows are out of view, the section draws a scroll thumb in the rail's edge rule, beside its rows and not its heading. The thumb takes the rule's own cell, so the rows keep their full width. Its size is the share of the rows on screen. It reaches the first line only at the top of the section, and the last line only at the bottom, where there is no +N row. A section that fits draws the plain rule.
A hover peek of another session's panes shows the peeked list from its top, ending in +N when the list is longer than the section. When the terminals section is above the sessions section, the peek keeps the section's resting height, so the session row under the pointer does not move. When it is below, as in the default order, the section is sized for the peeked list like any other. The peek gets a thumb on the same terms, at the top of the column, and leaves the attached session's scroll where it was. There is no thumb on the collapsed strip or on a section of one line.
The glyph is the active glyph set's scrollbar_thumb role. It is ┃ by default, # under --ascii-only, and █ in the heavy set. Where the thumb would be the same glyph as the rule, as with a heavy border style, it is drawn as █ (# under --ascii-only). appearance.scrollbar.* and appearance.hide_scrollbar style the pane scroll bar only, not the rail's thumb.
The rail has one section whose rows are the output of a command you write.
Place custom in appearance.sidebar.sections like any built-in section and
describe it in [appearance.sidebar.custom]:
[appearance.sidebar]
sections = "sessions:25,terminals,custom:35,agents:30"
[appearance.sidebar.custom]
title = "Brief"
command = "agent-brief render"
refresh = "event:window-focused,agent-state"| Key | Meaning |
|---|---|
title |
The section's heading. Defaults to Custom. |
command |
Run through sh -c, as dock commands are. Every line of stdout is a row. |
refresh |
once (the default), a duration such as "30s" with a one-second floor, or event:TYPE[,TYPE...] with the dock's event types. push is refused: a command that stays running cannot see the focus or the section's size change. |
The command is a dock component that draws on the rail, so the dock's rules
apply: it runs in the client, under a three second timeout, with bounded
output; SGR colour survives and every other control sequence is stripped; a
command that fails, times out or prints nothing leaves the title over an
empty section, never an earlier run's rows; five failures in a row stop it
until the config reloads or tuios refresh-dock rail/custom; and
tuios list-dock-components lists it as rail/custom with why it drew
nothing. Rows are cut to the rail's width and to the lines the section's
share gives it, with … +N for the ones below the fold. While some are below
the fold the section draws a scroll thumb in the rail's edge rule (see
A rail section that overflows).
Each run gets the client's environment plus TUIOS_SESSION, TUIOS_SOCKET,
TUIOS_RAIL_SECTION (custom), TUIOS_RAIL_WIDTH (the columns a row may
use), TUIOS_RAIL_HEIGHT (the most rows the section's share can give it; the
rail may give fewer when other sections need the lines),
TUIOS_ACTIVE_PANE_ID and TUIOS_ACTIVE_PANE_CWD (the focused pane and its
folder, named as [[keybindings.command]] names them). They are read for
each run, and an event that lands while a run is going costs one more run
after it, so a quick focus change never leaves the old pane's rows on
screen. agent-state fires on every change of a pane's agent state, whichever
states [notifications.agent] alerts on.
The command does not run while the rail is hidden or folded, so the width it
is told is never 0, and it runs again when the rail opens.
appearance.sidebar.sections is a settable option, so tuios set-config can
place the section. [appearance.sidebar.custom] is read from the file only:
the command runs outside every pane on every refresh, as dock commands and
hooks do, and in the default open mode any pane can call set-option, so
the command stays out of its reach, as daemon.respond_from_shell does.
The rail can show a live terminal pane under its own heading. Place pane in
appearance.sidebar.sections and describe it in [appearance.sidebar.pane]:
[appearance.sidebar]
sections = "sessions:25,terminals,files:25,pane,agents:34"
[appearance.sidebar.pane]
title = "tv"
command = "tuiostv-helper view"
height = 12| Key | Meaning |
|---|---|
title |
The section's heading. Defaults to pane. |
command |
The program the pane runs, through sh -c (cmd /C on Windows). |
height |
The most rows the pane takes. 0 or unset takes the section's share, such as pane:30 in the layout. |
The program runs in a real terminal in the session, so it survives a detach.
The pane is not one of your panes: tiling, layouts, the terminals section and
tuios list-windows leave it out. tuios list-windows --rail lists it.
The pane is as wide as the rail's content and as tall as the lines the section gets. When the rail changes size, the program gets the new size. When the rail is hidden, folded, narrower than 10 columns or gives the section no lines, the program keeps running at its last size and the client draws nothing for it. An idle program costs no frames. The pane shows text only: kitty images are not drawn in the rail.
Click the pane, or press P in window mode (focus_rail_pane), to type into
it. A program that asks for the mouse gets clicks, motion and the wheel.
alt+esc, a click outside the pane, or the leader key gives the keyboard back.
esc goes to the program.
When the program exits, the pane closes and the section says so. Click the
line to start it again. A session has one rail pane, and only the main rail
draws it. If two clients name different commands, the last one to ask
replaces the pane. [appearance.sidebar.pane] is read from the file only, for
the reason [appearance.sidebar.custom] is.
Use this setting if you want only the multiplexer:
[agents]
enabled = falseThe default is true. You can also change it on the settings page. It is the
first row, "Agent features". While a tuios client is open, you can also run
tuios set-config agents.enabled false from a shell outside tuios.
When the agent features are off:
- The daemon does not look for agents. It does not read the process table, pane titles, screens or agent transcripts. It ignores the agent state that programs send.
- The rail shows no agents section. The other sections move up to fill the space.
- The Inbox, agent mail, approvals and agent alerts are off. The Inbox key shows a message and does nothing else.
- The prefix menu, the command palette, the help and the settings page do not show agent items.
- Agent hooks, such as
after-agent-state, do not run. - Agent commands, such as
tuios start-agent,tuios fanandtuios list-attention, stop and print this message: "Agent features are off. Set agents.enabled = true in the config to use this command." A program that calls an agent verb gets the error codeagents_disabledand this message: "Agent features are off on this machine. Set agents.enabled = true in its config.toml to turn them on."
The change applies at once. You do not have to restart tuios. When you turn the features off, the daemon clears the agent state of every pane and closes every Inbox item. A held approval goes back to the agent, which asks in its pane. When you turn the features on, the daemon looks at every pane again.
Typing between panes is stricter when the features are off. A pane can answer
another pane's prompt if it types into that pane. Usually tuios knows which
pane waits on a prompt. With the features off it does not know. Thus a pane
that does not hold the respond grant can type only into these panes:
- A pane that it opened, for example with
tuios new-windowor the tmux shim'ssplit-window. - A pane whose own shell is at its prompt, with no program running.
It cannot type into other panes. This includes send-keys, send-text and
run from a script in a pane. Your own keys, and commands from a shell outside
tuios, are not affected. To let every pane type into other panes, give panes
respond:
[agents.permissions]
grants = ["admin", "respond"]See What a pane may do.
Only you can change the switch: on the settings page, in config.toml, or with
tuios set-config from a shell outside tuios. A process in a pane gets
forbidden. tuios mcp started with the features off does not list the
agent tools. An agent command aimed at another machine, such as
tuios list-agents -s build:work, follows this machine's switch.
The [agents.approvals] table lets the Inbox answer a harness's permission
prompt, so you do not have to go to the pane. It is off by default: while a
prompt is held for the Inbox, the harness shows nothing in its pane.
[agents.approvals]
enabled = ["claude-code", "opencode"]
hold_seconds = 120enabled names the harnesses, by id or alias: claude-code (or claude),
opencode, kilo and qwen have a decision channel tuios can answer through. Other
names are accepted and hold nothing. Even for these, only a call the Inbox can
show whole on one line is held, such as a short shell command or a file read;
an edit, an MCP tool or a long command is answered in the pane.
hold_seconds is how long a prompt waits for your answer before the harness
asks in its pane after all: 120 when unset, kept between 10 and 300.
The daemon reads the table when it starts and again when the file changes; a
change applies to the next prompt. Like [dock] and [hosts], it is not in
list-options and tuios set-config cannot change it. Turning it on gives no
program the power to answer: only you, at an attached client, can. It also
needs version 2 of the integration:
run tuios integration install claude-code (or opencode, kilo, qwen)
again after upgrading. AGENT_STATE.md says how
a prompt is held, answered and handed back.
A program in a pane rings the bell when it prints the BEL character. For
example, printf '\a' rings it. notifications.bell sets what tuios does then:
[notifications]
bell = "toast" # or "forward", "both", "off"toastshows "bell" in the dock. This is the default.forwardsends the bell to the terminal that tuios runs in. That terminal then plays its sound, flashes, or marks the window or tab.bothdoes the two.offdoes neither.
tuios sends the bell for every pane, also for the pane you are in. It sends
at most one bell from each pane in 200 milliseconds. A client attached to the
daemon sends the bell to its own terminal. Over tuios ssh and in tuios-web,
the bell goes to that client, not to the machine that runs tuios.
The terminal decides what a bell does. In Ghostty, for example, set
bell-features to choose a sound, an attention request or a title mark.
The workspace bell marker does not change with this setting. Set
bell_marker under [dock.tabs] to change it. You can also change
the setting on the settings page, in the "Alerts" tab, as "Pane bell".
The [notify] table sends a push notification when the Inbox gets an item
that waits for you. The daemon sends it, so it works when no client is
attached. Set one provider or more. Each provider gets every notification.
[notify]
web_url = "https://term.example.com/" # tuios-web, for a link to the item
content = "summary" # or "title"
quiet_active_seconds = 120
cooldown_seconds = 60
max_per_hour = 30
[notify.triggers]
approval = true
plan = true
question = true
ask = true
mail = false
errored = false
finished = false
[notify.ntfy]
url = "https://ntfy.sh/a-long-random-topic"
token_file = "~/.config/tuios/ntfy-token" # optional
[notify.pushover]
user_env = "PUSHOVER_USER"
token_file = "~/.config/tuios/pushover-token"
[notify.webhook]
url = "https://hooks.example.com/tuios"
token_env = "TUIOS_HOOK_TOKEN" # optional, sent as a bearer token| Key | Default | What it does |
|---|---|---|
enabled |
true |
Turns every notification off when false. Nothing is sent without a provider. |
web_url |
empty | The address of tuios-web. Each notification links to web_url/inbox?item=ID, which opens the agents view on the item. See WEB.md. |
content |
summary |
summary sends the kind, the pane name and the one line the Inbox shows. title sends only the kind and the session. |
quiet_active_seconds |
120 |
Holds a notification while you typed into a pane at an attached client in this many seconds. When you stay away that long and the item is still open, tuios sends it. 0 sends at once. |
cooldown_seconds |
60 |
The shortest time between two notifications for the same pane and kind. |
max_per_hour |
30 |
The most notifications in one hour. 0 sets no limit. |
allow_http_redirects |
false |
Lets a provider redirect to a plain http address. |
[notify.triggers] selects the Inbox kinds that send a notification. The
kinds that wait for you are on by default: approval, plan, question and
ask (a question from tuios ask-human). The kinds that only report are off:
mail, errored and finished.
Each item sends one notification at most. A repeated state report, or a new line on the same item, sends nothing. An item from a linked host sends nothing here. That host sends its own.
[notify.ntfy]:urlis the topic address.priorityis 1 to 5. When it is unset, an item that waits for you gets 4 and other items get 3. The link goes in theClickheader.[notify.pushover]: your user key and an application token.urlreplaces the Pushover API address, for a relay.[notify.webhook]: tuios sends a JSONPOSTtourlwith these fields:event(tuios.inbox),kind,title,body,link,urgent,item_id,session,window,harness, andtestfor a test notification. Atcontent = "title"the body is empty.
Give each token in one of three ways. tuios uses the first one that is set.
token = "..."in config.toml.token_env = "NAME": an environment variable of the daemon. The daemon keeps the environment of the shell that started it.token_file = "PATH": a file that only you can read (mode 600 or 400).
Pushover uses user, user_env and user_file for the user key in the
same way. tuios reads the value each time it sends, so a new token in the
file applies at once.
tuios sends each notification itself and needs no other program. It uses
HTTPS_PROXY, NO_PROXY and the system certificates. Each send waits at
most 10 seconds. It follows at most 3 redirects. The token goes in a request
header and is never logged.
tuios does not write a secret to its logs or its output. The address is
also secret on a public ntfy server, so logs and tuios notify test show
only the host. A redirect to a plain http address is refused, unless you
set allow_http_redirects. A redirect to another host is always refused,
because the message and its token would go there. Set the url to the
address the server redirects to.
The daemon reads [notify] when it starts, and again when the file changes.
A change that turns notifications off, or changes the triggers, applies at
once. A new address, or a new source for a secret, waits for
tuios config apply from a terminal outside tuios, or a daemon restart. A
program in a pane can write config.toml, and this stops it from sending your
Inbox to another address. Like [hosts], [notify] is not in
list-options, and tuios set-config cannot change it.
Run tuios notify test to send a test notification through each provider.
It does not send to the phones of [notify.webpush].
It says the result for each provider. See
CLI_REFERENCE.md.
A phone can get Inbox items by Web Push. The phone does not keep a
connection to tuios. Its push service, or its UnifiedPush distributor such as
the ntfy app, wakes it. The phone app registers with the register-push
verb, or you register it with tuios notify push register. Only you can
register a phone. A program in a pane cannot. See
protocol.md.
[notify.webpush]
subject = "https://example.com/tuios-contact" # optional
allow_insecure = false| Key | Default | What it does |
|---|---|---|
subject |
https://tuios.dev/push |
The VAPID subject: a URL that a push service can use to contact you. It must be an https URL or a mailto: URI. The push service reads it, so the default names no machine. |
allow_insecure |
false |
Lets tuios send to a push service on a loopback or private address, by https or http, for a push service on your own network. A change to true waits for tuios config apply. |
When allow_insecure is false, tuios does not send to a loopback or
private address. This applies also to a host name that resolves to one. tuios
never sends to a link-local, multicast or unspecified address. A push does not
follow a redirect.
When an item opens, tuios encrypts it for each phone that asked for its kind
and sends it to the phone's push service. When the item closes, tuios sends a
close, so the phone can remove the notification. A phone gets approval,
plan, ask and question unless it asked for other kinds. The triggers,
quiet_active_seconds and cooldown_seconds are for the other providers.
enabled = false stops the pushes of new items. A phone still gets the close
of an item, so it can remove a notification. max_per_hour applies to each
phone. An item from another machine is not pushed. That machine pushes to its
own phones.
A push of approval, plan, ask or question lives one hour on the push
service. Other pushes live 120 seconds. tuios tries a failed push again after
1, 4 and 15 seconds. When the push service says that the phone is gone (404
or 410), tuios removes the phone and shows an Inbox item about it.
Each registration shows an Inbox item named phone NAME. It says who
registered the phone. Another machine can register a phone over a link with
respond, but it can not replace a registered phone. Check the item. Remove
a phone you do not know with tuios notify push rm NAME. The phones and the VAPID key are in
the state directory, in push/, with mode 600.
Run tuios notify push ls to see the phones and the VAPID public key. Run
tuios notify push rm NAME to remove a phone.
Crush, and other agents that herdr lists as reporting by themselves, report
their state to herdr, another multiplexer, when they find herdr's environment
in their pane. tuios accepts the same reports on a socket of its own,
<daemon socket>.herdr. herdr_protocol in [agents] says which panes get
herdr's environment:
[agents]
herdr_protocol = "always" # always (default), agents, offalways gives it to every pane, as herdr does, so a Crush that you start from
a shell prompt reports. agents gives it only to a pane that starts a known
reporter directly, as tuios new-window NAME crush, start-agent crush or
fan --agent crush do. off gives it to no pane.
A pane with herdr's environment gets HERDR_ENV=1, HERDR_SOCKET_PATH naming
tuios's socket, HERDR_PANE_ID, HERDR_TAB_ID and HERDR_WORKSPACE_ID naming
the pane, its workspace and its session, and HERDR_BIN_PATH naming a herdr
link to tuios that answers herdr's command line. Programs that check
HERDR_ENV read the pane as a herdr pane. With the default, herdr refuses to
start inside a tuios pane. Set herdr_protocol = "agents" to run herdr nested.
The socket also answers herdr's socket API, so tools built for herdr work with
tuios (see herdr compatibility).
herdr's own hook scripts, if installed, report to tuios from such a pane.
An unknown value reads as always, with a warning. The daemon reads the value
when it starts and again when the file changes. A change applies to the next
pane. AGENT_STATE.md says what is
accepted.
A hooked agent reports at each prompt and each tool call. When it sends no
report for silent_after, tuios shows it as unknown with the reason
silent, and its row reads "No report for N min":
[agents]
silent_after = "10m" # default; "0" or "off" turns it offThe value is a Go duration, such as 90s or 20m. A value that does not
parse reads as 10m, with a warning. The daemon reads the value when it
starts and again when the file changes. See
Silent agents.
The agents view (ctrl+b i) can open with the Working group folded. A folded
group shows its heading, its count and the state mark of each agent in it.
[agents]
fold_working = true # default falseF folds or opens any group in the view. tuios keeps that choice in
sidebar.json in its state folder ($XDG_STATE_HOME/tuios on Linux). The
choice wins over fold_working. The settings page has this option on the
Behavior tab, as "Fold Working".
When tuios runs in a terminal that reads OSC 7501, the Program Status
Protocol (Rex does), it reports the agent state of each pane there.
host_program_status in [agents] turns this off:
[agents]
host_program_status = "auto" # auto (default), offauto asks the terminal when a client starts, and reports only when the
terminal answers. off never asks. A client reads the value when it starts.
An unknown value reads as auto, with a warning. See
PROGRAM_STATUS.md.
tuios runs herdr plugins: folders with a herdr-plugin.toml. The [plugins]
table says which plugins run and where tuios finds more of them:
[plugins]
enabled = ["example.notes"] # the ids of the plugins that run
dirs = ["~/src/my-herdr-plugin"] # more plugin folders, or manifeststuios also finds plugins in $XDG_CONFIG_HOME/tuios/plugins/<folder>/, in
plugins.lock, and in herdr's own plugins.json and managed checkouts. To
find a plugin runs nothing. A plugin runs only when its id is in enabled.
tuios plugins install SOURCE writes plugins.lock beside config.toml.
It pins each installed plugin to one commit. Keep the file with your config,
and run tuios plugins install on a new machine to get the same commits.
tuios plugins uninstall ID removes the plugin from the file.
An enabled plugin runs its commands with your rights, outside every pane. Enable only plugins that you trust.
Use the commands to change the table. They change only the enabled or
dirs line and keep the rest of the file:
tuios plugins enable example.notes
tuios plugins disable example.notes
tuios plugins link ~/src/my-herdr-plugin
tuios plugins install owner/repo # also adds the id to enabled
tuios plugins uninstall example.notes # also removes the id from enabledYou must run them from a terminal outside tuios. A change to the file that
removes a plugin from enabled applies at once. A change that adds a plugin
to enabled, or a folder to dirs, waits for tuios config apply or a
daemon restart. tuios plugins enable ID applies only the plugin that it
names. A new folder waits because it can hold a
plugin with the id of a plugin that you enabled.
PLUGINS.md says what runs and when.
These tables configure the agent review, triage, reply and approval work,
which is built. Every value has a default, so a file without them behaves as
the defaults say. Like [agents.approvals], they are
file-plane config: not in list-options, and tuios set-config cannot change
them, so a pane cannot switch a risk rule off through tuios.
[agents.approvals]
hold_plans = true # a plan follows enabled
[agents.approvals.risk]
builtin = true # keep the shipped rules
panes_may_allow = false # a pane with the respond grant may not allow a risky call
[[agents.approvals.risk.rule]]
name = "kubectl apply"
tools = ["Bash"]
pattern = '\bkubectl\s+(apply|delete)\b'
[agents.recap]
mode = "toast" # toast, inbox or off
away = "10m"
test_patterns = ["go test", "pytest"]
[agents.queue]
max = 8
[agents.mail]
wake = true
[agents.checkpoints]
enabled = true
keep = 50
max_untracked_mb = 50hold_plansalso hands a plan an agent in plan mode asks to have approved to the Inbox, for the harnessesenablednames (Claude Code'sExitPlanMode). Unset is true. See Plans.[agents.approvals.risk]marks an approval risky when its command matches a rule:builtinkeeps the shipped rules (default true), eachruleadds one with a name, the tools it applies to (empty for every tool) and an RE2pattern, matched against each command of a shell call and the whole text of any other. Naming one shell tool, such asBash, covers every shell tool, includingexecute, the word a protocol pane's line uses for a command, and a line with no tool; naming one file tool, such asWrite, covers every file tool, including a protocol pane'sedit. A daemon that could read no config file at all still uses the shipped rules. A rule with no name or a pattern that does not compile is ignored, with a warning. An allow of a risky approval takes a second press of the same key in the Inbox, and the daemon refuses one that does not name the rules.panes_may_allowlets a pane holding therespondgrant allow a risky call; it is off, so only you can. The shipped rules are listed under Risk rules. They are a speed bump, not a sandbox. The daemon reads the risk rules again when the file changes. A change that adds a rule applies at once. A change that removes a rule, or setspanes_may_allow, waits, because a process in a pane can write config.toml. The rules in force stay, and panes may not allow a risky call. Runtuios config applyfrom a terminal outside tuios to apply the change, or restart the daemon. Until then the Inbox shows that a change waits.[agents.recap]is the summary of what an agent did while you were away:modesays where it is shown (toastin the dock when you come back to the pane, and in the Inbox;inboxonly in the Inbox;offonly inagent-log),awayhow long you must have been away for the dock to show it, andtest_patternswhich commands count as a test run. The daemon readstest_patternsfortuios agent-log --recapand theagent-activityverb, and picks up a change when the file is saved; the client readsmodeandawaywhen it loads the config. The recap is built: see The away recap.[agents.queue]bounds the messages waiting to be typed to one agent when it comes to rest (tuios queue,queue-prompt):max, 8 by default, at most 64. A message queued past it is refused withqueue_full. The daemon reads it at start and again when the file changes; a queue already longer keeps what it holds. The queue is built: see AGENT_STATE.md.[agents.mail]waketypes one line into an agent at rest that has mail nobody told it about, through the delivery queue: the count, the senders and the command that reads the mail, never the mail itself. It is true by default. Set it to false to stop it. The daemon reads it at start. A file change turns it off at once. Turning it on again waits fortuios config applyfrom outside tuios, or a restart. See AGENT_STATE.md.[agents.checkpoints]saves the git work tree of an agent's pane each time the agent finishes a turn that changed a file (tuios checkpoint).enabledis true by default.keepis how many checkpoints one pane keeps: 50 by default, at most 1000.max_untracked_mbleaves an untracked file larger than this many megabytes out of a checkpoint: 50 by default, and a negative value means no limit.tuios checkpoint listnames the files left out, and a restore does not change them. The same limit holds fortuios review, review-stage, fan compare andtuios fan diff, which never read such a file: review lists it as not read. Reading one would copy it into the repository's object store, and through a clean filter such as Git LFS's into that store too, on every read. The daemon reads these at start and again when the file changes. See Turn checkpoints.
One rail option goes with them: appearance.sidebar.agent_rest_fold, how long
an agent row rests (idle, unknown, or done and already seen) before the rail
folds it into one line, as a duration such as 1h (the default) or off. The
settings page shows it on its Sidebar tab once an agent has been seen. The
fold is one muted +3 at rest line at the end of the agents section; enter
or a click on it shows the rows until the rail lets go of the keyboard, or,
when the rail did not have the keyboard, until a click outside the rail or a
pane is focused. It
takes two rows or more, and never a row that needs you, a finished turn not
yet seen, a working agent, the pane you are in, one with messages queued, or
one whose agent has subagents at work.
A second rail option goes with it: appearance.sidebar.subagent_rows, how an
agent row shows its subagents. rows (the default) draws a row for each
subagent under its agent's row, with its mark (● running, ■ done, ×
failed, ○ stopped), what it was asked to do, and on a tall section the tool
it runs or how it ended, while the agent's own row says 3 of 4 run. When the
section has too little room it leaves the finished ones out first, then
shows the newest few and +2 more, then none; it never hides an agent row to
show a subagent row. tree draws the same rows on tree lines (├─ and
└─, |- and `- in ASCII). count draws the count alone, as the rail
did before.
An agent at rest with subagents at work wears ◊ (% in ASCII) in the
working ink, on the rail, its title bar and the switcher. The settings page
shows the option on its Sidebar tab once an agent has been seen. See
Subagents on the rail.
Four more rail options change how the subagent rows look. Each default draws the rows as described above.
| Option | Values | Default | What it does |
|---|---|---|---|
subagent_collapse |
off, expanded, collapsed |
off |
expanded and collapsed draw a heading line, ▾ 4 subagents, over each agent's subagent rows. The value is the state at the start. |
subagent_fields |
a comma list of mark, name, type, time, note |
mark,name,time,note |
The parts each subagent row shows. type is the agent type, such as Explore. note is the second line. |
subagent_max_rows |
0 to 16 |
0 |
The most lines one agent's subagents use, the +N more line included. 0 means no limit. |
subagent_hide_finished |
a duration such as 30s, or off |
off |
Removes the row of a finished subagent this long after it ends. |
A click on the heading, or enter on it, opens or closes the rows. space
does the same with the rail cursor on the agent row or on its heading. The
rail keeps the state of each pane until the client stops. A closed heading
shows ▸, and on a tall section its second line names the subagents.
The daemon keeps a finished subagent for 60 seconds (a failed one for 5
minutes). subagent_hide_finished can make a row go earlier, but it cannot
keep a row longer than the daemon keeps it.
[appearance.sidebar]
subagent_rows = "tree"
subagent_collapse = "expanded"
subagent_fields = "mark,name,time"
subagent_max_rows = 4
subagent_hide_finished = "20s"Each of the five subagent options can be set again in
[appearance.sidebar.left] or [appearance.sidebar.right], for the rail on
that edge. An option that the edge table does not set uses the value from
[appearance.sidebar].
The agent row in [appearance.sidebar.agent_row] has five tokens for what
tuios feeds itself: now (what a working agent is doing, drawn only while it
works), context (ctx 84% in the warning ink, drawn only at 80% or more),
subagents (2 subagents, how many subagents the agent has at work, drawn
on any row while any run, from Claude Code's hooks, and 3 of 4 run when
the subagents have rows of their own), pr (PR #12 open pass, the pull request of the session's worktree branch and its checks, in
the error ink for failing checks, the warning ink for pending ones and the
success ink for passing ones and a merge, see
Shipping a worktree) and prompt (the
first line of the last prompt, not shipped on the row). The shipped tokens
list is now ["session", "need", "harness", "name", "progress", "elapsed", "context", "subagents", "pr", "meta", "now", "message"]. progress (40%)
is the progress a program reported with OSC 7501, drawn while it works or
waits (see PROGRAM_STATUS.md). A list you wrote keeps its own
order and gains nothing. Add subagents or pr to it to see them. The meta token no longer
draws the fed keys (now, prompt, model, context, cost, plan,
subagents), so place any of them you want with its own token. See
What the second line says.
The $name tokens can place the metadata keys tuios now feeds: $model,
$context, $cost and $plan.
They come from Claude Code's status line once tuios integration install claude-code --statusline is installed, from the opencode and Kilo plugin, and
from protocol panes, and a key the harness never states draws nothing (see
Agent metadata). No option is needed to turn
the feeds on, and there is nothing to configure for them.
b in the agents view shows the task board: the cards of every session,
grouped by stage (AGENT_STATE.md). [tasks] stages picks the stages it shows. The order stays todo, plan, doing, verify,
review, shipped, done whatever the list's order, and a stage the list leaves
out has no head. Empty, or naming no known stage, shows every stage. An
unknown name is reported as a config warning and ignored.
[tasks]
stages = ["todo", "doing", "verify"]tuios moves a card on its own when it records a fact about the card's work: a
plan held for approval and then approved, a fan check, a pull request
opening, merging or closing (CLI_REFERENCE.md
has the table). A move fires only from the stages that fact moves a card
from, never on a blocked or done card, and the card's history names the
reason. The plan stage is reached only when [agents.approvals] holds plans
for the harness and the pane is not in front of you. auto_stage = false
leaves every stage to you. A fan's stamp on a card (tuios fan --task) is
still written, though the card then stays where it is.
[tasks]
auto_stage = falseEvery pane holds grants that say what a process in it may do through tuios:
read (its own session and fan group), write (type into its own session,
into panes that hold nothing it does not), fan (write in its fan group and
start agents), respond (answer prompts without you, and type into a pane
waiting on one), files (copy files with tuios cp, see below) and admin
(everything else, as before grants). A pane started
with --grants (tuios start-agent, fan, new-window) or given grants
with tuios set-pane-grants holds those. Every other pane holds the default
this table sets:
[agents.permissions]
mode = "strict"
grants = ["read", "write", "fan"]mode is open, the default, or strict. Under open a pane holds admin,
so every script in a pane works as it always has. Under strict it holds
grants, which is read, write and fan when unset; an empty list gives
nothing but the right to report about itself. Any other mode is read as
strict, and an unknown grant is dropped: both are reported as config
warnings, and both fail toward less. admin never includes respond, and no
default gives it, so listing respond here is how you let every pane answer
prompts for you.
files lets a pane copy files without admin. A copy may start when each
end on this machine is inside the pane's folder (its worktree, else its
working folder). Each end on a host must be under its home folder, or be its
receive folder. The pane sees and controls only the copies it started.
admin includes files. To let every pane copy near its work under
strict, add it to the list:
[agents.permissions]
mode = "strict"
grants = ["read", "write", "fan", "files"]task lets a pane add, claim and finish the task cards of its own session
(tuios task) without admin. A pane claims only for itself and changes only
the card it holds. admin includes task. Under strict, give it to the
panes that take work from a conductor:
[agents.permissions]
mode = "strict"
grants = ["read", "write", "fan", "task"]The daemon reads the table when it starts and again when the file changes. A
change that gives panes less reaches every pane on the default at its next
call. A change that gives more, such as strict to open, waits, because a
process in a pane can write config.toml. Run tuios config apply from a
terminal outside tuios to apply it, or restart the daemon. Until then the
daemon logs it, and tuios pane-grants says so. When a start finds that
panes hold more than at the last run, it says so in the log, in
tuios pane-grants and in the Inbox. Like
[agents.approvals], it is not in list-options and tuios set-config cannot
change it, so no pane can loosen it. The paste buffers hold
what you copied, so a pane needs read to read them, write to change them,
and both to paste one.
AGENT_STATE.md has the whole model.
The [ssh] section sets the limits of the tuios ssh server. The server reads
it when it starts. The defaults do not stop a normal login.
[ssh]
handshake_timeout = "120s" # time to finish the login; 0 turns the limit off
idle_timeout = "0" # close a connection with no traffic for this long; 0 is off
max_handshakes = 32 # connections that log in at the same time; 0 is no limit
max_sessions = 64 # sessions open at the same time; 0 is no limithandshake_timeout: The server closes a connection that does not log in within this time. It is the same asLoginGraceTimein OpenSSH. The limit ends when the login is complete, so it does not close a session that you use.idle_timeout: The server closes a connection after this time with no traffic in either direction. It is off by default. A session that shows a clock or other output that changes stays open.max_handshakes: When this many connections are logging in, the server closes a new connection at once. A connection that is logged in does not count.max_sessions: When this many sessions are open, over all connections, the server refuses a new session. The client sees a message. The refused session does not attach to a session and does not open a pane.
A duration is a value such as 90s, 30m or 2h. A value that does not
parse, or a negative value, gives a warning and the default.
tuios ssh --handshake-timeout, --idle-timeout, --max-handshakes and
--max-sessions replace these values for one run. The server writes the limits
it uses to its log when it starts.
Like [hosts], the section is not in list-options, and tuios set-config
cannot change it.
The next_host and prev_host keybind actions go to a session on the next or
the previous machine: this machine first, then the [hosts] table. They have
no default key. See
KEYBINDINGS.md.
[keybindings.window_management]
next_host = ["alt+."]
prev_host = ["alt+,"]A [hosts.NAME] table names a machine this one links to. Read on the machine
a link arrives at, the same table also says what the machine of that name may
do there:
[hosts.laptop]
addr = "laptop" # optional: without it nothing is dialled
allow = ["list", "mail", "open", "write", "respond"]
hold_mail = true
hosted_grace = "10m"
[hosts."*"] # every machine with no table of its own
allow = ["list", "mail"]allow is the capabilities, and anything not in it is refused with
forbidden and nothing done:
| Capability | What it lets the other machine do here |
|---|---|
list |
Read: sessions, windows, captures, screenshots, agent state, the Inbox, prompts, waits and the event stream. It also gives a live stream of the bytes of any pane (stream-pane). |
mail |
Send and read agent mail, and use the stash. |
open |
Start processes: sessions, windows, worktrees, fans, start-agent, clones of a repository by its URL, and panes this machine runs for it. |
write |
Change what is here: type into panes, run a line at a prompt, close and move windows, set options, layouts and names, report agent state, and attach. Also read a worktree's work out with bundle-worktree (tuios worktree pull), since a machine that may type into a shell here can read those files already. |
respond |
Answer for the person: prompts, held approvals, ask-human questions, dismissing Inbox items, and passing on held mail. Also type into a pane that waits on a prompt, from a pane on the other machine. The person on the other machine, outside every pane, needs only write for that. |
files |
Read and write files here with the file verbs: a file's bytes, copies in and out (transfer-start on the other machine), new folders, moves and removes. It is apart from write, so you can stop one machine's copies and keep its typing. A write that arrives over the link lands only in the folders of files_roots (the receive folder unless you add more, see below), and the drop folder for dropped files, never in ~/.ssh, ~/.gnupg, credentials, shell start files, login items or tuios's own config, whatever allow and files_roots say. A read never returns keys or credentials. It also lets a window here that the other machine draws show kitty images that a program names by path (kitten icat --transfer-mode=file): this machine reads the file under the same read rules and files_read_roots, and sends the bytes. See protocol.md. |
With no table, a machine may list, mail, open, write and files,
which is what every link could do before the policy existed. respond is
opt-in. Relaying on to this machine's own hosts needs the five capabilities a
link had before files (list, mail, open, write, respond), because
the next machine sees the relay as coming from this one; a table written for an
older tuios that allows those five still relays. A copy this machine runs for
another one also needs files. A table that sets allow without files keeps
that machine from the file verbs. An allow that is set replaces the inherited
list; allow = [] allows nothing but hello.
Where a link may write: files_roots. A machine with files writes only
under the folders files_roots names, plus the drop folder. Unset, it is one
receive folder: ~/Downloads/tuios on macOS and Windows, and
$XDG_DOWNLOAD_DIR/tuios (or ~/Downloads/tuios) on other systems. tuios
makes it on first use. Widen it on purpose, per machine:
[hosts.laptop]
allow = ["list", "files"]
files_roots = ["~/Downloads/tuios", "~/dev"]A path is absolute or starts with ~. files_roots = ["~"] lets the machine
write anywhere in your home folder; the deny list below still holds inside
every root, so even then it cannot touch ~/.ssh, a shell start file, or the
config folders of the agent harnesses tuios integrates (Claude Code, Codex,
Gemini CLI, opencode, Cursor and the others), which name commands those tools
run. A write outside the roots is refused, and the refusal says where writes
may land and which setting to change. A reload that widens files_roots, like
one that widens allow, waits for tuios config apply.
The mode of a file a link writes. A file that another machine writes here
gets the sender's permission bits under this machine's umask, as cp and
scp without -p do. It never gets group or world write. The execute bits
stay, so a script stays runnable. This holds for a paired device too.
Where a link may read: files_read_roots. Unset, a machine with files
reads any file you can read, apart from the read deny list: keys, credentials
(also the logins and API keys of agent tools such as Gemini CLI, opencode, Qwen
Code, Kilo, Pi, oh-my-pi, Hermes, Amp and the Copilot CLI, Cursor's MCP servers, the sops age keys, atuin's
key and history, Ollama's key, and the OCI, doctl and ngrok credentials),
shell start files, ~/.gitconfig, keyrings, shell histories, browser profiles,
/proc and tuios's own state. A
deny list can never name every secret on a machine. Set files_read_roots to
hold reads to the folders you name, plus the folders the machine may write.
That is the stronger choice:
[hosts.laptop]
files_read_roots = ["~/share"]The rule covers file-read, file-preview, file-hash, file-walk, a copy out, and the
names and sizes file-stat and file-list show. The read deny list still
holds inside every root. A file with more than one name (a hard link) is never
read over a link, inside a root or outside one. Copy it to a new file to share
it. A reload that removes or widens files_read_roots
waits for tuios config apply. A reload that narrows it applies at once.
How much a link may write. A write over a link must leave
files_min_free free on its disk: "1GiB" unless you set it. The value is a
whole number with B, KB, MB, GB, TB, KiB, MiB, GiB or TiB.
"0" turns the check off, and a reload that lowers it waits for
tuios config apply. A write that does not fit is refused as
disk_full before a byte moves. One machine may have at most 8 write streams
and 64 unfinished part files here at once. Past that, a write is busy, and
a copy waits and tries again.
A machine may also make at most files_max_entries new files and folders
here in ten minutes: 20,000 unless you set it. An empty file uses no bytes,
so files_min_free does not stop a machine from making millions of them.
Past the bound, a new file or folder is busy, and a copy waits and goes on.
The count goes down evenly over the ten minutes. A folder copy of 20,000 files
lands at full speed, and a larger one goes on at about 33 files a second.
0 turns the bound off. A reload that raises it waits for tuios config apply. The drop folder holds at most 256 MiB for all
drops together, and a machine writes there only inside a drop that
file-drop-dir made.
[hosts."*"]
files_min_free = "5GiB"
files_max_entries = 50000A copy that another machine starts here with transfer-start or drop-files
is held to these rules too, also after tuios restarts. When a reload or a
restart takes files away from that machine, its copies here stop.
Trust. A machine with
filescan write any file inside its roots and read any file outside the deny list (or insidefiles_read_roots). A file copy is a way to run code: a binary dropped on yourPATH, a config a program reads. Givefiles, and a widefiles_roots, only to machines you trust as you trust your own.
hold_mail holds mail from that machine to any agent here in your Inbox,
marked held for NAME, until you pass it on with p there (or
release-agent-message). Mail to you is yours already and is not held.
hosted_grace is how long a pane this machine runs for the other one (a
window opened with --host there) outlives a dropped link, still running and
keeping its last 64 KB of output, waiting to be reattached: a Go duration,
"0" to end it with the link as before, 10 minutes when unset, at most 24
hours. See Limits.
Each field inherits from [hosts."*"], which inherits from the default. The
name is matched without regard to case. It is the one the other machine gives
for itself, its host name up to the first dot, unless the ssh key it logs in
with pins one:
command="tuios stdio-proxy --as laptop",restrict ssh-ed25519 AAAA...
Only a pinned name is a boundary: a key that may run any command can run a
shell, and can claim any name. A table with a policy and no addr is not
dialled and not listed by tuios hosts, and [hosts."*"] never is.
The daemon follows the file. A change that gives a machine less applies to the
next call on every link, including links already open. A change that gives a
machine more, a new host, or a host that dials another way waits for
tuios config apply from a terminal outside tuios, or a daemon restart.
tuios hosts add applies its change that way, so from a pane it waits.
tuios hosts add on a known name keeps these fields.
ssh_options takes only options that cannot run code or write files on this
machine, each written as -o Keyword=value with one plain value: for example
-p, -i, -l, -J, Port, User, HostName, HostKeyAlias,
IdentityFile, StrictHostKeyChecking, the ServerAlive options and the
algorithm lists. ProxyCommand, LocalCommand, the known hosts files,
ControlPath, socket forwards, -F and the like are refused. -J and
ProxyJump take host names only. addr and command may not start with a
dash. A host with a refused entry is ignored, and tuios hosts and the Inbox
name the reason. Put such options in ~/.ssh/config. protocol.md has
the verb by verb table.
The daemon's links never ask for a password, a passphrase or a code. They
run ssh with BatchMode=yes. A client that can ask you, such as tuios-gpui,
signs in to a machine itself and keeps that connection open as an ssh master
in the folder cm beside the daemon's socket. That folder is
$XDG_RUNTIME_DIR/tuios/cm, /run/user/<uid>/tuios/cm when
XDG_RUNTIME_DIR is not set, or /tmp/tuios-<uid>/cm when neither exists. When that folder and its parent exist, are not
symbolic links, belong to you and only you can open them, each link and
tuios hosts test run ssh with -o ControlMaster=no and -o ControlPath=<folder>/%C. The link then
uses the master when one is open for that machine, and connects as usual when
none is. A machine that needs a password or a second factor then works for
as long as the master is open. No secret is stored.
A link connects as usual and does not use the folder in these cases:
- Your
~/.ssh/configgives the host aControlPath. The link uses the master you open there. - The host forwards the agent (
ForwardAgent). A connection through a master gets the forwarding of the master, so the link would lose its agent. - A socket path in the folder is too long. The limit is 107 characters on
Linux and 103 on macOS. Set
XDG_RUNTIME_DIRto a shorter folder. - tuios runs on Windows. Win32-OpenSSH has no
ControlMaster.
A master connection can outlive the network under it, for example when a
laptop sleeps. The link's ssh then waits on it, and the link's keepalives do
not reach it. The link pings its host when it carried nothing for 15 seconds.
When the host does not answer, the link ends, and the next dial runs ssh with
-o ControlPath=none and opens a connection of its own. The link does the
same after this machine wakes from sleep, and after a dial that timed out. A machine that needs
a password or a second factor fails that one dial, and the dial after it uses
the master again.
%C names a master by the local host, the remote host, the port and the
user. It does not include a ProxyCommand, so a host reached through two
different proxy commands shares one master. It includes the jump host only
on OpenSSH 10.6 and later. On an earlier version, a host reached through two
different jump hosts also shares one master.
tailscale_login is the origin of a Headscale server that sends the Tailscale
SSH check for this host, for example "https://headscale.example". tuios
shows and opens a sign-in link only on login.tailscale.com,
controlplane.tailscale.com and this origin, over https, with the host matched
exactly. Anything on the host can print the banner that carries the link. See
Tailscale SSH check mode.