splash

cy is an experimental terminal multiplexer. It builds on the traditional paradigms of tmux and screen with advances in form, function, and customization.

Key features:

Why not tmux?

tmux is just not weird enough.

cy shares some basic similarities with tmux: it has panes, it sticks around even if you disconnect, and multiple clients can connect at the same time. But for cy to be a compelling alternative, it has to do more than just mimic tmux's functionality and be written in a fashionable systems programming language.

I started writing cy because I felt like terminal multiplexers had stagnated. There have been some recent attempts to modernize the concept, but I didn't feel like they went far enough.

cy improves on tmux in three main ways:

  1. Session playback: cy records your terminal sessions and lets you play back and search through them. It can also detect the commands you run and revisit the output they produced on demand.
  2. Configuration: cy uses a real programming language, Janet, for configuration.
  3. Interface: cy has a simple layout designed for use on large screens. If that's not enough, you can also build arbitrary layouts using Janet.

Session playback

If you use tmux, you might be familiar with copy-mode, which allows you to view lines from the scrollback buffer. This is nice when you're using a program like bash, where you issue commands that produce static output.

But by definition, copy-mode ceases to be useful when you use a program with any interactivity, such as vim: you only see the most recent state of the screen, and certainly cannot see what it used to look like.

cy solves this problem by recording all of your terminal sessions. In replay mode you can seek, play back, and search through the history of a pane--regardless of the application that was running in it. cy saves these recordings to disk and allows you to open them later at your leisure. You can also quickly search through all of the sessions you have recorded using search mode.

This sounds like it would be a lot, but it isn't: after many hundreds of hours of work in the terminal, all of my recordings occupy a total of 200MB (!) on disk.

After enabling command detection, cy provides an augmented version of ctrl-r (the command history function common in shells) with which you can see the output of every command you have ever run, in addition to inserting commands from the past into your current shell.

Without trying it for yourself, it's hard to appreciate just how useful it is to be able to go back in time to replay everything you've seen or done in the terminal. cy aims to augment your memory in a way that other programs cannot.

Interface

placeholder

cy has a flexible layout system. In addition, the panes you run behave like buffers in vim: they stick around regardless of whether they are visible on your screen. In other words, unlike tmux, the visual presentation of panes is unrelated to their state.

Most of the time, however, you can keep things simple, because cy makes it easy to switch between panes using fuzzy finding (with previews!). It also contains a minimal, filesystem-like abstraction for grouping panes together.

Configuration

Anyone who has tried to do anything sophisticated with tmux runs into a familiar set of problems:

  1. tmux's configuration language is hacky and primitive, which makes it hard to do anything interesting without running an external command.
  2. Its key binding system is limited.

cy allows you to bind arbitrary sequences of keys to Janet functions. It even supports binding regexes, matches for which will be passed to the function you provide. You can also create bindings and settings that are in effect only when attached to a specific pane or group of panes. You can even set color schemes on a per-pane basis.

Installation

You can install cy using pre-compiled binaries or from source.

Note: cy is still highly experimental. Some things may not work very well, but I would appreciate it if you created an issue if something breaks or you spot something cy could do better.

brew

You can use brew to install cy on macOS and Linux:

# Install the latest version:
brew install cfoust/taps/cy

# Or a specific one:
brew install cfoust/taps/cy@0.1.8

From binary

Download the latest version of cy on the releases page and extract the archive that corresponds to your operating system and architecture into your $PATH.

From source

git clone git@github.com:cfoust/cy.git
cd cy
go install ./cmd/cy/.

Terminal emulators

It is recommended that you use alacritty to run cy. alacritty is the fastest terminal emulator around and it will make cy that much more pleasant to use. However, cy has been tested extensively in iTerm and kitty; your mileage may vary.

About this site

This documentation is divided into three sections:

  • Quick start: a basic tutorial on using cy.
  • User guide: an introduction to cy's concepts and functionality.
  • Reference: a technical reference for cy's API, its default key bindings, and much more.

Documentation features

Key bindings

Key bindings are rendered like this: ctrl+a p [?].

The left portion is a sequence of keys that you can enter to trigger that key binding, which in this case is ctrl+a followed by p. In other words, hold control and hit the a key, release both keys, and then hit the p key.

The link on the right ([?]) points to the documentation for the Janet function that that key sequence executes.

Recordings

This documentation makes extensive use of asciinema, a player for terminal sessions. It uses asciinema to demonstrate cy's functionality and teach you how to use it. The white section on the left side of the window in the session rendered below shows a history of all of the keys entered so you can easily follow along on your own.

These are generated in CI and therefore should always be up to date.

Starting cy

To start cy after installation, just run cy without any arguments. If a cy server is not already running, cy will start one.

splash

cy shows a splash screen on startup that looks like this. To clear it, press any key.

You can kill the cy server by hitting ctrl+a q [?] and detach from it (but leave the server running) with ctrl+a d [?].

Creating a shell

To create a new pane, type ctrl+a j [?]. This starts a pane with your default shell in your current working directory. You can return to the old one with ctrl+l [?]; this cycles between all of the panes in the current group. In cy, a group is just a container for panes or other groups.

You remove panes using ctrl+a x [?].

Every group and pane in cy has a path, just like a file in a filesystem. The new shell you created has a path like /shells/3.

The collection of all groups and panes is referred to as the node tree. When you first start cy, the node tree looks like this:

/ (group)
├── /shells (group)
│   └── /2 (pane) <- you start here
└── /logs (pane)

And here's how it looks after you create a new shell with ctrl+a j [?]:

/ (group)
├── /shells (group)
│   ├── /2 (pane)
│   └── /3 (pane) <- now you're here
└── /logs (pane)

When you are attached to a pane in the /shells group, ctrl+l [?] cycles to the next sibling pane.

Entering replay mode

cy records all of your terminal sessions in their entirety. You play them back in an interface called replay mode.

Replay mode is conceptually similar to tmux's copy-mode, but also gives you access to time controls. To open it, type ctrl+a p [?]. You can also scroll up with the mouse if the pane's content is scrollable, such as when using a shell.

For basic usage you can use the following keys:

  • Step through time using left [?] and right [?]
  • Use space [?] to toggle playback
  • Go to the beginning of the recording with g g [?] and the end with G [?]
  • Quit with ctrl+c [?]

For more information, refer to the chapter dedicated to replay mode and the list of all of its key bindings.

Switching panes

cy allows you to quickly jump between panes using a built-in fuzzy finder. Try it out by hitting ctrl+a ; [?], which presents you with a list of all of the running panes.

The controls should be familiar to you if you have ever used a fuzzy finder:

  • Typing filters the list.
  • Use ctrl+j and ctrl+k (or the arrow keys) to move up and down.
  • Press enter to make a selection.
  • Quit without making a choice by typing ctrl+c or esc.

cy ships with a few different key bindings for choosing a pane:

  • ctrl+a k [?]: Jump to a project.
  • ctrl+a l [?]: Jump to a shell based on its current working directory.
  • ctrl+a : [?]: Jump to a pane by fuzzy-finding lines on the screen.

As you can tell, swapping to a new pane does not affect the existence of panes that are no longer on the screen.

Creating a project

Often, you will be doing some work in a single directory, like a Git repository. cy comes with a way to create a new group of panes for exactly this purpose. To use it, navigate to a directory and type ctrl+a n [?].

This creates two panes:

  1. /projects/[base-name]/editor: A pane running the program specified by the $EDITOR environment variable.
  2. /projects/[base-name]/shell: A pane running your default shell (or the value of $SHELL).

[base-name] is the basename (a la the Bash basename command) of the directory in which you opened the project.

For example, if you type ctrl+a n [?] while in a pane with the working directory /tmp/test-dir, [base-name] would be test-dir and the node tree would have the following structure:

/ (group)
├── /shells (group)
│   └── [...]
├── /projects (group)
│   └── /test-dir (group)
│       ├── /editor (pane) <- attached here
│       └── /shell (pane)
└── /logs (pane)

Using the command palette

Like most modern applications, cy includes a command palette, which lets you search through and execute all of the available actions. You can access it by typing ctrl+a ctrl+p [?]. The command palette shows the key to which each action is bound on the right side of the fuzzy finder.

There are some interesting things buried in the command palette. Try out the Browse animations. action!

Changing the layout

When you first connect, cy creates a new pane and attaches to it.

By default, cy centers the pane and fills the rest of the horizontal space with a patterned background it calls the frame. If your terminal is smaller than the minimum number of columns (80 by default), cy will not show a background.

placeholder

cy refers to the configuration of the content shown on your screen as the layout. Unlike tmux, every client on the cy server has their own layout that can be configured independently of other clients. The layout shown above has a single pane that has its horizontal sized fixed to 80 columns.

Panes and splits

cy's has pane and split functionality that should feel familiar to users of tmux. Like other terminal multiplexers, in cy you can divide your screen up into any number of panes. This can be done with ctrl+a - [?] to split the current pane along a horizontal line and ctrl+a | [?] to split along a vertical one.

After creating a few, you can move between them using directional keys:

  • ctrl+a K [?] or ctrl+a up [?] to move up
  • ctrl+a J [?] or ctrl+a down [?] to move down
  • ctrl+a H [?] or ctrl+a left [?] to move left
  • ctrl+a L [?] or ctrl+a right [?] to move right

Tabs

cy also has tabs:

  • ctrl+a t [?] to create a new tab
  • ctrl+a R [?] to rename the current tab
  • ctrl+a tab [?] to move to the next tab
  • ctrl+a shift+tab [?] to move to the previous tab

Removing panes

You can also remove panes from your layout with ctrl+a X [?]. However, it is important to note that unlike in other terminal multiplexers, removing a pane from your layout does not kill it; you can still attach to it again, such as by using ctrl+a ; [?]. To kill the underlying process of a pane and swap to a new one, you can use ctrl+a x [?].

Margins

The outer margins can be adjusted or disabled with a few keybindings. Note that these will only have an effect if your terminal is wider than 80 columns and the margins are visible.

  1. To make the inner pane fill the entire viewport, type ctrl+a g [?].
  2. To set the width of the inner pane to 80 columns, type ctrl+a 1 [?].
  3. To set it to 160 columns, type ctrl+a 2 [?].

CLI

cy supports a range of command line options. For the most authoritative information relevant to your version of cy, run cy --help.

All cy functionality is divided into subcommands, which are executed using cy <subcommand>. If you do not provide a subcommand, cy defaults to the connect subcommand described below.

The --socket-name flag

Just like tmux, cy supports running multiple servers at once. All subcommands support the --socket-name (short: -L) flag, which determines which cy server to connect to. For example, to start a new cy server named foo, run cy --socket-name foo.

Subcommands

connect

cy connect connects to a cy server, starting a new one if there isn't one already running. It is similar to tmux attach.

exec

cy exec runs Janet code on the cy server. This is useful for controlling cy programmatically, such as from a shell script or other program.

If you run cy exec in a terminal session inside of cy, it will infer the client on behalf of whom the Janet code should be run. This means that API functions that take over the client's cy session, like input/find, will work as expected.

Some examples:

# Create a new cy shell in the current directory and attach the client to it
cy exec -c "(shell/attach \"$(pwd)\")"

# Set a parameter on the client
cy exec -c "(param/set :client :default-frame \"big-hex\")"

Reading data from cy

Janet code run using cy exec can also return values using (yield), which will be printed to standard output in your desired format. In addition to letting you use cy's input/* API functions for getting user input in arbitrary shell scripts, you can also read any state you want, such as parameters, from the cy server.

cy exec -c "(yield (param/get :default-frame))"
# Output: big-hex

cy exec supports the --format flag, which determines the output format of (yield)ed Janet values. Valid values for --format are raw (default), json, and janet.

raw

raw is designed for easy interoperation with other programs. Primitive types such as strings, numbers, and booleans are printed as-is, without any additional formatting. For example, a string value "hello" will be printed as hello. Non-primitive types such as structs and tables cannot be printed in raw format.

json

json is designed for easy interoperation with other programs that can parse JSON, such as jq. All Janet values are converted to JSON, and the resulting JSON string is printed. For example, a table value {:a 1} will be printed as {"a":1}. Any Janet value that cannot be represented in JSON, such as functions, will cause an error.

For example, the following code gets the current layout and prints it as JSON:

$ cy exec -c "(yield (layout/get))" -f json | jq
{
    "rows": 0,
    "cols": 80,
    "type": "margins",
    "node": {
        "id": 4,
        "attached": true,
        "type": "pane"
    }
}
janet

The janet format prints the (yield)ed value as a valid Janet expression. This is useful for debugging and for passing Janet values between cy and other Janet programs. However, just like json, the janet formatter does not support printing complex values like functions.

recall

For this to work, you must have enabled command detection and cy must be installed on your system (ie available in your $PATH.)

cy recall <reference> prints the output of a command run in cy to standard output. In other words, if you run a command and later need to filter its output or pipe it to a file, you can do so without rerunning the command. <reference> is an identifier for a command run in cy.

Relative references

Inside of a pane in cy, running cy recall -1 will write the output of the most recent command to standard output, e.g.:

cy recall -1 | grep 'some string'
cy recall -1 > out.log

A negative number refers to a command relative to the end of the "list" of all commands run in the current pane so far. So cy recall -2 refers to the second-latest command.

cy recall -1 can also be written as cy -1, a la:

cy -1 | grep 'some string'
cy -1 > out.log

Note that running cy -1 in two different panes will produce different output; cy understands where you run a cy command and uses that context to direct your query.

Absolute references

recall also supports absolute references in the form [[server:]node:]index.

You do not have to come up with these yourself. Running the action/recall-command action will let you choose a command, after which a cy <reference> command will be written to your shell.

index can be any integer and it refers to the index of a command inside of a pane starting from 0. The command referred to by cy -1 changes on every command run; cy 0 on the other hand will always refer to the first command run in the pane.

server and node are optional. These properties are used by references generated by cy to disambiguate a reference to a particular command.

  • node is an integer NodeID that specifies the pane from which the command will be read.
  • server is the name of the socket the cy server is running on (the value of the --socket-name flag above).

Both server and node can be derived by cy when cy recall is run in a pane in a cy server, but if server is specified, you can also run cy recall outside of a cy server: cy recall default:0:1.

Configuration

cy is not really a terminal multiplexer. It's actually an API for making terminal multiplexers that happens to come with a decent default configuration. This is because cy was designed to be as configurable as possible:

  1. Features are first built to expand the functionality available in the API.
  2. They then are incorporated into the default configuration where appropriate.

As a result, cy is the best choice for users who enjoy tinkering with the tools they use.

cy uses Janet as its configuration language. Janet is a fun, embeddable Lisp-like language that is easy to learn. If you are new to Janet, I recommend starting out with its documentation and Ian Henry's fantastic Janet for Mortals.

Why Janet?

This might come as a surprise, but I am far from being a Lisp zealot and have never seriously used emacs. I found Janet several years ago and enjoyed experimenting with it. The language avoids most of the scary levels of abstraction present in some Lisps and strikes a balance between functional programming orthodoxy and practical imperative programming needs.

Configuration files

On startup, cy will search for and execute the first file containing Janet source code that it finds in the following locations. cy adheres to the XDG base directory specification.

  1. $XDG_CONFIG_HOME/cy/cyrc.janet
  2. $XDG_CONFIG_HOME/cyrc.janet
  3. $XDG_CONFIG_HOME/.cy.janet
  4. $HOME/cy/cyrc.janet
  5. $HOME/cyrc.janet
  6. $HOME/.cy.janet
  7. $HOME/.config/cy/cyrc.janet
  8. $HOME/.config/cyrc.janet
  9. $HOME/.config/.cy.janet

You can reload your configuration at any time using action/reload-config, which by default is bound to ctrl+a r [?].

Example configuration

An example configuration that uses functionality from this API is shown below. You may also refer to the default configuration for examples of more advanced API usage.

# Define a new action (which is just a function) with the name toast-pane-path
(key/action
  # the name of the function by which it can be referenced in the current
  # scope
  toast-pane-path
  # a (required) docstring for the action
  "show the path of the current pane"
  # all subsequent statements are executed when this action is invoked (usually
  # by a key binding)
  #
  # (pane/current): gets the ID of the current pane
  # (cmd/path): gets the path of the pane with the given ID
  # (msg/toast): shows a toast popup in the top-right corner of the screen
  (msg/toast :info (cmd/path (pane/current))))

# Bind a key sequence to this function
(key/bind :root ["ctrl+a" "g"] toast-pane-path)

Execution context

The Janet code executed in cy can be executed in two different contexts:

  • The context of the server: This is the context in which your configuration file is run, because when the cy server starts up, there are no clients.
  • The context of a client: When a client invokes an action or types a keybinding sequence, the executed Janet code can see what client ran the action and respond appropriately.

Because of this, some API functionality can only be used in a keybinding or action. This is because some kinds of state, such as the state that (viewport/*) family of functions uses to work, only makes sense in the context of a user.

This is a confusing aspect of cy that I plan to improve over time, such as by letting you execute custom code in the context of a client whenever one connects.

Error handling

If an uncaught error is thrown while running Janet code, cy will send all connected users a toast with that error and log the error to the /logs pane, which you can attach to like any other pane.

Keybindings

In cy, a keybinding consists of a sequence of one or more keys that executes Janet code after completion. You define new key sequences with the key/bind function.

For example:

(key/bind :root ["ctrl+l"] (fn [&] (msg/toast :info "you hit ctrl+l")))

This tells cy that whenever you type ctrl+l it should show a toast with the text "you hit ctrl+l".

The key/bind function takes three parameters:

  1. A scope: The circumstances in which this binding should apply, such as a group or mode (e.g. :time). In this case we use the :root keyword, which is a handy way of saying this binding should apply everywhere.
  2. A key sequence: A Janet tuple that indicates the keys that must be typed for the callback to execute.
  3. A function: The callback that should be executed when this key sequence matches.

Scopes will be covered in a later chapter: here we will cover key sequences and functions at length.

You can avoid calling key/bind over and over by using the key/bind-many macro. Here is an example:

(defn do-something [] )
(defn do-something-else [] )
(key/bind-many :root
               ["ctrl+b" "1"] do-something
               ["ctrl+b" "2"] do-something-else)

You can also clear previously bound key bindings with key/unbind or rebind them with key/remap.

Key sequences

Key sequences in cy are more flexible than they appear at first glance. Valid sequences can consist of the following elements:

  1. Printable Unicode characters: , Щ, a
  2. Preset keys: return, ctrl+a, f1 You can find a comprehensive list of the available keys here.
  3. Regexes: [:re "^[a-z]$"]

The first two work exactly as you expect them to: cy will execute the first complete match for the keys that you type. After each key, cy gives you a second (=1000ms) to type the next key in the sequence. If you do not, cy does nothing. All keys that are not matched by any sequence are sent to the current pane.

Here are some valid key sequences:

# This is a match when you type these three characters in succession
["a" "b" "c"]
# This works similarly to tmux's notion of "prefixes"
["ctrl+a" "a"]
["ctrl+a" "ж"] # unicode is OK
[" " "l"]
# You can also prepend alt+, as expected
["alt+ctrl+a"]
["alt+o"]

It is important to note that cy does not send partial sequences to the current pane. In other words, defining a sequence that begins with " " means that you will no longer be able to type the space character.

Regexes

The most powerful aspect of cy's keybinding engine is the ability to define key sequences that include Perl-compatible regular expressions. Each element that matches a regex is passed to the callback as a string value.

To illustrate:

(defn toast-me [key] (msg/toast :info key))
(key/bind :root ["ctrl+b" [:re "[abc]"]] toast-me)

Now if you type ctrl+b followed by a, the toast-me function will be invoked with one argument, the Janet value "a". The same applies if you follow the ctrl+b with b or c.

This allows you to build more sophisticated functionality without defining a binding for every possible character.

A practical application of this can be found in cy's source code, where we use this functionality to support vim-like character movements in replay mode:

(key/bind :copy ["f" [:re "."]] replay/jump-forward)
(key/bind :copy ["F" [:re "."]] replay/jump-backward)

Key specifiers are matched as though their names were typed by the user; this means that providing the pattern "ctrl\+[a-c]" will match ctrl+a, ctrl+b, and ctrl+c.

Accessing individual match groups is not supported; functions always receive the full string that matched the pattern.

Functions

Any Janet function can be passed as a callback to key/bind. The arity of that function should match the output of the provided sequence; for key sequences that do not include any regex patterns, this means that the function should not take any arguments.

Like tmux, many users at once can connect to the same cy server. The function provided to key/bind is executed in the context of the user that invoked it. Certain functions in cy's API, such as pane/current, return information about the state of the current user, rather than the server as a whole. This means that if two users type the same sequence, they will get different results.

Actions

In some cases it is inconvenient to have to provide functions directly to key/bind. For example, if you are writing a plugin, you might want to be able to provide new actions that a user can take without forcing them to use your key bindings. The user also may not want to assign all of your plugin's functionality to arcane bindings they won't remember.

To assist with this, cy has a system for actions, which are similar in nature to commands in VSCode or in Sublime Text. An action consists of a short description and a function. When the user opens the command palette (which is bound by default to ctrl+a ctrl+p [?]), they can search for and execute an action based on that description.

You define new actions using the key/action macro. Here is an example from cy's source code:

(key/action
  # The identifier to which this action will be bound
  # This is never shown in the UI
  cy/kill-current-pane
  # The docstring
  # The user uses this to find the action you define
  "kill the current pane"
  # All subsequent forms comprise the body of the action, or the lines of code
  # that will be executed when it is invoked
  (tree/rm (pane/current)))

(key/bind :root ["ctrl+b" "b"] cy/kill-current-pane)

key/action actually just invokes Janet's (defn) macro under the hood. This means that actions are just ordinary Janet functions that happen to be registered with cy. key/action exists so that you can clearly identify to the user the functionality your plugin provides.

You can also just use actions to avoid memorizing a key binding you rarely use:

(key/action
  thing-i-rarely-do
  "this is something I do once a year"
  (pp "hi"))

Changing and deleting existing keybindings

cy provides two API functions for manipulating existing keybindings, key/remap and key/unbind.

It is sometimes convenient to change the activation sequence for many bindings at once. For example, you may want to change the prefix used for most of cy's bindings, ctrl+a, into ctrl+v:

(key/remap :root ["ctrl+a"] ["ctrl+v"])

Similarly, you may also delete keybindings with key/unbind.

Groups and panes

cy contains a simple abstraction for isolating settings and functionality to a particular set of panes. This is referred to as the node tree which consists of panes and groups.

Panes

A pane refers to a terminal window with a process running inside it, typically a shell or text editor. Every pane has a name. Panes in cy work exactly the same way that they do in tmux: you can have arbitrarily many panes open and switch between them on demand.

Groups

Every pane cy belongs to a group. A group has a name and children, which consist of either panes or other groups.

Groups also have two unique features:

  • Key bindings: You may define key bindings that will only activate when you type that sequence while attached to any descendant of that group.
  • Parameters: A key-value store that can be interacted with using param/get and param/set. Parameters are used both to configure aspects of cy and also to create any functionality you desire by storing state in cy's tree.

The preferred method of creating new groups is using the group/mkdir function, which allows you to create many groups at once just like the real mkdir command.

For example:

(group/mkdir :root "/some/other/group")

The group/mkdir function creates a group at the provided path (if it does not already exist) and returns its NodeID.

The node tree

The combination of groups and panes in cy form a tree that is similar to a filesystem. For example, a cy session may end up with a structure that looks like this:

/ (the root group, which has no name)
├── /my-project
│   ├── /pane-1
│   ├── /pane-2
│   └── /group-2
│       └── /pane-3
└── /another-group

Nodes in the tree can be referred to by their path, such as /my-project/pane-1. Node paths are not required to be unique, however. They are only presented as a convenient conceptual model for the user.

Instead, each node is permanently assigned a unique identifier (which is just an integer) referred to as a node ID and the related API calls only accept those IDs.

Inheritance

cy's flexibility comes from the way key bindings and parameters interact:

  • Key bindings are inherited down the tree, but can be overridden by descendant groups.
  • Parameters work the same way: param/get will get the value of a parameter from the closest parent group that defines it.

Imagine that you are attached to /my-project/group-2/pane-3 in the example above:

  • If /my-project defines a binding with the sequence ctrl+ab and /my-project/group-2 also defines one that begins with ctrl+a, the latter will take precedence.
  • If /my-project defines a value for a parameter :some-parameter and /my-project/group-2 does not, (param/get :some-parameter) will retrieve the value from /my-project.

One of cy's goals is for everything to be configured solely with key bindings and parameters; in this way cy can have completely different behavior depending on the environment and project.

Parameters

cy has a key-value store referred to as parameters. In addition to being available for use from Janet for arbitrary purposes, parameters are also the primary means of configuring cy's behavior.

Parameters are set with param/set and retrieved with param/get:

(param/set :root :some-parameter true)

(param/get :some-parameter)
# returns true

Parameters work just like bindings do in the node tree in that the parameter values in descendant nodes overrite those in their ancestors. This allows cy's functionality to be configured on a per-group (or even per-pane) basis.

cy's settings are known as default parameters. They are set and retrieved in the same way normal parameters are, but influence cy's behavior in ways not directly controllable using the API. A complete list can be found in the default parameters chapter.

Theming

You can configure almost anything about cy's appearance by changing default parameters to your liking. This includes terminal themes, UI colors, border styles, and UI copy (such as for localization.)

Parameters that affect visual styling work in the same way all parameters do: setting a parameter for a group affects the visual styling of all descendant panes, unless a pane or child group defines a different value for the parameter.

Color maps

Color maps are similar to color schemes. A color map is a mapping from one color to another. When cy renders a pane, it can use a color map that you define to translate the colors on the screen to change their appearance. This translation does not affect the underlying recording for a pane.

Most terminal programs only use 16 ANSI colors. All of cy's built-in interfaces also obey this rule. Terminal emulators support color schemes by allowing the user to set the actual RGB colors used to represent those 16 colors on the screen.

In cy, this works by using a color map to translate those ANSI colors to RGB true colors. For example:

# The built-in "zenburn" color map
(param/set :root :color-map @{
  "0" "#383838" # Map ANSI 0 -> RGB color #383838
  "1" "#dca3a3"
  "2" "#5f7f5f"
  "3" "#e0cf9f"
  "4" "#7cb8bb"
  "5" "#dc8cc3"
  "6" "#93e0e3"
  "7" "#dcdccc"
  "8" "#6f6f6f"
  "9" "#dfaf8f"
  "10" "#404040"
  "11" "#606060"
  "12" "#808080"
  "13" "#c0c0c0"
  "14" "#000000"
  "15" "#ffffff"})

Like any other parameter, the :color-map parameter can be set on a per-pane or per-group basis.

The important distinction between a color map and a color scheme is that color maps in cy allow you to map any color to any other color, not just the 16 ANSI colors as is the case with traditional terminal emulators. For example, if a program you use insists on hard-coding ANSI256 or RGB colors, you can swap them to something else:

(param/set :root :color-map @{
  # Map RGB "#ff0000" -> ANSI16 1 (red)
  "#ff0000" "1"
  # Map ANSI 256 -> ANSI 1
  "123" "1"})

cy comes with several hundred built-in color maps from the tinted-theming project. You can see a gallery of them here. Every built-in color map is identified by a unique keyword ID such as :google-dark.

API

The API has several functions for working with the built-in color maps:

  • color-maps/get-all: Get all of the color maps.
  • color-maps/get: Get a color map by its ID.
  • color-maps/set: Set the color map for a pane or group using a color map ID. This also sets the :color-map-id parameter for the node, which is a convention (but not a requirement) used for built-in color maps.
  • color-maps/get-id: Get the value of the :color-map-id parameter for the given node.

The inheritance behavior of parameters can be used to apply a color map to entire groups of panes and also override the color map defined by an ancestor group. For example, consider the following Janet code:

# All panes by default will use the built in atelier-forest-light color map
(color-maps/set :root :atelier-forest-light)
# All shells (created by (shell/new)) will use a different scheme. Since
# /shells is a child of :root (a.k.a /), its :color-map will take precedence.
(color-maps/set (group/mkdir :root "/shells") :atelier-estuary)

Actions

Using the action/set-pane-colors action you can quickly set the color map for a particular pane:

Built-in UI

cy defines a wide range of parameters that are used for affecting the visual styling of its built-in UIs, such as:

  • Replay mode
  • Search mode
  • The (input/*) family of API functions, for fuzzy finding et al
  • Toast colors

For example:

This theme was created with the following Janet code:

(param/set-many :root
                :replay-text-copy-mode "копировка"
                :replay-status-bar-bg "#6699cc"
                :replay-copy-bg "#99cc99"
                :replay-copy-fg "#2d2d2d"
                :input-prompt-bg "#f2777a"
                :color-error "#f2777a"
                :color-warning "#ffcc66"
                :color-info "#6699cc"
                :timestamp-format "начало революции: 2006-01-02 15:04:05")

Theme parameters work exactly like any other parameter. If you set a parameter with a target of :client, it overrides any other parameter. Parameters set on tree nodes override the parameter values of their ancestor nodes.

Replay mode

Using replay mode you can record, play back, and search through everything that happens in your terminal sessions. You can invoke replay mode at any time by typing the key sequence ctrl+a p [?] by default or by scrolling up with the mouse if the pane is connected to a shell.

Recording to disk

The history of a pane is not only stored in memory; it is also written to a file on your filesystem. This means that you (and only you--cy is careful to make sure the directory is only readable by you) can play back any session, even if it is no longer running in a cy instance.

By default, cy records all of the activity that occurs in a terminal session to .borg files, which it stores in one of the following locations:

  1. $XDG_DATA_HOME/cy (if $XDG_DATA_HOME is set)
  2. $HOME/.local/share/cy (if it's not)

The directory will be created if it does not exist.

You can access previous sessions through the action/open-log action, which by default can be invoked by searching for Open a .borg file. in the command palette (ctrl+a ctrl+p [?]).

You are also free to use the API function replay/open-file to open .borg files anywhere on your filesystem.

A warning about recording

cy does not and will never record what you type (otherwise known as "standard input" or stdin). It only records the output of the process (otherwise known as "standard output" or stdout) that is attached to your virtual terminal and nothing more.

This is a basic safety measure so that your passwords (such as for sudo) never appear in cy's recordings. Your secrets (such as authentication keys and tokens) still will if they ever appear on your screen, so caution is advised.

In the future, cy may give you more fine-grained control over specifically what it records and when, but for now this is not configurable.

If you wish to opt out of recording to disk entirely, set the :data-directory parameter to an empty string. Note that cy will continue to hold on to your terminal sessions in memory.

For example:

(param/set :root :data-directory "")

Modes

Like vim, replay mode is modal. It has several different modes that influence both what is shown on the screen and what you can do:

  • Time mode: allows you to pause, play, and move through the history of the current pane back to when it first began.
  • Copy mode: allows you to explore the state of the screen at a particular point in time. This includes the scrollback buffer, which is traditionally all that tmux's copy mode gave you access to.
    • Visual mode: This is a submode of copy mode that permits you to select text and copy it into your user-specific copy buffer that can then be pasted elsewhere.

Time mode

Time mode is similar to a video player: you can pause, play (both forwards and backwards!), and skip through time to inspect particular moments in the history of a pane. While playing back terminal events, time mode skips inactivity, here defined as any idle period longer than a second.

Searching

Time mode also allows you to search through the pane's history using regular expressions or string literals. In this way you can find all instances of a string if it ever appeared on the screen--even if it was subsequently cleared away. Note that this is different from searching in the scrollback. Searching in time mode will find matches that appeared on the screen at any point in time, including in full-screen applications such as vim or htop.

You can initiate a search by hitting / [?] to search forward in time and ? [?] to search backward (by default).

If the query string you enter is not a valid regex, it will be interpreted as a string literal.

The search bar also supports time expressions, which you can use to jump by a fixed amount of time. Time expressions are in the format NdNhNmNs where N is the number of that unit that you wish to move by. You will move in time in the direction of your search.

Some examples:

1h30s # one hour 30 seconds
3d # three days

Copy mode

To enter copy mode, all you need to do is invoke any action that would cause the cursor or the viewport to move. Like tmux's copy mode, you can explore the state of the screen and copy text to be pasted elsewhere. Copy mode supports a wide range of cursor and viewport movements that should feel familiar to users of CLI text editors such as vim. For a full list of supported motions, refer to the reference page for key bindings.

Copy mode also allows you to swap between the terminal's main and alt screens using s [?]. In other words, even if you run a full-screen application such as htop, you can still swap back to the scrollback buffer and see the output of commands you ran before running htop.

Visual mode

Visual mode is initiated when you press v [?] (by default). It works almost exactly like vim's visual mode does; after you have some selected some text, you can yank it into your buffer with y [?] and paste it elsewhere with ctrl+a P [?].

Registers

In cy, you can copy text to and paste text from registers. This system works almost identically to registers in vim. Each register is identified by a string key (e.g. "a") and can store a string value. The bindings described in the section above (y [?] and ctrl+a P [?]) copy and paste from the "" register. The system clipboard (if available) is accessible using a special register, "+".

By default, cy lets you copy to and paste from a range of registers that mimic those found in vim. In visual mode you can copy text into a register using " [a-zA-Z0-9+] y. For example, hitting " a y copies the current selection into register "a". Elsewhere in cy you can paste from "a" by hitting ctrl+a " a p.

The state of registers in cy is global, not per-client. This means that after one client yanks into a register, all connected clients can paste from that register. The contents of registers are lost when the cy server exits, however. This may change in the future.

You can access these registers programmatically using the register/* family of API functions such as register/get and register/set.

For convenience, cy also provides clipboard/get and clipboard/set to quickly access the system clipboard from Janet. These functions just read and write from the special "+" register.

Command history

One of cy's distinguishing features is that it can detect the commands that you run. It also records:

  • When the command was executed
  • When it finished executing
  • All of the output the command produced (by indexing into .borg files)
  • The directory in which the command was run

This metadata is stored in an SQLite database called cmd.db (in your :data-directory) and accessible in Janet via the cmd/query API.

cy uses this information to provide a range of functionality:

Installation

To enable command detection, you must add a special escape sequence to your prompt in the shell of your choice:

\033Pcy\033\\

This is a custom device control sequence: ESC P cy ESC \. It is a type of escape sequence that is seldom used by modern programs and is ignored by terminal emulators, so it will have no effect even when you are not using your shell inside of cy.

There are instructions for how to configure this correctly in popular shells below.

bash
# Place this anywhere in the PS1 variable:
\[\033Pcy\033\\\]

# For example:
PS1='\[\033Pcy\033\\\] ▸▸'

You can put it anywhere; its position does not matter and it does not contain any printable characters.

zsh
# Place this anywhere in the PROMPT variable:
%{\033Pcy\033\\%}

# For example:
PROMPT=$'%{\033Pcy\033\\%} >'
fish

Put this somewhere in your fish_prompt or just add \033Pcy\033\\ to any existing string that's already there.

printf '\033Pcy\033\\'

Directory detection

By default, cy detects the directory in which a command is executed by regularly getting the working directory of the process running in your terminal (such as a shell.) This produces incorrect results in cases like ssh, where the actual directory is distinct from the one reported by examining the process.

To address this, cy supports the OSC-7 quasi-standard. OSC-7 is a custom escape sequence that allows a program to indicate its current directory to the terminal emulator.

You must explicitly configure your shell (or any other program) to emit OSC-7 sequences. You can find instructions for doing so here. cy does not do any special processing of the string you emit using OSC-7: it need not be a valid directory.

How does it work?

A terminal multiplexer is like a proxy: it is an intermediary between your actual terminal and one or more virtual terminals that it controls. It also responsible for passing the output a program (such as a shell) produces to its corresponding terminal. This control means that your terminal multiplexer can do whatever calculations it wants on that output, including (in cy's case) recording it.

cy uses this ability to observe the state of the terminal before and after interpreting a particular output of the underlying process (otherwise known as a write made to standard output or error).

A terminal emulator is just a state machine that interprets a stream of bytes and adjusts the state of the screen accordingly. The screen is a grid of cells, each of which contains a Unicode character and has other styling information.

As a result, cy can detect the exact changes a byte string made to your screen. When cy detects the special escape sequence above, it applies a flag to the screen cells that particular output produced--in this case, all of the cells comprising your shell prompt. By doing so, it can determine which screen cells (and therefore, which writes) correspond to the prompt and which do not.

Almost invariably, shell sessions follow the following pattern:

PROMPT [user command]
OUTPUT
PROMPT

The commands a user enters manually also have a consistent structure (they always follow a prompt) and so once that step is done, all that's left is to collect the output a command produced between prompts and voilà.

This approach has downsides. One is that cy is unable to distinguish between the output produced when running multiple processes at once, such as with Bash's backgrounding functionality (i.e. &.)

ctrl+r

Hitting ctrl+a ctrl+r [?] allows you to choose a command from your command history and insert it into your current pane. This works just like fzf, atuin, hstr, et al, but it also gives you a preview of what the screen looked like when you executed that command.

You can also use the same interface to open a particular command from your history in replay mode using the ctrl+a f [?] binding. In effect, you can jump back to the exact moment in time you executed any command.

Why not bind this directly in the shell?

Most programs that replace the default ctrl+r functionality require you to overwrite the existing key binding, typically through per-shell configurations. The advantage of doing this at the level of the terminal multiplexer (as opposed to configuring individual shells) is that cy's ctrl+r works anywhere: in editors, over SSH, et cetera. It does not matter what system you're on, you can still just hit ctrl+a ctrl+r [?] and insert a command from your history.

Another subtle advantage is that once you've put cy's command detection sequence in your prompt on a system, you will never need to update it. In the example of a remote system, cy need not even be installed; when you connect from cy, command detection will work even if the remote does not have cy.

Switching panes

cy's default configuration also defines a few actions that use command detection to do interesting things:

  • action/jump-pane-command (ctrl+a c [?]): Choose from a list of all of the commands run since the cy server started and jump to the pane where that command was run. This is useful for tracking down a pane if you only remember a command that was run inside it.
  • action/jump-command (ctrl+a C [?]): Choose from a list of all commands and jump to the location of that command in its pane's scrollback history.

Replay mode

Time mode

You can quickly jump back to the instant in time just before a command began executing using the [ c [?] and ] c [?] bindings.

Copy mode

When the cursor is on the output of a command in copy mode, the command itself is shown in replay mode's status bar at the bottom of the screen.

Just like in time mode, you can also jump between commands using [ c [?] and ] c [?].

You can also quickly select the complete output of a command using [ C [?] and ] C [?].

Search mode

Using search mode you can search all of the terminal sessions you have recorded for a string literal or regex pattern. This works just like the search functionality found in replay mode, but over more than one terminal session at once.

You can access search mode using the action/search-borg action, which is bound by default to ctrl+a S [?]. This action prompts you for a query string and searches for that pattern in all of the .borg files present in your :data-directory.

Unlike replay mode, search mode behaves like a pane in the node tree: you can attach to, detach from, and rename it just like any other pane. The search/new API function can be used to create search mode nodes programmatically.

Usage

After a query finishes executing, search mode will load the .borg file for the first result in an instance of replay mode and prepopulate it with the results of the search.

You can move between matches inside of a .borg file in the same way you do normally in replay mode, using n [?] and N [?]. To move between .borg files in search mode, use ctrl+n [?] and ctrl+p [?] to move to the next and previous results, respectively.

It can be convenient to run another search in an existing search mode instance. To do so, hit the : [?] key, type a query string, and press enter.

Search mode is not very complicated, but it does have its own isolated binding scope should you wish to rebind its controls.

Layouts

cy uses a declarative, tree-based system for defining what is shown on the screen. It refers to this as the layout. It is composed of nodes of different types such as panes, splits, and tabs.

The structure of a typical layout might look something like this:

{
    :type :split
    :vertical false
    :a {
        :type :pane
        :id 1
        :attached true
    }
    :b {
        :type :pane
        :id 2
    }
}

Every node consists of a Janet struct with a :type property indicating the type of the node and a set of properties that describe how the node should look and behave. The layout above describes a horizontal split (two nodes side by side). The client is attached to the left pane.

A layout must have exactly one pane node where :attached is true. The attached node is the visual element to which the client's input (that does not match any bindings) is sent.

API

Layouts are just standard Janet values. You can retrieve the current layout with layout/get and set it with layout/set.

cy includes a comprehensive Janet library with handy tools for manipulating them more easily. For example, you can quickly create nodes with a family of creation functions like layout/pane, layout/split, layout/vsplit, et cetera.

There is also layout/new, which is a Janet macro that expands shortened forms of these node creation functions into their full forms for you. This lets you describe layouts succinctly:

(layout/new (split
              (pane)
              (vsplit
                (margins (attach))
                (borders (pane)))))

# Expands to:
{:type :split
 :vertical false
 :a {:attached false :type :pane}
 :b {:type :split
     :vertical true
     :a {:type :margins
         :node {:attached true :type :pane}}
     :b {:type :borders
         :node {:attached false :type :pane}}}}

Properties

Every node type has a set of properties that determines that node's appearance and functionality. Valid values for properties are usually primitive types in Janet, such as strings, numbers, and booleans. Here are some common examples:

  • The color and style of that node's borders
  • The children of that node, one or more of which may be shown at any one time
  • Titles shown above or below the node

But most node types also have dynamic properties. In addition to accepting static values, dynamic properties can be set to a function that returns a value of the proper type. That function is called when that node is rendered.

The arguments to that function vary based on the node and the property. The function does not need to be pure. However, functions passed to dynamic properties are only ever invoked with properties of that node.

In other words, these functions are only given access to the node they are passed to and the descendants of that node. They do not get to see the whole layout (though nothing stops you from calling layout/get in a dynamic property.)

To illustrate, border nodes have a property called :title, which is a string that is shown at the top-right of the node. The :border-fg property determines the color of the border surrounding the node.

By providing a function to both properties, we can change the color and title of the borders node as the user switches between panes. In this case, the functions are invoked with a single argument: the value of the :node property in each borders node, which contains whatever node is displayed inside of the borders.

This lets you accomplish things like this:

This example uses the following code:

(def cmd1 (shell/new))
(def cmd2 (shell/new))

(defn
  border-title
  [layout]
  # Get the NodeID of the node the user is attached to
  (def node (layout/attach-id layout))
  (if
    (nil? node) (style/text "detached" :bg "4" :italic true)
    (style/text (tree/path node) :bg "5")))

(defn
  border-fg
  [layout]
  (def node (layout/attach-id layout))

  (if (nil? node) "4" "5"))

(layout/set (layout/new
              (split
                (borders
                  (attach :id cmd1)
                  :title border-title
                  :border-fg border-fg)
                (borders
                  (pane :id cmd2)
                  :title border-title
                  :border-fg border-fg))))

Swapping panes causes the layout to change by changing the pane where :attached is true. This causes the layout to rerender, which reruns the functions we provided when we created the borders node. Since now the :node that each borders node renders has changed, the :title and :border-fg properties produce different results, all without explicitly calling layout/set again.

What if a function passed to a dynamic property throws an error?

Errors thrown by functions passed to dynamic properties do not prevent the layout from rendering. The layout will continue to render as if the function had returned that property's default value and an appropriate error message will be sent to the /logs pane.

Actions

You can access some layout functionality using actions that are available in the command palette:

Some node types have actions as well. Each node type in the next chapter describes those actions, where appropriate.

Frames

The patterned background shown underneath the layout is referred to as the frame. cy comes with a range of different frames. You can choose between all of the available frames using the action/choose-frame function, which is bound by default to ctrl+a F [?], and set the default frame for new clients using the :default-frame parameter.

Nodes

cy currently supports the following layout node types:

  • Bar: A node that adds a customizable status bar to the top or bottom of a node.
  • Borders: A node is enclosed in borders with an optional title on the top or bottom.
  • Color map: A node that applies a color map to its contents.
  • Margins: A margins mode constrains the size of its child node by adding transparent margins.
  • Pane: A pane node can be attached to panes that exist in the node tree.
  • Split: A split node divides the screen space of its parent node in two and provides it to two other nodes, drawing a line on the screen between them.
  • Tabs: A node that can display different pages of content navigable using a familiar tab bar.

The following sections go into these types in more detail.

Bar

A :bar node is a status bar shown above or below the node that it contains.

{
    :type :bar
    :bottom false # boolean, optional
    :text nil # string, dynamic
    :node {} # a node
}

:bottom

If true, the bar will be on the bottom of the node instead of the top.

:text

The contents of the bar.

:node

A valid layout node.

Dynamic

  1. The dimensions of the space available to either property as a tuple, [rows cols]. rows is always 1, but this structure is preserved for consistency.
  2. The current value of :node.

Borders

A :borders node surrounds its child in borders and adds an optional title to the top or bottom.

{
    :type :borders
    :title nil # string or nil, dynamic, optional
    :title-bottom nil # string or nil, dynamic, optional
    :border :rounded # border type, dynamic, optional
    :border-fg nil # color, dynamic, optional
    :border-bg nil # color, dynamic, optional
    :node {} # a node
}

:title and :title-bottom

These strings will be rendered on the top and the bottom of the window, respectively.

:border

The border style for this node. :none is not supported.

:border-fg and :border-bg

The foreground and background color of the border.

:node

A valid layout node.

Actions

Dynamic

:title and :title-bottom

  1. The dimensions of the space available to either property as a tuple, [rows, cols]. rows is always 1, but this structure is preserved for consistency.
  2. The current value of :node.

All other properties:

  1. The current value of :node.

Color map

A :color-map node applies a color map to its contents.

{
    :type :color-map
    :map @{} # color map, dynamic
    :node {} # a node
}

:map

A color map.

:node

A valid layout node.

Example

The example at the top of the page was created with the following Janet code:

(def cmd1 (cmd/new :root :command "htop"))
(def cmd2 (cmd/new :root :command "htop"))
(def cmd3 (cmd/new :root :command "htop"))
(defn
  theme [layout]
  (def node (layout/attach-id layout))
  (if
    # Apply one color scheme if the client is not attached to this node
    (nil? node) ((color-maps/get :atelier-sulphurpool) :map)
    # And a different one if they are
    ((color-maps/get :atelier-sulphurpool-light) :map)))

(layout/set (layout/new
              (split
	        (split
		  (color-map
		    theme
		    (attach :id cmd1))
		  (color-map
		    theme
		    (pane :id cmd2))
		  :vertical true)
                (color-map
                  theme
                  (pane :id cmd3)))))

Margins

A :margins node puts transparent margins around its child allowing the current frame to show through.

{
    :type :margins
    :cols 0 # number, optional
    :rows 0 # number, optional
    :border :rounded # border type, dynamic, optional
    :border-fg nil # color, dynamic, optional
    :border-bg nil # color, dynamic, optional
    :node {} # a node
}

:cols and :rows

These properties set the size of the node inside of the :margins node; they do not refer to the width of the margins. A value of 0 for either property means that the node will use the full space available to it in that dimension.

:border

The border style for the borders around the node.

:border-fg and :border-bg

The foreground and background color of the border.

:node

A valid layout node.

Dynamic

All dynamic properties are invoked with the same arguments:

  1. The current value of :node.

Pane

A :pane node is an attachment point for a pane.

{
    :type :pane
    :id nil # number or nil, optional
    :attached true # boolean or nil, optional
    :remove-on-exit false # boolean or nil, optional
}

:id

Specifies the NodeID this pane is connected to. If nil, the part of the screen occupied by this pane will just show a "disconnected" state.

:attached

If true, user input will be sent to the pane specified by :id.

:remove-on-exit

If true, when the pane specified by :id exits successfully (ie with exit code 0) or is killed using tree/rm, this pane will be removed from the layout. This works just like exiting from a shell in tmux does: the intent is to preserve that behavior for users who want it.

The value of :remove-on-exit, by default, is the value of the :remove-pane-on-exit parameter.

In other words, to make this global you can do the following:

(param/set :client :remove-pane-on-exit true)

Split

A :split node divides its visual space in two and gives it to two other nodes, rendering a line down the middle.

{
    :type :split
    :vertical false # boolean, optional
    :cells nil # int or nil, optional
    :percent nil # int or nil, optional
    :border :rounded # border type, dynamic, optional
    :border-fg nil # color, dynamic, optional
    :border-bg nil # color, dynamic, optional
    :a {} # a node
    :b {} # a node
}

:vertical

If true, the nodes are rendered on top of one another. If false, they are rendered side by side.

:cells and :percent

At most one of these can be defined. Both determine the amount of space given to the node described by :a. :cells is the number of cells along the split axis :a will be given; :percent is a percentage ([0, 100]) of the total size of the node along the split axis that will be allocated to :a.

:border

The border style to use for the dividing line.

:border-fg and :border-bg

The foreground and background color of the border.

:a and :b

Both must be valid layout nodes.

Dynamic

All dynamic properties are invoked with the same arguments:

  1. The current value of :a.
  2. The current value of :b.

Tabs

A :tabs node is a standard tab system with customizable nibs (these are also sometimes called "leaves").

{
    :type :tabs
    :active-fg nil # color, optional
    :active-bg nil # color, optional
    :inactive-fg nil # color, optional
    :inactive-bg nil # color, optional
    :bg nil # color, optional
    :bottom false # boolean, optional
    :tabs @[] # list of tabs
}

# tabs look like this:
{
    :active false # boolean, optional
    :name "" # string
    :node {} # a node
}

:active-fg, :active-bg, :inactive-fg, and :inactive-bg

These are all colors and are used to style the tab's title if the tab's :name property does not contain ANSI escape sequences, such as those generated by style/render et al.

:bg

This is the background color for the tab bar.

:bottom

If true, the tab bar will be on the bottom of the node instead of the top.

The :tabs property

There are some important constraints on the :tabs property:

  • You must provide at least one tab.
  • There must be exactly one tab with :active set to true.
  • All provided tabs must have :name fields with non-zero visual width.

Actions

Dynamic

All dynamic properties are invoked with the same arguments:

  1. The current value of :node of the active tab of this :tabs node.

Appearance

Borders

Some nodes have a :border property that determines the appearance of the node's borders. The border property can be one of the following keywords:

  • :normal
  • :rounded
  • :block
  • :outer-half
  • :inner-half
  • :thick
  • :double
  • :hidden
  • :none

:hidden still renders the border with blank cells; :none does not render a border at all.

Text styling

All string layout properties accept text styled with style/render (or style/text).

The layout shown in the monstrosity above was generated with the following code:

(def cmd1 (shell/new))
(def cmd2 (shell/new))
(layout/set
  (layout/new
    (margins
      (split
        (borders
          (attach :id cmd1)
          :border-fg "6"
          :title (style/text "some pane" :fg "0" :bg "6")
          :title-bottom (style/text "some subtitle" :fg "0" :bg "6"))
        (borders
          (pane :id cmd2)
          :border-fg "5"
          :title (style/text "some pane" :italic true :bg "5")
          :title-bottom (style/text "some subtitle" :italic true :bg "5"))
        :border-bg "3")
      :cols 70
      :border-bg "4")))

User input

The above uses both fuzzy finding and freeform text input.

cy has two API functions that solicit input from the user:

  • input/find: A fully-featured fuzzy finder for selecting one item from a list.
  • input/text: A freeform text input field for general user input.

Fuzzy finding

Simple, fast, and configurable fuzzy finding is one of cy's most important features. cy provides a purpose-built fuzzy finder (similar to fzf) in the form of input/find, which is a function available in the API.

Choosing a string from a list

In its simplest form, input/find takes a single parameter: a Janet array of strings that it will present to the user and from which they can choose a single option:

(input/find @["one" "two" "three"])

By default, the background will be animated with one of cy's animations. If the user does not choose anything, this function returns nil; if they do, it will return the option that they chose.

Choosing an arbitrary value from a list

input/find also allows you to ask the user to choose from a list of items, each of which has an underlying Janet value that is returned instead of the string value that the user filters.

You do this by providing a Janet array of tuples, each of which has two elements:

  • The text to be filtered
  • The value that should be returned
(input/find @[
    ["one" 1]
    ["two" 2]
    ["three" 3]])

If the user chooses "one", input/find will return 1.

Filtering tabular data

It is sometimes handy to be able to have the user choose from a row in a table rather than a single line of text. input/find allows you to provide tabular data in addition to titles for each column in the form of tuples.

(input/find
    @[
        [["boba" "$5" "not much"] 3]
        [["latte" "$7" "too much"] 2]
        [["black tea" "$2" "just right"] 1]
     ]
    # Providing headers is optional
    :headers ["drink" "price" "caffeine"])

Choosing with previews

Where input/find really shines, however, is in its ability to show a preview window for each option, which is conceptually similar to fzf's --preview command line flag. input/find can preview three different types of content:

  • Panes: Show the current state of a pane in cy's node tree. This is the live view of a pane, regardless of how many other clients are interacting with it or what is happening on the screen.
  • .borg files: Show a moment in time in a .borg file.
  • Scrollback buffer: Show the output of a particular command in a pane's scrollback buffer.
  • Text: Render some text.
  • Animations: Show one of cy's animations.
  • Frames: Show one of cy's frames.

Options with previews are passed to input/find as Janet tuples with three elements:

  1. The text (or columns) that the user will filter against
  2. A Janet struct describing how this option should be previewed
  3. The value that should be returned if the user chooses this option

Here are some examples:

(input/find @[
        # A standard text preview, which will be rendered in an 80x26 window
        ["some text" {:type :text :text "this is the preview"} 1]
        # A replay preview
        ["this is a borg file" {:type :replay :path "some-file.borg"} 2]
        # A pane preview
        ["this is some other pane" {:type :node :id (pane/current)} 3]
        # A scrollback preview
        ["this is a moment in a pane's history" {
            :type :scrollback
            :focus [0 0]
            :highlights @[]
            :id (pane/current)} 2]
        # An animation preview
        ["this is an animation" {
            :type :animation
            :name "midjo"} 2]
        # A frame preview
        ["this is a frame" {:type :frame :name "puzzle"} 2]
    ])

input/find is used extensively in cy's default startup script. You can find several idiomatic examples of its usage there.

Text

You can prompt the user for freeform text input using the input/text API function. The (input/text), like input/find, also animates the background while the user manipulates the value in the text input.

(input/text) lets you provide both placeholder text (shown when the form is empty) and preset text (to pre-fill the text input.) See the API listing for more details.

Here are some examples:

Preset text

(input/text "preset example" :preset "preset text")

Placeholder text

(input/text "placeholder example" :placeholder "placeholder")

Notifications

cy has two API calls for informing the user of something:

  • msg/log: Send a log message to the /logs pane, which the user can access like any other pane. Logs are also written to /tmp/cy-$(id -u)/*.log.
  • msg/toast: Show a toast message to the user. This message will appear in the top-right corner of their screen and disappear after a few seconds.

Both functions take a level (one of :info, :warn, or :error) and a message.

For example:

(msg/toast :info "this shows up in blue")
(msg/toast :warn "this shows up in yellow")
(msg/toast :error "this shows up in red")

You can configure the colors used for toasts with the color-* family of parameters.

This code would show toasts that look like this:

And for log messages:

(msg/log :info "this shows up in blue")
(msg/log :warn "this shows up in yellow (does it though?)")
(msg/log :error "this shows up in red")

Default keybindings

All of cy's default key bindings use actions defined in the global scope and therefore are easy to rebind should you so desire. For example, to assign action/command-palette to another key sequence:

(key/bind :root ["ctrl+b" "p"] action/command-palette)

Global

These bindings apply everywhere and can always be invoked.

Prefixed

All of the bindings in this table are prefixed by ctrl+a by default. You can change the prefix for cy's bindings using key/remap.

General

SequenceActionDescription
Faction/choose-frameChoose a frame.
ctrl+paction/command-paletteOpen the command palette.
ctrl+raction/ctrl-rFind a recent command and insert it into the current shell.
daction/detachDetach from the cy server.
qaction/kill-serverKill the cy server.
paction/open-replayEnter replay mode for the current pane.
Paction/pasteInsert the contents of the default register.
raction/reload-configReload the cy configuration.
Saction/search-borgSearch all recorded .borg files for a pattern.
" re:[a-zA-Z0-9+] pregister/insertInsert the contents of the given register in the current pane.

Panes

SequenceActionDescription
Caction/jump-commandJump to the output of a command.
faction/jump-history-commandFind a command and open its .borg file.
;action/jump-paneJump to a pane.
caction/jump-pane-commandJump to a pane based on a command.
kaction/jump-projectJump to a project.
:action/jump-screen-linesJump to a pane based on screen lines.
laction/jump-shellJump to a shell.
xaction/kill-and-reattachRemove the current pane from the node tree and attach to a new one.
Xaction/kill-layout-paneRemove the current pane from the layout and the node tree.
Jaction/move-downMove down to the next pane.
downaction/move-downMove down to the next pane.
Haction/move-leftMove left to the next pane.
leftaction/move-leftMove left to the next pane.
Laction/move-rightMove right to the next pane.
rightaction/move-rightMove right to the next pane.
upaction/move-upMove up to the next pane.
Kaction/move-upMove up to the next pane.
naction/new-projectCreate a new project.
jaction/new-shellCreate a new shell.
taction/new-tabCreate a new tab.
tabaction/next-tabSwitch to the next tab.
shift+tabaction/prev-tabSwitch to the previous tab.
Raction/rename-tabRename the current tab.
-action/split-downSplit the current pane downwards.
|action/split-rightSplit the current pane to the right.
ctrl+opane/history-backwardMove backward in the pane history. Works in a similar way to vim's ctrl+o.

Viewport

SequenceActionDescription
2action/margins-160Set size to 160 columns.
1action/margins-80Set margins size to 80 columns.
gaction/toggle-marginsToggle the screen's margins.

Unprefixed

These bindings are not prefixed by ctrl+a.

SequenceActionDescription
ctrl+laction/next-paneMove to the next sibling pane.

Fuzzy finding

input/find has several key bindings that are not yet configurable, but are worth documenting.

SequenceDescription
ctrl+k or upMove up one option.
ctrl+j or downMove down one option.
enterChoose the option under the cursor.
ctrl+c or escQuit without choosing.
homeJump to the top of the list.
endJump to the bottom of the list.
pgupMove upwards by a single page.
pgdownMove downwards by a single page.

Replay mode

The actions found in the tables below are only valid in a pane that is in replay mode. Replay mode uses two isolated binding scopes that can be accessed by providing :time (for time mode) or :copy (for copy mode) to a key/bind call:

(defn do-something [] )
(key/bind :time ["ctrl+b"] do-something)

Time mode

SequenceActionDescription
1action/replay-playback-1xSet the playback rate to 1x real time.
2action/replay-playback-2xSet the playback rate to 2x real time.
3action/replay-playback-5xSet the playback rate to 5x real time.
!action/replay-playback-reverse-1xSet the playback rate to -1x real time (backwards).
@action/replay-playback-reverse-2xSet the playback rate to -2x real time (backwards).
#action/replay-playback-reverse-5xSet the playback rate to -5x real time (backwards).
g greplay/beginningGo to the beginning of the time range (in time mode) or the first line of the screen (in copy mode).
[ creplay/command-backwardIn time mode, jump to the moment in time just before the previous command was executed. In copy mode, move the cursor to the first character of the previous command that was executed.
] creplay/command-forwardIn time mode, jump to the moment in time just before the next command was executed. In copy mode, move the cursor to the first character of the next command.
Greplay/endGo to the end of the time range (in time mode) or the last line of the screen (in copy mode).
ctrl+creplay/quitQuit replay mode.
qreplay/quitQuit replay mode.
escreplay/quitQuit replay mode.
nreplay/search-againGo to the next match in the direction of the last search.
?replay/search-backwardSearch for a string backwards in time (in time mode) or in the scrollback buffer (in copy mode).
/replay/search-forwardSearch for a string forwards in time (in time mode) or in the scrollback buffer (in copy mode).
Nreplay/search-reverseGo to the previous match in the direction of the last search.
spacereplay/time-playToggle playback.
leftreplay/time-step-backStep one event backward in time.
rightreplay/time-step-forwardStep one event forward in time.

Copy mode

Copy mode is entered from time mode by triggering any form of movement, whether that be scrolling or manipulating the cursor.

SequenceActionDescription
" re:[a-zA-Z0-9] yreplay/copyYank the selection into the copy buffer.
" + yreplay/copy-clipboardYank the selection into the system clipboard.
yreplay/copy-defaultYank the selection into the default register.
vreplay/selectEnter visual select mode.

Movements

Movements in copy mode are intended to be as close to vim as possible, but many more will be added over time.

SequenceActionDescription
g greplay/beginningGo to the beginning of the time range (in time mode) or the first line of the screen (in copy mode).
Breplay/big-word-backwardMove to the beginning of the previous WORD. Equivalent to vim's B.
g Ereplay/big-word-end-backwardMove to the end of the previous WORD. Equivalent to vim's gE.
Ereplay/big-word-end-forwardMove to the end of the next WORD. Equivalent to vim's E.
Wreplay/big-word-forwardMove to the beginning of the next WORD. Equivalent to vim's W.
[ creplay/command-backwardIn time mode, jump to the moment in time just before the previous command was executed. In copy mode, move the cursor to the first character of the previous command that was executed.
] creplay/command-forwardIn time mode, jump to the moment in time just before the next command was executed. In copy mode, move the cursor to the first character of the next command.
[ Creplay/command-select-backwardMove the cursor to the first character of the output of the previous command and select its output.
] Creplay/command-select-forwardMove the cursor to the first character of the output of the next command and select its output.
jreplay/cursor-downMove cursor down one cell.
ctrl+hreplay/cursor-leftMove cursor left one cell.
hreplay/cursor-leftMove cursor left one cell.
backspacereplay/cursor-leftMove cursor left one cell.
leftreplay/cursor-leftMove cursor left one cell.
lreplay/cursor-rightMove cursor right one cell.
rightreplay/cursor-rightMove cursor right one cell.
spacereplay/cursor-rightMove cursor right one cell.
kreplay/cursor-upMove cursor up one cell.
Greplay/endGo to the end of the time range (in time mode) or the last line of the screen (in copy mode).
$replay/end-of-lineMove to the last character of the physical line. Equivalent to vim's $.
g $replay/end-of-screen-lineMove to the end of the screen line. Equivalent to vim's g$.
^replay/first-non-blankMove to the first non-blank character of the physical line. Equivalent to vim's ^.
g ^replay/first-non-blank-screenMove to the first non-blank character of the screen line. Equivalent to vim's g^.
ctrl+dreplay/half-page-downScroll the viewport half a page (half the viewport height) down.
ctrl+ureplay/half-page-upScroll the viewport half a page (half the viewport height) up.
;replay/jump-againRepeat the last character jump.
F re:.replay/jump-backwardJump to the previous instance of char on the current line.
f re:.replay/jump-forwardJump to the next instance of char on the current line.
,replay/jump-reverseRepeat the inverse of the last character jump.
T re:.replay/jump-to-backwardJump to the cell before char after the cursor on the current line.
t re:.replay/jump-to-forwardJump to the cell before char after the cursor on the current line.
g _replay/last-non-blankMove to the last non-blank character of the physical line. Equivalent to vim's g_.
g endreplay/last-non-blank-screenMove to the last non-blank character of the screen line. Equivalent to vim's g<end>.
g Mreplay/middle-of-lineMove to the middle of the physical line. Equivalent to vim's gM.
g mreplay/middle-of-screen-lineMove to the middle of the screen line. Equivalent to vim's gm.
escreplay/quitQuit replay mode.
qreplay/quitQuit replay mode.
ctrl+creplay/quitQuit replay mode.
downreplay/scroll-downScroll the viewport one line down.
upreplay/scroll-upScroll the viewport one line up.
nreplay/search-againGo to the next match in the direction of the last search.
?replay/search-backwardSearch for a string backwards in time (in time mode) or in the scrollback buffer (in copy mode).
/replay/search-forwardSearch for a string forwards in time (in time mode) or in the scrollback buffer (in copy mode).
Nreplay/search-reverseGo to the previous match in the direction of the last search.
homereplay/start-of-lineMove to the first character of the physical line. Equivalent to vim's 0.
0replay/start-of-lineMove to the first character of the physical line. Equivalent to vim's 0.
g homereplay/start-of-screen-lineMove to the first character of the screen line. Equivalent to vim's g0.
g 0replay/start-of-screen-lineMove to the first character of the screen line. Equivalent to vim's g0.
sreplay/swap-screenSwap between the alt screen and the main screen. This allows you to return to the pane's scrollback without quitting a program that is using the alternate screen, such as vim or htop.
breplay/word-backwardMove to the beginning of the previous word. Equivalent to vim's b.
g ereplay/word-end-backwardMove to the end of the previous word. Equivalent to vim's ge.
ereplay/word-end-forwardMove to the end of the next word. Equivalent to vim's e.
wreplay/word-forwardMove to the beginning of the next word. Equivalent to vim's w.

Search mode

Search mode has its own binding scope, :search.

SequenceActionDescription
ctrl+csearch/cancelCancel the current operation or the input of a query string.
escsearch/cancelCancel the current operation or the input of a query string.
:search/focus-inputFocus search mode's input bar so you can enter a new query string.
ctrl+nsearch/nextMove to the next .borg file in the search results.
ctrl+psearch/prevMove to the previous .borg file in the search results.

Default parameters

Note that this document omits the initial :. To change the value of a default parameter, you can do something like this:

(param/set :root :default-frame "big-hex")

animate animations color-error color-info color-map color-warn data-directory default-frame default-shell input-find-active-bg input-find-active-fg input-find-inactive-bg input-find-inactive-fg input-preview-border input-preview-border-fg input-prompt-bg input-prompt-fg num-search-workers remove-pane-on-exit replay-copy-bg replay-copy-fg replay-incremental-bg replay-incremental-fg replay-match-active-bg replay-match-active-fg replay-match-inactive-bg replay-match-inactive-fg replay-play-bg replay-play-fg replay-selection-bg replay-selection-fg replay-status-bar-bg replay-status-bar-fg replay-text-copy-mode replay-text-play-mode replay-text-time-mode replay-text-visual-mode replay-time-bg replay-time-fg replay-visual-bg replay-visual-fg search-status-bar-bg search-status-bar-fg search-text-no-matches-found search-text-searching terminal-text-exited timestamp-format


animate

Type: :boolean

Default: true

Whether to enable animation.

animations

Type: :array

Default: @[]

A list of all of the enabled animations that will be used by (input/find). If this is an empty array, all built-in animations will be enabled.

color-error

Type: :string

Default: "1"

The color used for error messages.

color-info

Type: :string

Default: "6"

The color used for info messages.

color-map

Type: :table

Default: @{}

The color map used to translate the colors used for rendering a pane.

color-warn

Type: :string

Default: "3"

The color used for warning messages.

data-directory

Type: :string

Default: ""

The directory in which .borg files will be saved. This is inferred on startup. If set to an empty string, recording to disk is disabled.

default-frame

Type: :string

Default: ""

The frame used for all new clients. A blank string means a random frame will be chosen from all frames.

default-shell

Type: :string

Default: "/bin/bash"

The default shell with which to start panes. Defaults to the value of $SHELL on startup.

input-find-active-bg

Type: :string

Default: "15"

The background color of the active row in (input/find).

input-find-active-fg

Type: :string

Default: "0"

The foreground color of the active row in (input/find).

input-find-inactive-bg

Type: :string

Default: "7"

The background color of the inactive row in (input/find).

input-find-inactive-fg

Type: :string

Default: "0"

The foreground color of the inactive row in (input/find).

input-preview-border

Type: :keyword

Default: :normal

The border style of the preview border in (input/find).

input-preview-border-fg

Type: :string

Default: "5"

The color of the preview border in (input/find).

input-prompt-bg

Type: :string

Default: "3"

The background color of the input prompt in (input/*) functions.

input-prompt-fg

Type: :string

Default: "0"

The foreground color of the input prompt in (input/*) functions.

num-search-workers

Type: :number

Default: 0

The number of goroutines to use for searching in .borg files. Defaults to the number of CPUs.

remove-pane-on-exit

Type: :boolean

Default: false

If this is true, when a pane's process exits or its node is killed (such as with tree/rm), the portion of the layout related to that node will be removed. This makes cy's layout functionality work a bit more like tmux.

replay-copy-bg

Type: :string

Default: "3"

The color used to represent copy mode.

replay-copy-fg

Type: :string

Default: "0"

The foreground color used in time mode when the player is playing.

replay-incremental-bg

Type: :string

Default: "3"

The background color for incremental search in replay mode.

replay-incremental-fg

Type: :string

Default: "0"

The foreground color for incremental search in replay mode.

replay-match-active-bg

Type: :string

Default: "13"

The background color for the current search match in replay mode.

replay-match-active-fg

Type: :string

Default: "1"

The foreground color for the current search match in replay mode.

replay-match-inactive-bg

Type: :string

Default: "14"

The background color for search matches in replay mode.

replay-match-inactive-fg

Type: :string

Default: "1"

The foreground color for search matches in replay mode.

replay-play-bg

Type: :string

Default: "12"

The color used in time mode when the player is playing.

replay-play-fg

Type: :string

Default: "15"

The foreground color used in time mode when the player is playing.

replay-selection-bg

Type: :string

Default: "8"

The background color for selections in replay mode.

replay-selection-fg

Type: :string

Default: "9"

The foreground color for selections in replay mode.

replay-status-bar-bg

Type: :string

Default: "8"

The background color of the status bar in replay mode.

replay-status-bar-fg

Type: :string

Default: "15"

The foreground color of the status bar in replay mode.

replay-text-copy-mode

Type: :string

Default: "COPY"

The text shown in the status bar when in copy mode.

replay-text-play-mode

Type: :string

Default: "⏸"

The text shown in the status bar when playing.

replay-text-time-mode

Type: :string

Default: "⏵"

The text shown in the status bar when in time mode.

replay-text-visual-mode

Type: :string

Default: "VISUAL"

The text shown in the status bar when in visual mode.

replay-time-bg

Type: :string

Default: "4"

The color used to represent time mode.

replay-time-fg

Type: :string

Default: "15"

The foreground color used to represent time mode.

replay-visual-bg

Type: :string

Default: "10"

The color used to represent visual mode.

replay-visual-fg

Type: :string

Default: "0"

The foreground color used in time mode when the player is playing.

search-status-bar-bg

Type: :string

Default: "4"

The background color of the status bar in search mode.

search-status-bar-fg

Type: :string

Default: "15"

The foreground color of the status bar in search mode.

search-text-no-matches-found

Type: :string

Default: "no matches found for"

The text shown in the status bar when no matches are found.

search-text-searching

Type: :string

Default: "searching"

The text shown in the status bar when searching.

terminal-text-exited

Type: :string

Default: "exited"

The text shown when a terminal session exits.

timestamp-format

Type: :string

Default: "2006-01-02 15:04:05"

The format for all timestamps shown in cy. This uses Go's time.Layout format described here.

Key specifiers

Key specifiers are strings that have a special meaning to functions like key/bind. They are a shorthand for the arcane byte sequences actually sent by your terminal when you enter these key combinations.

Want to bind the alt key? Read this.

cy defines the following key specifiers:

SpecifierNotes
"ctrl+@"
"ctrl+a"
"ctrl+b"
"ctrl+c"
"ctrl+d"
"ctrl+e"
"ctrl+f"
"ctrl+g"
"ctrl+h"
"tab"(this is what "ctrl+i" outputs)
"ctrl+j"
"ctrl+k"
"ctrl+l"
"enter" or "return"
"ctrl+n"
"ctrl+o"
"ctrl+p"
"ctrl+q"
"ctrl+r"
"ctrl+s"
"ctrl+t"
"ctrl+u"
"ctrl+v"
"ctrl+w"
"ctrl+x"
"ctrl+y"
"ctrl+z"
"esc"
"ctrl+\\"
"ctrl+]"
"ctrl+^"
"ctrl+\_"
"backspace"
"up"
"down"
"right"
" " or "space"
"left"
"shift+tab"
"home"
"end"
"ctrl+home"
"ctrl+end"
"shift+home"
"shift+end"
"ctrl+shift+home"
"ctrl+shift+end"
"pgup"
"pgdown"
"ctrl+pgup"
"ctrl+pgdown"
"delete"
"insert"
"ctrl+up"
"ctrl+down"
"ctrl+right"
"ctrl+left"
"shift+up"
"shift+down"
"shift+right"
"shift+left"
"ctrl+shift+up"
"ctrl+shift+down"
"ctrl+shift+left"
"ctrl+shift+right"
"f1"
"f2"
"f3"
"f4"
"f5"
"f6"
"f7"
"f8"
"f9"
"f10"
"f11"
"f12"
"f13"
"f14"
"f15"
"f16"
"f17"
"f18"
"f19"
"f20"

Frames

You can browse these in cy using the action/choose-frame action, which is bound to ctrl+a F [?] by default.


big-hex

(viewport/set-frame "big-hex")

frame/big-hex


brick

(viewport/set-frame "brick")

frame/brick


cheerios

(viewport/set-frame "cheerios")

frame/cheerios


cross

(viewport/set-frame "cross")

frame/cross


cross-stitch

(viewport/set-frame "cross-stitch")

frame/cross-stitch


dot-bricks

(viewport/set-frame "dot-bricks")

frame/dot-bricks


hive

(viewport/set-frame "hive")

frame/hive


hive-thick

(viewport/set-frame "hive-thick")

frame/hive-thick


none

(viewport/set-frame "none")

frame/none


puzzle

(viewport/set-frame "puzzle")

frame/puzzle


squares

(viewport/set-frame "squares")

frame/squares


stars

(viewport/set-frame "stars")

frame/stars


tiles

(viewport/set-frame "tiles")

frame/tiles


wallpaper

(viewport/set-frame "wallpaper")

frame/wallpaper


zigzag

(viewport/set-frame "zigzag")

frame/zigzag


Animations

You can browse these in cy using the action/browse-animations action.


collapse

animation/collapse


conway

animation/conway


cos

animation/cos


cy

animation/cy


fluid

animation/fluid


midjo

animation/midjo


musicforprogramming

animation/musicforprogramming


slime

animation/slime


API

This is a complete listing of all of the API functions that cy provides in the top-level Janet environment (ie you do not need to import or use anything.)

Janet code executed with cy can also access everything from Janet's standard library except for anything in spork. In addition, Janet's ev family of functions probably will not work; I have never tested them.

Concepts

Binding

Several API functions related to binding keys return a Binding. A Binding table represents a single key sequence and its associated function. Each table has the following properties:

  • :node: the NodeID where the binding is defined.
  • :sequence: a list of strings representing the key sequence that will execute this action. If the original call to key/bind used a regex, it will be returned as a string with a re: prefix.
  • :function: the Janet function that will be called when this sequence is executed.

For example:

{
  :node 1
  :sequence ["ctrl+a" "t"]
  :function <some function>
}

Color

Some API functions, such as style/render, accept colors as input. cy supports True Color colors specified in hexadecimal along with ANSI 16 and ANSI-256 colors. Under the hood, cy uses charmbracelet/lipgloss and thus supports its color references.

You can read more about color support in terminal emulators here.

Examples of valid colors:

"#ffffff" # true color
"#123456" # true color
"255" # ANSI 256
"0" # ANSI 16

Color map

A color map is a mapping from one color to another. They can represented by Janet structs or tables:

# The "zenburn" color map
# This maps the default 16 ANSI colors to hand-picked RGB colors
@{"0" "#383838"
  "1" "#dca3a3"
  "2" "#5f7f5f"
  "3" "#e0cf9f"
  "4" "#7cb8bb"
  "5" "#dc8cc3"
  "6" "#93e0e3"
  "7" "#dcdccc"
  "8" "#6f6f6f"
  "9" "#dfaf8f"
  "10" "#404040"
  "11" "#606060"
  "12" "#808080"
  "13" "#c0c0c0"
  "14" "#000000"
  "15" "#ffffff"}

For more information about color maps, see the relevant section in the parameters chapter.

Command

cmd/query and {{api/commands}} return Commands, which are structs that look like this:

{:text "echo 'Hello, World!'" # The command that was run
 :directory "/some/dir"
 # When the command started executing
 :executed-at {} # Time
 # When it finished
 :completed-at {} # Time
 # These three properties only have internal meaning for now
 :prompted 23
 :executed 26
 :completed 52
 :pending false
 # Input and output are Selections, which can currently only be passed to
 # input/find previews. Refer to the standard library.
 :input @[{:from (97 6)
           :to (97 7)}]
 :output {:from (98 0)
          :to (98 42)}}

NodeID

Many API functions have a parameter of type NodeID, which can be one of two values:

  • :root which is a short way of referring to cy's top-level group.
  • An integer that refers to a node in cy's node tree. You cannot infer these yourself, but they are returned from API functions like pane/current and group/children.

Time

The (time/*) family of functions works with Time values, which are similar (but not identical) to the value returned by Janet's built-in (os/date) function.

Time values are structs with the following properties:

{:dst false # unused, just to mimic (os/date)
 :hours 10
 :milliseconds 624
 :minutes 15
 :month 9
 :month-day 15
 :seconds 31
 :utc false # if false, properties are in local time
 :week-day 0
 :year 2024
 :year-day 259}

For specifics on the range of each scalar property, consult the documentation for Go's time package, such as for time.Weekday().

Symbols

action/add-node action/browse-animations action/choose-frame action/clear-layout action/command-palette action/cpu-profile action/ctrl-r action/detach action/jump-command action/jump-group-pane action/jump-history-command action/jump-pane action/jump-pane-command action/jump-project action/jump-screen-lines action/jump-shell action/kill-and-reattach action/kill-layout-pane action/kill-pane action/kill-server action/margins-160 action/margins-80 action/margins-bigger action/margins-smaller action/move-down action/move-left action/move-right action/move-up action/new-project action/new-shell action/new-tab action/next-pane action/next-tab action/open-log action/open-replay action/paste action/paste-clipboard action/prev-pane action/prev-tab action/random-frame action/recall-command action/reload-config action/remove-layout-pane action/remove-parent action/rename-pane action/rename-tab action/replay-playback-1x action/replay-playback-2x action/replay-playback-5x action/replay-playback-reverse-1x action/replay-playback-reverse-2x action/replay-playback-reverse-5x action/search-borg action/set-borders-title action/set-borders-title-bottom action/set-layout-borders action/set-pane-colors action/show-color-map action/split-down action/split-left action/split-right action/split-up action/toggle-margins action/trace assoc clipboard/get clipboard/set cmd/commands cmd/kill cmd/new cmd/path cmd/query color-maps/get color-maps/get-all color-maps/get-id color-maps/set cy/cpu-profile cy/detach cy/kill-server cy/paste cy/reload-config cy/trace exec/file group/children group/leaves group/mkdir group/new input/find input/text key/action key/bind key/bind-many key/bind-many-tag key/current key/get key/get-actions key/remap key/unbind layout/assoc layout/attach layout/attach-first layout/attach-id layout/attach-path layout/attached? layout/bar layout/borders layout/color-map layout/detach layout/find layout/find-last layout/get layout/has? layout/hsplit layout/map layout/margins layout/move layout/move-down layout/move-left layout/move-right layout/move-up layout/new layout/pane layout/pane? layout/path layout/remove-attached layout/replace layout/replace-attached layout/set layout/split layout/split-down layout/split-left layout/split-right layout/split-up layout/successors layout/tab layout/tabs layout/type? layout/vsplit msg/log msg/toast pane/attach pane/current pane/history-backward pane/history-forward pane/screen pane/send-keys param/get param/rset param/set param/set-many path/abs path/base path/glob path/join register/get register/insert register/set replay/beginning replay/big-word-backward replay/big-word-end-backward replay/big-word-end-forward replay/big-word-forward replay/command-backward replay/command-forward replay/command-select-backward replay/command-select-forward replay/copy replay/copy-clipboard replay/copy-default replay/cursor-down replay/cursor-left replay/cursor-right replay/cursor-up replay/end replay/end-of-line replay/end-of-screen-line replay/first-non-blank replay/first-non-blank-screen replay/half-page-down replay/half-page-up replay/jump-again replay/jump-backward replay/jump-forward replay/jump-reverse replay/jump-to-backward replay/jump-to-forward replay/last-non-blank replay/last-non-blank-screen replay/middle-of-line replay/middle-of-screen-line replay/open replay/open-file replay/quit replay/scroll-down replay/scroll-up replay/search-again replay/search-backward replay/search-forward replay/search-reverse replay/select replay/start-of-line replay/start-of-screen-line replay/swap-screen replay/time-play replay/time-playback-rate replay/time-step-back replay/time-step-forward replay/word-backward replay/word-end-backward replay/word-end-forward replay/word-forward search/cancel search/first search/focus-input search/last search/new search/next search/prev shell/attach shell/new style/render style/text time/format time/now tree/group? tree/name tree/pane? tree/parent tree/path tree/rm tree/root tree/set-name viewport/get-animations viewport/get-frames viewport/set-frame


action/add-node

function

(action/add-node)

Add a node to the layout.

source

action/browse-animations

function

(action/browse-animations)

Browse animations.

source

action/choose-frame

function

(action/choose-frame)

Choose a frame.

source

action/clear-layout

function

(action/clear-layout)

Clear out the layout.

source

action/command-palette

function

(action/command-palette)

Open the command palette.

source

action/cpu-profile

function

(action/cpu-profile)

Save a CPU profile to cy's socket directory.

source

action/ctrl-r

function

(action/ctrl-r)

Find a recent command and insert it into the current shell.

source

action/detach

function

(action/detach)

Detach from the cy server.

source

action/jump-command

function

(action/jump-command)

Jump to the output of a command.

source

action/jump-group-pane

function

(action/jump-group-pane)

Jump to a pane that is a descendant of the current group.

source

action/jump-history-command

function

(action/jump-history-command)

Find a command and open its .borg file.

source

action/jump-pane

function

(action/jump-pane)

Jump to a pane.

source

action/jump-pane-command

function

(action/jump-pane-command)

Jump to a pane based on a command.

source

action/jump-project

function

(action/jump-project)

Jump to a project.

source

action/jump-screen-lines

function

(action/jump-screen-lines)

Jump to a pane based on screen lines.

source

action/jump-shell

function

(action/jump-shell)

Jump to a shell.

source

action/kill-and-reattach

function

(action/kill-and-reattach)

Remove the current pane from the node tree and attach to a new one.

A new shell will be created if the current pane is the last one in the layout.

source

action/kill-layout-pane

function

(action/kill-layout-pane)

Remove the current pane from the layout and the node tree.

source

action/kill-pane

function

(action/kill-pane)

Kill the process of the current pane, but do not detach from it.

source

action/kill-server

function

(action/kill-server)

Kill the cy server.

source

action/margins-160

function

(action/margins-160)

Set size to 160 columns.

source

action/margins-80

function

(action/margins-80)

Set margins size to 80 columns.

source

action/margins-bigger

function

(action/margins-bigger)

Increase margins by 5 columns.

source

action/margins-smaller

function

(action/margins-smaller)

Decrease margins by 5 columns.

source

action/move-down

function

(action/move-down)

Move down to the next pane.

source

action/move-left

function

(action/move-left)

Move left to the next pane.

source

action/move-right

function

(action/move-right)

Move right to the next pane.

source

action/move-up

function

(action/move-up)

Move up to the next pane.

source

action/new-project

function

(action/new-project)

Create a new project.

source

action/new-shell

function

(action/new-shell)

Create a new shell.

source

action/new-tab

function

(action/new-tab)

Create a new tab.

source

action/next-pane

function

(action/next-pane)

Move to the next sibling pane.

source

action/next-tab

function

(action/next-tab)

Switch to the next tab.

source

action/open-log

function

(action/open-log)

Open a .borg file.

source

action/open-replay

function

(action/open-replay)

Enter replay mode for the current pane.

source

action/paste

function

(action/paste)

Insert the contents of the default register.

source

action/paste-clipboard

function

(action/paste-clipboard)

Insert the contents of the system clipboard.

source

action/prev-pane

function

(action/prev-pane)

Move to the previous sibling pane.

source

action/prev-tab

function

(action/prev-tab)

Switch to the previous tab.

source

action/random-frame

function

(action/random-frame)

Switch to a random frame.

source

action/recall-command

function

(action/recall-command)

Recall the output of a command to the current shell.

source

action/reload-config

function

(action/reload-config)

Reload the cy configuration.

source

action/remove-layout-pane

function

(action/remove-layout-pane)

Remove the current pane from the layout.

source

action/remove-parent

function

(action/remove-parent)

Remove the parent of the current node.

source

action/rename-pane

function

(action/rename-pane)

Rename the current pane.

source

action/rename-tab

function

(action/rename-tab)

Rename the current tab.

source

action/replay-playback-1x

function

(action/replay-playback-1x)

Set the playback rate to 1x real time.

source

action/replay-playback-2x

function

(action/replay-playback-2x)

Set the playback rate to 2x real time.

source

action/replay-playback-5x

function

(action/replay-playback-5x)

Set the playback rate to 5x real time.

source

action/replay-playback-reverse-1x

function

(action/replay-playback-reverse-1x)

Set the playback rate to -1x real time (backwards).

source

action/replay-playback-reverse-2x

function

(action/replay-playback-reverse-2x)

Set the playback rate to -2x real time (backwards).

source

action/replay-playback-reverse-5x

function

(action/replay-playback-reverse-5x)

Set the playback rate to -5x real time (backwards).

source

action/search-borg

function

(action/search-borg)

Search all recorded .borg files for a pattern.

source

action/set-borders-title

function

(action/set-borders-title)

Set the :title for a :borders node.

source

action/set-borders-title-bottom

function

(action/set-borders-title-bottom)

Set the :title-bottom for a :borders node.

source

action/set-layout-borders

function

(action/set-layout-borders)

Change the border style across the entire layout.

source

action/set-pane-colors

function

(action/set-pane-colors)

Set the color map for the current pane.

source

action/show-color-map

function

(action/show-color-map)

Send a toast with the ID of the current color map.

source

action/split-down

function

(action/split-down)

Split the current pane downwards.

source

action/split-left

function

(action/split-left)

Split the current pane to the left.

source

action/split-right

function

(action/split-right)

Split the current pane to the right.

source

action/split-up

function

(action/split-up)

Split the current pane upwards.

source

action/toggle-margins

function

(action/toggle-margins)

Toggle the screen's margins.

source

action/trace

function

(action/trace)

Save a trace to cy's socket directory.

source

assoc

function

(assoc s key value)

Set a property in a struct, returning a new struct.

source

clipboard/get

function

(clipboard/get)

Get the contents of the system clipboard.

source

clipboard/set

function

(clipboard/set text)

Set the contents of the system clipboard.

source

cmd/commands

function

(cmd/commands target)

Get the commands executed in a particular pane. Returns an array of Commands. target is a NodeID.

source

cmd/kill

function

(cmd/kill target)

Kill the pane specified by target. target is a NodeID.

source

cmd/new

function

(cmd/new parent &named path restart command args name)

Run command with args and working directory path in a new pane as a child of the group specified by parent. You may also provide the name of the new pane. If command is not specified, (cmd/new) defaults to the current user's shell. parent is a NodeID.

If restart is true, when the command exits, it will be rerun. However, if the command exits with a non-zero exit code more than three times in a second, it will not be run again.

Some examples:

# Create a new shell in the root group
(cmd/new :root)

# `args` is a list of strings
(cmd/new :root :command "less" :args @["README.md"])

source

cmd/path

function

(cmd/path target)

Get the working directory of the program running in the pane pane specified by target. target is a NodeID.

source

cmd/query

function

(cmd/query)

Query all of the commands stored in the commmand database.

source

color-maps/get

function

(color-maps/get id)

Get a color map by id.

source

color-maps/get-all

function

(color-maps/get-all)

Get all of the built-in color maps.

source

color-maps/get-id

function

(color-maps/get-id target)

Get the ID of the color map of the target node.

source

color-maps/set

function

(color-maps/set target id)

Set the :color-map parameter of the target node to the color map specified by id. Also sets the :color-map-id parameter.

target is a NodeID.

source

cy/cpu-profile

function

(cy/cpu-profile)

Save a 15-second CPU profile captured with pprof to the socket directory. This is only useful for debugging.

source

cy/detach

function

(cy/detach)

Detach from the cy server.

source

cy/kill-server

function

(cy/kill-server)

Kill the cy server, disconnecting all clients.

source

cy/paste

function

(cy/paste arg0)

Paste the text in the copy buffer to the current pane.

source

cy/reload-config

function

(cy/reload-config)

Detect and (re)evaluate cy's configuration. This uses the same configuration detection scheme described in the Configuration chapter.

source

cy/trace

function

(cy/trace)

Save a 15-second trace captured with runtime/trace to the socket directory. This is only useful for debugging.

source

exec/file

function

(exec/file path)

Execute the Janet file found at path. Throws any errors that occur during execution.

source

group/children

function

(group/children group)

Get the NodeIDs for all of group's child nodes.

source

group/leaves

function

(group/leaves group)

Get the NodeIDs for all of the leaf nodes reachable from group. In other words, this is a list of all of the NodeIDs for all panes that are descendants of group.

source

group/mkdir

function

(group/mkdir group path)

Get the group at the end of path from the perspective of group, creating any groups that do not exist. Returns the NodeID of the final group.

For example:

(group/mkdir :root "/foo/bar/baz")
# Returns the NodeID of "baz"

This function throws an error if any node in the path already exists and is not a group.

source

group/new

function

(group/new parent &named name)

Create a new group with parent and (optionally) name.

parent is a NodeID.

source

input/find

function

(input/find inputs &named prompt full reverse animated headers)

(input/find) is a general-purpose fuzzy finder that is similar to fzf. When invoked, it prompts the user to choose from one of the items provided in inputs. (input/find) does not return until the user makes a choice; if they choose nothing (such as by hitting ctrl+c), it returns nil.

inputs is an array with elements that can take different forms depending on the desired behavior. See more on the page about fuzzy finding.

This function supports a range of named parameters that adjust its functionality:

  • :animated (boolean): Enable and disable background animation.
  • :case-sensitive (boolean): Whether the matching algorithm should respect differences in case. The default is false.
  • :full (boolean): If true, occupy the entire screen.
  • :headers ([]string): Provide a title for each column. This mostly used for filtering tabular data.
  • :prompt (string): The text that will be shown beneath the search window.
  • :reverse (boolean): Display from the top of the screen (rather than the bottom.)
  • :width (int): Set the width of the match window (if not in full screen mode.)
  • :height (int): Set the maximum height of the match window. This applies bth in full screen and floating mode.

source

input/text

function

(input/text prompt &named preset placeholder full reverse animated)

(input/text) prompts the user with a freeform text input. If the input is non-empty when the user presses enter, (input/text) returns the value of the text input; otherwise it returns nil.

The only required parameter, prompt, is a string that determines the prompt text that is shown below the text input.

This function supports a range of named parameters that adjust its functionality:

  • :preset (string): Pre-fill the value of the text input.
  • :placeholder (string): This string will be shown when the text input is empty.
  • :full (boolean): If true, occupy the entire screen.
  • :reverse (boolean): Display from the top of the screen (rather than the bottom.)
  • :animated (boolean): Enable and disable background animation.

source

key/action

macro

(key/action name docstring & body)

Register an action. Equivalent to the Janet built-in (defn), but requires a docstring.

An action is just a Janet function that is registered to the cy server with a short human-readable string description. It provides a convenient method for making some functionality you use often more discoverable.

In a similar way to other modern applications, cy has a command palette (invoked by default with ctrl+a ctrl+p [?], see action/command-palette) in which all registered actions will appear.

source

key/bind

function

(key/bind target sequence callback)

Bind the key sequence sequence to callback for node target, which is a NodeID, :time (for time mode), or :copy (for copy mode). target can refer to any group or pane.

sequence is a key sequence, which consists of a tuple with string elements that are either key literals ("h"), preset key specifiers ("ctrl+a"), or regex patterns ([:re "^[a-z]$"]).

Read more about binding keys in the dedicated chapter.

source

key/bind-many

macro

(key/bind-many scope & body)

Bind many bindings at once in the same scope.

For example:

(key/bind-many :root
               [prefix "j"] action/new-shell
               [prefix "n"] action/new-project)

source

key/bind-many-tag

macro

(key/bind-many-tag scope tag & body)

Bind many bindings at once in the same scope, adding the provided tag.

source

key/current

function

(key/current)

Get all of the bindings accessible to the current client as an array of Bindings. It contains all of the bindings defined by the node to which the client is attached and its ancestors. In other words, this is equivalent to the list of bindings against which the client's key presses are compared.

source

key/get

function

(key/get target)

Get all of target's bindings. target is a NodeID, :time, or :copy. Returns an array of Bindings. Note that this does not return bindings defined in an ancestor node, only those defined on the node itself.

source

key/get-actions

function

(key/get-actions)

Get all registered actions.

source

key/remap

function

(key/remap target from to)

Remap all bindings that begin with sequence from to sequence to for node target, which is a NodeID, :time, or :copy. Empty sequences ([]) are not currently supported for from and to.

For example, to remap all of the default bindings that begin with ctrl+a to ctrl+v:

(key/remap :root ["ctrl+a"] ["ctrl+v"])

source

key/unbind

function

(key/unbind target sequence)

Clear all bindings that begin with sequence for node target, which is a NodeID, :time, or :copy. Note that the empty sequence [] will unbind all keys in the scope.

sequence is a key sequence, which consists of a tuple with string elements that are either key literals ("h"), preset key specifiers ("ctrl+a"), or regex patterns ([:re "^[a-z]$"]).

The following code snippet will unbind all of cy's default keybindings that begin with ctrl+a:

(key/unbind :root ["ctrl+a"])

source

layout/assoc

function

(layout/assoc layout path node)

Set the node at the given path in layout to the provided node. Returns a copy of the original layout with the node changed.

source

layout/attach

function

(layout/attach layout path)

Attach to the node at path in layout.

source

layout/attach-first

function

(layout/attach-first layout)

Attach to the first pane found in the layout.

source

layout/attach-id

function

(layout/attach-id node)

Get the NodeID of the attached node for the given node.

source

layout/attach-path

function

(layout/attach-path node)

Get the path to the attached node for the given node.

source

layout/attached?

function

(layout/attached? node)

Report whether node or one of its descendants is attached.

source

layout/bar

function

(layout/bar text node &named bottom)

Convenience function for creating a new :bar node.

source

layout/borders

function

(layout/borders node &named title title-bottom border border-fg border-bg)

Convenience function for creating a new :borders node.

source

layout/color-map

function

(layout/color-map map node)

Convenience function for creating a new :color-map node.

source

layout/detach

function

(layout/detach node)

Detach the attached node in the tree.

source

layout/find

function

(layout/find node predicate)

Get the path to the first node satisfying the predicate function or nil if none exists.

source

layout/find-last

function

(layout/find-last layout path predicate)

Get the path to the last node in the path where (predicate node) evaluates to true.

source

layout/get

function

(layout/get)

Get the layout of the current user.

source

layout/has?

function

(layout/has? node predicate)

Report whether this node or one of its descendants matches the predicate function.

source

layout/hsplit

function

(layout/hsplit a b &named cells percent border border-fg border-bg)

Convenience function for creating a new horizontal :split node.

source

layout/map

function

(layout/map mapping layout)

Pass all nodes in the tree into a mapping function.

source

layout/margins

function

(layout/margins node &named cols rows border border-fg border-bg)

Convenience function for creating a new :margins node.

source

layout/move

function

(layout/move layout is-axis axis-successors)

This function attaches to the pane nearest to the one the user is currently attached to along an axis. It returns a new copy of layout with the attachment point changed or returns the same layout if no motion could be completed.

is-axis is a unary function that, given a node, returns a boolean that indicates whether the node is arranged along the axis in question. For example, when moving vertically, a vertical split (two panes on top of each other) would return true.

successors is a unary function that, given a node where is-axis was true, returns the paths of all of the child nodes accessible from the node in the order of their appearance along the axis.

For example, when moving vertically upwards, for a vertical split node this function would return @[[:b] [:a]], because :b is the first node from the bottom, and when moving vertically downwards it would return @[[:a] [:b]] because :a is the first node from the top.

source

layout/move-down

function

(layout/move-down layout)

Change the layout by moving to the next node "below" the attached pane.

source

layout/move-left

function

(layout/move-left layout)

Change the layout by moving to the next node to the left of the attached pane.

source

layout/move-right

function

(layout/move-right layout)

Change the layout by moving to the next node to the right of the attached pane.

source

layout/move-up

function

(layout/move-up layout)

Change the layout by moving to the next node "above" the attached pane.

source

layout/new

macro

(layout/new body)

Macro for quickly creating layouts. layout/new replaces shorthand versions of node creation functions with their longform versions and also includes a few abbreviations that do not exist elsewhere in the API.

Supported short forms:

  • active-tab: A :tab with :active=true inside of a :tabs node.
  • attach: An attached :pane node.
  • bar: A :bar node.
  • borders: A :borders node.
  • color-map: A :color-map node.
  • hsplit: A :split node with :vertical=false.
  • margins: A :margins node.
  • pane: A detached :pane node.
  • split: A :split node.
  • tab: A :tab inside of a :tabs node.
  • tabs: A :tabs node.
  • vsplit: A :split node with :vertical=true.

See the layouts chapter for more information.

source

layout/pane

function

(layout/pane &named id attached remove-on-exit)

Convenience function for creating a new :pane node.

source

layout/pane?

function

(layout/pane? node)

Report whether node is of type :pane.

source

layout/path

function

(layout/path node path)

Resolve the path to a node. Returns nil if any portion of the path is invalid.

source

layout/remove-attached

function

(layout/remove-attached layout)

Remove the attached node from the layout, simplifying the nearest ancestor with children.

source

layout/replace

function

(layout/replace node path replacer)

Replace the node at the path by passing it through a replacer function.

source

layout/replace-attached

function

(layout/replace-attached node replacer)

Replace the attached pane in this tree with a new one using the provided replacer function. This function will be invoked with a single argument, the node that is currently attached, and it should return a new node.

source

layout/set

function

(layout/set layout)

Set the layout of the current user.

source

layout/split

function

(layout/split a b &named vertical cells percent border border-fg border-bg)

Convenience function for creating a new :split node.

source

layout/split-down

function

(layout/split-down layout node)

Split the currently attached pane into two vertically, replacing the bottom pane with the given node.

source

layout/split-left

function

(layout/split-left layout node)

Split the currently attached pane into two horizontally, replacing the left pane with the given node.

source

layout/split-right

function

(layout/split-right layout node)

Split the currently attached pane into two horizontally, replacing the right pane with the given node.

source

layout/split-up

function

(layout/split-up layout node)

Split the currently attached pane into two vertically, replacing the top pane with the given node.

source

layout/successors

function

(layout/successors node)

Get the paths to all of the direct children of this node.

For example:

# For a node of type :split
@[[:a] [:b]]
# For a node of type :pane (it has no children)
@[]

source

layout/tab

function

(layout/tab name node &named active)

Convenience function for creating a new tab (inside of a :tabs node).

source

layout/tabs

function

(layout/tabs tabs &named active-fg active-bg inactive-fg inactive-bg bg bottom)

Convenience function for creating a new :tabs node.

source

layout/type?

function

(layout/type? type node)

Report whether node is of the provided type.

source

layout/vsplit

function

(layout/vsplit a b &named cells percent border border-fg border-bg)

Convenience function for creating a new vertical :split node.

source

msg/log

function

(msg/log level message)

Log message to the /logs pane. level must be one of :info, :warn, :error.

source

msg/toast

function

(msg/toast level message)

Send a toast with message to the client. level must be one of :info, :warn, :error.

source

pane/attach

function

(pane/attach pane)

Attach to pane, which is a NodeID that must correspond to a pane. This is similar to tmux's attach command.

source

pane/current

function

(pane/current)

Get the NodeID of the current pane.

source

pane/history-backward

function

(pane/history-backward)

Move backward in the pane history. Works in a similar way to vim's ctrl+o.

source

pane/history-forward

function

(pane/history-forward)

Move forward in the pane history. Works in a similar way to vim's ctrl+i.

source

pane/screen

function

(pane/screen pane)

Get the visible screen lines of the pane referred to by NodeID. Returns an array of strings.

source

pane/send-keys

function

(pane/send-keys pane keys)

Send keys to the pane referred to by NodeID. keys is an array of strings. Strings that are not key specifiers will be written as-is.

(def pane (cmd/new :root))

# Send the string "test" to the pane
(pane/send-keys pane @["test"])

# Send ctrl+c to the pane
(pane/send-keys pane @["ctrl+c"])

source

param/get

function

(param/get key &named target)

Get the value of the parameter with key, which must be a keyword.

Parameter values are retrieved in the following order:

  1. The client's parameter table, which overrides all other parameters and can be set with (param/set :client ...).
  2. The parameter table for the node the client is attached to and every parent node's parameter table, recursively, until the root node is reached.
  3. The default value for default parameters.

If target is provided, it must be either :client or a NodeID. This can be used to get parameter values from the perspective of another node in the tree, since it does the same cascade that (param/get) does normally.

source

param/rset

function

(param/rset key value)

Set the value of a parameter at :root.

source

param/set

function

(param/set target key value)

Set the value of the parameter at target for key to value. target must be either :client or a NodeID (such as :root.) value can be any Janet value, though (param/set) does enforce the type of default parameters.

source

param/set-many

macro

(param/set-many target & body)

Set many params at once with the same target.

For example:

(param/set-many :root
               :replay-play-bg "#ff0000"
               :replay-time-bg "#00ff00")

source

path/abs

function

(path/abs path)

Return the full absolute path for path. Calls Go's path/filepath.Abs.

source

path/base

function

(path/base path)

Return the last element of path. Calls Go's path/filepath.Base.

source

path/glob

function

(path/glob pattern)

Return an array of all files matching pattern. Calls Go's path/filepath.Glob.

source

path/join

function

(path/join paths)

Join the elements of the string array paths with the OS's file path separator. Calls Go's path/filepath.Join.

source

register/get

function

(register/get register)

Get the value stored in the given register. The "+" register refers to the system clipboard.

source

register/insert

function

(register/insert register)

Insert the contents of the given register in the current pane.

source

register/set

function

(register/set register value)

Store a string in the given register, which can be any string. The "+" register refers to the system clipboard.

source

replay/beginning

function

(replay/beginning)

Go to the beginning of the time range (in time mode) or the first line of the screen (in copy mode).

source

replay/big-word-backward

function

(replay/big-word-backward)

Move to the beginning of the previous WORD. Equivalent to vim's B.

source

replay/big-word-end-backward

function

(replay/big-word-end-backward)

Move to the end of the previous WORD. Equivalent to vim's gE.

source

replay/big-word-end-forward

function

(replay/big-word-end-forward)

Move to the end of the next WORD. Equivalent to vim's E.

source

replay/big-word-forward

function

(replay/big-word-forward)

Move to the beginning of the next WORD. Equivalent to vim's W.

source

replay/command-backward

function

(replay/command-backward)

In time mode, jump to the moment in time just before the previous command was executed. In copy mode, move the cursor to the first character of the previous command that was executed.

source

replay/command-forward

function

(replay/command-forward)

In time mode, jump to the moment in time just before the next command was executed. In copy mode, move the cursor to the first character of the next command.

source

replay/command-select-backward

function

(replay/command-select-backward)

Move the cursor to the first character of the output of the previous command and select its output.

source

replay/command-select-forward

function

(replay/command-select-forward)

Move the cursor to the first character of the output of the next command and select its output.

source

replay/copy

function

(replay/copy arg0)

Yank the selection into the copy buffer.

source

replay/copy-clipboard

function

(replay/copy-clipboard)

Yank the selection into the system clipboard.

source

replay/copy-default

function

(replay/copy-default)

Yank the selection into the default register.

source

replay/cursor-down

function

(replay/cursor-down)

Move cursor down one cell.

source

replay/cursor-left

function

(replay/cursor-left)

Move cursor left one cell.

source

replay/cursor-right

function

(replay/cursor-right)

Move cursor right one cell.

source

replay/cursor-up

function

(replay/cursor-up)

Move cursor up one cell.

source

replay/end

function

(replay/end)

Go to the end of the time range (in time mode) or the last line of the screen (in copy mode).

source

replay/end-of-line

function

(replay/end-of-line)

Move to the last character of the physical line. Equivalent to vim's $.

source

replay/end-of-screen-line

function

(replay/end-of-screen-line)

Move to the end of the screen line. Equivalent to vim's g$.

source

replay/first-non-blank

function

(replay/first-non-blank)

Move to the first non-blank character of the physical line. Equivalent to vim's ^.

source

replay/first-non-blank-screen

function

(replay/first-non-blank-screen)

Move to the first non-blank character of the screen line. Equivalent to vim's g^.

source

replay/half-page-down

function

(replay/half-page-down)

Scroll the viewport half a page (half the viewport height) down.

source

replay/half-page-up

function

(replay/half-page-up)

Scroll the viewport half a page (half the viewport height) up.

source

replay/jump-again

function

(replay/jump-again)

Repeat the last character jump.

source

replay/jump-backward

function

(replay/jump-backward char)

Jump to the previous instance of char on the current line.

source

replay/jump-forward

function

(replay/jump-forward char)

Jump to the next instance of char on the current line.

source

replay/jump-reverse

function

(replay/jump-reverse)

Repeat the inverse of the last character jump.

source

replay/jump-to-backward

function

(replay/jump-to-backward char)

Jump to the cell before char after the cursor on the current line.

source

replay/jump-to-forward

function

(replay/jump-to-forward char)

Jump to the cell before char after the cursor on the current line.

source

replay/last-non-blank

function

(replay/last-non-blank)

Move to the last non-blank character of the physical line. Equivalent to vim's g_.

source

replay/last-non-blank-screen

function

(replay/last-non-blank-screen)

Move to the last non-blank character of the screen line. Equivalent to vim's g<end>.

source

replay/middle-of-line

function

(replay/middle-of-line)

Move to the middle of the physical line. Equivalent to vim's gM.

source

replay/middle-of-screen-line

function

(replay/middle-of-screen-line)

Move to the middle of the screen line. Equivalent to vim's gm.

source

replay/open

function

(replay/open id &named copy focus alt-screen)

Enter replay mode for pane id (which is a NodeID).

This function supports a range of named parameters that adjust its functionality:

  • :alt-screen (boolean): If true, attempt to swap to the terminal's alt screen.
  • :focus (vec2): A coordinate in the reference frame of the terminal. These cannot be derived from scratch; typically this comes from a Command.

source

replay/open-file

function

(replay/open-file group path &named focus alt-screen highlights)

Open the .borg file found at path in a new replay window in group.

This function supports a range of named parameters that adjust its functionality:

  • :focus and :alt-screen from replay/open
  • :highlights (selection): Just like :focus, but is a struct with :from and :to fields. For the time being, this is not practical to derive by hand. See examples from the standard library.

For example:

(replay/open-file :root "some_borg.borg")

source

replay/quit

function

(replay/quit)

Quit replay mode.

source

replay/scroll-down

function

(replay/scroll-down)

Scroll the viewport one line down.

source

replay/scroll-up

function

(replay/scroll-up)

Scroll the viewport one line up.

source

replay/search-again

function

(replay/search-again)

Go to the next match in the direction of the last search.

source

replay/search-backward

function

(replay/search-backward)

Search for a string backwards in time (in time mode) or in the scrollback buffer (in copy mode).

source

replay/search-forward

function

(replay/search-forward)

Search for a string forwards in time (in time mode) or in the scrollback buffer (in copy mode).

source

replay/search-reverse

function

(replay/search-reverse)

Go to the previous match in the direction of the last search.

source

replay/select

function

(replay/select)

Enter visual select mode.

source

replay/start-of-line

function

(replay/start-of-line)

Move to the first character of the physical line. Equivalent to vim's 0.

source

replay/start-of-screen-line

function

(replay/start-of-screen-line)

Move to the first character of the screen line. Equivalent to vim's g0.

source

replay/swap-screen

function

(replay/swap-screen)

Swap between the alt screen and the main screen. This allows you to return to the pane's scrollback without quitting a program that is using the alternate screen, such as vim or htop.

source

replay/time-play

function

(replay/time-play)

Toggle playback.

source

replay/time-playback-rate

function

(replay/time-playback-rate rate)

Set the playback rate to rate. Positive numbers indicate a multiplier of real time moving forwards, negative numbers, backwards. For example, a rate of 2 means that time will advance at twice the normal speed; -2 means that time will go backwards at -2x.

rate is clamped to the range [10, 10].

source

replay/time-step-back

function

(replay/time-step-back)

Step one event backward in time.

source

replay/time-step-forward

function

(replay/time-step-forward)

Step one event forward in time.

source

replay/word-backward

function

(replay/word-backward)

Move to the beginning of the previous word. Equivalent to vim's b.

source

replay/word-end-backward

function

(replay/word-end-backward)

Move to the end of the previous word. Equivalent to vim's ge.

source

replay/word-end-forward

function

(replay/word-end-forward)

Move to the end of the next word. Equivalent to vim's e.

source

replay/word-forward

function

(replay/word-forward)

Move to the beginning of the next word. Equivalent to vim's w.

source

search/cancel

function

(search/cancel)

Cancel the current operation or the input of a query string.

source

search/first

function

(search/first)

Move to the first .borg file in the search results.

source

search/focus-input

function

(search/focus-input)

Focus search mode's input bar so you can enter a new query string.

source

search/last

function

(search/last)

Move to the last .borg file in the search results.

source

search/new

function

(search/new parent query &named files workers)

Create a new instance of search mode as a child of parent with the given string query. parent is a NodeID. Calling this function will begin executing the search immediately, regardless of whether you attach to the new node.

This function supports a range of named parameters that adjust its functionality:

  • :files (list of strings): All of the .borg files to search in. By default this is populated with all of the .borg files present in the user's :data-directory.
  • :workers (integer): The number of workers (threads) to use to perform the search. Defaults to :num-search-workers.

source

search/next

function

(search/next)

Move to the next .borg file in the search results.

source

search/prev

function

(search/prev)

Move to the previous .borg file in the search results.

source

shell/attach

function

(shell/attach &opt path)

Create a new shell initialized in the working directory path and attach to it.

source

shell/new

function

(shell/new &opt path)

Create a new shell initialized in the working directory path. If path is not provided, this uses the path of the current pane.

source

style/render

function

(style/render style text)

Apply styling effects to some text. This function generates a string containing ANSI escape sequences that will style the provided text according to style.

All cy API functions that render text to the screen, such as layout/set and input/find accept input styled with style/render.

style is a struct with any of the following properties:

  • :fg: The foreground color of the text.
  • :bg: The background color of the text.
  • :width: The number of horizontal cells the text should occupy. Padding is added if this value exceeds the length of text.
  • :height: The number of vertical cells the text should occupy. Padding is added if this value exceeds the height of text.
  • :align-horizontal: One of :left, :center, or :right. If :width is greater than the length of the text, the text will be aligned according to this property.
  • :align-vertical: One of :top, :center, or :bottom. If :height is greater than the height of the text, the text will be aligned according to this property.
  • :bold: A boolean indicating whether the text should be bolded.
  • :italic: A boolean indicating whether the text should be italic.
  • :underline: A boolean indicating whether the text should be underlined.
  • :strikethrough: A boolean indicating whether the text should be struck through.
  • :reverse: A boolean indicating whether the foreground and background colorshould be reversed.
  • :blink: A boolean indicating whether the text should blink.
  • :faint: A boolean indicating whether the text should be faint.

For example:

(style/render
    {:bg "4"
     :bold true
     :width 15
    } "some text")

source

style/text

function

(style/text text &named fg bg width height align-horizontal align-vertical bold italic underline strikethrough reverse blink faint)

Style the provided text with the attributes provided. This function is a convenient wrapper around style/render; instead of providing a struct, you may pass any of the attributes style/render supports as named parameters.

For example:

(style/text "foobar" :bg "#00ff00")
(style/text "foobar" :italic true :bold true :width 15)

source

time/format

function

(time/format ts format)

Format a Time struct using the provided format string. This uses Go's Time.Format. cy provides all of Go's built-in time format layouts as constants, such as time/format/rfc-822.

For example:

(time/format (time/now) time/format/rfc-822)

source

time/now

function

(time/now)

Return a Time struct representing the current local time.

source

tree/group?

function

(tree/group? node)

Return true if node is a group, false otherwise. node is a NodeID.

source

tree/name

function

(tree/name node)

Get the name of node, which is a NodeID. This is the name of the node itself, not its path.

source

tree/pane?

function

(tree/pane? node)

Return true if node is a pane, false otherwise. node is a NodeID.

source

tree/parent

function

(tree/parent node)

Get the NodeID for the parent of node. If node is :root, return (tree/parent) returns nil.

source

tree/path

function

(tree/path node)

Get the path of node, which is a NodeID.

source

tree/rm

function

(tree/rm node)

Remove the node and all of its child nodes. This will halt execution of any descendant panes. node is a NodeID.

source

tree/root

function

(tree/root)

Get the NodeID that corresponds to the root node.

source

tree/set-name

function

(tree/set-name node name)

Set the name of node to name. name will be stripped of all whitespace and slashes. node is a NodeID.

source

viewport/get-animations

function

(viewport/get-animations)

Get a list of all of the available animations.

source

viewport/get-frames

function

(viewport/get-frames)

Get a list of all of the available frames.

source

viewport/set-frame

function

(viewport/set-frame frame)

Set the frame to frame, which is an identifier for the desired frame.

You can get all of the available frames with (viewport/get-frames) and also browse them here.

source

Contributing

cy is still a young project and consequently does not have a rigorous contribution process nor a comprehensive list of upcoming features. Contributions of any kind are welcome.

Most new code should have tests. It should also pass cy's continuous integration (CI), which is described in more detail below. For more specific information of interest to contributors, you can peruse the other pages that follow this one in the Developer guide section.

Common commands

To hack on cy, you only need a recent Go toolchain and a clone of the cy repository.

You can build and run cy from source with the following command. This is convenient for testing small changes.

go run ./cmd/cy/...

Note that this will connect to the default cy socket. If you already use cy to write code, you may want to run cy with cy -L dev to ensure the server you're using to code is unaffected by the version you have checked out.

You can also build or install cy from source:

go build ./cmd/cy/...
go install ./cmd/cy/...

Most code in cy has tests. You can run all of cy's tests like this:

go test ./cmd/... ./pkg/...

Formatting code works similarly:

go fmt ./cmd/... ./pkg/...

The cy development workflow

There are a few different workflows I use when contributing to cy:

  1. Run cy directly (e.g. with go run). This works best for small changes or UI states that are easy to access.
  2. Write tests. A good rule of thumb is that anything resembling a state machine should have tests. Any changes or additions to the Janet API also should have comprehensive tests.
  3. Write stories. All UI changes can be represented as cy stories, which are small, preconfigured scenarios that you can browse easily with the story interface.

Janet tests

The cy codebase has a special mechanism for writing tests for the Janet API. You can run these tests with the following command:

go test ./pkg/cy/ -run TestAPI

To write a new test, add a new Janet file in the pkg/cy/api directory matching the *_test.janet pattern.

Janet tests look like this:

(test "name of the test"
      # do some stuff
      (assert (= true true)))

The test macro runs the code that it contains in an isolated, in-memory cy environment.

Passing CI

CI is simple. Your code need only:

  1. Pass tests
  2. Be properly formatted

Pull requests

There is currently no standard format for pull requests. I (cfoust) will review your code, make suggestions for improvements if necessary, or merge it if there is nothing left to address.

cy has a loosely-obeyed release cycle, so depending on the contribution I may either merge it straight to main or put it into the next release branch. Bug fixes always go out as soon as possible.

Roadmap

This document outlines the features I plan on implementing in the next few months. Features denoted with "*" are tentative and may not make the cut.

  • Performance: cy is not slow, but there's still room for improvement in rendering speed.
    • Smarter rendering algorithm: cy uses a "damage" algorithm to detect what parts of the screen have changed and only rerender those portions. This is intended to minimize the burden on the client's terminal emulator. The current version is inefficient, since the algorithm still emits escape sequences for styles for every cell, even if adjacent cells have the same styling.
    • Automated performance measurement: In addition to testing functionality, cy should have a system for measuring the performance of Screens. This could happen in CI as well.
    • Benchmarking against other terminal multiplexers: It would be simple enough to design tests for checking cy's performance against other commonly used multiplexers.
  • Plugin system: Ideally this would be nothing more than something like vim-plug or lazy.nvim, but there are several things we need in order for this to be possible.
    • API functions for executing external commands
    • Progress bars*: It would be nice for Janet code to be able to show the user the progress of some long-running operation.
    • tmux emulation: This would be the first proof-of-concept cy plugin. Some people really like tmux, and I intentionally built cy's layout functionality so that it would be flexible enough to emulate tmux's user interface. This does not have to be that comprehensive, it could just mimic most of tmux's common keybindings.
  • Persistent parameter store: This would add another target for param/set called :persist (name pending) which would be a key-value store backed by an SQLite database stored in $XDG_STATE_HOME. Any serializable Janet value could be written to this parameter store. This persistent parameter store could be used for things like saving and restoring layouts from previous sessions, reopening frequently used projects, et cetera.
  • fzf-cy*: cy literally uses fzf's algorithm and its fuzzy finder should be able to be used as a drop-in replacement for fzf just like in fzf-tmux. In other words, cy's fuzzy finder should support everything (within reason) that fzf does.

Architecture

This document is intended to be a brief introduction to cy's code structure and its commonly used abstractions. The intended audience is anyone interested in contributing to cy or any of its constituent libraries, some of which may (eventually) be broken out into separate projects.

It is safe to assume that the high-level description in this document will remain reliable despite changes in the actual implementation, but if you are ever in doubt:

  1. Read the README for the package you are modifying. Most packages in pkg have their own READMEs (along with some sub-packages.)
  2. Ask for help in Discord.
  3. Consult the code itself.

cy is written in Go and Janet. I chose Go because I had written other projects with significant concurrency needs and it seemed like a natural fit. Janet is a Lisp-like scripting language that I chose because it sounded like fun.

This document assumes basic familiarity with Go.

Introduction

cy is a terminal multiplexer. Just like tmux, it uses a server-client model and daemonizes itself on server startup. In simple terms this means that irrespective of where, when, or how you start cy, if a cy server is running you can connect to it and resume your work exactly as you left it. Clients connect to the cy server using a WebSocket connection via a Unix domain socket.

As the name "terminal multiplexer" implies, most of the complexity comes from doing two things:

  1. Emulating a terminal: Just like in tmux et al, cy works by pretending to be a valid VT100 terminal and attaching to the programs that you run (typically shells).
  2. Multiplexing: Users expect to be able to switch between the terminals cy emulates in order to fulfill the basic requirement of being a terminal multiplexer.

Terminal emulation, though tedious and error-prone to write yourself, is critical for any terminal multiplexer. Because of the paucity of Go libraries that accomplish this, this was implemented mostly from scratch in the emu package.

Multiplexing, of course, is where things get interesting. cy's codebase has a range of different tools for compositing and rendering terminal windows, all of which it does to be able to support an arbitrary number of clients, all of whom may have different screen sizes and need to use cy for different things.

cy's main feature is being able to replay terminal sessions. You would think that it would be a source of significant complexity. But it really isn't: once you have the above, making this functionality is just a matter of recording every write to a virtual terminal, then replaying it on demand. Of course, the devil is in the details.

Codebase organization

cy's code is divided into three directories found at the repository root:

  • cmd: Contains the code for all executables (in this case, programs with main.go files.)
    • cy: The main cy executable and the code necessary to connect to and create sockets.
    • stories: A system for quickly iterating on cy's visual design. Covered in more detail in a dedicated chapter.
    • perf: A (seldom-used) program for testing the performance of cy's history search feature.
    • docs: A simple executable that dumps various information about cy to standard out as JSON, such as all of its API functions, built in key bindings, et cetera. This is used in an mdbook preprocessor called gendoc that generates Markdown content for cy on demand.
  • pkg: Contains a range of different Go packages, all of which might be charitably called libraries. The list below is not intended to be exhaustive, but just highlight several important ones.
    • cy: The cy server, API, default configuration, et cetera.
    • geom: Simple, high-level geometric primitives (think Vec2) used everywhere in the codebase.
    • mux: A few useful abstractions for multiplexing.
    • janet: A library for Janet/Go interoperation.
    • emu: A vt100 terminal emulator.
    • input: A collection of user input mechanisms.
    • replay: A terminal session player, otherwise known as replay mode.
    • taro: A fork of charmbracelet/bubbletea adapted for use in cy's windowing abstraction (described below.)
  • docs: Contains all of cy's documentation. cy uses mdbook to build the documentation site.

Screens and streams

The two most important abstractions in cy's codebase are Screens and Streams, which are defined in the mux package.

Stream

A Stream is just a resizable (this is important!) bidirectional stream of bytes that can be read from and written to. As of writing, it looks like this:

type Stream interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Resize(size Vec2) error
}

From the perspective of the process you're running, this interface concisely describes the functionality of your terminal emulator (e.g. xterm, kitty.) Typing into your terminal writes to the process; any output it produces is read and interpreted in a predictable, standard way (the VT100 quasi-standard.) Resizing your terminal sends a resize event, SIGWINCH, which the process can react to.

This is useful because you can represent lots of things as a Stream:

  1. Pseudo-terminals: By connecting a process to a pseudo-terminal, it behaves as though a user had run it interactively.
    • Write: Writes are written directly to that process' standard input.
    • Read: Reads correspond to whatever that process writes to standard output.
    • Resize: Will set the size of the pseudo-terminal (and thus send SIGWINCH to the process).
  2. Clients: cy clients that connect to the server can be written to and read from.
    • Write: Writes are interpreted as user input, typically sequences of keys.
    • Read: Reads consist of the shortest sequence of bytes necessary to update the client's terminal to match cy's understanding of that client's screen.
    • Resize: Resizing a client indicates to cy that it should resize everything on that client's screen and redraw accordingly.

Streams can be composed and form pipes of arbitrary complexity. For example, cy records terminal sessions by proxying a Stream (sort of like tee.)

However, for a terminal multiplexer this is clearly not enough. A Stream is stateless. In other words, there is no way to know what the state of the terminal that is attached to that Stream. That's where Screens come in.

Screen

A Screen can be thought of, conceptually, as an application to which you can send events (such as user input) and receive any updates it produces (such as changes to the screen's contents).

The state of a screen (represented in the cy codebase as a tty.State) is identical to that of a terminal emulator:

  • A two-dimensional buffer of Unicode characters
  • The state of the cursor including its position and style

A pane, described elsewhere, is a good example of a Screen.

If that all sounds abstract, the interface for Screen looks like this:

type Screen interface {
    // State gets the current visual state of the Screen.
    State() *tty.State

    // Resize adjusts the screen to fit `size`.
    Resize(size Vec2) error

    // Subscribe subscribes to any updates to the screen, which are usually
    // caused by changes to the screen's state.
    Subscribe(context.Context) *Updater

    // Send sends a message to the Screen.
    Send(message interface{})
}

Send looks scary, but it's used in cy mostly for key and mouse events.

The easiest way to understand this is to think of a Screen as something that can render a Stream and turn it into something that can be composed with other Screens. In fact, there is a Screen that does just that.

cy's fuzzy finder and replay mode are both just Screens, albeit complicated ones.

Some Screens just exist to compose other screens in some way, which is the bread and butter of any terminal multiplexer.

The simplest example of this is cy's Layers, a Screen that lets you render one or more Screens on top of one another, letting the screens underneath show through if any cells of the layer above are transparent.

Layers is used to place the pane the user is currently interacting with on top of a frame, such as in the default viewport:

placeholder

It is also used for cy's toast messages (msg/toast), which are implemented using a noninteractive Screen that is layered over the rest of the content on the client's screen.

Tying it all together

To illustrate the difference between Screens and Streams, consider the following description of how data flows back and forth from a client to its Screens and back again.

The flow for client input works like this:

  1. The client presses a key in the terminal where they originally connected to cy. The terminal emulator writes the byte sequence for that key to the standard input of the process controlling the terminal, which in this case is cy running as a client.
    • When cy is running in client mode, it represents its connection to the server with a Stream (ClientIO), the Read, Write, and Resize methods of which are connected directly to the standard output, standard input, and SIGWINCH events of the controlling terminal.
  2. All of the events are sent using the WebSocket protocol via a Unix socket to the cy server, which is a separate process.
  3. The cy server writes the incoming bytes it received from the client to the corresponding Client on the server. A Client is just a Stream.
  4. The Client translates the bytes into key and mouse events that are then sent (via Send) to the Screen the Client is attached to. These events usually travel through several different Screens before reaching their destination, but ultimately they are passed into whatever Screen the client is currently attached to--whether that be a pane, the fuzzy finder, or replay mode.

The flow for client output is somewhat simpler:

  1. Whenever the Screen the Client is attached to changes in some way (in other words, it produces an event that is published to its subscribers via Subscribe).
  2. The client's Renderer receives this event and calls State() on the client's Screen, which produces a tty.State. The Renderer then calculates the sequence of bytes necessary to transform the actual client's terminal screen to match the cy server's state.
  3. This byte string is sent via the aforementioned WebSocket connection.
  4. It is ultimately Read by the user's terminal and written to standard output, thus triggering the visual changes the user expects.

Packages

This chapter contains an index of all of the Go packages in cy's pkg directory, the READMEs of which are consolidated here for your convenience.

anim

source

Package anim contains a range of terminal animations. These are used on cy's splash screen and in the background while fuzzy finding.

anim/fluid

source

Package fluid is a viscoelastic fluid simulator ported from kotsoft/particle_based_viscoelastic_fluid. Out of fear of making a mistake, I made no effort to translate the original JavaScript into idiomatic Go.

bind

source

Package bind is a key binding engine. It checks incoming key events against all registered key bindings to determine whether an action should be fired. bind uses a trie data structure, implemented in the bind/trie package, to describe sequences of keys.

As distinct from a traditional trie, in which nodes have a fixed value, bind's trie also supports regex values. Key events are stringified and then compared against the regex pattern to determine if the state machine should transition to that node.

cy

source

Package cy contains cy's server and Janet API.

emu

source

Package emu provides a VT100-compatible terminal emulator. For the most part it attempts to emulate xterm as closely as possible (ie to be used with TERM=xterm-256color.)

emu's basic mode of operation is quite simple: you Write() some bytes and it will correctly calculate the state of the virtual terminal which you can happily capture and send elsewhere (with Terminal.View()).

emu's magic, however, comes from Terminal.Flow(), which is an API for viewing the terminal's scrollback buffer with a viewport of arbitrary size. This is important because cy's core feature is to be able to replay terminal sessions, the lines of which should wrap appropriately to fit your terminal screen.

This package is a fork of github.com/hinshun/vt10x. The original library was rough, incomplete, and had a range of serious bugs that I discovered after integrating it. Because of this, little of the original code remains.

frames

source

Package frames contains static backgrounds.

geom

source

Package geom provides a range of geometric primitives and convenience methods. Notably, it contains:

  • Data types for representing static bitmaps of terminal data (image.Image) and terminal state (tty.State).
  • Vec2, a traditional vector data type but with a terminal flavor: ie it uses R and C (for rows and columns) instead of X and Y

input/fuzzy

source

Package fuzzy is a fully-featured fuzzy finder a la fzf. In fact, it makes use of fzf's actual agorithm, forked here as the fuzzy/fzf package.

input/fuzzy/fzf

source

This is a fork of fzf's matching algorithm.

io

source

Package io is an assortment of packages with a general theme of IO, including the protocol used by clients to interact with the cy server.

janet

source

Package janet contains a Janet virtual machine for interoperation between Go and Janet code. Users of its API can register callbacks, define symbols in the Janet environment, execute Janet code, and convert between Go and Janet values.

This code is, admittedly, a little terrifying. Suffice it to say that Janet was not designed to be used from Go, and as a result there are a lot of silly tricks this library uses to ensure we only access memory used by the Janet VM in the goroutine where it was initialized.

Updating Janet

To update the janet version, clone the janet-lang/janet repository and run make, then copy build/c/janet.c, src/include/janet.h, and src/conf/janetconf.h to this directory.

mux

source

Package mux defines Screen and Stream, two of cy's core abstractions for representing interactive windows and streams of terminal data, respectively. It also contains a wide range of useful Screens and Streams used across cy.

params

source

Package params is a thread-safe map data structure used as a key-value store for all nodes in cy's node tree.

replay

source

Package replay is an interface for playing, searching, and copying text from recorded terminal sessions.

replay/replayable

source

Package replayable is a Screen that pipes a Stream into a Terminal, intercepting all events so that it can be replayed.

sessions

source

Package sessions contains a data type for recorded terminal sessions and a range of utilities for (de)serializing, searching through, and exporting them.

sessions/search

source

Package search is a high-performance search algorithm to find matches for a regex pattern on the terminal screen over the course of a recorded terminal session. This is more complicated than it seems: it must track the exact byte at which a match first appeared and calculate how long that match remained intact on the screen.

stories

source

Package stories is an interface for registering and viewing stories. Stories are predefined configurations of cy's UI components that may also describe a sequence of user inputs that are "played" into the story after it is loaded. This is similar to Storybook.

style/colormaps

source

Package colormaps contains color schemes imported from tinted-theming/schemes.

style/colormaps/schemes

source

Have a look at the Gallery to preview these colorschemes.

Imported Scheme Repositories

These are the original locations all schemes were imported from.

FAQ

Where did these all come from?

These schemes were originally imported from the original schemes source repo on May 25th, 2022 around 11pm UTC.

They were migrated from our old base16-schemes repo on Dec 15th, 2023 around 8pm UTC.

taro

source

Package taro is a high level framework for defining terminal interfaces that obey cy's Screen interface. It is a fork of charmbracelet/bubbletea and borrows that library's state machine paradigm, originally inspired by the Elm framework.

I wanted bubbletea Programs to be able to write to arbitrary parts of the screen without futzing with strings. I also needed to improve on bubbletea's key/mouse event parsing (which, at any rate, has since been patched).

util/dir

source

Taken from robertknight/rd. No license specified.

Stories

The above is the stories interface. Typing filters the list of stories and you can use up and down to move between them. Stories are not interactive, though this may change.

cy's user interface is complex and some UI states are tedious to navigate to when you're trying to iterate quickly. To remedy this, the cy repository contains a mechanism (uncreatively) called stories.

A story is a preconfigured Screen along with an (optional) sequence of user inputs that will be played back on that screen. Every story also has a unique string name that looks like a path, e.g. input/find/search. After defining a story in Go code, you can open a special interface that lets you quickly view that story.

Stories can be registered by any package in cy and can be browsed in a central interface.

This functionality was inspired by Storybook, a framework used to develop UI components.

Viewing stories

Run the following to open the stories interface:

go run ./cmd/stories/...

Press q to quit at any time.

The stories executable accepts a range of arguments. Provide --help to see them all.

To run only a single story:

go run ./cmd/stories/... -s input/find/search

To filter the list of stories with a prefix:

go run ./cmd/stories/... -p input

Any stories with names that do not begin with input will be filtered out.

Registering a new story

Stories are registered using the Register function in the stories package. Search the codebase for usage examples.

Documentation site

cy's documentation site lives in the repository's docs directory. It uses mdbook. After installing mdbook, you can serve the documentation site by running the following in the docs directory:

mdbook serve

Preprocessors

cy's documentation makes extensive use of mdbook preprocessors to generate content and assets on the fly with the version of cy currently checked out in the repository.

A preprocessor allows you to define custom transformations of the site's raw Markdown. cy uses this for a range of things described below. Using preprocessors, the cy documentation site defines a suite of special markup tags used to generate documentation and assets from the cy code.

All of these markup tags are enclosed in double curly brackets (e.g. {{some-tag}}) to avoid interfering with Markdown directives. The documentation below omits these double brackets for the sake of implementation simplicity.

Stories

The story tag allows you to render stories as static PNGs, animated GIFs, or an interactive asciinema player.

The default filename for generated assets is the hash of the tag's arguments.

Some examples:

# Generate a gif of the splash story and insert it
story gif splash

# You can also specify a file name
story main.gif splash

# Insert a png snapshot of the splash story
story png splash

# Render an asciinema cast of the cy/replay story
story cast cy/replay

# You can also specify the terminal dimensions of the story, which will
# overwrite the dimensions in the story's configuration
story cast cy/viewport --width 120 --height 26

API symbols

You can reference symbols in cy's API using the api tag, which will link to that symbol's documentation in the API reference. This is also useful because if you reference a symbol that does not exist, the preprocessor reports the error and fails CI. This is an effort to prevent broken links after API changes.

For example:

api input/find

Janet code

All multiline Janet code blocks (e.g. those that begin with ```janet) are compiled and executed in a cy testing environment. An example that fails to compile or triggers an error while executing will cause documentation generation to fail. In other words, CI will not pass.

Ignoring code blocks

You can tell the preprocessor to ignore a code block by putting # ignore in the first line. This line will be stripped.

# ignore

Executing, but hiding parts of code blocks

You may also include some lines when the code block is being executed, but exclude them from what's displayed on the documentation site. This is useful for running any necessary setup.

You do this using # { (to start hiding lines) and # } (to stop hiding them).

Here's an example taken from the keybindings documentation:

# {
(defn do-something [] )
(defn do-something-else [] )
# }
(key/bind-many :root
               ["ctrl+b" "1"] do-something
               ["ctrl+b" "2"] do-something-else)

The resulting code block would only have the following contents on the actual site:

(key/bind-many :root
               ["ctrl+b" "1"] do-something
               ["ctrl+b" "2"] do-something-else)

Acknowledgements

cy is the result of hundreds of hours of work1. I would like to thank the following people for their efforts to support me in this project:

  • A.J., with particular gratitude for contributing the name taro and a few of the original ideas that inspired cy
  • S.K., for pushing me to pursue this bizarre blend of art and utility
  • 然然, for being a persistent source of inspiration
  • B.P., for being a sounding board in the early days when I had no idea whether cy was even possible
  • And countless others who noticed what I was doing when I worked on cy in coffee shops, on airplanes, and elsewhere
1

No kidding: WakaTime badge. Logged with WakaTime.