shipship, back to the landing page

Ship reads ~/.config/ship/config.toml, or ship/config.toml under $XDG_CONFIG_HOME. Without one, it uses its defaults. ship config defaults prints them all, with what each setting accepts:

Terminal window
ship config defaults

Your file only needs what you change. Copy just those lines rather than the whole output: anything you copy stays fixed, and stops following Ship's defaults when they change. --config <file> or SHIP_CONFIG points somewhere else.

Open windows pick up changes when you save. If the file has a mistake, open windows keep their last working settings and show the error. Run ship config check to list the findings and their locations.

[server.defaults.pane]
shell = "/bin/sh" # default: your login shell
scrollback_bytes = 10_000_000 # bytes of history per pane
right_click_passthrough = false
exit_behavior = "close" # "close", "keep" or "keep_failed"

These are defaults for new panes, including a new tab's starter pane. Save the file and the next pane takes them; panes already open keep their values. A relative shell path resolves from the config file's folder. If the file can't be read or has errors, new panes use the built-in defaults and the server logs the error.

ship pane create --exit-behavior keep --scrollback-bytes 50000000 overrides the defaults for that pane. Panes explains the exit behaviors. --right-click-passthrough turns passthrough on, and --right-click-passthrough=false turns it off even when the default is on.

You can change an existing pane's history limit without restarting its program:

Terminal window
ship pane set --pane "$PANE" --scrollback-bytes 50000000

Raising the limit keeps the history already there and lets it grow. Lowering it drops the oldest history beyond the new limit right away. The limit is in bytes, not lines.

[client]
sidebar_width = 40 # default: 26 columns
mouse = true # false leaves the mouse to your terminal

With the git example plugin running, this puts each tab's branch and pull request under its name:

[client.tabs]
rows = [["name"], [{ tag = "git.branch" }, { tag = "git.pr" }]] # default: [["name"]]
▲ ▾ ship
main · ○ #112 ✓
▲ api
fix-login · ◆ #118
● site
main
notes

rows has one list for each line of a tab's entry. A piece on a line is name or a tag such as { tag = "git.branch" }, and the pieces on a line are joined with ·.

  • A tag shows the tab's own value, or else its first pane's, the way a tab without a name takes its first pane's folder.
  • A piece without a value is left out, and a line with nothing on it disappears. That's why notes above stays on one line.
  • A long piece is cut with …, as in agent rows.

Ship checks for new releases at startup and about once an hour, and shows what it finds in the sidebar and in the updates panel. These settings change that:

[client.updates]
check = true # default; false turns automatic checks off
# command = "brew upgrade ship" # override the build's update method
# latest_command = "echo 9.9.9" # override where the latest version comes from

With check = false Ship stops looking, but you can still ask:

Terminal window
ship update --check
0.1.0 → 0.2.0

It prints up to date when there's nothing newer, and no releases yet when there are none. It always asks for a fresh result, even with automatic checks off, and never starts a server.

command and latest_command are both unset by default, and each is a shell line: sh -c on Unix, cmd /C elsewhere.

  • command is what ship update runs. It comes ahead of the build-time SHIP_UPDATE_COMMAND.
  • latest_command prints a version, instead of Ship asking GitHub for one. It comes ahead of the build-time SHIP_LATEST_COMMAND. Without either, Ship uses the newest stable GitHub release, and release notes still come from GitHub when a command supplies the version.

Ship accepts versions such as 0.2.0, v0.2.0 and 0.2.0-1, and ignores a packager suffix when comparing them.

[client.agents]
rows = [["icon", "app", "state"], ["tab"]] # what each sidebar entry shows
delay_ms = 1000 # how long a state must hold before notifying
popups = false # true also sends desktop notifications
[client.agents.sounds.default]
done = { path = "~/sounds/ding.mp3" }
attention = { builtin = "attention" }
[client.agents.sounds.apps.codex]
done = "off"

rows lists the lines of each entry. The pieces you can use are icon, app, state, tab, pane (the pane's label), and title and msg (whatever the agent reported). A tag on the agent's pane goes in as { tag = "git.branch" }. Anything without a value is left out, along with its space, and a line with nothing on it disappears. When a line is too wide for the sidebar, its pieces share the width and the long ones are cut with ….

A sound is "off", a built-in ({ builtin = "done" } or { builtin = "attention" }), or a file ({ path = "..." }) in MP3, WAV, Ogg Vorbis or FLAC. done plays when an agent finishes and attention when it's blocked or errors. [client.agents.sounds.apps.<name>] overrides sounds for one agent. If a sound file is missing or broken, Ship plays the built-in one instead and says so in the client log.

The built-in sounds are from Kenney's Interface Sounds pack (attribution).

Keys live in modes, and you start in normal. Binding a key replaces only that key; everything else keeps its default. In normal mode, {} unbinds a key so it reaches the program instead:

[client.modes.normal.keys]
"alt-q" = {} # alt-q goes to the program
"alt-t" = { action = { server.tab.create = {} } } # new empty tab
"ctrl-b" = { action = { client.mode = "prefix" } } # tmux habits
[client.modes.prefix]
kind = "oneshot" # back to normal after one key
[client.modes.prefix.keys]
"c" = { action = { server.tab.create = { starter = "shell", select = true } } }
"%" = { action = { server.pane.create = { place = "right", select = true } } }
"ctrl-b" = { action = { client.send = "ctrl-b" } } # a real ctrl-b for the pane

There are two kinds of actions:

  • server. actions normally have a CLI command. server.tab.create = { starter = "shell" } does the same as ship tab create --starter shell, so ship <command> --help documents its options. The exception is server.run, which starts a program on the server's machine and has no CLI form.
  • client. actions work in your window, like client.pane.focus, client.sidebar.toggle, client.mode and client.detach. client.run starts a program on your machine.

A oneshot mode takes one key and returns to normal, unless that key enters another mode. A sticky mode stays until a binding leaves it. Outside normal mode, unbound keys are ignored rather than sent to the program. clear_defaults = true starts a mode with none of Ship's bindings. Add label = "..." to a binding to change how it's shown in the hints.

When you're in a mode other than normal, a hint bar shows at the bottom with that mode's keys, wrapping into aligned columns when they don't fit on one line. The same bar shows errors and when a pane's program has exited.

The default keys are in defaults.toml. Key names like ctrl-alt-x and shift-tab follow crokey.

Open the docs in your browser with alt-u, or name the selected pane picked with alt-i:

[client.modes.normal.keys]
"alt-u" = { action = { client.run = { command = ["xdg-open", "https://shipmux.github.io/ship/"] } } }
"alt-i" = { action = { server.run = { command = ["sh", "-c", "\"$SHIP_BIN\" pane set --name picked"] } } }

The browser example uses Linux's xdg-open. On macOS, replace it with open.

client.run starts a program on the client's machine, in the directory you started the client from. Use it for things that need your desktop or login session, like opening a URL.

server.run starts a program on the server's machine, with the same environment and starting directory as a new pane created from the selected pane. With nothing selected, it starts in the server's home directory. The example renames the selected pane to picked by calling Ship back.

Both run in the background. Each press starts another run without waiting for earlier ones, and you can keep typing while they run. Menus accept the same actions.

command must be a non-empty array of strings. The first string is the program and the rest are its arguments. There is no shell expansion unless you name a shell, as the rename example does.

The program gets SHIP_BIN, SHIP_SERVER_URL and SHIP_ATTACHMENT_ID so it can call Ship back, plus SHIP_PANE_ID when a pane is selected. It gets no input, and its output goes to that side's log, never onto the screen.

A program that can't start shows an error naming it in the status line. A non-zero exit or signal also shows there for client.run, but is only logged for server.run.

Ship never stops a run, even when you quit the client, close its terminal or stop the server. Put a program that should end with Ship in a pane instead.

Each right-click menu can be replaced with your own list of actions, written like key bindings:

[client.menus]
pane = [
{ action = { client.pane.zoom = { zoomed = "toggle" } }, label = "fill" },
{ action = { server.pane.close = {} } },
]
tab = [] # no tab menu

The menus are pane, tab and global. Any you leave out keep their defaults.