Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
clackatoo

Reference

Single-page view · as markdown

The clackatoo command

clackatoo / Reference

clackatoo [--font <FAMILY>] [--font-size <POINTS>] [--renderer <API>] [--remote <HOST>]
clackatoo <COMMAND> [OPTIONS]

With no command, clackatoo opens the window, starting the daemon first if none is running. With --remote, the daemon is on another machine.

On failure, every command prints clackatoo: <what went wrong> to stderr and exits with status 1.

daemon #

clackatoo daemon [--scrollback-compaction <WHEN>]

Runs the daemon in the foreground until interrupted. Its log goes to stderr, at the level RUST_LOG sets (default info). Terminals live exactly as long as the daemon does: when it stops, their programs are hung up. Upgrading it doesn't stop it.

CLACKATOO_AGENT sets the agent chats talk to: a command speaking ACP, split at spaces. By default it's Claude Code's adapter, run with npx.

CLACKATOO_UDP_PORTS sets the UDP ports connections from other machines may move onto: a port, a range such as 60000-61000 (the default), or off.

Option
--scrollback-compaction <WHEN>When scrollback is compacted to save memory: lazy (the default), eager or off. See scrollback.

The daemon keeps its files in a per-user runtime directory: $XDG_RUNTIME_DIR/clackatoo/, or /tmp/clackatoo-<uid>/ if XDG_RUNTIME_DIR is unset.

File
daemon.sockThe Unix socket clients connect to.
daemon.lockLocked while a daemon runs, so only one runs at a time. A second clackatoo daemon fails with "a clackatoo daemon is already running".
shell/The scripts that make shells mark their commands, written by the daemon when it starts.

Anyone who can connect to the socket can run commands as you, so the directory must be accessible only by you. The daemon creates it with mode 0700, and refuses to start if it exists with any group or other permissions, or belongs to someone else.

Unix socket paths are limited to about 100 bytes. A very long XDG_RUNTIME_DIR makes the daemon fail with "path must be shorter than SUN_LEN".

A daemon the window starts runs in the background, with its log in daemon.log in the runtime directory.

upgrade #

clackatoo upgrade

Moves the running daemon onto this build of clackatoo without stopping it. Its terminals, the programs in them, their history and the workspaces all carry on. Run it after building or installing a new clackatoo.

The daemon replaces its own program with this one, keeping its process, so the programs in its terminals stay its children. It hands its state over as it goes: everything on screen and in history, the cursor and modes, workspaces and tabs. Every connection drops for a moment. Windows on this machine reconnect by themselves; windows on other machines say the connection was lost, and Enter reconnects them. Recordings stop.

Before handing anything over, the daemon checks that the new program is a clackatoo that can take over from it. If it isn't, or starting it fails, the daemon carries on as it was, and upgrade says why; the daemon's log has the details. A daemon won't upgrade to an older build. Daemons from before protocol 11 can't upgrade at all: upgrade says so, and clackatoo stop is the way to replace them, closing their terminals.

stop #

clackatoo stop

Stops the daemon, closing every terminal and hanging up their programs. It works on a daemon from any build of clackatoo, which matters because a build refuses to talk to a daemon speaking another version of the protocol: other commands then fail with "the daemon is from another version of clackatoo", and suggest clackatoo upgrade where the daemon can upgrade. If no daemon is running, stop says so and succeeds.

config #

clackatoo config [<NAME> [<VALUE>]] [--unset <NAME>]

Shows or changes the window's settings in client/startup.toml:

  • With no arguments, prints every setting as the file would hold it, with those the file doesn't set commented out at their defaults.
  • With a name, prints that setting's value, without quotes, so scripts can use it.
  • With a name and a value, sets it. The value is checked first: clackatoo config renderer metal fails, saying which values renderer takes.
  • With --unset, removes the setting, so it takes its default.

Changes keep the rest of the file, comments included, as it was, and the file is replaced all at once, so a window starting at the same moment reads either the old file or the new one.

language #

clackatoo language list
clackatoo language install <NAME>... | --all

The languages files are parsed and coloured in. Each is a pack in ~/.local/share/clackatoo/languages (or $XDG_DATA_HOME/clackatoo/languages).

  • list lists the languages clackatoo knows where to get, with where each comes from, marking those installed, then any other packs installed.
  • install downloads each language's grammar and highlight queries, with curl, from the grammar's own repository, at the version clackatoo was tried with, and checks each file is exactly what was tried. --all installs every language list lists. Installing one again replaces it.

Files opened after a language is installed are parsed in it. A file that was already open is parsed once a window attaches to it again, as when the window is next opened.

bridge #

clackatoo bridge [--replace]

Passes everything on its standard input to the daemon, and everything the daemon sends to its standard output, starting the daemon first if none is running. It exits when either side hangs up. --remote runs it on the other machine through ssh; see another machine.

Option
--replaceReplace the daemon that's running first, as when it's from another build: upgrade it if it can, keeping its terminals, or else stop it, closing them.

Client commands #

The other commands talk to a running daemon. Each accepts:

Option
--socket <PATH>Connect to this socket instead of the one in your runtime directory.
--remote <HOST>Use the daemon on another machine, through ssh, starting it there if it isn't running. See another machine.
--remote-command <COMMAND>With --remote, the command that runs clackatoo there. Default: clackatoo, or CLACKATOO_REMOTE_COMMAND.

Terminals are named by the number new prints and list shows. Numbers are never reused while the daemon runs.

list #

clackatoo list

One line per terminal: its ID, the PID of its program (- for a chat, which has none), its size, its status (running, exited (<code>) or killed (signal <n>)), and the window title its program set.

new #

clackatoo new [--size <COLSxROWS>] [--cwd <DIR>] [--record <FILE>] [-- <COMMAND>...]

Starts a terminal and prints its ID.

Option
--size <COLSxROWS>The terminal's size, like 120x40. Default 80x24. Each dimension must be between 1 and 4096.
--cwd <DIR>The working directory. Default: the current directory.
--record <FILE>Write everything the program prints to FILE, as a recording. The path is on the daemon's machine.
-- <COMMAND>...The program to run, and its arguments. Default: the daemon's $SHELL, or /bin/sh.

What a program sees in its environment is described in terminal emulation.

edit #

clackatoo edit <PATH>

Edits the file at PATH, from the current directory, in a new tab of the workspace last used, or in a new workspace if there are none. A file already being edited gets another pane on it. The window doesn't switch to the new tab yet.

run #

clackatoo run <TERMINAL> <WORDS>...

Types the words, joined with spaces, into the terminal, then presses Enter (a carriage return, \r).

send #

clackatoo send <TERMINAL> <TEXT>

Types TEXT exactly as given, without pressing Enter. To send control keys, use your shell's escapes: \e (Escape) or \r (Enter) in fish, $'\e' in bash. Fails if the terminal's program has exited.

capture #

clackatoo capture <TERMINAL> [--ansi]

Prints what the terminal is showing: one line per row, without trailing spaces or trailing blank lines. With --ansi, colours and attributes are kept as escape sequences, so printing the result in another terminal reproduces the screen. A terminal whose program has exited still shows its last screen.

resize #

clackatoo resize <TERMINAL> <COLSxROWS>

Changes the terminal's size, such as clackatoo resize 1 120x40. Its program is told (the kernel sends it SIGWINCH), and lines on the screen are rewrapped to the new width. A recording started with new --record stops here, because a recording has a single size.

close #

clackatoo close <TERMINAL>

Closes the terminal and forgets it. Its program is hung up (sent SIGHUP by the kernel), as when a terminal window closes.

ping #

clackatoo ping [--count <N>] [--interval <MS>]

Times how long the daemon takes to answer, and how long a key takes to come back from a program, which is most useful with --remote. It starts a terminal of its own running cat, and for each round:

  1. waits, then times a request that does nothing, which is what the connection costs;
  2. waits again, then times a key, from sending it to the update that shows cat's echo, which is what typing costs.

With --remote, each round also says whether it went over UDP or through ssh. To the same machine, where the network costs next to nothing:

$ clackatoo ping --remote localhost --count 5
connected to localhost in 146.1 ms
PING localhost: a request, then a key echoed by cat
round 1: request 0.168 ms, key echo 0.216 ms (over UDP)
round 2: request 0.147 ms, key echo 0.226 ms (over UDP)
round 3: request 0.158 ms, key echo 0.187 ms (over UDP)
round 4: request 0.147 ms, key echo 0.187 ms (over UDP)
round 5: request 0.151 ms, key echo 0.178 ms (over UDP)

--- localhost ping statistics ---
request   min 0.147 ms, median 0.151 ms, p90 0.168 ms, max 0.168 ms
key echo  min 0.178 ms, median 0.187 ms, p90 0.226 ms, max 0.226 ms

The connection time includes ssh logging in. When it's done, ping closes its terminal.

Option
-c, --count <N>How many rounds. Default: 10.
-i, --interval <MS>The wait before each request and each key, in milliseconds. Default: 200. The first message after a pause is the slowest, because whatever carries it has to wake up, and typing always has pauses, so a short interval flatters the result.

The window

clackatoo / Reference

The window is a client of the daemon, like the clackatoo command. It shows a workspace's terminals, keeps up with them as their programs print, and sends your keys to the one with the focus. Closing the window closes nothing: the terminals keep running in the daemon, and the next window shows them again, laid out as they were.

Opening it #

clackatoo

With no command, clackatoo opens a window on your workspaces: the one last used, at the tab and pane last used. If there are none, it makes one, showing your newest running terminal, such as one started with clackatoo new, or else a new terminal running your $SHELL. If no daemon is running, it starts one in the background first. Until the daemon answers, the window says it's connecting, and Esc quits. That daemon logs to daemon.log in the runtime directory, and keeps running after the window closes.

The window sizes itself so that its one pane holds 100×30 cells. Resizing the window resizes each pane's terminal to fit, and its program is told the new size. When several windows show one terminal, the terminal takes the size of whichever was last opened on it, resized or focused.

With --remote <HOST>, the window shows the workspaces on another machine, through ssh. See another machine.

OptionEnvironment variableSetting
--font <FAMILY>CLACKATOO_FONTfontThe font family. Default: your system's monospace font.
--font-size <POINTS>CLACKATOO_FONT_SIZEfont-sizeThe text size. Default: 12.
--renderer <API>CLACKATOO_RENDERERrendererThe graphics API to draw with: vulkan (the default) or opengl. See drawing.
--remote <HOST>Use the daemon on another machine.
--remote-command <COMMAND>CLACKATOO_REMOTE_COMMANDThe command that runs clackatoo there. Default: clackatoo.

An option on the command line wins over its environment variable, which wins over the setting.

The frame #

The window draws its own frame, in place of the desktop's:

  • The top bar holds your workspaces, each with a coloured dot and how many tabs it has (unless its name is just a number, as unnamed workspaces' are), the one shown picked out, and a + after them that makes a new one. The window's buttons sit at whichever end your desktop puts them, in the same order. Drag the bar to move the window, double-click it to maximise or restore it, and right-click it for the desktop's window menu.
  • The bottom bar starts with the focused pane's mode, then the tabs of the workspace shown, with a + after them that opens a new one. On its right are how far the view is scrolled or a message (such as why a command failed), how many panes the tab has, and a reminder of the key to press next. When the tabs need the room, the reminder goes first, then the count of panes.
  • The panes fill the space between the bars, side by side with a thin line between them. When a tab has more than one, a gold line runs round the focused pane.
  • The palette opens in the middle of the window for anything typed to clackatoo itself: commands, searches and new names.

Drag the window's edges or corners to resize it. Interface text, such as the names in the bars, is in your desktop's interface font. On GNOME, both the font and where the buttons go come from its settings.

With the mouse:

  • Click a workspace or tab to show it, and a pane to focus it.
  • Click a + to make a workspace, or open a tab.
  • Right-click a workspace or tab for Rename or Close. Rename opens the palette with the name to change. Clearing a tab's name names it after its focused pane's program again. Close closes every pane in it, hanging up their programs.

Settings #

Settings you rarely change, or change to tune how the window starts, live in startup.toml: $XDG_CONFIG_HOME/clackatoo/client/startup.toml, or ~/.config/clackatoo/client/startup.toml. The window reads it before anything else starts, which takes a few microseconds.

font = "JetBrains Mono"
font-size = 12
renderer = "opengl"

clackatoo config shows and changes them, keeping the rest of the file, comments included, as it was:

clackatoo config renderer opengl   # set one
clackatoo config renderer          # show one
clackatoo config --unset renderer  # back to its default
clackatoo config                   # all of them, and where the file is

A mistake in the file never stops the window opening. Each setting with a mistake, such as an unknown name or a value of the wrong kind, is reported on stderr and left at its default, and the rest still apply.

Keys, and what the window does, are configured in Lua instead, in init.lua beside startup.toml. See configuration.

The settings are on this machine. With --remote, the window's settings come from the machine the window opens on.

A font family is named as fontconfig knows it, as fc-list : family prints. Characters the font doesn't have, such as CJK text or emoji, come from fallback fonts, found the same way.

A daemon from another build #

Each build of clackatoo speaks one version of the protocol between windows and the daemon, and refuses a daemon speaking another. When the window finds a daemon from another build, it says so, and what Enter does depends on which is older:

  • An older daemon, from protocol 11 on: Enter upgrades it. The daemon becomes this build without stopping, so its terminals and the programs in them carry on, and the window opens on them.
  • An older daemon, from before protocol 11: it's too old to upgrade, so Enter stops it and starts a new one. This closes every terminal in the old daemon, and hangs up their programs.
  • A daemon newer than the window: the window says to open a window from the newer build instead. It never offers to replace a newer daemon.

Esc quits, leaving the daemon running. clackatoo upgrade and clackatoo stop do the same from the command line.

Losing the daemon #

If the connection to the daemon is lost, because the daemon stopped or, on another machine, the network went, the window says so and why, in place of the terminal:

  • Enter reconnects, starting a daemon if none is running. If the workspace shown is still there, the window returns to it, with each pane as it was, in the same mode and scrolled to the same place.
  • Esc quits.

When the daemon upgrades, every connection to it drops for a moment. A window whose daemon on this machine is still there afterwards reconnects by itself, without asking.

Modes #

The window is in one of two modes, like Vim:

  • Insert mode, where it starts. Keys go to the program.
  • Normal mode. Keys drive clackatoo. The bottom bar starts NORMAL, on gold, in place of INSERT, on lilac. A visual selection shows VISUAL or V-LINE, on blue.

Each pane has its own mode.

KeyInDoes
Ctrl+;anySwitches to normal mode: from insert mode, and out of a visual selection, a search or the command line.
Shift+Esc or Ctrl+\anyThe same.
inormal modeSwitches to insert mode, back at the live screen.
Ctrl+\, twiceinsert modeSends a real Ctrl+\ to the program.
Ctrl+CeitherInterrupts the program.

Plain Esc always goes to the program, since programs such as Vim and less use it. Ctrl+; costs programs nothing: a terminal has no code to send them for it, so none can be listening for it. It's found by where the key is, so on layouts where ; takes Shift, such as German, it's Ctrl and the key to the right of L.

Workspaces, tabs and panes #

The window shows your work at three levels:

  • Workspaces, one for each piece of distinct work, such as a project. Their names run along the top bar. Often there's just one.
  • Tabs, inside a workspace. They run along the bottom bar, numbered, and named after the title of their focused pane's program, unless you've named them.
  • Panes, which a tab is split into, side by side or stacked. Each shows a terminal, a chat with an agent, or a file being edited. A split shares its space equally until you resize it.

The daemon keeps all of them, as it keeps terminals. Closing the window closes nothing, and the next window opens on the workspace, tab and pane last used. Each window shows one workspace, so two windows can show two workspaces, or two tabs of one.

A pane closes when its program exits, as in other terminals. A tab closes with its last pane, and a workspace with its last tab. When the last workspace closes, so does the window.

The command line #

In normal mode, : opens a command line, in the palette in the middle of the window. Enter runs the command. Esc, or Backspace past the colon, closes it.

CommandDoes
:TerminalOpens a terminal in a new tab.
:|TerminalSplits the focused pane, with a new terminal to its right.
:|<TerminalThe same, to its left.
:-TerminalThe same, below it.
:-^TerminalThe same, above it.
:Agent, :|Agent…Opens a chat with an agent, in a new tab or beside the focused pane.
:Editor PATH, :|Editor PATH…, :e PATHEdits the file, in a new tab or beside the focused pane.
:1, :2…Moves to that tab of the workspace shown.
:workspace NAME, :ws NAMESwitches to the workspace called NAME, making it if there's none. Without a name, makes a new workspace, named with the lowest number no workspace has.
:rename NAMERenames the workspace shown.
:closeCloses the focused pane, hanging up its program.
:qThe same, unless the pane is the last showing a file with changes that aren't written. :q! closes it anyway, throwing them away.
:w, :wq, :e!…Write the focused pane's file, and the rest of Vim's commands for files: see Editing files.
:reloadRuns the config again, from the start.

The first character of a split says which way the divider runs, | upright or - flat, and < or ^ after it puts the new pane first. A mark alone opens a terminal, so :| is :|Terminal. Capitalised words name kinds of pane: Terminal, Agent and Editor, which takes the file's path after it. Lowercase words are the window's own commands, and numbers are tabs.

A new terminal starts in the directory that the pane it splits is in. A new tab's terminal starts in the directory of the workspace's focused pane. That's the directory of whatever is in front in that pane, such as a nested shell or an editor that changed directory, or else the shell's own.

Workspace names are unique, and can't contain @, which will name workspaces on other machines. If a command fails, such as renaming a workspace to a name another has, the bottom bar says why until the next key.

Resizing panes #

Drag the gap between two panes to move it. It moves a whole column or row at a time, and the programs on either side are told their new size as it goes. A double click on the gap shares that split's space equally again.

In normal mode, Alt+H, Alt+J, Alt+K and Alt+L move the focused pane's edge on that side by a column or row, so Alt+L widens a pane with a pane to its right. A pane with no edge on that side moves its other edge instead, so Alt+L narrows the rightmost pane. No pane gets smaller than one cell.

A new pane in a split gets the split's average share, so a split you never resized stays equal. Sizes are kept by the daemon, like the rest of the layout, and other windows show the change.

Moving around #

In normal mode, Ctrl+H, Ctrl+J, Ctrl+K and Ctrl+L move to the pane to the left, below, above or right. Tabs are :1, :2 and so on, and workspaces :ws NAME. Ctrl+B does all of this in either mode.

You stay in normal mode as you move: the pane you arrive at, including a pane a command just opened, is in normal mode too, and i starts typing into it. The pane you left keeps its mode and its place in history.

The window remembers where the focus has been, as Vim's jump list or a browser's history does:

KeysInDoes
Ctrl+O Ctrl+Inormal modeBack and forward through the panes of the tab shown.
Ctrl+Shift+O Ctrl+Shift+Ieither modeBack and forward through everywhere: panes, tabs and workspaces.

Going back and then moving somewhere new forgets the way forward, and panes that have closed are skipped. Each window has its own history.

Ctrl+B #

Ctrl+B and one more key work in either mode, as tmux's prefix does. While Ctrl+B waits, the bottom bar lists what it takes. Esc, or any other key, cancels. These keys, and the others on this page that the window takes rather than the program, are bindings like any you make, so the config can change them or take them away.

Ctrl+B, thenDoes
1–9Moves to that tab.
cOpens a new tab.
] [Moves to the next tab, or the one before, round from the last to the first.
,Renames the tab.
p, then 1–9Moves to that pane. Each pane shows its number meanwhile, counting left to right and top to bottom.
h j k lMoves to the pane to the left, below, above or right.
| -Splits the pane, with the new one to the right or below.
aOpens a chat with an agent to the right.
xCloses the pane.
wOpens the workspace picker.
o iBack and forward through the tab's panes, as Ctrl+O and Ctrl+I do in normal mode.
Ctrl+BSends Ctrl+B to the program.

The workspace picker lists the workspaces whose names hold what you type, those starting with it first. Up and Down, or Tab and Shift+Tab, pick one, and Enter moves to it. If nothing matches, Enter makes a workspace by the name typed.

Typing #

In insert mode, keys reach the program the way xterm sends them, which nearly every program understands:

  • Text types itself. Ctrl with a letter sends its control character (Ctrl+C, Ctrl+D…). Alt sends Escape before the key.
  • Enter, Backspace, Tab, Shift+Tab and Escape send their usual codes. Ctrl+Backspace deletes a word in most shells.
  • Arrows, Home, End, Insert, Delete, Page Up, Page Down and F1 to F12 send escape sequences, with any modifiers held.
  • When a program asks for them, arrows and the number pad send application codes, which Vim and less expect.

Ctrl+Shift+V pastes, and Ctrl+Shift+C copies text selected with the mouse; neither reaches the program. If the program has asked for bracketed paste, the text is marked as pasted, so editors don't treat it as typing and shells don't run it line by line. Otherwise newlines are sent as Enter.

Typing while scrolled up returns the view to the live screen.

Text can be typed through an input method, as Chinese, Japanese and Korean are, such as IBus on GNOME. What's being composed shows underlined at the cursor, with the input method's choices beside it, and the text chosen is typed as if keyed. The input method is used in insert mode, the command line and searches; in normal mode, keys are clackatoo's commands, so it's left out.

Scrolling #

The mouse wheel scrolls history in either mode, in the pane under the pointer. On the alternate screen, which full-screen programs use and which has no history, the wheel sends arrow keys instead, so less, Vim and htop scroll themselves. A program that takes the mouse gets the wheel itself.

While the view is scrolled up, the bottom bar's note shows how far, as ↑42, and new output doesn't move what you're reading.

The mouse #

Clicking a pane focuses it. What else the mouse does depends on the program in the pane.

Selecting. Usually the mouse selects text:

  • Dragging selects characters. Dragging past the top or bottom of the pane scrolls it.
  • A double click selects a word. Paths and URLs count as one word, so ~/src/main.rs comes whole.
  • A triple click selects whole lines.

Letting go copies the selection to the primary selection, and a middle click pastes it, as elsewhere on Linux. Ctrl+Shift+C, or y in normal mode, copies it to the clipboard. The selection stays highlighted until the next key or click. In normal mode, a click also moves the cursor there.

Programs that take the mouse. Programs such as Neovim, htop and less can ask for clicks, drags and the wheel, and get them while their pane is in insert mode on the live screen. The pointer is then an arrow rather than a text cursor. Hold Shift to select text anyway. A drag stays with the pane it started in, even when the pointer leaves it.

Programs that ask are also told when their pane gains or loses the focus, which Neovim uses to reload files changed meanwhile.

Normal mode #

Normal mode has a cursor of its own, a yellow block, which moves through the terminal's history and screen as one document. It starts where the program's cursor is, and the program's cursor is hidden until you return to insert mode.

KeysMove
h j k l, arrowsLeft, down, up, right. Moving up and down returns to the column you started from after passing shorter lines.
w b eTo the next word's start, this or the previous word's start, this or the next word's end. Words are runs of letters, digits and _, or runs of other characters; both cross line ends.
0 $, Home EndThe line's first column, its last character.
gg GThe oldest line of history, the screen's last line.
H M LThe top, middle and bottom lines in view.
Ctrl+D Ctrl+UHalf a screen down or up, view and cursor together.
Ctrl+F, Page Down Page UpA screen down or up. Ctrl+B is the window's.
Ctrl+E Ctrl+YThe view a line down or up, with the cursor kept in it.

Moving past the top of the view scrolls into history. In normal mode the view holds still while output arrives, so the cursor stays on its text. On the alternate screen the cursor moves over the screen only.

Selecting and copying #

KeysDoes
vSelects characters, from the cursor to wherever it moves.
VSelects whole lines.
oMoves the other end of the selection.
y, Ctrl+Shift+CCopies the selection to the clipboard, and returns to normal mode.
yy, YCopies the cursor's line.
y and a motionCopies what the motion moves over: yw to the next word, y$ to the line's end, yj this line and the next, ygg up to the top.
y and a text objectCopies the object around the cursor: yiw the word, yi" what's in the quotes, yib a command's output.
EscDrops the selection.

Any motion extends a selection, including gg, G and searches. What you copy to the clipboard lights up for a moment, so you can see what went. Copied text is what the program printed: a line that wrapped only because it reached the window's edge comes out as one line, blanks at the ends of lines are left out, and there is no newline after the last line, so pasting it into a shell never runs it.

Text objects #

After y, or in visual mode, i or a and one more key pick out a piece of text around the cursor, as in Vim. i takes what's inside, a takes it all. In visual mode, the object becomes the selection.

KeysPicks out
iw awA word, and with a the blanks after it (or before it, at the end of a line).
iW aWThe same, for anything between blanks.
i" a", i' a', i` a`What's between the quotes around the cursor, or the next pair on its line; with a, the quotes too.
i( a(, i[ a[, i{ a{, i< a<What's between the brackets around the cursor, across lines; with a, the brackets too. The closing bracket works as well.
ib abA command's output; with a, its prompt and command line too.

In Vim, b is another name for (. Here it's the command, the more useful thing in a terminal, and ( still means parentheses.

Commands #

When the shell marks its prompts and commands, clackatoo knows where each command's prompt and output are, and how it exited.

KeysDoes
[[ ]]Moves to the previous or next command's prompt.
yibCopies the output of the command the cursor is in.
yabCopies the command with its prompt and output, ready to paste into a bug report.
vib vabSelects them.

A command's output runs from its first line to the line before the next prompt, leaving out blank lines at the end. If the command printed nothing, yib says no output. If the cursor isn't in a command, the bottom bar says no block here.

Which shells mark their commands:

  • fish 4 and later, by itself.
  • bash, when it's your shell ($SHELL) and clackatoo starts it. clackatoo starts it with a startup script of its own, which reads your ~/.bashrc first, then adds the marks to PS1, PS0 and PROMPT_COMMAND. bash started any other way, such as clackatoo new -- bash, is left as it is.
  • zsh, and fish before 4, don't yet.

Commands are kept through an upgrade.

Searching #

KeysDoes
/Searches forward from the cursor. Type the text, shown in the palette, then Enter. Esc cancels.
?Searches backward.
n NSearches again, the same way or the other way.
EscClears the highlight on the last match.

A search ignores case unless the text has a capital letter in it. It covers all of history and wraps around at either end. The cursor moves to the match, which is highlighted. If there's none, the bottom bar says not found.

Copying and searching both happen in the daemon, which holds all of history, so neither waits for the window to fetch the lines involved.

The cursor #

In insert mode, the cursor has the shape the program asks for: a block, an underline or a bar, which Vim and shells in vi mode switch between. If the program hasn't asked, it's a bar. In normal mode it's always clackatoo's yellow block. In panes without the focus, and while the window isn't focused, either cursor is drawn as an outline.

Drawing #

The window draws with the GPU, and only when something changed: a quiet terminal costs no CPU. It redraws at most once per frame, so a flood of output costs one frame's drawing per frame.

On a machine with two GPUs, the window uses the one the system lists first, which is the one driving the screen: on a desktop, usually the graphics card the monitors are plugged into, and on a laptop, the integrated GPU. Frames drawn on the other GPU may never reach the screen. To choose differently, set WGPU_POWER_PREF to high for the faster GPU, or low for the one that uses less power.

The window draws with Vulkan, or with OpenGL where Vulkan can't draw. On some laptops with two GPUs, OpenGL opens a window much sooner: to choose between them, Vulkan asks both about themselves, and that can take more than a second, while OpenGL can use the screen's GPU without asking the other. When setting up the GPU takes over half a second, the window says so on stderr, and suggests trying OpenGL:

clackatoo config renderer opengl

If the API chosen can't draw, the window uses the other.

Box-drawing characters (─ │ ┌ ╭ ═…) and block elements (█ ▄ ░…) are drawn by clackatoo rather than taken from the font, so that borders in htop, tmux or Vim join up across lines at any size. The diagonal lines come from the font.

A character with others joined to it, such as a letter with accents, an emoji sequence joined with zero-width joiners (👩‍💻), or a keycap (#️⃣), is shaped: the font's own rules decide which glyphs draw it and where they go. An emoji presentation selector (U+FE0F, as in ❤️) draws the character from an emoji font.

Not supported yet #

  • Moving panes to another place in the layout.
  • Panes from other machines, as :Terminal@host will open. Today the window says this isn't built yet.
  • Flags (🇬🇧) and emoji with a skin tone (👍🏽) show as their parts: two letters for a flag, and the emoji then the tone. Terminals and programs disagree about how many columns these take, so they're printed as programs count them.
  • History lines are shown at the width they were printed at. A line that wrapped in a narrower window doesn't rewrap when the window gets wider.

Agents

clackatoo / Reference

A pane can hold a chat with an agent, such as Claude Code, instead of a shell. You type a message in the box along the bottom, the agent answers above it, and it works on the files in the pane's directory as it would in its own program.

Your messages are bubbles on the right. The agent's answers run the width of a column down the middle of the pane, with Markdown laid out as it's meant to look: headings, lists, quotes, links, and code in the terminal's font on a background of its own.

Opening a chat #

Does
Ctrl+B aOpens a chat to the right of the focused pane.
:AgentOpens a chat in a new tab.
:|Agent, :-Agent…Opens a chat beside the focused pane, as `:

A chat works in the directory a new terminal would start in: the one the pane it splits is in, or for a new tab, the one the workspace's focused pane is in. That's where the agent reads and changes files, and runs its commands.

Like any new pane, a chat opens in normal mode: i starts typing into it. The first chat starts the agent, which takes a second or two. Until then the chat says it's starting, and a message sent meanwhile waits and goes when it can.

Typing #

In insert mode, what you type goes in the box at the bottom.

KeyDoes
EnterSends the message.
Shift+EnterStarts a new line in the message.
Ctrl+CCopies what's selected in the message, or else, while the agent answers, asks it to stop.
Backspace, DeleteDelete a character, or with Ctrl, a word.
Left, RightMove by a character, or with Ctrl, a word. With Shift, they select.
Up, Down, Home, EndMove by a line, or to its start or end. With Shift, they select.
Ctrl+ASelects the whole message.
Shift+TabMoves the agent on to its next mode.
EscNormal mode. There's no program in a chat to need Esc.

Clicking in the box puts the cursor there, dragging selects, and a double or triple click selects a word or a line. A paste goes into the message whole, lines and all, without sending it. Text can be typed through an input method: what's being composed shows in the box, at the cursor, until it's committed.

A message beginning with / goes to the agent as typed, which is how agents take their own commands, such as Claude Code's /compact.

One message is answered at a time. While the agent answers, the button in the box's corner is a stop button, and Enter keeps what you typed until it's done. Clicking the button with nothing being answered sends the message, as Enter does.

What you've typed but not sent belongs to the window: another window, or this one after it closes, starts with an empty box.

The agent's settings #

Along the bottom of the box are the agent's settings for the chat, as the agent offers them. For Claude Code:

  • On the left, its mode: how much it does without asking. Manual asks before every change, Accept edits makes edits without asking, Plan plans before it acts, and Auto lets Claude decide what needs asking about. A new chat starts in Auto.
  • On the right, its model and effort, such as Sonnet 5.5 · High.
  • Beside them, how much of the model's context the chat fills, such as 38k / 200k.

Click a setting to open its menu, above the box. Up and Down move through it, Enter or a click picks, and Esc or a click elsewhere closes it. Shift+Tab steps through the modes without opening the menu.

The model and effort share a menu:

  • Models by family: Opus, Fable, Sonnet and Haiku, each with the version it picks and what it's for, and the one Claude Code recommends marked. Picking a family picks its newest version, and keeps up as new ones come out.
  • A family's versions: the ▸ button at the end of its row, or Right, shows its other versions, to pin the chat to one. Left hides them. A chat on a pinned version opens the menu with that version shown and ticked.
  • Effort on a slider, from Low to Max: click or drag along it, or with the slider picked out, press Left and Right. It stays open as you slide, so you can try a few. Default, beside it, leaves effort to Claude Code, which picks it for each model.

Models from another agent are grouped only when one choice in a family always means its newest version, as Claude Code's opus does. Otherwise they're listed one per row.

A chat keeps the settings you pick. If the agent stops, or the daemon is upgraded, the chat asks for them again as its session reopens.

Normal mode #

In normal mode, keys move the view instead:

KeyDoes
j k, Down UpA line down or up.
Ctrl+D Ctrl+U, Page Down Page UpHalf a view down or up.
gg GThe start or end of the chat.
i, a, EnterBack to insert mode.

The mouse wheel scrolls in either mode. While you're at the end, the view follows the answer as it grows. Scrolled up, it stays put, and the bottom bar says so.

What the chat shows #

Below each message you send, what the agent did with it, in order:

  • its words
  • its thinking, if it shows any, quieter and slanted
  • each tool it uses, on a line of its own: ○ while it runs, then ✓ once it has finished, or ✗ if it failed
  • each command it runs and each change it makes to a file, on a card of its own

A command's card has the command at its head, after $. While the command runs, the card says what it's for. Once it's done, it shows the last 8 lines of its output, which is where what went wrong usually is.

A change's card has what the agent did at its head, such as Edit Cargo.toml, and how many lines it adds and removes, such as +3 −1. Under that come the lines, with those added on green after + and those removed on red after −. A new file is all added. A card shows the part of the file changed, not the whole file. Once the change is made, Claude Code also gives 3 lines either side of it, and where in the file it is.

Lines are numbered by where they are in the file: added and unchanged lines by where they are now, and removed lines by where they were. Before the change is made, as while the agent asks to make it, an edit's lines have no numbers yet. A new file is numbered from 1.

Folded, a card shows its first 8 lines (for a command's output, its last 8), each cut off at the card's edge. Click a card with more to show to open it: its lines wrap, to be read whole. Click again to fold it. Whether a card is open belongs to the window.

The daemon keeps the last 32 KB of a command's output, and the first 256 KB of each side of a change.

While the agent works, the turn ends with Working…. A turn the agent didn't finish says why: Stopped., or what went wrong. The tab is named after the title the agent gives the conversation.

When the agent asks #

In Manual mode, before it does anything that changes something, such as writing a file or running a command that isn't only reading, the agent asks. Other modes ask about less, and Auto leaves it to Claude. Its question takes the box's place, with an answer on each button: the first way to say yes in the accent colour, and the ways to say no quieter.

Click an answer, or press its number. Esc says no. Ctrl+C says no and stops the answer.

What it asks about follows the agent's own settings. Claude Code reads its usual settings files, so commands you've allowed in Claude Code are allowed here too.

Which agent #

Chats talk to Claude Code. clackatoo speaks ACP, the Agent Client Protocol, which Claude Code doesn't speak itself, so the daemon runs Claude Code's ACP adapter, @agentclientprotocol/claude-agent-acp, with npx. That needs Node.js, and the first chat downloads the adapter. The adapter runs a copy of Claude Code that comes with it, at a version tried with clackatoo, not the claude you may have installed.

It uses the account Claude Code is logged in to. Log in once with claude in a terminal, and every chat uses that login. With a Claude subscription, chats count towards the subscription's limits, as Claude Code does.

To talk to another agent that speaks ACP, set CLACKATOO_AGENT for the daemon to its command, split at spaces. The daemon reads it when it starts.

One agent process serves every chat. Claude Code's adapter starts a Claude Code process for each chat as well, of about 250 MB, and the adapter itself takes about 115 MB. Closing a chat's pane ends its process, and closing the last chat stops the agent.

What lasts #

The daemon keeps chats, as it keeps terminals. Closing the window closes nothing: a chat keeps answering, and the next window shows it.

Upgrading the daemon keeps each chat's conversation, but stops the agent. An answer that was under way is cut off, and the turn says so. The next message starts the agent again and carries on the same conversation. A chat from a build before chats were kept as turns comes through as one block of its text. Tools from a build before cards come through as lines, without their output or changes. Changes from a build before line numbers come through without them.

If the agent stops on its own, the chat says so, and the next message starts it again, carrying on the same conversation.

When the daemon itself stops, its chats go with its terminals.

clackatoo capture prints a chat as text: each message after a >, and what the agent said.

Not yet #

  • Text in the conversation can't be selected or copied yet: only the message being typed.
  • The thinking and tool calls of a turn don't fold away under a "Worked for…" line yet.
  • Commands the agent runs show their output once they're done, not as they run. They'll run in terminals beside the chat, which you can watch and take over.
  • Claude's 5-hour and weekly usage limits aren't shown.
  • Images can't be pasted into a message.
  • / commands aren't listed or completed as you type them.

Another machine

clackatoo / Reference

The window and the clackatoo command can use the daemon on another machine instead of this one: your desktop from your laptop, or a server. The terminals run there, and keep running when you disconnect.

clackatoo --remote desktop                  # a window on desktop's workspaces
clackatoo list --remote desktop             # its terminals
clackatoo new --remote jack@server -- htop  # start one there

The host is anything ssh accepts: a hostname, user@host, or a name from your ~/.ssh/config, where ports, keys and jump hosts belong.

How it connects #

--remote host runs ssh host clackatoo bridge. On the other machine, the bridge starts the daemon if it isn't running, then passes everything between ssh and the daemon.

Through it, the window or command first checks that the daemon speaks its version of the protocol, as it does on this one. Then the two talk over QUIC, a network protocol that runs several independent streams over one connection, carried inside the ssh connection.

ssh does the logging in, and it's how each end knows the other is genuine. The daemon only takes QUIC connections that come through ssh, and the window only trusts the certificate the daemon sent it through ssh. Anyone who can use the daemon could already log in to that machine as you.

Each terminal's screen updates come on a QUIC stream of their own, apart from everything else, so a lost packet holds up only the terminal it belonged to.

Moving onto UDP #

Once connected, the window or command tries reaching the daemon straight over UDP, at the address ssh reached that machine at. If that works, the connection moves there, out of ssh:

  • It survives your network changing, such as moving from home Wi-Fi to a phone's hotspot, or a short sleep. ssh's connection dies when that happens; the QUIC connection carries on from the new network, and the window doesn't notice.
  • Keys come back a little faster, with ssh out of the way.

It finds out with a few probes, small datagrams the daemon sends straight back, while the connection carries on inside ssh, so nothing waits on the attempt. If none comes back within two seconds, as when a firewall blocks UDP, the connection stays inside ssh. If UDP stops getting through later, the connection goes back into ssh after about ten seconds, if ssh's connection is still up, and tries UDP again a minute after.

clackatoo ping --remote host says which way each round went: (over UDP) or (through ssh).

The daemon takes UDP on the first free port from 60000 to 61000, the same range as mosh, when the first connection through ssh arrives. It never takes a new connection over UDP, only packets for connections already set up through ssh. Probes carry a key the daemon only gives out through ssh, and it answers nothing else, so the port gives nothing away to a scan.

Firewalls #

A firewall on the other machine has to let UDP in on those ports. If it already does for mosh, there's nothing to do. Otherwise, with ufw:

sudo ufw allow 60000:61000/udp

or with firewalld:

sudo firewall-cmd --permanent --add-port=60000-61000/udp && sudo firewall-cmd --reload

To use other ports, or none, set CLACKATOO_UDP_PORTS for the daemon on the other machine: a port, a range such as 60100-60200, or off to keep every connection inside ssh. The daemon reads it when it starts, and one started by --remote gets it from the remote command:

clackatoo --remote server --remote-command 'env CLACKATOO_UDP_PORTS=60100-60200 clackatoo'

Through a jump host (ProxyJump), ssh reached the other machine at an address on its own network, which you usually can't reach directly, so the connection stays inside ssh.

What the other machine needs #

  • clackatoo, built from the same version. A daemon from another build speaks another protocol, which the window offers to replace.

  • clackatoo on the PATH ssh's commands get. That is often shorter than an interactive shell's, and may leave out ~/.cargo/bin or ~/.local/bin. If it isn't there, the error is the remote shell's "command not found". --remote-command names it instead:

    clackatoo --remote desktop --remote-command ~/.cargo/bin/clackatoo
    

    The command is run by the remote shell, so it can be any command line that ends by running clackatoo, such as env XDG_RUNTIME_DIR=/tmp/ck clackatoo. Setting CLACKATOO_REMOTE_COMMAND saves typing it each time.

  • A way to log in. A key loaded into ssh-agent logs in without asking anything. Otherwise, the window asks for ssh's password, or the key's passphrase, itself, in place of the terminal, and asks again on every reconnect. It also asks whether to trust a host ssh hasn't seen before. The clackatoo command leaves ssh to ask on the terminal it runs in.

  • On a server nobody is logged in to: systemd removes your runtime directory (/run/user/<uid>) when your last session ends, taking the daemon's socket with it. The daemon keeps running, but the next connection can't find it and starts another. Run this once on the server so your runtime directory stays:

    loginctl enable-linger
    

Options #

OptionEnvironment variable
--remote <HOST>Use the daemon on HOST.
--remote-command <COMMAND>CLACKATOO_REMOTE_COMMANDThe command that runs clackatoo on the other machine. Default: clackatoo.
CLACKATOO_SSHThe program run as ssh. Default: ssh. It is given ssh's options, then --, the host and the command.

clackatoo passes ssh a few options of its own:

  • -T: no terminal on the other end; the bridge doesn't want one.
  • ConnectTimeout=10: an unreachable host fails after 10 seconds.
  • ServerAliveInterval=5 and ServerAliveCountMax=3: a connection that has died, such as when the laptop sleeps or changes network, is noticed within 15 seconds. A connection that has moved onto UDP carries on without it.

When the other machine stops answering #

If what the window sends goes unanswered for about three seconds, a red notice at the right of the top bar says so: No contact with desktop for 12 s, counting up. The window stays as it was, and anything you type waits to be sent. The notice goes as soon as the other machine answers again, as when your laptop finds a network.

An idle window notices within about half a minute, at its next keep-alive.

When the connection drops #

A connection inside ssh drops when ssh's does. One on UDP drops after ten minutes without hearing from the other end, such as a laptop asleep for longer than that. An idle window stays connected: it sends the daemon a few bytes every 25 seconds, which also keeps your router from forgetting the connection.

When the connection drops, the window says so, and why:

  • Enter reconnects, to the same terminal if it's still running. The window keeps its mode, its scroll position and the history it fetched.
  • Esc quits.

Nothing on the other machine stops when the connection drops: the terminals carry on, and output that arrives meanwhile is there when you reconnect.

A daemon from another build #

If the daemon on the other machine speaks another version of the protocol, the window says so, as it does on this machine. Enter replaces that daemon using the clackatoo installed there: it upgrades it if it can, keeping its terminals, or else stops it, closing them. If that clackatoo is itself from another build, the window says to update it.

The clackatoo command doesn't replace anything. On the other machine, clackatoo upgrade upgrades its daemon, and clackatoo stop stops it.

Two windows on one terminal #

A terminal has one size, so when windows of different sizes show the same terminal, such as your laptop's and your desktop's, they take turns: the terminal takes the size of whichever window last opened on it, was resized or was focused.

Typing over a slow connection #

Every key goes to the other machine and back before it shows, so a connection with 100 ms round trips shows each key 100 ms late. On a local network this is a millisecond or two.

clackatoo ping --remote host measures it: how long the daemon takes to answer, and how long a key takes to come back from a program. clackatoo's own share is small. To the same machine, where the network costs next to nothing, a key comes back in about a fifth of a millisecond over UDP, and a third through ssh, so nearly all of anything above that is the network.

Configuration

clackatoo / Reference

The window's behaviour is configured in code: an init.lua, as Neovim's is, written in Luau, a fast dialect of Lua with optional types and a sandbox. Without one, the window works as it always does. Settings needed before the window starts, such as the font, stay in startup.toml, beside it.

-- ~/.config/clackatoo/client/init.lua

-- tmux's split keys, as well as clackatoo's own.
client.keys.map("niv", '<C-b>"', function(ev) ev.pane:split("below") end)
client.keys.map("niv", "<C-b>%", function(ev) ev.pane:split("right") end)

-- Closing a pane takes :close, not a key.
client.keys.unmap("niv", "<C-b>x")

-- x in normal mode closes a chat, and does what it always did elsewhere.
client.keys.map("n", "x", function(ev)
  if ev.pane.kind ~= "chat" then return ev:propagate() end
  ev.pane:close()
end)

Where it goes #

~/.config/clackatoo/        or $XDG_CONFIG_HOME/clackatoo/
  client/init.lua           the window's config
  client/lua/               modules for it to require
  client/startup.toml       the window's settings, needed before it starts
  lua/                      modules shared with the daemon's config, once it has one

require("theme") loads theme.lua from client/lua/, or else lua/, or theme/init.lua from either. A dot in a name is a directory: require("keys.tmux") is keys/tmux.lua. Each module runs once, and later requires get what it returned.

The config runs once for the client, the process that opens windows, so its bindings apply in every window. It runs on a thread of its own, alongside the window starting up, and a typical config takes under a millisecond. The window never waits for it to show itself: keys pressed before it has run do what they do without it.

:reload runs the config again, from the start. Everything the old config bound or listened for goes, and the window's own keys come back first.

How a key travels #

Each key you press goes through, in order:

  1. Keypress handlers, added with on("keypress", …), in the order they were added. A handler on a pane only hears keys pressed in that pane.
  2. Bindings, the window's own and those made with client.keys.map.
  3. The pane: the program in a terminal, the box in a chat, or the pane's normal mode.

A handler lets the key go on unless it calls ev:stop(), which ends its journey there. A binding stops its keys unless it calls ev:propagate(), which lets them go on to the pane, as if they weren't bound. If the binding took several keys, such as <C-b>x, they all go on, in order.

Whether a key goes on is decided when the handler finishes, or first waits for something, as pane:split waits for the new pane. Calling stop or propagate after that is a mistake. A key waits at most 300 milliseconds for the config to decide. After that, it's as if the config had done nothing more: a binding's keys stop there, and a key a handler heard goes on.

Keys #

client.keys.map(modes, keys, function) binds keys in one or more modes, replacing what they were bound to. client.keys.unmap(modes, keys) takes a binding away, the window's own included.

Modes are written as letters, together: "niv" is all three of normal, insert and visual. c is typing into a pane's prompt, such as a search.

Keys are written as in Vim. A character is itself, and <…> holds a key with modifiers or a named key:

WrittenIs
<C-b>, <A-h>Ctrl+B, Alt+H
<C-O>, <C-S-o>Ctrl+Shift+O. Shift is part of a letter: <C-o> is Ctrl+O.
<S-Tab>Shift with a named key
<Esc>, <CR>, <Tab>, <BS>, <Del>, <Insert>, <Space>Named keys. <Enter> and <Return> are <CR>.
<Up>, <Down>, <Left>, <Right>, <Home>, <End>, <PageUp>, <PageDown>, <F1>–<F12>More named keys
<lt><

Several keys in a row, such as <C-b>", make a sequence. A key that starts one waits for the rest as long as it takes, as tmux's prefix does, and a key that matches nothing drops the keys typed so far. So a binding whose keys start a longer binding's, such as <C-b> beside <C-b>x, can't be reached.

Events and handles #

client.on("keypress", function) listens for keys anywhere. Windows, tabs and panes have :on too, to listen only there. Each returns a subscription, whose :off() stops it. A pane's or tab's handlers go when it closes.

A handler gets the event, ev:

FieldIs
ev.keyThe key, as a binding writes it, such as <C-b> or q
ev.keysFor a binding, all of its keys
ev.textThe text the key types, if any
ev.modeThe mode's letter
ev.pane, ev.windowWhere it was pressed

Windows, workspaces, tabs and panes are handles: values that stand for one of them, whose fields are read as they are at the time. Reading a pane that has closed is a mistake. Two handles to the same pane are equal.

FieldsMethods
clientwindows, focused (the focused pane)on, keys.map, keys.unmap
A windowworkspace, tab, paneon
A workspacename, tabson
A tabname, panes, focused, workspaceon
A panekind ("terminal", "chat" or "editor"), title, tabon, split, close, focus

pane:split(side) puts a new pane on that side, "right", "left", "below" or "above", of the same kind, or another with { kind = "chat" }. An editor can't be made from the config yet, so splitting one needs a kind. It waits for the daemon to make it, then focuses it, as Ctrl+B | does, and returns it:

client.keys.map("n", "<C-b>t", function(ev)
  local below = ev.pane:split("below")
  below:split("right")
end)

While it waits, the window carries on, and so does the key. Each handler and binding runs in a coroutine, a function that can pause partway and carry on later, so waiting holds up nothing else.

Nothing is shown while init.lua first runs, so client.focused is nil then: handles are for handlers.

When it goes wrong #

A mistake, such as a key that doesn't exist, a mode that isn't one, or a pane that has closed, is shown in the bottom bar, with the file and line, until the next key, and printed on stderr in full. A mistake in init.lua itself stops it there, as an error in any Lua program does: what it bound before that line stays bound.

A handler or binding that runs for more than a quarter of a second at a time, as a loop that never ends would, is stopped there, and its key is decided as if it had done nothing more.

print writes to stderr.

Not yet #

  • The daemon has no config yet.
  • init.lua isn't run again when it changes: use :reload.
  • Bindings made in Lua don't show in Ctrl+B's list in the bottom bar.
  • Keypresses are the only event.
  • There are no type definitions for editors to complete the API with.

Editing files

clackatoo / Reference

An editor pane edits one file, with Vim's keys. Its text lives in the daemon, in a buffer, as a terminal's screen does, and each window showing it keeps a copy that it changes as you type, so a key never waits for the daemon.

:e src/main.rs          edit a file in a new tab
:|Editor src/main.rs    the same, beside the focused pane
:w                      write it
:q                      close the pane

Opening a file #

CommandDoes
:e PATH, :edit PATH, :Editor PATHOpens the file in a new tab.
:|Editor PATH, :|<Editor PATH, :-Editor PATH, :-^Editor PATHOpens it beside the focused pane: right, left, below or above, as the split marks do for terminals.
clackatoo edit PATHFrom a shell: opens it in a new tab of the workspace last used. See the CLI.

A relative path is from the focused pane's directory: for a terminal, the directory of the program in front, as for a new terminal; for an editor, the directory its file was opened from. ~/ is your home directory.

A file that's already open is shown again, not read again: two panes on one file are two views of one buffer, each with its own cursor. A file that doesn't exist yet opens empty, and is made when it's first written. A directory, or a file that isn't UTF-8 text, can't be opened, and the bottom bar says so.

Buffers #

The daemon keeps each buffer, as it keeps terminals:

  • Every window edits the same text. Edits made in one window, or by anyone else, show in the others, and move their cursors with the text around them.
  • Edits made at once meet. If an agent edits a file while you type in it, both edits are kept, and every copy ends up with the same text. The daemon puts edits in order, and brings each one up to date past those made since, by operational transformation, as Google Docs does.
  • Undo undoes your edits, not anyone else's. u takes back your last change, from whichever window you made it in, and leaves an agent's edits made since in place.
  • Nothing is lost when the window closes, or the daemon upgrades. Buffers come through an upgrade with their undo history.

Each command in normal mode is one undo step, as is each go of insert mode, with the command that started it, such as cw.

A buffer closes with its last pane, unless it has changes that aren't written. :q won't close that pane, and :q! throws the changes away. A pane closed another way, such as with Ctrl+B x or by closing its tab, keeps the buffer's changes, unseen: opening the file again shows them.

Languages #

A file in a language that's installed is coloured as the language's grammar reads it: keywords, strings, comments, functions, types and so on. Install languages with clackatoo language:

clackatoo language list            what can be installed, and what is
clackatoo language install rust    install one, or several
clackatoo language install --all   install every one

clackatoo knows where to get Bash, C, CSS, Go, HTML, JavaScript, JSON, Lua, Python, Rust, TOML, TypeScript (and TSX) and YAML. A file's language is found from its name: its extension, such as .rs, or the whole name, such as .bashrc.

How it works:

  • Grammars are tree-sitter's, compiled to WebAssembly by their authors, and run in the daemon by wasmtime. A grammar can't reach anything outside itself, so one that goes wrong can't take the daemon, and every terminal it keeps, with it. Nor does a new language need a new clackatoo.
  • The daemon parses each file once, whichever windows show it, on a thread of its own, so typing never waits for a parse. After an edit, only what it changed is read again: about a tenth of a millisecond in a big source file. The colours of text you've just typed follow within a frame or so; until then, the colours already there move with the text.
  • Opening a file reads it all, which takes about 50 ms for an 80 KB file in a language not used since the daemon started, most of it getting the grammar ready, and less after.

Language packs #

Each language is a directory in ~/.local/share/clackatoo/languages (or $XDG_DATA_HOME/clackatoo/languages), which clackatoo language install writes, and you can write yourself for a language clackatoo doesn't know:

File
language.tomlgrammar: the grammar's name, which the WebAssembly is built with (tree_sitter_NAME). extensions: the endings of the names of files it's for, without the dot. filenames: whole names of files it's for.
grammar.wasmThe grammar, as tree-sitter build --wasm makes it. Many grammars' releases have one.
highlights.scmThe query naming what each piece of text is, as tree-sitter's own highlighter reads it: where two patterns name the same text, the first wins.
grammar = "rust"
extensions = ["rs"]
filenames = []

A query's names are coloured by the first part: @function.method.call as a function, @string.escape as an escape. A pack the daemon can't load is passed over, and its log says why.

Writing #

CommandDoes
:w, :writeWrites the file.
:w!Writes it even if it's changed on disk since it was read or last written.
:wq, :xWrites it, then closes the pane. :wq! writes it even if it's changed.
:q, :quitCloses the pane, unless it's the last showing a file with changes not written.
:q!Closes the pane, throwing away the file's changes not written.
:e!Throws away the changes not written, reading the file again. That's an edit like any other: u brings the changes back.

Writing writes a new file beside the old one, with its permissions, then renames it over the old one, so the file is never half written. If something else has changed the file since it was read or last written, :w refuses, so as not to write over the change; :w! writes over it. The bottom bar shows [+] after the file's name while it has changes that aren't written.

Keys #

An editor starts in normal mode. The bottom bar shows the mode, the file's name, and the cursor's line and column.

Moving #

KeysMoves to
h j k l, the arrowsThe next character or line. A count moves that many.
w b eThe start of the next word, the start of this one or the one before, the end of this one or the next.
W B EThe same, for anything between blanks.
0 ^ $The line's start, its first character that isn't blank, its last character.
gg GThe first line, the last. 12G is line 12.
f t F T and a characterThe next one on the line, or the character before it with t; back with F and T. ; does it again, and , back.
/ ?Searches forward or back. n and N find the next and previous match, going round the file's ends. Case is ignored unless the search has a capital letter.
Ctrl+D, Ctrl+UHalf a page down or up.
Ctrl+F, Page Down, Page UpA page down, or up with Page Up.
zt zz zbScrolls the cursor's line to the top, middle or bottom.

Changing #

KeysDoes
i a I AInsert mode before or after the cursor, at the line's first character that isn't blank, at its end.
o OInsert mode on a new line below or above, indented as the cursor's line is.
d, c, y and a motionDeletes, changes or copies what the motion moves over: dw, c$, y2j.
d, c, y and an objectThe same, for a text object: diw, ci", ya(.
dd cc yyWhole lines: the cursor's, and with a count, those after it.
x XDeletes the character under the cursor, or before it.
D C YDeletes or changes to the line's end, or copies the line.
s SChanges the character under the cursor, or the line.
r and a characterReplaces the character under the cursor with it.
JJoins the next line onto the cursor's, with a space.
p PPuts what was last deleted or copied after the cursor, or before. Lines go on the line below or above.
u Ctrl+RUndoes your last change, or redoes the last undone.
.Makes the last change again, with what was typed in insert mode after it. A count makes it that many times.
v VVisual mode, by characters or whole lines. d, x, c, s and y act on what's selected, o goes to its other end, and i or a and an object selects the object.

cw changes to the end of the word, not up to the next word, as in Vim.

What y copies goes on the clipboard too. What d and c delete only goes where p puts from, so deleting doesn't lose what you copied from somewhere else.

Insert mode #

Keys type into the file. Esc, or Ctrl+; as in a terminal, goes back to normal mode.

KeysDoes
EnterA new line, indented as the line before.
TabIndents to the next multiple of 4 columns, with spaces, or with a tab on a line indented with tabs.
Backspace, Ctrl+HDeletes the character before the cursor, joining the line to the one before at its start.
DeleteDeletes the character under the cursor.
Ctrl+WDeletes the word before the cursor.
Ctrl+UDeletes back to the end of the line's indent, or from there, to the line's start.
The arrows, Home, EndMove, and start a new undo step.

A count before i, a or o types what's typed that many times: 3ix Esc types xxx.

The mouse #

A click puts the cursor where it's clicked, and the wheel scrolls.

Not yet #

  • Everything a language server gives: errors, completion, going to definitions. Language servers will run in the daemon.
  • Text objects and indenting by the grammar, such as af for a function.
  • Languages inside others, such as code in a Markdown file, or CSS in HTML: they're coloured as the outer language only. So there's no Markdown yet.
  • Choosing the colours. They're the palette's named colours where there's one to suit, so they follow the terminal's.
  • Line numbers. Long lines don't wrap: the view scrolls sideways to follow the cursor. A tab is always 4 columns wide.
  • $ and l stop at a line's last character that isn't blank, as on a terminal's screen, so blanks at a line's end can be reached only with A and D.
  • Files with Windows line endings show ^M at each line's end.
  • :e opens a new tab, rather than replacing the file the pane shows, as Vim's does.
  • clackatoo edit opens the tab, but the window doesn't switch to it.
  • Only one register; no macros, marks, :s, >>, ~ or %.
  • Dragging the mouse to select.
  • A change typed just as the connection to the daemon drops can be lost.

Terminal emulation

clackatoo / Reference

clackatoo emulates enough of xterm for shells, ncurses programs such as htop, and vim, under TERM=xterm-256color. Sequences it doesn't support are ignored, as xterm ignores ones it doesn't know.

The environment #

A program started in a clackatoo terminal gets the daemon's environment, with these changes:

VariableValue
TERMxterm-256color
COLORTERMtruecolor
TERM_PROGRAMclackatoo
TERM_PROGRAM_VERSIONThe clackatoo version.

Variables describing whatever terminal the daemon was started in are removed, so programs don't think they're inside it. These include TMUX, TMUX_PANE, STY, VTE_VERSION, KITTY_WINDOW_ID, WEZTERM_PANE, ALACRITTY_WINDOW_ID, ITERM_SESSION_ID, WT_SESSION, COLUMNS and LINES.

The program runs in its own session, with the terminal as its controlling terminal, so job control and Ctrl-C work as usual.

Screen behaviour #

  • Scrollback: the primary screen keeps the last 10,000 lines that scroll off its top. Lines only enter scrollback when the whole screen scrolls. Scrolling inside a region an app has narrowed (as vim does) discards them. Scrollback is stored as compact text, typically under 100 bytes a line, and remembers which lines were wrapped, so they can be rewrapped to another width. Compacting a line takes about as long as printing it did, so by default the daemon waits until a terminal's output has paused for 100 ms. Lines that scroll away during a flood (cat of a huge file) are never compacted at all. Until then, history keeps lines as they were on screen, about 8 bytes per column, capped at 16 MiB per terminal. The daemon's --scrollback-compaction option changes this: eager compacts each line as it scrolls off, which is the smallest but the slowest, and off never compacts, which is the fastest but the largest.
  • Resizing: lines on the primary screen are rewrapped to the new width, and the cursor stays on the same character. When the screen gets shorter, blank lines below the cursor go first, then lines above it move into scrollback. The alternate screen is cut off or extended instead, because the full-screen programs that use it redraw after a resize.
  • Alternate screen: full-screen programs switch to a second screen with no scrollback. Leaving it restores the primary screen and cursor exactly.
  • Wrapping: printing in the last column leaves the cursor there, and the line wraps only when the next character arrives.
  • Background colour erase: clearing part of the screen fills it with the current background colour.
  • Wide characters take two columns. A wide character that doesn't fit at the end of a line wraps to the next one.
  • Combining characters (accents, variation selectors) join the character before them. So do characters after a zero-width joiner, so emoji sequences such as 👩‍💻 stay one character.

Supported sequences #

KindSupported
Control charactersBS, HT, LF, VT, FF, CR, SO, SI. BEL is accepted and ignored.
ESC7 8 (save/restore cursor), D (index), E (next line), H (set tab stop), M (reverse index), c (full reset), #8 (screen alignment test), ( and ) with B (ASCII) or 0 (DEC line drawing)
CSI cursorA B C D E F G H I Z ` a d e f, and s u without parameters
CSI editing@ J (including 3J to clear scrollback) K L M P S T X b (repeat)
CSI otherg (clear tab stops), r (scroll region), m (SGR), !p (soft reset), q (cursor shape: 0 the user's default, 1 and 2 block, 3 and 4 underline, 5 and 6 bar; blinking isn't drawn)
ESC keypad= and > (application and numeric keypad)
ANSI modes (CSI h/l)4 (insert), 20 (line feed is newline)
DEC modes (CSI ? h/l)1 (application cursor keys), 6 (origin), 7 (autowrap), 25 (show cursor), 47, 1047 and 1049 (alternate screen), 66 (application keypad), 1048 (save cursor), 2004 (bracketed paste), 9, 1000, 1002 and 1003 (mouse tracking), 1005, 1006 and 1015 (mouse encodings), 1004 (focus events)
SGRBold, dim, italic, blink, inverse, hidden, strikethrough and their resets. Underlines: single, double (21 or 4:2), curly (4:3), dotted (4:4), dashed (4:5). 8, 16, 256 and 24-bit colours for text, background and underline (58), in both the ; and : spellings.
OSC0 and 2 (window title), 7 (working directory, as a file:// URL), 133 (shell integration: A prompt, C output, with an optional cmdline_url=, and D exit status; see commands)

Queries answered #

QueryReply
CSI c (primary device attributes)CSI ? 62 ; 22 c: a VT220 with colour
CSI 5 n (status)CSI 0 n
CSI 6 n and CSI ? 6 n (cursor position)CSI <line> ; <col> R, with ? for the second form. Relative to the scroll region in origin mode.
CSI > q (terminal version)DCS > | clackatoo <version> ST

Not supported yet #

These are ignored:

  • OSC 8 (hyperlinks) and OSC 52 (clipboard).
  • Secondary device attributes (CSI > c) and the kitty keyboard protocol.
  • Left and right margins (CSI s with parameters).
  • Variation selector 16 does not widen the character before it.

Recordings

clackatoo / Reference

clackatoo new --record <FILE> saves everything the terminal's program prints. Replaying a recording through clackatoo's emulator, at the recorded size, rebuilds the same screen every time, however the bytes are split up. clackatoo's tests are built on recordings of real programs, and its throughput benchmarks replay one too.

Format #

A single header line, then the program's output, byte for byte:

clackatoo-recording v1 cols=80 rows=24
<raw output…>

The header is UTF-8 text ending in a newline. It starts with clackatoo-recording v1, followed by space-separated key=value fields:

Field
colsThe terminal's width. Required.
rowsThe terminal's height. Required.

Readers ignore fields they don't know, so later versions can add fields without breaking v1 readers. Everything after the header's newline is output, including any further newlines.

The output is written as it arrives, so a recording can be read while the program is still running. Input typed into the terminal is not recorded. A recording has one size, so resizing the terminal stops it.

Privacy #

A recording contains everything the program displayed: paths, hostnames, file contents and anything else that appeared on screen. Check a recording before sharing it.

Peios Learn — documentation for the Peios project.

Built with Trail.