splash

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

Features:

Why not tmux?

cy shares some basic similarities with tmux. For example, it runs as a daemon, so its state is preserved across sessions. 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.

cy improves on tmux in three main ways:

  1. Session playback: cy records your terminal sessions and lets you play back and search through them.
  2. Interface: cy has a simple layout designed for use on large screens.
  3. Configuration: cy uses a real programming language, Janet, for configuration.

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 last lines it left on the screen, and certainly cannot see what the screen 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.

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 ever seen or done in the terminal. cy aims to augment your memory in a way that other programs cannot.

Interface

placeholder

cy does not (yet) have windows and panes like tmux does. It displays one terminal session at a time, and by default that session is centered on your screen with a fixed number of columns. This lets you concentrate on one "pane" at a time.

To some, this may seem like a surprising decision. The predominant abstraction for terminal multiplexers for at least a couple of decades has been the familiar pattern of vertical and horizontal splits.

Yet I found that I have spent more time fighting with this (or writing plugins to remove it) than benefiting from it. Rarely do I feel like I need to see more than one pane at a time.

This is practical because cy makes it easy to switch between panes. It emphasizes using fuzzy finding (with previews!) so you can quickly find what you're looking for. It also contains a minimal, filesystem-like abstraction for grouping panes together.

As of writing, cy also lacks tmux's status line. Because there is no notion of windows, there is nothing akin to the tab-like behavior that tmux encourages (and thus little immediate need to display it.)

Neither of these are principled omissions. My intent with cy is to, sooner or later, address these use cases in a flexible way.

Configuration

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

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

cy allows you to bind arbitrary sequences of keys to Janet functions. It even supports binding regexes, matches for which will be passed to the function you bound.

You can also create bindings that apply only in a specific pane or group of panes.

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/.

Quick start

This guide introduces cy's basic concepts in a step-by-step tutorial.

1. Starting cy

To start cy after installation, just run cy without any arguments. cy will connect to the cy server, starting one if necessary.

splash

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

2. Using the viewport

When you first connect, cy creates a new pane and attaches to it. By default, it runs your default shell. cy centers the pane and fills the rest of the horizontal space with a patterned background. cy refers to the state of your screen as the viewport.

placeholder

All actions in cy, such as creating panes and switching between them, are triggered by sequences of keys.

Here are a few you can try:

  1. To make the pane fill the entire viewport, type ctrl+a g. (Repeat to center it again.)
  2. To set the width of the pane to 80 columns, type ctrl+a 1.
  3. To set it to 160 columns, type ctrl+a 2.
  4. To increase the width by 5 columns, type ctrl+a +.
  5. To decrease the width by 5 columns, type ctrl+a -.

3. Entering replay mode

cy's most important feature is that it records all of your terminal sessions in their entirety and lets you jump back in time and replay them at will. It has an interface called replay mode that is conceptually similar to tmux's copy-mode, but also gives you access to time controls.

You open replay mode for a given pane by typing ctrl+a p. You can also scroll up with the mouse if the pane's content is scrollable (such as when using a shell.)

Replay mode is powerful and complicated, but for basic usage you can move through time using the left and right arrow keys and hit space to play and pause. For more information, refer to the chapter dedicated to replay mode and the list of all of its key bindings.

4. Creating a new shell

To create a new shell, type ctrl+a j. This creates a new pane running 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.

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.

5. Creating a new project

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

This creates two panes:

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

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

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

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

6. Switching between shells and projects

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.

7. 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.

8. Quitting cy

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

Configuration

All of cy's behavior is determined using a programming language called Janet, which 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.

Janet looks like this:

(print "hello world!")

If that doesn't scare you, read on.

Configuration files

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

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

You can reload your configuration at runtime using action/reload-config, which by default is bound to ctrl+ar.

Your cy configuration can contain any valid Janet statement, but cy also provides additions to the standard library in the form of an API for controlling every aspect of how cy works.

Example configuration

An example configuration that uses functionality from this API is shown below. Very little of this will make sense right now; this is just to give you a taste of how configuration works in cy before moving on to the next section.

# 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
  # (cy/toast): shows a toast popup in the top-right corner of the screen
  (cy/toast :info (cmd/path (pane/current)))
  )

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

Viewport

For the time being, cy only allows each user to view and interact with one pane at once, which is centered inside of the viewport and automatically resized as that user changes the bounds of their terminal.

By default, cy restricts that pane to a width of 80 columns or the width of your terminal window, whichever is smaller, but this is configurable. This is practical because cy makes it easy to switch between panes.

cy has two main abstractions for showing terminal art in the viewport:

  • frames: static, configurable backgrounds that cy uses to fill the empty space in the viewport
  • animations: shown on the splash screen and when fuzzy finding

Frames


big-hex

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

frame/big-hex


brick

(viewport/set-frame "brick")

frame/brick


cheerios

(viewport/set-frame "cheerios")

frame/cheerios


cross

(viewport/set-frame "cross")

frame/cross


cross-stitch

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

frame/cross-stitch


dot-bricks

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

frame/dot-bricks


hive

(viewport/set-frame "hive")

frame/hive


hive-thick

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

frame/hive-thick


none

(viewport/set-frame "none")

frame/none


puzzle

(viewport/set-frame "puzzle")

frame/puzzle


squares

(viewport/set-frame "squares")

frame/squares


stars

(viewport/set-frame "stars")

frame/stars


tiles

(viewport/set-frame "tiles")

frame/tiles


wallpaper

(viewport/set-frame "wallpaper")

frame/wallpaper


zigzag

(viewport/set-frame "zigzag")

frame/zigzag


Animations


collapse

animation/collapse


conway

animation/conway


cos

animation/cos


cy

animation/cy


midjo

animation/midjo


Keybindings

In cy, keybindings consist of a sequence of one or more keys that execute Janet code when you type them. You define new key sequences with the key/bind function.

For example:

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

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

The key/bind function takes three parameters:

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

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

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

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

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

Key sequences

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

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

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

Here are some valid key sequences:

# This is a match when you type these three characters in succession
["a" "b" "c"]
# This works similarly to tmux's notion of "prefixes"
["ctrl+a" "a"]
["ctrl+a" "ж"] # unicode is OK
[" " "l"]

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] (cy/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/kill (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.

Key specifiers

cy defines the following preset keys:

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

Default key bindings

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 cy/command-palette to another key sequence:

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

Global

These bindings apply everywhere and can always be invoked.

Prefixed

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

General

SequenceActionDescription
ctrl+paction/command-paletteOpen the command palette.
raction/reload-configReload the cy configuration.
dcy/detachDetach from the cy server.
qcy/kill-serverKill the cy server, disconnecting all clients.
Pcy/pastePaste the text in the copy buffer to the current pane.
pcy/replayEnter replay mode for the current pane.

Panes

SequenceActionDescription
Caction/jump-commandJump to the output of a command.
;action/jump-paneJump to a pane.
caction/jump-pane-commandJump to a pane based on a command.
kaction/jump-projectJump to a project.
:action/jump-screen-linesJump to a pane based on screen lines.
laction/jump-shellJump to a shell.
xaction/kill-current-paneKill the current pane.
naction/new-projectCreate a new project.
jaction/new-shellCreate a new shell.

Viewport

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

Unprefixed

These bindings are not prefixed by ctrl+a.

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

Fuzzy finding

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

SequenceDescription
ctrl+k or upMove up one option.
ctrl+j or downMove down one option.
enterChoose the option under the cursor.
ctrl+c or escQuit without choosing.

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:

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

Time mode

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

Copy mode

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

SequenceActionDescription
yreplay/copyYank the selection into the copy buffer.
vreplay/selectEnter visual select mode.

Movements

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

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

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 cy/get and cy/set. Parameters are used both to configure aspects of cy and also to create any functionality you desire by storing state in cy's tree.

The 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: cy/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+a b 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, (cy/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 contains a primitive key-value store it refers 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 cy/set and retrieved with cy/get:

(cy/set :some-parameter true)

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

Default parameters

Some parameters are used by cy to change how it performs certain operations.

ParameterDefaultDescription
:data-dirinferred on startupthe directory in which .borg files are saved; if empty, recording to file is disabled
:animatetruewhether animations are enabled (disabled over SSH connections by default)
:default-shellinferred from $SHELL on startupthe default command used for cmd/new

Replay mode

One of cy's most important features is the ability to 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.

A note about recording

cy does not and will never record what you type (otherwise known as "standard in" or stdin). It only records the output of the process (otherwise known as "standard out" 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-dir parameter to an empty string. Note that cy will continue to hold on to your terminal sessions in memory.

Modes

Like vim, replay mode is modal, meaning that it has several different modes that it can be in 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 entire 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, 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. 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.

You can initiate a search by hitting / to search forward in time and ? to search backward (by default). Searching supports full regex patterns; you must escape any special characters with \ if you wish to avoid this behavior.

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.

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.

Recording terminal sessions to disk

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

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

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

The directory will be created if it does not exist.

You can access previous sessions through the cy/open-log action, which by default can be invoked by searching for open an existing log file in the command palette (ctrl+a ctrl+p).

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

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.
  • Text Render some text.

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

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

Here are some examples:

(input/find @[
        # A standard text preview, which will be rendered in an 80x26 window
        ["some text" {:type :text :text "this is the preview"} 1]
        # A replay preview
        ["this is a borg file" {:type :replay :path "some-file.borg"} 2]
        # A pane preview
        ["this is some other pane" {:type :node :id (pane/current)} 3]
    ])

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

API

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

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

Concepts

Binding

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

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

For example:

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

NodeID

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

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

Symbols

action/choose-frame action/command-palette action/jump-command action/jump-pane action/jump-pane-command action/jump-project action/jump-screen-lines action/jump-shell action/kill-current-pane action/margins-160 action/margins-80 action/margins-bigger action/margins-smaller action/new-project action/new-shell action/next-pane action/open-log action/random-frame action/reload-config action/toggle-margins cmd/commands cmd/new cmd/path cy/detach cy/env cy/get cy/kill-server cy/log cy/paste cy/reload-config cy/replay cy/set cy/toast exec/file group/children group/leaves group/new input/find key/action key/bind key/bind-many key/bind-many-tag key/current key/get key/remap key/unbind pane/attach pane/current pane/screen path/abs path/base path/glob path/join replay/beginning replay/command-backward replay/command-forward replay/copy 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/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 shell/attach shell/new tree/group? tree/kill tree/name tree/pane? tree/parent tree/path tree/root tree/set-name viewport/get-frames viewport/set-frame viewport/set-size viewport/size


action/choose-frame

function

(action/choose-frame)

Choose a frame.

source

action/command-palette

function

(action/command-palette)

Open the command palette.

source

action/jump-command

function

(action/jump-command)

Jump to the output of a command.

source

action/jump-pane

function

(action/jump-pane)

Jump to a pane.

source

action/jump-pane-command

function

(action/jump-pane-command)

Jump to a pane based on a command.

source

action/jump-project

function

(action/jump-project)

Jump to a project.

source

action/jump-screen-lines

function

(action/jump-screen-lines)

Jump to a pane based on screen lines.

source

action/jump-shell

function

(action/jump-shell)

Jump to a shell.

source

action/kill-current-pane

function

(action/kill-current-pane)

Kill the current pane.

source

action/margins-160

function

(action/margins-160)

Set size to 160 columns.

source

action/margins-80

function

(action/margins-80)

Set size to 80 columns.

source

action/margins-bigger

function

(action/margins-bigger)

Increase margins by 5 columns.

source

action/margins-smaller

function

(action/margins-smaller)

Decrease margins by 5 columns.

source

action/new-project

function

(action/new-project)

Create a new project.

source

action/new-shell

function

(action/new-shell)

Create a new shell.

source

action/next-pane

function

(action/next-pane)

Move to the next pane.

source

action/open-log

function

(action/open-log)

Open a .borg file.

source

action/random-frame

function

(action/random-frame)

Switch to a random frame.

source

action/reload-config

function

(action/reload-config)

Reload the cy configuration.

source

action/toggle-margins

function

(action/toggle-margins)

Toggle the screen's margins.

source

cmd/commands

function

(cmd/commands arg0)

cmd/new

function

(cmd/new parent &named path 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.

When the command exits, it will be rerun. This is not currently configurable. 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.

cy/detach

function

(cy/detach)

Detach from the cy server.

cy/env

function

(cy/env)

cy/get

function

(cy/get key)

Get the value of the parameter with key key.

cy/kill-server

function

(cy/kill-server)

Kill the cy server, disconnecting all clients.

cy/log

function

(cy/log level message)

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

cy/paste

function

(cy/paste)

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/replay

function

(cy/replay &named main copy location)

Enter replay mode for the current pane.

cy/set

function

(cy/set key value)

Set the value of the parameter with key key to value value. For the time being, parameter values can only be strings, booleans, and integers. This is expected to change in the future.

cy/toast

function

(cy/toast level message)

Send a toast with message to all attached clients. level must be one of :info, :warn, :error.

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/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:

  • :full (boolean): If true, occupy the entire screen.
  • :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.)
  • :animated (boolean): Enable and disable background animation.
  • :headers ([]string): Provide a title for each column. This mostly used for filtering tabular data.

key/action

macro

(key/action name docstring & body)

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

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

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

source

key/bind

function

(key/bind target sequence callback)

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

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

Read more about binding keys in the dedicated chapter.

key/bind-many

macro

(key/bind-many scope & body)

Bind many bindings at once in the same scope.

For example:

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

source

key/bind-many-tag

macro

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

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

source

key/current

function

(key/current)

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

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/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"])

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/screen

function

(pane/screen pane)

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

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.

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/command-backward

function

(replay/command-backward)

In time mode, jump to the moment in time just before the last command was executed. In copy mode, move the cursor to the first character of the last 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/copy

function

(replay/copy)

Yank the selection into the copy buffer.

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 group path)

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

For example:

(replay/open (tree/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.

shell/attach

function

(shell/attach &opt path)

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

source

shell/new

function

(shell/new &opt path)

Create a new shell initialized in the working directory path.

source

tree/group?

function

(tree/group? node)

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

tree/kill

function

(tree/kill node)

Remove the node and all of its child nodes. This will halt execution of any descendant panes. 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/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-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.

viewport/set-size

function

(viewport/set-size size)

Set the size of the inner viewport. size is a tuple in the form [rows, columns].

viewport/size

function

(viewport/size)

Get the size of the inner viewport. Returns a tuple [rows, columns].

Acknowledgments

cy is the result of hundreds of hours of work (and, perhaps, hundreds more.) I would like to thank the following people for their efforts to support me in this project:

  • A.J., with particular gratitude for contributing the name taro and a few of the original ideas that inspired cy
  • S.K., for pushing me to pursue this bizarre blend of art and utility
  • V.P., 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