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:
- Replay and search through any terminal session
- Shell command history that lets you insert (and revisit) any command you've ever run
- Flexible configuration with Janet, a simple imperative programming language
- Built-in fuzzy finding using concepts from fzf
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:
- 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. - Configuration:
cy
uses a real programming language, Janet, for configuration. - 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
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:
tmux
's configuration language is hacky and primitive, which makes it hard to do anything interesting without running an external command.- 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.
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:
/projects/[base-name]/editor
: A pane running the program specified by the$EDITOR
environment variable./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.
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.
- To make the inner pane fill the entire viewport, type ctrl+a g [?].
- To set the width of the inner pane to 80 columns, type ctrl+a 1 [?].
- 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 thecy
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:
- Features are first built to expand the functionality available in the API.
- 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.
$XDG_CONFIG_HOME/cy/cyrc.janet
$XDG_CONFIG_HOME/cyrc.janet
$XDG_CONFIG_HOME/.cy.janet
$HOME/cy/cyrc.janet
$HOME/cyrc.janet
$HOME/.cy.janet
$HOME/.config/cy/cyrc.janet
$HOME/.config/cyrc.janet
$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:
- 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. - A key sequence: A Janet tuple that indicates the keys that must be typed for the callback to execute.
- 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:
- Printable Unicode characters:
你
,Щ
,a
- Preset keys:
return
,ctrl+a
,f1
You can find a comprehensive list of the available keys here. - 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 incy
'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:
$XDG_DATA_HOME/cy
(if$XDG_DATA_HOME
is set)$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:
- A replacement for ctrl+r that lets you see the context of a command when it was executed
- Quick shortcuts for switching panes based on the commands executed in them
- Additional features in replay mode for jumping between commands
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:
- action/add-node: Quickly add nodes of any type to the layout.
- action/remove-parent: Remove the most recent parent of the currently attached node.
- action/clear-layout: Completely empty out the layout.
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
- The dimensions of the space available to either property as a tuple,
[rows cols]
.rows
is always1
, but this structure is preserved for consistency. - 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
- The dimensions of the space available to either property as a tuple,
[rows, cols]
.rows
is always1
, but this structure is preserved for consistency. - The current value of
:node
.
All other properties:
- 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:
- 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 exit
ing 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:
- The current value of
:a
. - 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 totrue
. - All provided tabs must have
:name
fields with non-zero visual width.
Actions
Dynamic
All dynamic properties are invoked with the same arguments:
- 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:
- The text (or columns) that the user will filter against
- A Janet struct describing how this option should be previewed
- 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
Sequence | Action | Description |
---|---|---|
F | action/choose-frame | Choose a frame. |
ctrl+p | action/command-palette | Open the command palette. |
ctrl+r | action/ctrl-r | Find a recent command and insert it into the current shell. |
d | action/detach | Detach from the cy server. |
q | action/kill-server | Kill the cy server. |
p | action/open-replay | Enter replay mode for the current pane. |
P | action/paste | Insert the contents of the default register. |
r | action/reload-config | Reload the cy configuration. |
S | action/search-borg | Search all recorded .borg files for a pattern. |
" re:[a-zA-Z0-9+] p | register/insert | Insert the contents of the given register in the current pane. |
Panes
Sequence | Action | Description |
---|---|---|
C | action/jump-command | Jump to the output of a command. |
f | action/jump-history-command | Find a command and open its .borg file. |
; | action/jump-pane | Jump to a pane. |
c | action/jump-pane-command | Jump to a pane based on a command. |
k | action/jump-project | Jump to a project. |
: | action/jump-screen-lines | Jump to a pane based on screen lines. |
l | action/jump-shell | Jump to a shell. |
x | action/kill-and-reattach | Remove the current pane from the node tree and attach to a new one. |
X | action/kill-layout-pane | Remove the current pane from the layout and the node tree. |
J | action/move-down | Move down to the next pane. |
down | action/move-down | Move down to the next pane. |
H | action/move-left | Move left to the next pane. |
left | action/move-left | Move left to the next pane. |
L | action/move-right | Move right to the next pane. |
right | action/move-right | Move right to the next pane. |
up | action/move-up | Move up to the next pane. |
K | action/move-up | Move up to the next pane. |
n | action/new-project | Create a new project. |
j | action/new-shell | Create a new shell. |
t | action/new-tab | Create a new tab. |
tab | action/next-tab | Switch to the next tab. |
shift+tab | action/prev-tab | Switch to the previous tab. |
R | action/rename-tab | Rename the current tab. |
- | action/split-down | Split the current pane downwards. |
| | action/split-right | Split the current pane to the right. |
ctrl+o | pane/history-backward | Move backward in the pane history. Works in a similar way to vim's ctrl+o. |
Viewport
Sequence | Action | Description |
---|---|---|
2 | action/margins-160 | Set size to 160 columns. |
1 | action/margins-80 | Set margins size to 80 columns. |
g | action/toggle-margins | Toggle the screen's margins. |
Unprefixed
These bindings are not prefixed by ctrl+a.
Sequence | Action | Description |
---|---|---|
ctrl+l | action/next-pane | Move to the next sibling pane. |
Fuzzy finding
input/find has several key bindings that are not yet configurable, but are worth documenting.
Sequence | Description |
---|---|
ctrl+k or up | Move up one option. |
ctrl+j or down | Move down one option. |
enter | Choose the option under the cursor. |
ctrl+c or esc | Quit without choosing. |
home | Jump to the top of the list. |
end | Jump to the bottom of the list. |
pgup | Move upwards by a single page. |
pgdown | Move 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
Sequence | Action | Description |
---|---|---|
1 | action/replay-playback-1x | Set the playback rate to 1x real time. |
2 | action/replay-playback-2x | Set the playback rate to 2x real time. |
3 | action/replay-playback-5x | Set the playback rate to 5x real time. |
! | action/replay-playback-reverse-1x | Set the playback rate to -1x real time (backwards). |
@ | action/replay-playback-reverse-2x | Set the playback rate to -2x real time (backwards). |
# | action/replay-playback-reverse-5x | Set the playback rate to -5x real time (backwards). |
g g | replay/beginning | Go to the beginning of the time range (in time mode) or the first line of the screen (in copy mode). |
[ c | 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. |
] c | 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. |
G | replay/end | Go to the end of the time range (in time mode) or the last line of the screen (in copy mode). |
ctrl+c | replay/quit | Quit replay mode. |
q | replay/quit | Quit replay mode. |
esc | replay/quit | Quit replay mode. |
n | replay/search-again | Go to the next match in the direction of the last search. |
? | replay/search-backward | Search for a string backwards in time (in time mode) or in the scrollback buffer (in copy mode). |
/ | replay/search-forward | Search for a string forwards in time (in time mode) or in the scrollback buffer (in copy mode). |
N | replay/search-reverse | Go to the previous match in the direction of the last search. |
space | replay/time-play | Toggle playback. |
left | replay/time-step-back | Step one event backward in time. |
right | replay/time-step-forward | Step 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.
Sequence | Action | Description |
---|---|---|
" re:[a-zA-Z0-9] y | replay/copy | Yank the selection into the copy buffer. |
" + y | replay/copy-clipboard | Yank the selection into the system clipboard. |
y | replay/copy-default | Yank the selection into the default register. |
v | replay/select | Enter 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.
Sequence | Action | Description |
---|---|---|
g g | replay/beginning | Go to the beginning of the time range (in time mode) or the first line of the screen (in copy mode). |
B | replay/big-word-backward | Move to the beginning of the previous WORD. Equivalent to vim's B . |
g E | replay/big-word-end-backward | Move to the end of the previous WORD. Equivalent to vim's gE . |
E | replay/big-word-end-forward | Move to the end of the next WORD. Equivalent to vim's E . |
W | replay/big-word-forward | Move to the beginning of the next WORD. Equivalent to vim's W . |
[ c | 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. |
] c | 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. |
[ C | replay/command-select-backward | Move the cursor to the first character of the output of the previous command and select its output. |
] C | replay/command-select-forward | Move the cursor to the first character of the output of the next command and select its output. |
j | replay/cursor-down | Move cursor down one cell. |
ctrl+h | replay/cursor-left | Move cursor left one cell. |
h | replay/cursor-left | Move cursor left one cell. |
backspace | replay/cursor-left | Move cursor left one cell. |
left | replay/cursor-left | Move cursor left one cell. |
l | replay/cursor-right | Move cursor right one cell. |
right | replay/cursor-right | Move cursor right one cell. |
space | replay/cursor-right | Move cursor right one cell. |
k | replay/cursor-up | Move cursor up one cell. |
G | replay/end | Go to the end of the time range (in time mode) or the last line of the screen (in copy mode). |
$ | replay/end-of-line | Move to the last character of the physical line. Equivalent to vim's $ . |
g $ | replay/end-of-screen-line | Move to the end of the screen line. Equivalent to vim's g$ . |
^ | replay/first-non-blank | Move to the first non-blank character of the physical line. Equivalent to vim's ^ . |
g ^ | replay/first-non-blank-screen | Move to the first non-blank character of the screen line. Equivalent to vim's g^ . |
ctrl+d | replay/half-page-down | Scroll the viewport half a page (half the viewport height) down. |
ctrl+u | replay/half-page-up | Scroll the viewport half a page (half the viewport height) up. |
; | replay/jump-again | Repeat the last character jump. |
F re:. | replay/jump-backward | Jump to the previous instance of char on the current line. |
f re:. | replay/jump-forward | Jump to the next instance of char on the current line. |
, | replay/jump-reverse | Repeat the inverse of the last character jump. |
T re:. | replay/jump-to-backward | Jump to the cell before char after the cursor on the current line. |
t re:. | replay/jump-to-forward | Jump to the cell before char after the cursor on the current line. |
g _ | replay/last-non-blank | Move to the last non-blank character of the physical line. Equivalent to vim's g_ . |
g end | replay/last-non-blank-screen | Move to the last non-blank character of the screen line. Equivalent to vim's g<end> . |
g M | replay/middle-of-line | Move to the middle of the physical line. Equivalent to vim's gM . |
g m | replay/middle-of-screen-line | Move to the middle of the screen line. Equivalent to vim's gm . |
esc | replay/quit | Quit replay mode. |
q | replay/quit | Quit replay mode. |
ctrl+c | replay/quit | Quit replay mode. |
down | replay/scroll-down | Scroll the viewport one line down. |
up | replay/scroll-up | Scroll the viewport one line up. |
n | replay/search-again | Go to the next match in the direction of the last search. |
? | replay/search-backward | Search for a string backwards in time (in time mode) or in the scrollback buffer (in copy mode). |
/ | replay/search-forward | Search for a string forwards in time (in time mode) or in the scrollback buffer (in copy mode). |
N | replay/search-reverse | Go to the previous match in the direction of the last search. |
home | replay/start-of-line | Move to the first character of the physical line. Equivalent to vim's 0 . |
0 | replay/start-of-line | Move to the first character of the physical line. Equivalent to vim's 0 . |
g home | replay/start-of-screen-line | Move to the first character of the screen line. Equivalent to vim's g0 . |
g 0 | replay/start-of-screen-line | Move to the first character of the screen line. Equivalent to vim's g0 . |
s | 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. |
b | replay/word-backward | Move to the beginning of the previous word. Equivalent to vim's b . |
g e | replay/word-end-backward | Move to the end of the previous word. Equivalent to vim's ge . |
e | replay/word-end-forward | Move to the end of the next word. Equivalent to vim's e . |
w | replay/word-forward | Move to the beginning of the next word. Equivalent to vim's w . |
Search mode
Search mode has its own binding scope, :search
.
Sequence | Action | Description |
---|---|---|
ctrl+c | search/cancel | Cancel the current operation or the input of a query string. |
esc | search/cancel | Cancel the current operation or the input of a query string. |
: | search/focus-input | Focus search mode's input bar so you can enter a new query string. |
ctrl+n | search/next | Move to the next .borg file in the search results. |
ctrl+p | search/prev | Move 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:
Specifier | Notes |
---|---|
"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")
brick
(viewport/set-frame "brick")
cheerios
(viewport/set-frame "cheerios")
cross
(viewport/set-frame "cross")
cross-stitch
(viewport/set-frame "cross-stitch")
dot-bricks
(viewport/set-frame "dot-bricks")
hive
(viewport/set-frame "hive")
hive-thick
(viewport/set-frame "hive-thick")
none
(viewport/set-frame "none")
puzzle
(viewport/set-frame "puzzle")
squares
(viewport/set-frame "squares")
stars
(viewport/set-frame "stars")
tiles
(viewport/set-frame "tiles")
wallpaper
(viewport/set-frame "wallpaper")
zigzag
(viewport/set-frame "zigzag")
Animations
You can browse these in cy
using the action/browse-animations action.
collapse
conway
cos
cy
fluid
midjo
musicforprogramming
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 are:
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 tocy
'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.
action/browse-animations
function
(action/browse-animations)
Browse animations.
action/choose-frame
function
(action/choose-frame)
Choose a frame.
action/clear-layout
function
(action/clear-layout)
Clear out the layout.
action/command-palette
function
(action/command-palette)
Open the command palette.
action/cpu-profile
function
(action/cpu-profile)
Save a CPU profile to cy's socket directory.
action/ctrl-r
function
(action/ctrl-r)
Find a recent command and insert it into the current shell.
action/detach
function
(action/detach)
Detach from the cy server.
action/jump-command
function
(action/jump-command)
Jump to the output of a command.
action/jump-group-pane
function
(action/jump-group-pane)
Jump to a pane that is a descendant of the current group.
action/jump-history-command
function
(action/jump-history-command)
Find a command and open its .borg file.
action/jump-pane
function
(action/jump-pane)
Jump to a pane.
action/jump-pane-command
function
(action/jump-pane-command)
Jump to a pane based on a command.
action/jump-project
function
(action/jump-project)
Jump to a project.
action/jump-screen-lines
function
(action/jump-screen-lines)
Jump to a pane based on screen lines.
action/jump-shell
function
(action/jump-shell)
Jump to a shell.
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.
action/kill-layout-pane
function
(action/kill-layout-pane)
Remove the current pane from the layout and the node tree.
action/kill-pane
function
(action/kill-pane)
Kill the process of the current pane, but do not detach from it.
action/kill-server
function
(action/kill-server)
Kill the cy server.
action/margins-160
function
(action/margins-160)
Set size to 160 columns.
action/margins-80
function
(action/margins-80)
Set margins size to 80 columns.
action/margins-bigger
function
(action/margins-bigger)
Increase margins by 5 columns.
action/margins-smaller
function
(action/margins-smaller)
Decrease margins by 5 columns.
action/move-down
function
(action/move-down)
Move down to the next pane.
action/move-left
function
(action/move-left)
Move left to the next pane.
action/move-right
function
(action/move-right)
Move right to the next pane.
action/move-up
function
(action/move-up)
Move up to the next pane.
action/new-project
function
(action/new-project)
Create a new project.
action/new-shell
function
(action/new-shell)
Create a new shell.
action/new-tab
function
(action/new-tab)
Create a new tab.
action/next-pane
function
(action/next-pane)
Move to the next sibling pane.
action/next-tab
function
(action/next-tab)
Switch to the next tab.
action/open-log
function
(action/open-log)
Open a .borg file.
action/open-replay
function
(action/open-replay)
Enter replay mode for the current pane.
action/paste
function
(action/paste)
Insert the contents of the default register.
action/paste-clipboard
function
(action/paste-clipboard)
Insert the contents of the system clipboard.
action/prev-pane
function
(action/prev-pane)
Move to the previous sibling pane.
action/prev-tab
function
(action/prev-tab)
Switch to the previous tab.
action/random-frame
function
(action/random-frame)
Switch to a random frame.
action/recall-command
function
(action/recall-command)
Recall the output of a command to the current shell.
action/reload-config
function
(action/reload-config)
Reload the cy configuration.
action/remove-layout-pane
function
(action/remove-layout-pane)
Remove the current pane from the layout.
action/remove-parent
function
(action/remove-parent)
Remove the parent of the current node.
action/rename-pane
function
(action/rename-pane)
Rename the current pane.
action/rename-tab
function
(action/rename-tab)
Rename the current tab.
action/replay-playback-1x
function
(action/replay-playback-1x)
Set the playback rate to 1x real time.
action/replay-playback-2x
function
(action/replay-playback-2x)
Set the playback rate to 2x real time.
action/replay-playback-5x
function
(action/replay-playback-5x)
Set the playback rate to 5x real time.
action/replay-playback-reverse-1x
function
(action/replay-playback-reverse-1x)
Set the playback rate to -1x real time (backwards).
action/replay-playback-reverse-2x
function
(action/replay-playback-reverse-2x)
Set the playback rate to -2x real time (backwards).
action/replay-playback-reverse-5x
function
(action/replay-playback-reverse-5x)
Set the playback rate to -5x real time (backwards).
action/search-borg
function
(action/search-borg)
Search all recorded .borg files for a pattern.
action/set-borders-title
function
(action/set-borders-title)
Set the :title for a :borders node.
action/set-borders-title-bottom
function
(action/set-borders-title-bottom)
Set the :title-bottom for a :borders node.
action/set-layout-borders
function
(action/set-layout-borders)
Change the border style across the entire layout.
action/set-pane-colors
function
(action/set-pane-colors)
Set the color map for the current pane.
action/show-color-map
function
(action/show-color-map)
Send a toast with the ID of the current color map.
action/split-down
function
(action/split-down)
Split the current pane downwards.
action/split-left
function
(action/split-left)
Split the current pane to the left.
action/split-right
function
(action/split-right)
Split the current pane to the right.
action/split-up
function
(action/split-up)
Split the current pane upwards.
action/toggle-margins
function
(action/toggle-margins)
Toggle the screen's margins.
action/trace
function
(action/trace)
Save a trace to cy's socket directory.
assoc
function
(assoc s key value)
Set a property in a struct, returning a new struct.
clipboard/get
function
(clipboard/get)
Get the contents of the system clipboard.
clipboard/set
function
(clipboard/set text)
Set the contents of the system clipboard.
cmd/commands
function
(cmd/commands target)
Get the commands executed in a particular pane. Returns an array of Commands. target
is a NodeID.
cmd/kill
function
(cmd/kill target)
Kill the pane specified by target. target
is a NodeID.
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"])
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.
cmd/query
function
(cmd/query)
Query all of the commands stored in the commmand database.
color-maps/get
function
(color-maps/get id)
Get a color map by id.
color-maps/get-all
function
(color-maps/get-all)
Get all of the built-in color maps.
color-maps/get-id
function
(color-maps/get-id target)
Get the ID of the color map of the target node.
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.
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.
cy/detach
function
(cy/detach)
Detach from the cy
server.
cy/kill-server
function
(cy/kill-server)
Kill the cy
server, disconnecting all clients.
cy/paste
function
(cy/paste arg0)
Paste the text in the copy buffer to the current pane.
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.
cy/trace
function
(cy/trace)
Save a 15-second trace captured with runtime/trace to the socket directory. This is only useful for debugging.
exec/file
function
(exec/file path)
Execute the Janet file found at path
. Throws any errors that occur during execution.
group/children
function
(group/children group)
Get the NodeIDs for all of group
's child nodes.
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
.
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.
group/new
function
(group/new parent &named name)
Create a new group with parent
and (optionally) name
.
parent
is a NodeID.
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 isfalse
.: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.
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.
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.
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.
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)
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.
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.
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.
key/get-actions
function
(key/get-actions)
Get all registered actions.
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"])
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"])
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.
layout/attach
function
(layout/attach layout path)
Attach to the node at path in layout.
layout/attach-first
function
(layout/attach-first layout)
Attach to the first pane found in the layout.
layout/attach-id
function
(layout/attach-id node)
Get the NodeID of the attached node for the given node.
layout/attach-path
function
(layout/attach-path node)
Get the path to the attached node for the given node.
layout/attached?
function
(layout/attached? node)
Report whether node or one of its descendants is attached.
layout/bar
function
(layout/bar text node &named bottom)
Convenience function for creating a new :bar node.
layout/borders
function
(layout/borders node &named title title-bottom border border-fg border-bg)
Convenience function for creating a new :borders node.
layout/color-map
function
(layout/color-map map node)
Convenience function for creating a new :color-map node.
layout/detach
function
(layout/detach node)
Detach the attached node in the tree.
layout/find
function
(layout/find node predicate)
Get the path to the first node satisfying the predicate function or nil if none exists.
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.
layout/get
function
(layout/get)
Get the layout of the current user.
layout/has?
function
(layout/has? node predicate)
Report whether this node or one of its descendants matches the predicate function.
layout/hsplit
function
(layout/hsplit a b &named cells percent border border-fg border-bg)
Convenience function for creating a new horizontal :split node.
layout/map
function
(layout/map mapping layout)
Pass all nodes in the tree into a mapping function.
layout/margins
function
(layout/margins node &named cols rows border border-fg border-bg)
Convenience function for creating a new :margins node.
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.
layout/move-down
function
(layout/move-down layout)
Change the layout by moving to the next node "below" the attached pane.
layout/move-left
function
(layout/move-left layout)
Change the layout by moving to the next node to the left of the attached pane.
layout/move-right
function
(layout/move-right layout)
Change the layout by moving to the next node to the right of the attached pane.
layout/move-up
function
(layout/move-up layout)
Change the layout by moving to the next node "above" the attached pane.
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.
layout/pane
function
(layout/pane &named id attached remove-on-exit)
Convenience function for creating a new :pane node.
layout/pane?
function
(layout/pane? node)
Report whether node is of type :pane.
layout/path
function
(layout/path node path)
Resolve the path to a node. Returns nil if any portion of the path is invalid.
layout/remove-attached
function
(layout/remove-attached layout)
Remove the attached node from the layout, simplifying the nearest ancestor with children.
layout/replace
function
(layout/replace node path replacer)
Replace the node at the path by passing it through a replacer function.
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.
layout/set
function
(layout/set layout)
Set the layout of the current user.
layout/split
function
(layout/split a b &named vertical cells percent border border-fg border-bg)
Convenience function for creating a new :split node.
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.
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.
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.
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.
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)
@[]
layout/tab
function
(layout/tab name node &named active)
Convenience function for creating a new tab (inside of a :tabs node).
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.
layout/type?
function
(layout/type? type node)
Report whether node is of the provided type.
layout/vsplit
function
(layout/vsplit a b &named cells percent border border-fg border-bg)
Convenience function for creating a new vertical :split node.
msg/log
function
(msg/log level message)
Log message
to the /logs
pane. level
must be one of :info
, :warn
, :error
.
msg/toast
function
(msg/toast level message)
Send a toast with message
to the client. level
must be one of :info
, :warn
, :error
.
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.
pane/current
function
(pane/current)
Get the NodeID of the current pane.
pane/history-backward
function
(pane/history-backward)
Move backward in the pane history. Works in a similar way to vim's ctrl+o.
pane/history-forward
function
(pane/history-forward)
Move forward in the pane history. Works in a similar way to vim's ctrl+i.
pane/screen
function
(pane/screen pane)
Get the visible screen lines of the pane referred to by NodeID. Returns an array of strings.
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"])
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:
- The client's parameter table, which overrides all other parameters and can be set with
(param/set :client ...)
. - 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.
- 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.
param/rset
function
(param/rset key value)
Set the value of a parameter at :root
.
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.
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")
path/abs
function
(path/abs path)
Return the full absolute path for path
. Calls Go's path/filepath.Abs
.
path/base
function
(path/base path)
Return the last element of path
. Calls Go's path/filepath.Base
.
path/glob
function
(path/glob pattern)
Return an array of all files matching pattern
. Calls Go's path/filepath.Glob
.
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
.
register/get
function
(register/get register)
Get the value stored in the given register. The "+" register refers to the system clipboard.
register/insert
function
(register/insert register)
Insert the contents of the given register in the current pane.
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.
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).
replay/big-word-backward
function
(replay/big-word-backward)
Move to the beginning of the previous WORD. Equivalent to vim's B
.
replay/big-word-end-backward
function
(replay/big-word-end-backward)
Move to the end of the previous WORD. Equivalent to vim's gE
.
replay/big-word-end-forward
function
(replay/big-word-end-forward)
Move to the end of the next WORD. Equivalent to vim's E
.
replay/big-word-forward
function
(replay/big-word-forward)
Move to the beginning of the next WORD. Equivalent to vim's W
.
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.
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.
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.
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.
replay/copy
function
(replay/copy arg0)
Yank the selection into the copy buffer.
replay/copy-clipboard
function
(replay/copy-clipboard)
Yank the selection into the system clipboard.
replay/copy-default
function
(replay/copy-default)
Yank the selection into the default register.
replay/cursor-down
function
(replay/cursor-down)
Move cursor down one cell.
replay/cursor-left
function
(replay/cursor-left)
Move cursor left one cell.
replay/cursor-right
function
(replay/cursor-right)
Move cursor right one cell.
replay/cursor-up
function
(replay/cursor-up)
Move cursor up one cell.
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).
replay/end-of-line
function
(replay/end-of-line)
Move to the last character of the physical line. Equivalent to vim's $
.
replay/end-of-screen-line
function
(replay/end-of-screen-line)
Move to the end of the screen line. Equivalent to vim's g$
.
replay/first-non-blank
function
(replay/first-non-blank)
Move to the first non-blank character of the physical line. Equivalent to vim's ^
.
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^
.
replay/half-page-down
function
(replay/half-page-down)
Scroll the viewport half a page (half the viewport height) down.
replay/half-page-up
function
(replay/half-page-up)
Scroll the viewport half a page (half the viewport height) up.
replay/jump-again
function
(replay/jump-again)
Repeat the last character jump.
replay/jump-backward
function
(replay/jump-backward char)
Jump to the previous instance of char
on the current line.
replay/jump-forward
function
(replay/jump-forward char)
Jump to the next instance of char
on the current line.
replay/jump-reverse
function
(replay/jump-reverse)
Repeat the inverse of the last character jump.
replay/jump-to-backward
function
(replay/jump-to-backward char)
Jump to the cell before char
after the cursor on the current line.
replay/jump-to-forward
function
(replay/jump-to-forward char)
Jump to the cell before char
after the cursor on the current line.
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_
.
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>
.
replay/middle-of-line
function
(replay/middle-of-line)
Move to the middle of the physical line. Equivalent to vim's gM
.
replay/middle-of-screen-line
function
(replay/middle-of-screen-line)
Move to the middle of the screen line. Equivalent to vim's gm
.
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): Iftrue
, 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.
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")
replay/quit
function
(replay/quit)
Quit replay mode.
replay/scroll-down
function
(replay/scroll-down)
Scroll the viewport one line down.
replay/scroll-up
function
(replay/scroll-up)
Scroll the viewport one line up.
replay/search-again
function
(replay/search-again)
Go to the next match in the direction of the last search.
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).
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).
replay/search-reverse
function
(replay/search-reverse)
Go to the previous match in the direction of the last search.
replay/select
function
(replay/select)
Enter visual select mode.
replay/start-of-line
function
(replay/start-of-line)
Move to the first character of the physical line. Equivalent to vim's 0
.
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
.
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.
replay/time-play
function
(replay/time-play)
Toggle playback.
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].
replay/time-step-back
function
(replay/time-step-back)
Step one event backward in time.
replay/time-step-forward
function
(replay/time-step-forward)
Step one event forward in time.
replay/word-backward
function
(replay/word-backward)
Move to the beginning of the previous word. Equivalent to vim's b
.
replay/word-end-backward
function
(replay/word-end-backward)
Move to the end of the previous word. Equivalent to vim's ge
.
replay/word-end-forward
function
(replay/word-end-forward)
Move to the end of the next word. Equivalent to vim's e
.
replay/word-forward
function
(replay/word-forward)
Move to the beginning of the next word. Equivalent to vim's w
.
search/cancel
function
(search/cancel)
Cancel the current operation or the input of a query string.
search/first
function
(search/first)
Move to the first .borg
file in the search results.
search/focus-input
function
(search/focus-input)
Focus search mode's input bar so you can enter a new query string.
search/last
function
(search/last)
Move to the last .borg
file in the search results.
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
.
search/next
function
(search/next)
Move to the next .borg
file in the search results.
search/prev
function
(search/prev)
Move to the previous .borg
file in the search results.
shell/attach
function
(shell/attach &opt path)
Create a new shell initialized in the working directory path
and attach to it.
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.
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 oftext
.:height
: The number of vertical cells the text should occupy. Padding is added if this value exceeds the height oftext
.: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")
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)
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)
time/now
function
(time/now)
Return a Time struct representing the current local time.
tree/group?
function
(tree/group? node)
Return true
if node
is a group, false
otherwise. node
is a NodeID.
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.
tree/pane?
function
(tree/pane? node)
Return true
if node
is a pane, false
otherwise. node
is a NodeID.
tree/parent
function
(tree/parent node)
Get the NodeID for the parent of node
. If node
is :root
, return (tree/parent)
returns nil
.
tree/path
function
(tree/path node)
Get the path of node
, which is a NodeID.
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.
tree/root
function
(tree/root)
Get the NodeID that corresponds to the root node.
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.
viewport/get-animations
function
(viewport/get-animations)
Get a list of all of the available animations.
viewport/get-frames
function
(viewport/get-frames)
Get a list of all of the available frames.
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.
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
:
- Run
cy
directly (e.g. withgo run
). This works best for small changes or UI states that are easy to access. - 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.
- 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:
- Pass tests
- 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.
-
Smarter rendering algorithm:
-
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 usesfzf
's algorithm and its fuzzy finder should be able to be used as a drop-in replacement forfzf
just like in fzf-tmux. In other words,cy
's fuzzy finder should support everything (within reason) thatfzf
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:
- Read the README for the package you are modifying. Most packages in
pkg
have their own READMEs (along with some sub-packages.) - Ask for help in Discord.
- 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:
- 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). - 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 withmain.go
files.)cy
: The maincy
executable and the code necessary to connect to and create sockets.stories
: A system for quickly iterating oncy
's visual design. Covered in more detail in a dedicated chapter.perf
: A (seldom-used) program for testing the performance ofcy
's history search feature.docs
: A simple executable that dumps various information aboutcy
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 forcy
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
: Thecy
server, API, default configuration, et cetera.geom
: Simple, high-level geometric primitives (thinkVec2
) 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.fuzzy
: A fuzzy finder.text
: A simple text input field.
replay
: A terminal session player, otherwise known as replay mode.taro
: A fork of charmbracelet/bubbletea adapted for use incy
's windowing abstraction (described below.)
docs
: Contains all ofcy
's documentation.cy
uses mdbook to build the documentation site.
Screens and streams
The two most important abstractions in cy
's codebase are Screen
s and Stream
s, 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
:
- 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 sendSIGWINCH
to the process).
- 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 matchcy
's understanding of that client's screen.Resize
: Resizing a client indicates tocy
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 Screen
s 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 Screen
s. In fact, there is a Screen
that does just that.
cy
's fuzzy finder and replay mode are both just Screen
s, albeit complicated ones.
Some Screen
s 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 Screen
s 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:
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 Screen
s and Streams
, consider the following description of how data flows back and forth from a client to its Screen
s and back again.
The flow for client input works like this:
- 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 iscy
running as a client. - All of the events are sent using the WebSocket protocol via a Unix socket to the
cy
server, which is a separate process. - The
cy
server writes the incoming bytes it received from the client to the correspondingClient
on the server. AClient
is just aStream
. - The
Client
translates the bytes into key and mouse events that are then sent (viaSend
) to theScreen
theClient
is attached to. These events usually travel through several differentScreen
s before reaching their destination, but ultimately they are passed into whateverScreen
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:
- Whenever the
Screen
theClient
is attached to changes in some way (in other words, it produces an event that is published to its subscribers viaSubscribe
). - The client's
Renderer
receives this event and callsState()
on the client'sScreen
, which produces atty.State
. TheRenderer
then calculates the sequence of bytes necessary to transform the actual client's terminal screen to match thecy
server's state. - This byte string is sent via the aforementioned WebSocket connection.
- 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
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
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
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
Package cy contains cy
's server and Janet API.
emu
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
Package frames contains static backgrounds.
geom
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 usesR
andC
(for rows and columns) instead ofX
andY
input/fuzzy
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
This is a fork of fzf's matching algorithm.
io
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
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
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 Screen
s and Stream
s used across cy
.
params
Package params is a thread-safe map data structure used as a key-value store for all nodes in cy
's node tree.
replay
Package replay is an interface for playing, searching, and copying text from recorded terminal sessions.
replay/replayable
Package replayable is a Screen that pipes a Stream into a Terminal, intercepting all events so that it can be replayed.
sessions
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
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
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
Package colormaps contains color schemes imported from tinted-theming/schemes.
style/colormaps/schemes
Have a look at the Gallery to preview these colorschemes.
Imported Scheme Repositories
These are the original locations all schemes were imported from.
- Apprentice maintained by casonadams
- Atelier maintained by atelierbram
- Atlas maintained by ajlende
- Ayu maintained by Z5483
- Black Metal maintained by metalelf0
- BlueForest maintained by alonsodomin
- Blueish maintained by TheMayoras
- Brogrammer maintained by piggyslasher
- Brush Trees maintained by whiteabelincoln
- Catppuccin maintained by the catppuccin team
- Circus maintained by stepchowfun and ewang12
- Classic maintained by detly
- Codeschool maintained by blockloop
- Colors maintained by hakatashi
- Cupertino maintained by Defman21
- Da One maintained by NNBnh
- DanQing maintained by CosmosAtlas
- Darcula maintained by casonadams
- Darkviolet maintained by ruler501
- Default maintained by chriskempson
- Dracula maintained by mikebarkmin
- Evenok maintained by Mekeor Melire
- Equilibrium maintained by carloabelli
- Espresso maintained by alexmirrington
- Eva maintained by kjakapat
- Framer maintained by jssee
- Fruit Soda maintained by jozip
- Gigavolt maintained by Whillikers
- Github maintained by Defman21
- Gotham maintained by sboysel
- Gruber maintained by nimaipatel
- Gruvbox Material maintained by MayushKumar
- Gruvbox maintained by dawikur
- Hardcore maintained by callerc1
- Helios maintained by reyemxela
- Heetch maintained by tealeg
- Horizon maintained by michael-ball
- Humanoid maintained by tasmo
- iA maintained by aramisgithub
- Icy maintained by icyphox
- Kanagawa maintained by montdor
- Katy maintained by gessig
- Kimber maintained by Mishka
- Limelight maintained by limelier
- Materia maintained by Defman21
- Material Theme maintained by ntpeters
- Material Vivid maintained by joshyrobot
- Mellow maintained by gidsi
- Mexico-Light maintained by drzel
- Nebula maintained by Misterio77
- Nord maintained by 8-uh
- Nova maintained by gessig
- One Light maintained by purpleKarrot
- Onedark maintained by tilal6991
- Outrun maintained by hugodelahousse
- Oxocarbon maintained by ludovicopiero
- Pandora maintained by pandorasfox
- PaperColor maintained by jonleopard
- Pasque maintained by Misterio77
- pinky maintained by b3nj5m1n
- Porple maintained by AuditeMarlow
- Precious Themes maintained by precious-themes (4lex4)
- Primer maintained by jmlntw
- Purpledream maintained by archmalet
- Qualia maintained by isaacwhanson
- Rebecca maintained by vic
- Rosé Pine maintained by edunfelt
- Sagelight maintained by cveldy
- Sakura maintained by Misterio77
- Sandcastle maintained by gessig
- ShadeSmear maintained by HiRoS-neko
- Shades of Purple maintained by Ahmad Awais and Demartini
- Silk maintained by Misterio77
- Snazzy maintained by h404bi
- Solarflare maintained by mnussbaum
- Solarized maintained by aramisgithub
- Stella maintained by Shrimpram
- Still Alive maintained by deadly-platypus
- Summercamp maintained by zoe firi
- Summerfruit maintained by cscorley
- Synth Midnight maintained by michael-ball
- Tender maintained by DanManN
- Tokyo City maintained by michael-ball
- Tokyo Night maintained by michael-ball
- Tomorrow maintained by chriskempson
- Twilight maintained by hartbit
- Unikitty maintained by joshwlewis
- Vice maintained by Thomashighbaugh
- Windows maintained by C-Fergus
- Woodland maintained by jcornwall
- Zenburn maintained by elnawe
- XCode Dust maintained by gonsie
- Old Unclaimed Schemes - If your scheme is in this repository, please give it a new home!
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
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 Program
s 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
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 inspiredcy
- 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
No kidding: . Logged with WakaTime.