"The purpose of a tool is to disappear."
Most editors hand you a cockpit: toolbars, sidebars, palettes, panes within panes, settings buried six dialogs deep. z takes the opposite bet. It gives you text, a tag line, and three mouse buttons — then gets out of the way.
This tutorial will take you from zero to genuinely productive. By the end you will understand not just what z does but why it works the way it does, and that understanding will make everything click together naturally.
- Getting Started
- The Three Mouse Buttons
- Opening, Navigating & Closing Files
- Editing Essentials
- Running External Commands
- Working with Multiple Windows & Columns — including
RotateView - Scratch Buffers
- Appearance & Fonts
- Syntax Highlighting & Themes
- Language Server Protocol (LSP)
- Session Management
- Command-Line Flags (Advanced)
- User Scripts
- Plumbing Rules
- Putting It All Together
Pre-built packages for Linux, macOS, and Windows are available on the Releases page. Native packages (.deb, .dmg, .msi) bundle their own JRE — nothing else to install.
If you prefer to build from source:
sbt assembly
mkdir -p ~/.local/lib/z
cp target/scala-3.8.2/z.jar ~/.local/lib/z/z.jarThen create a launcher at ~/bin/z:
#!/bin/sh
exec java \
-Dawt.useSystemAAFontSettings=on \
-Dswing.aatext=true \
-jar "${HOME}/.local/lib/z/z.jar" \
"$@"Make it executable: chmod +x ~/bin/z
z # Open with an empty scratch buffer
z myfile.scala # Open a specific file
z ~/projects/myapp # Open a directory
z file1.go file2.go # Open multiple filesWhen z opens, you see three distinct zones stacked vertically:
┌─────────────────────────────────────────────────────┐
│ Help NewCol History Put Dump Load Dir ← App tag line
├──────────────────────────┬──────────────────────────┤
│ CloseCol New Sort │ CloseCol New Sort ← Column tag lines
├──────────────────────────┼──────────────────────────┤
│ ~/proj/main.go Get Put │ ~/proj/util.go Get Put ← Window tag lines
├──────────────────────────┼──────────────────────────┤
│ │ │
│ file content here │ file content here │ ← Window bodies
│ │ │
└──────────────────────────┴──────────────────────────┘
│ 1/342 @ 0 Tab:4 NoWrap Hack 14 │ [14:32:05] Sort ← Status bar
└─────────────────────────────────────────────────────┘
There are three levels of hierarchy:
- App — the whole editor. Its tag line runs across the very top.
- Columns — vertical strips. Each has its own tag line.
- Windows — individual file panes within columns. Each has a tag line above its body.
This hierarchy is not just organizational — it determines the scope of commands. A command run from the app tag line affects everything. The same command run from a window tag line affects only that window. This one rule explains a huge amount of z's behavior.
Each tag line is just editable text. The commands pre-loaded into it are there for convenience — you can add your own, remove ones you never use, or type a command anywhere in any tag line and execute it. There is nothing special about the words already there; they are just text that happens to be commands.
The status bar at the bottom is split into two lanes:
12/340 @ 4 Tab:4 NoWrap NoIndent Hack 14 │ [14:32:05] Sort
←── window state (left) ──────────────────────────── command echo (right) ──→
Left — live window state, updated whenever you move the mouse over a window: current line / total lines, cursor column, tab size, wrap state, indent state, active font. When LSP or syntax highlighting is active, those appear here too. Clears when the mouse is not over any window.
Right — timestamped echo of the last command run, updated only when a command executes. It is never overwritten by mouse movement, so you can always see what you last did.
This is the heart of z. Nothing else in this tutorial will make complete sense until the mouse model clicks for you, so we will take it slowly.
B1 behaves exactly as you expect from any editor:
- Click to place the caret
- Click and drag to select text
- Shift+B1 to extend the selection by character
- Shift+Ctrl+B1 to extend by word
- Ctrl+B1 for brace/symbol matching — click inside any
{},[],(), or<>pair and z selects everything between the matching delimiters. Works with any repeated character as a delimiter too.
B2-drag is the execute gesture. Hold B2, drag across some text to select it, release — z runs it as a command.
That's it. Any text in any window or tag line can become a command with a B2-drag. You can write a shell pipeline directly in a file you're editing, B2-drag across it to run it, and see the output appear in a +Results window — without ever leaving the editor or touching a terminal.
Tip: You do not need to select precisely. If your B2 drag starts inside an already-selected region, z will use the existing selection.
B3 is the smart button. It does not just execute — it thinks first, working through a priority list:
- Plumbing rules → first matching rule in
~/.z/plumbingwins (see Section 14) - Valid file or directory path → opens it in a new window
:n→ jumps to line n in the current file:/regexp→ searches forward for regexp from the current positionfilename:n→ opens filename at line nfilename:/regexp→ opens filename, then searches for regexp- Anything else → searches forward for the text in the current window
- Search fails → executes the text as a command
This cascade is elegant. B3 on main.go opens main.go. B3 on :42 jumps to line 42. B3 on foo finds the next occurrence of "foo". B3 on Put saves the file. The right thing happens naturally.
Like B2, B3 also works as a drag gesture: hold B3, drag to select, release to act on the selection.
When a window has a relative path, B3 navigation preserves that style — any path opened from it gets a relative tag rather than an absolute one. This applies uniformly: directory listings, paths in body text, and tag path segments all behave consistently.
For example, a directory window showing src/main:
| You B3-click | New window tag |
|---|---|
Foo.scala in the listing |
src/main/Foo.scala |
util/ in the listing |
src/main/util |
main in the tag line |
src/main |
src in the tag line |
src |
In the tag line, this applies only to the path at the very start. Relative paths appearing later in the tag — such as arguments to Get or other commands — resolve normally (relative to the file's parent directory).
Sometimes z's smart B3 gets in the way. If you have a word like Put in a file you're editing and you want to run it rather than search for it, prefix it with %:
%Put
%ls -la
%Close
The % tells z: skip the look/search logic, treat this as a command directly.
Tip: Placing the caret at the very end of the file also bypasses look logic — another way to force command execution.
B2 and B3 require mouse precision for multi-word commands. Capture mode lets you type a command with the keyboard, see it highlighted as you go, then choose how to execute it.
Press Ctrl+Enter to start capture mode. Everything you type from that point is highlighted as you type. When you're ready:
Ctrl+Enter— execute the captured text as a command (B2 analog)Ctrl+F— look/navigate on the captured text (B3 analog: opens files, jumps to lines, searches)Escape— cancel without executing; the typed text stays
If text is already selected when you press Ctrl+Enter or Ctrl+F, the shortcuts act on that selection immediately — no capture mode is entered, and the selection is not deleted.
Body vs. tag behaviour:
- In the body: the typed command text is deleted after execution (it was a transient command prompt, not content).
- In the tag: the text stays and remains highlighted after execution, just like a B3-click on a tag command.
Cascade behaviour from app and column tag lines:
Ctrl+Enterfrom a column tag runs the command across all windows in that column.Ctrl+Enterfrom the app tag runs the command across all columns (which each cascade to their windows).Ctrl+Ffrom a column tag performs a look traversing all windows in that column.Ctrl+Ffrom the app tag performs a look across all columns.
Example — change the font without leaving the keyboard:
- Press
Ctrl+Enterin any body → capture mode on. - Type
Font Hack 16→ text appears, highlighted as you type. - Press
Ctrl+Enter→ font changes, typed text deleted.
Example — open a file by typing its path:
- Press
Ctrl+Enter→ capture mode on. - Type
src/main/z.scala→ highlighted. - Press
Ctrl+F→ file opens, path text deleted from body.
Once these three buttons become second nature, z starts to feel very fast. You select with B1, execute with B2, navigate with B3 — and your hands almost never leave the mouse for the common operations of an editing session.
The simplest way to open a file is to type its path anywhere — in a tag line, in a body — and B3-click on it. z recognizes paths automatically and opens them in a new window.
Path prefixes are supported everywhere:
| Prefix | Expands to |
|---|---|
~ |
Your home directory |
~/foo |
$HOME/foo |
. |
The window's current root directory |
./foo |
root/foo |
.. |
Parent of the current root |
../foo |
root/../foo (canonicalized) |
Expansion is invisible — the tag line keeps the text as you typed it. The resolved absolute path is used only when z actually reads or writes the file.
Using Get: Type a path in a window's tag line and execute Get to load that path into the window. Get without an argument reloads the current file from disk — useful for picking up external changes.
Directories: Opening a directory shows a sorted listing with subdirectories marked by a trailing /. B3 on any entry in the listing opens it.
| What you type | What B3 does |
|---|---|
:42 |
Jump to line 42 in current file |
:/TODO |
Search forward for "TODO" |
server.go:10 |
Open server.go at line 10 |
server.go:/main |
Open server.go, find "main" |
someword |
Search forward for "someword" |
These work from tag lines, from body text, anywhere. B3 on an error message that says main.go:47: will open main.go at line 47.
Note: Searches run forward from the caret to the end of the file only — there is no wrap-around. If the text is not found, z falls through to executing it as a command instead. To search again from the top, move the caret to the beginning of the file first (
Ctrl+Home).
Ctrl+P opens a keyboard-driven file picker rooted at the current window's project root. Type any fragment of a filename to filter; matching characters do not need to be consecutive — the picker scores runs, filename hits, and path-segment starts to rank results. Use arrow keys to navigate, then Enter to confirm. When invoked from a tag line (app, column, or window) the chosen file opens immediately as a new window; when invoked from the body the path is inserted as text at the cursor.
Path prefix navigation: prefix your query with /, ~/, ./, or ../ to re-root the search at a different directory. After the picker settles (150 ms debounce), the prefix is consumed and the box shows only your fuzzy query.
ZWnd → matches ZWnd.scala, ZWndHelper.go, …
../config → re-roots one level up, then filters by "config"
~/dotfiles → re-roots at $HOME, filters by "dotfiles"
The inserted path is relative to the current file's directory, so B3-clicking it navigates correctly without a double prefix.
Put saves the current file. From a column tag line, it saves all files in that column. From the app tag line, it saves everything open. The dirty flag (* prefix in the tag line) is cleared on a successful save.
Put [fname] saves to a different filename without changing the tag line.
Close— closes the window, prompting if there are unsaved changesCloseCol— closes an entire column and all its windowsCLOSE— closes without any confirmation, regardless of dirty state
Warning:
CLOSE(all caps) is intentionally destructive. It discards unsaved changes silently.
By default, B3 navigation opens a new window for each destination. Bind changes this: when bind mode is on, navigating replaces the current window's content instead of opening a new one. This is useful when you want a single "reference" window that follows your jumps.
Execute Bind from a window's tag line to toggle it. The status bar will show Bind when it is active.
Each window has a root directory — the base against which relative paths are resolved. By default this is the directory of the file the window opened with. Dir changes it:
Dir ~/otherproject
Dir ./src
Dir ..
Dir accepts the same path prefixes as everywhere else in z (~, ./, ../). From a column or app tag line, it applies to all windows in scope.
Watch for unexpected dirty flags: If a window's tag line contains a relative path (e.g.,
./main.go), changing its root withDirmeans that path now resolves to a different file — so z marks the window dirty to signal that its tag line path has changed meaning. Windows with absolute paths are unaffected.
The standard shortcuts work as expected:
| Action | Shortcut |
|---|---|
| Cut | Ctrl+X |
| Copy (Snarf) | Ctrl+C |
| Paste | Ctrl+V |
| Undo | Ctrl+Z |
| Redo | Ctrl+R |
| Select all | Ctrl+A |
| Delete word left | Ctrl+Backspace |
| Delete word right | Ctrl+Delete |
| Go to top/bottom | Ctrl+Home / Ctrl+End |
| Previous/next word | Ctrl+Left / Ctrl+Right |
| Extend selection | Shift+cursor keys |
| Extend by word | Shift+Ctrl+cursor |
Note that z uses the Acme term snarf for copy — you will see it in commands (Snarf) and in the help text. It means exactly what you think.
Undo and Redo are available as tag line commands as well as keyboard shortcuts. Both are per-window and maintain a full history for the session.
Wrap — toggles line wrapping. Off by default. Execute from any tag line.
Indent — toggles auto-indent. When on, pressing Enter preserves the leading whitespace of the current line. Off by default.
Tab n — sets the tab width to n spaces. For example, Tab 2 or Tab 8. The default is 4.
Mark appends a bookmark for the current cursor position to a scratch window called path+Mark (e.g., ~/myproject+Mark). Each entry is recorded as:
filename:linenum line content
B3 on any entry in the mark window jumps straight to that location. This makes Mark a lightweight jump list — execute it whenever you are about to wander away from a spot you know you will need to return to.
A * before the filename in a window's tag line means the file has unsaved changes. You can manually control this with Clean (mark as unmodified) and Dirty (mark as modified). These are occasionally useful when you want to suppress a save prompt or force one.
This is where z becomes genuinely powerful. Rather than embedding a terminal emulator as an afterthought, z treats external commands as first-class citizens: their output flows directly into your editing session.
There are four external command operators, each with a distinct relationship between the command, the selection, and the output.
The < operator runs cmd and replaces the current selection with its standard output, or inserts at the caret position if nothing is selected. Scroll mode does not affect placement.
< date
< echo "Hello, world"
< git log --oneline -10
Practical use: select a placeholder in a file and run < myscript.py to replace it with generated content, or position the caret and run < myscript.py to insert inline.
The > operator takes the current selection (or the entire file if nothing is selected) and sends it as standard input to cmd. Output goes to a +Results scratch window.
> wc -l
> python3
> jq .
Practical use: select a JSON blob and run > jq . to pretty-print it in +Results without touching the original.
The | operator pipes the selection through cmd and replaces the selection with the output. The selection goes in as stdin; stdout replaces it. With no selection (e.g. in capture mode), the entire body content is piped and replaced with the output.
| sort
| sort -u
| column -t
| python3 -c "import sys; print(sys.stdin.read().upper())"
Practical use: select a list of items, run | sort -u to sort and deduplicate, get the result back in the same spot.
The ! operator runs cmd and replaces the entire window content with its output.
! ls -la
! git diff
! cat README.md
Where the output goes depends on where you execute ! from:
- Window tag line → replaces that window's content
- Column tag line → opens a new window in that column
- App tag line → opens a new window in the rightmost column
Practical use: keep a window showing live git diff output by running ! git diff whenever you want a refresh.
Understanding where an external command runs is important — and it is not quite what you might expect.
z derives the working directory from the resolved tag line path, not from the window's root directly:
- If the tag line resolves to a directory → the command runs there
- If the tag line resolves to a file → the command runs in that file's parent directory
- If the tag line has no path (a scratch buffer or empty window) → the command runs in the window's root
Dir sets the root, which matters only when the tag line contains a relative path — because that is when the root is used to resolve it. If a window's tag line has an absolute path (e.g., /home/user/myapp/main.go), the command always runs in /home/user/myapp/ regardless of what Dir is set to.
In practice this means: if you open ~/myapp/src/server.go by absolute path and run ! sbt test, sbt runs from ~/myapp/src/ — which is probably not your project root and will fail. You have two options: change the tag line path to point at the project root directory, or use a cd inside the command itself:
! cd ~/myapp && sbt test
Dir is most powerful when working with relative tag line paths or scratch buffers, where the root directly determines where commands land.
Diris not the same as the LSP workspace root. They are independent settings. See Section 10 for the distinction.
The tag line shows <!> while an external command is active. To terminate it early, execute Kill from that window's tag line.
When you start a long-running process (a REPL, a debugger, a build tool), you can talk to it interactively using Input mode.
- Start the process:
< python3or! bash - Execute
Inputfrom the tag line to toggle interactive mode - z watches for prompt lines (lines ending in
>,$,%,?, or#by default) - Type your response on the same line as the prompt, then press Enter — z sends it to the process
To customise the prompt detection pattern:
Input [>$]
This sets the prompt regexp to any line ending in > or $.
X and Y let you run a command across multiple open windows at once.
X '.*\.go' Put ← Save all open Go files
Y '.*_test\.go' Hilite go ← Enable Go highlighting on all non-test files
X '.*\.scala' > scalafmt ← Format all Scala files
X 'pattern' cmd runs cmd in every window whose path matches the pattern. Y is the inverse — it runs cmd in every window whose path does not match. Matching is anchored (Java .matches), so use .*foo.* to match paths containing "foo".
When a command or path contains spaces, wrap it in single quotes:
< 'wc -l'
Get '/home/user/My Documents/notes.txt'
| 'awk {print $2}'
Single-quote wrapping applies anywhere z parses a command or path — file names, Get, Put, Dir, and all four external command operators. Inside single quotes, the text is treated as a single token regardless of spaces.
Every external command launched by z has access to two environment variables describing the current window's file:
| Variable | Value |
|---|---|
Z_FILE |
File path as written in the tag line (~ and ./ expanded, symlinks not resolved) |
Z_FP |
Canonical absolute path (symlinks fully resolved) |
Z_DIR |
Working directory where the command runs |
Z_SELECTION |
Currently selected text (empty string if nothing selected) |
All four variables are available to every external command — < > | ! operators and user scripts alike.
New— creates a new empty window. From the app tag line, creates one in each column.NewCol— creates a new column (app tag line only).NewZ [path]— launches an independent z instance. Without an argument, opens at the current file's directory (from a window), the column's directory, or the app's directory. With a path argument, opens at that directory (or the parent directory if a file path is given). Only works when running from a built JAR — no-op fromsbt run.Zerox— clones the current window into a new window. Both windows show the same file; changes in one are reflected in the other.
Windows can be moved freely within and between columns.
| Command | Effect |
|---|---|
Up |
Move window up within its column |
Dn |
Move window down within its column |
Lt |
Move window to the column on the left (or move a column left) |
Rt |
Move window to the column on the right (or move a column right) |
Run these from the window's tag line to move that window. Run Lt or Rt from a column tag line to move the entire column.
RotateView toggles the layout orientation.
- From a column tag line: flips that column's windows between top-to-bottom stacking (the default) and side-by-side stacking.
- From the app tag line: also flips the column layout (columns go from left-to-right to top-to-bottom), and each existing column independently toggles its window orientation.
New columns created after a rotation inherit the current app orientation.
Rotation state is persisted automatically: per-column state survives Dump/Load, and the app-level orientation is saved to ~/.z/settings on exit so it is restored on next launch.
Sort — executed from a column tag line — sorts that column's windows alphabetically by path. From the app tag line, it sorts each column independently.
A scratch buffer is any window whose filename contains a +. z will never write these to disk — Put is a no-op on them.
+scratch
/home/user/+notes
/tmp/+Results
~/myproject+Mark
Scratch buffers appear in Dump state and survive across sessions. They are ideal for accumulating notes, command output, or anything you want to keep handy without creating actual files.
z creates several scratch buffers automatically:
| Buffer | Created by |
|---|---|
+Results |
>, ` |
+Diagnostics |
LSP diagnostic messages |
+Props |
The Props command |
+Fonts |
The Fonts command |
+Help |
The Help command |
+History |
The History command (timestamped command log) |
+plumb |
Plumbing rules with plumb to exec (exec output) |
+Cmd |
User scripts ,cmd / ,,cmd run from col or app tag line |
path+Mark |
The Mark command |
By default, z scrolls a window as new content arrives. For a +Results window receiving a lot of output, this can be distracting. Execute Scroll off from the window's tag line to stop auto-scrolling — the content still streams in, but the view stays put. Scroll on restores the default.
Note:
Scrollmode applies to>,|, and!output only. The<operator replaces the selection (or inserts at the caret) and is unaffected by the scroll setting.
z ships with three bundled fonts:
- Hack Regular — fixed-width, used by default in body and tag lines
- Bitstream Vera Sans — proportional
- Bitstream Vera Serif — proportional
Two font slots exist: a fixed-width slot (FONT) and a variable-width slot (Font). You can switch between them instantly:
FONT ← Apply the stored fixed-width font (Hack Regular 14)
Font ← Apply the stored variable-width font (Bitstream Vera Sans 14)
FONT 'Hack Regular' 16 ← Set and apply a fixed-width font at size 16
Font 'Bitstream Vera Serif' 13 ← Set and apply a variable-width font
Fonts (from the app tag line) lists every font available to z, in a +Fonts scratch window.
Tag line fonts are set with TagFont:
TagFont 'Hack Regular' 12
Font commands cascade with the scope hierarchy:
- From a window tag line: affects that window only
- From a column tag line: affects the column tag and all window tags in it
- From the app tag line: affects all tag lines editor-wide and sets the default for new windows
Ln toggles line numbers in the body gutter. Off by default.
CLine toggles the highlight on the line containing the caret. On by default.
Body and tag line colours are set by RGB values (integers 0–255):
ColorBack 30 30 30 ← Body background
ColorFore 220 220 200 ← Body foreground
ColorCaret 255 200 0 ← Caret colour
ColorSelBack 60 80 120 ← Selection background
ColorSelFore 255 255 255 ← Selection foreground
ColorTBack 50 50 60 ← Tag background
ColorTFore 180 180 220 ← Tag foreground
ColorTCaret 180 180 220 ← Tag caret
ColorTSelBack 96 96 96 ← Tag selection background
ColorTSelFore 255 255 255 ← Tag selection foreground
Scope rules:
Color T*— applies to the current level's tag line only. Run from a window tag to style that window's tag, from a column tag to style the column's tag, or from the app tag to style the app tag.ColorAll T*— applies to the current level's tag and cascades down to all children. Run from the app tag to restyle every tag line in the editor at once; from a column tag to restyle that column and all its windows.- Body colour variants (
ColorBack,ColorFore, etc.) only apply at the window level. Running them from a column or app tag line has no effect.
Props will show you the current RGB values for all colour settings if you need to inspect or copy them.
For files with a known extension (.scala, .go, .py, .md, and many others), highlighting turns on automatically when you open the file — no command needed. For files with unrecognised extensions, or to switch languages, use Hilite:
Hilite ← Auto-detect from extension (or re-enable after Hilite off)
Hilite scala ← Force Scala highlighting
Hilite go
Hilite python
Hilite off ← Disable highlighting
Once highlighting is on, apply a colour theme with Theme:
Theme z ← Default (matches editor colour scheme)
Theme dark
Theme monokai
Theme idea
Theme eclipse
Theme druid
Theme vs
Markdown files (.md, .markdown) get enhanced typography automatically:
- H1 / H2 / H3 headings render in Bitstream Vera Serif Bold at 18 / 15 / 14pt — visually distinct from body prose.
- Bold and italic text use font-weight and font-style changes rather than just colour.
- Code spans and fenced code blocks switch to Hack monospace.
- Blockquotes render in italic; link text is underlined.
To adjust fonts per element, use MdFont from the app tag line:
MdFont h1 'Bitstream Vera Serif' 20
MdFont code 'Hack' 13
MdFont bold 'Bitstream Vera Sans' 14
Elements: h1 h2 h3 bold em bolditalic code quote. Settings persist across sessions.
When you restore a session with Load, the highlighting state is restored. For files with known extensions, the colouring is re-applied automatically on the next Get. Files without a known extension that were manually highlighted will need Hilite re-run after loading.
LSP connects z to a language server for the file you are editing, enabling real-time diagnostics, hover documentation, and code completion.
These are two independent settings that are easy to conflate:
Dirsets the window's root directory — where relative paths resolve and where external commands run (when the tag line has a relative or missing path).Lsp [projRoot]sets the LSP workspace root — the directory the language server uses to locate build files (build.sbt,go.mod,Cargo.toml, etc.) and index the project.
Running Dir ~/myapp/src does not tell the language server that your project lives in ~/myapp. The server needs to be started with Lsp ~/myapp so it finds the build file at the project root. Pointing it at a subdirectory is a common source of "why isn't LSP finding my symbols?" confusion.
Equally, Lsp ~/myapp does not change where your shell commands run — for that you still need Dir.
From a window's tag line, execute:
Lsp
Lsp . ← Use the window's current root as project root
Lsp ~/myproject ← Explicit project root
z has built-in defaults for common languages — no configuration needed for:
go, python, java, scala, javascript, typescript, rust, kotlin, shellscript
Once active, you get:
- Squiggly underlines for errors, warnings, and info
- Hover tooltips with type information and documentation (hover over any underlined or interesting symbol)
- A
+Diagnosticswindow listing all current issues, updated as you type - Code completion via
Ctrl+Spaceor theCompletecommand
Check forces an immediate update of diagnostics. Normally they update automatically after a short pause when you stop typing, but Check is useful when you want an instant result.
Lsp off
This shuts down the language server for that window and clears the diagnostics.
To override built-in server commands or add new languages, create ~/.z/lsp.conf:
go = gopls
python = pylsp
scala = metals
One langId = command per line. z reads this file at startup, and your entries take priority over the built-in defaults.
Dump saves the full editor state — every column, every window, the content of dirty scratch buffers, fonts, and colours — to a flat file.
Dump ← Save to z.dump in the working directory
Dump ~/mysession ← Save to a specific path
Load restores it:
Load ← Load z.dump from the working directory
Load ~/mysession
Both commands run from the app tag line only. The window geometry (size and position of the main window) is saved automatically to ~/.z/settings on exit.
The commands pre-loaded into each tag line are just defaults — they can be customised globally via ~/.z/settings. z reads this file at startup and writes window geometry back to it on exit, so any keys you add are preserved across launches.
| Key | What it controls | Built-in default |
|---|---|---|
tag.app |
App tag line content | Help NewCol History Put Dump Load Dir |
tag.col |
Column tag line default | CloseCol Close New Sort |
tag.wnd |
Window tag line default | Get Put Zerox Close | Undo Redo Wrap Ln Indent Mark Bind |
tag.cmd |
Command/results window tag line default | Close | Undo Redo Wrap Kill Clear Font Scroll Input |
history.limit |
Max entries kept in the command history ring buffer | 500 |
For example, to slim down your tag lines:
tag.app = Help NewCol Dump Load Fonts
tag.wnd = Get Put Close | Undo Redo Wrap Ln
tag.cmd = Close | Kill Clear Scroll
Tag lines remain fully editable at runtime — these settings only control what text they start with when z launches or opens a new window.
Props opens a +Props scratch window with a complete property listing for the app, column, or window — depending on where you run it from. It includes paths, dirty state, scroll settings, font and colour values, LSP status, line count, cursor position, and more. Useful for debugging your setup or copying colour values to tweak elsewhere.
These flags are most useful once you are comfortable with the editor — they let you script your startup layout and automate initial actions from the command line.
z file1.go file2.go # Open two files in one column
z -c file1.go -c file2.go # Open each file in its own column-c creates a new column; files following it are placed into that column.
z -! 'git log --oneline' myproject/-! executes a command in the context of the last-opened file or directory. After the files are loaded, the command runs as if you had typed it in that window.
z -c! 'Hilite go' file1.go file2.go-c! runs a command from the current column's tag line — affecting all windows in that column.
z -a! 'Put' file1.go file2.go-a! runs a command from the app tag line — affecting all columns.
z -l 'func main' main.go # Open main.go and search for "func main"
z -cl 'TODO' src/ # Search for "TODO" across the current column
z -al 'FIXME' . # Search for "FIXME" across all columns-l, -cl, and -al run a regexp search after loading, at window, column, and app scope respectively.
z -r file1.go-r resets: any flags before it are processed, then subsequent arguments are treated as plain file or directory paths. Useful in shell aliases or scripts where the argument list might otherwise be ambiguous.
Flags compose naturally, left to right:
z -c main.go -! 'Hilite go' -c test.go -! 'Hilite go'This opens main.go in one column with Go highlighting, then test.go in a second column, also highlighted. Each -! applies to the most recently opened file.
z lets you place executable scripts in a well-known directory and invoke them from any tag line using a comma prefix — no full path required. They look and feel like built-in commands.
z searches for scripts in this order:
.z/scripts/in the current working directory (project-local — different per project)~/.z/scripts/(global — available in every project)- Any additional directories listed in
~/.z/scripts.conf
The global directory is created automatically on first launch. The project-local directory is just a convention — create .z/scripts/ in your project root and z picks it up automatically.
Use a leading comma to invoke a script:
,Build
,Test --watch
,Deploy staging
,Format
z resolves the script name against the search directories and runs it. If not found, an error dialog shows which directories were searched.
,cmd runs the script once, in the context of where you invoke it:
- From a window tag line → output goes to a
path+Resultswindow - From a column tag line → output goes to a
+Cmdwindow in that column - From the app tag line → output goes to
+Cmdin the rightmost column
,,cmd runs the script once per window in scope:
- From a window tag line → same as
,cmd(the window is the leaf) - From a column tag line → runs on every window in that column
- From the app tag line → runs on every window in every column
,,Format from the app tag line, for example, runs your formatter on every open file simultaneously.
Scripts receive these environment variables from z:
| Variable | Value |
|---|---|
Z_FILE |
File path as written in the tag line (~ and ./ expanded, symlinks not resolved) |
Z_FP |
Canonical absolute path (symlinks fully resolved) |
Z_DIR |
Working directory where the script runs |
Z_SELECTION |
Currently selected text (empty string if nothing selected) |
A script can ignore these entirely, or use them to operate on the current file:
#!/bin/sh
# .z/scripts/Format — format the current file in place
scalafmt "$Z_FILE"#!/bin/sh
# .z/scripts/Test — run tests for the current project
cd "$Z_DIR" && sbt testTo add extra script directories beyond the two defaults, create ~/.z/scripts.conf:
scripts.path = /work/team-scripts:/home/user/bin/z-scripts
Colon-separated, appended after the auto-discovered directories.
Scripts are plain executables — shell scripts, Python scripts, anything. They have no knowledge of z. The same script can be run directly from a terminal. The comma prefix is purely z's way of finding and invoking them; the script itself is just a file.
Plumbing rules are z's extension point for B3 dispatch. Before the editor applies its built-in look/navigate/execute logic, it checks your plumbing rules top-to-bottom. The first rule whose conditions all match wins.
The rule format follows Plan 9 Acme's plumbing model: multi-line blocks with separate condition and action verbs, giving you expressive guards (file existence checks, working-directory context, source window) before committing to an action.
The built-in B3 rules cover files, line numbers, and searches. But real-world output contains patterns the editor cannot know in advance: Go compiler errors (./main.go:42:5:), Java stack frames (Foo.java:99), Rust diagnostics, Jira ticket IDs, custom log formats. Plumbing lets you teach z to act on these without modifying the editor itself.
Rules live in ~/.z/plumbing. The file is optional; if it does not exist, z uses the built-in rules. If it exists, it completely replaces them — copy the built-ins into your file if you want to keep them.
Reload at any time by executing Plumb from any tag line (no restart needed).
Rules are blank-line-separated blocks. Each block is a sequence of condition lines followed by action lines. All conditions must pass; actions are applied in order once they do.
# Comment lines start with #
# Condition lines (all must pass):
data matches <regex> # match the selected text; sets $1..$n from groups
arg isfile <path-template> # expanded path must be an existing file; sets $arg
arg isdir <path-template> # expanded path must be an existing directory; sets $arg
wdir matches <regex> # match the working directory
src is <value> # match the source window's path exactly
type is <value> # always passes (Plan 9 compat)
# Action lines (applied in order):
data set <template> # rewrite the matched data
attr add <key>=<template> # set an attribute (e.g. addr=<line-number>)
attr set <key>=<template> # alias for attr add
plumb to edit # open result in editor (look)
plumb to exec # run result as a shell command
plumb start <cmd-template> # command to run when port is exec
plumb client <program> # ignored (Plan 9 compat)
A block must contain a plumb to action to be valid.
| Variable | Value |
|---|---|
$0 |
Current value of the data field |
$1…$n |
Capture groups from data matches |
$wdir |
Working directory |
$arg |
Absolute path resolved by arg isfile or arg isdir |
$file |
Alias for $arg |
All variables are available in every action template — data set, attr add, and plumb start. $1…$n are frozen at condition-evaluation time and do not change across actions. $0 is live: it reflects the current value of data, so after a data set action, $0 in the next action sees the updated value.
plumb to edit— opens the data as a file or look target. Ifattr add addr=<n>was set, navigation jumps to that line.plumb to exec— runs the command fromplumb startin a+plumbscratch window.
When ~/.z/plumbing does not exist, three blocks are active:
# URL → browser
data matches https?://\S+
plumb to exec
plumb start xdg-open $0
# file:line:col → navigate to line (file must exist)
data matches ^(.+):(\d+):(\d+)$
arg isfile $1
data set $1
attr add addr=$2
plumb to edit
# file:line → navigate to line (file must exist)
data matches ^(.+):(\d+)$
arg isfile $1
data set $1
attr add addr=$2
plumb to edit
The arg isfile guard is what makes these safe: foo:42:7 only triggers file navigation if foo is an existing file in the current working directory. Plain text that happens to match word:number:number is ignored.
# ~/.z/plumbing
# Open URLs in the browser
data matches https?://\S+
plumb to exec
plumb start xdg-open $0
# Jump from Go/Rust compiler errors: ./main.go:42:5: undefined: foo
data matches ^([^:]+\.(go|rs)):(\d+):
arg isfile $1
data set $1
attr add addr=$3
plumb to edit
# Jump from Java/Kotlin stack frames: at com.example.Foo(Foo.java:99)
data matches \((\S+\.java):(\d+)\)
arg isfile $1
data set $1
attr add addr=$2
plumb to edit
# Open Jira tickets in the browser (only in work directories)
wdir matches /work/
data matches [A-Z]+-\d+
plumb to exec
plumb start xdg-open https://jira.example.com/browse/$0
# Generic file:line:col navigation (fallback, no existence check)
data matches ^(.+):(\d+):(\d+)$
data set $1
attr add addr=$2
plumb to edit
The old single-line format is still accepted alongside new-format blocks:
match url /https?:\/\/\S+/ exec xdg-open $0
match filecol /^(.+):(\d+):(\d+)/ look $1:$2
Old-format lines use / as the regex delimiter (literal / must be escaped as \/) and $0 means the full match from the regex (not the full data). New-format blocks are recommended for all new rules.
| Variable | Value |
|---|---|
Z_FILE |
File path as written in the tag line |
Z_FP |
Canonical absolute path |
Z_DIR |
Working directory |
Z_SELECTION |
Currently selected text |
Let us walk through a realistic workflow from scratch — opening a Scala project, using LSP, running tests, navigating errors, and saving the session.
z ~/myapp/src -c ~/myapp/src/testTwo columns: source files on the left, tests on the right.
From the app tag line:
Hilite scala
Theme monokai
Ln
All open windows are now highlighted, themed, and showing line numbers.
Click into a source window. From its tag line:
Lsp ~/myapp
The path ~/myapp is the LSP workspace root — the directory where Metals will find build.sbt and index the project. This is distinct from where your shell commands run (see the next step).
Wait a moment while Metals indexes. Watch the status bar — it will show the LSP status. The +Diagnostics window appears with any issues found.
The windows were opened by path (~/myapp/src and ~/myapp/src/test), so external commands run in those subdirectories by default — not at the project root where build.sbt lives. Fix that before running sbt.
From the app tag line:
Dir ~/myapp
This sets all windows' root to ~/myapp. Now from the test column tag line:
! sbt test
A new window opens in that column with streaming test output. While it runs, <!> appears in the tag. When it finishes, scroll up through the output.
Spot a failure: MainSpec.scala:42: assertion failed. B3 on that text — z opens MainSpec.scala at line 42.
Edit the code. LSP underlines the fix as you type if anything is wrong. Press Ctrl+Space for completion suggestions. When the diagnostics window is clean, Put to save.
X '.*_test\.scala' ! sbt testOnly
Runs sbt testOnly in every test window. Watch all of them refresh simultaneously.
Navigate to a tricky piece of logic. Execute Mark. Move somewhere else. Navigate to a second location. Execute Mark again. Now open +Mark and B3 on entries to jump between them.
From the app tag line:
Dump ~/myapp/session
Tomorrow:
zFrom the app tag line:
Load ~/myapp/session
Everything is back: every window, every scratch buffer, every colour setting. For files with known extensions the highlighting re-activates automatically on the next Get; for files with unrecognised extensions, re-run Hilite manually.
| Gesture | Action |
|---|---|
| B1 click/drag | Cursor, select |
| Shift+B1 | Extend selection |
| Ctrl+B1 | Brace/symbol matching |
| B2 drag | Execute selected text as command |
| B3 click/drag | Look/navigate/execute (smart) |
%text then B3 |
Force text as command |
| Shortcut | Action |
|---|---|
Ctrl+Z / Ctrl+R |
Undo / Redo |
Ctrl+X / Ctrl+C / Ctrl+V |
Cut / Snarf / Paste |
Ctrl+A |
Select all |
Ctrl+Home / Ctrl+End |
Top / Bottom |
Ctrl+Left / Ctrl+Right |
Prev / Next word |
Ctrl+Backspace / Ctrl+Delete |
Delete word left / right |
Ctrl+Enter |
Execute selection as command, or toggle capture mode |
Ctrl+F |
Look on selection, or end capture mode as look |
Ctrl+P |
Fuzzy file picker (Enter opens file from tag line, inserts path from body) |
Ctrl+Space |
LSP completion |
| Command | Scope | Action |
|---|---|---|
Get |
Win | Reload from tag line path |
Get [fname] |
Win | Load fname |
Put |
Win/Col/App | Save file(s) |
New |
Col/App | New empty window |
NewCol |
App | New column |
NewZ [path] |
Win/Col/App | Launch new independent z instance |
Zerox |
Win | Clone window |
Close |
Win/Col/App | Close (prompts if dirty) |
CloseCol |
Col | Close column |
CLOSE |
Win | Force close, no prompt |
Dump [fname] |
App | Save session |
Load [fname] |
App | Restore session |
History |
App | Open +History scratch buffer with timestamped command log |
| Text | B3 action |
|---|---|
path/to/file |
Open file |
:42 |
Go to line 42 |
:/regexp |
Search forward |
file:42 |
Open file at line |
file:/regexp |
Open file, search |
| Command | Action |
|---|---|
Lt / Rt |
Move window/column left/right |
Up / Dn |
Move window up/down |
Sort |
Sort windows alphabetically |
RotateView |
Toggle window/column layout orientation |
| Command | Input | Output |
|---|---|---|
< cmd |
— | Replaces selection (or inserts at caret) |
> cmd |
Selection (or full file) | +Results window |
| cmd |
Selection (or full body) | Replaces selection (or full body) |
! cmd |
— | Replaces window (or new window from col/app) |
X 'pat' cmd |
— | Runs cmd in matching windows |
Y 'pat' cmd |
— | Runs cmd in non-matching windows |
Kill |
— | Terminate running command |
Input |
— | Toggle interactive mode |
| Invocation | Scope | Action |
|---|---|---|
,scriptname [args] |
Win/Col/App | Run script once at invoked level |
,,scriptname [args] |
Col/App | Run script on every window in scope |
Scripts live in .z/scripts/ (project) or ~/.z/scripts/ (global). See Section 13.
| Element | Value |
|---|---|
| Rule file | ~/.z/plumbing (optional) |
| Reload | Plumb command from any tag line |
| Rule format | Blank-line-separated blocks (Plan 9 style) |
| Conditions | data matches, arg isfile, arg isdir, wdir matches, src is |
| Actions | data set, attr add, plumb to edit|exec, plumb start |
| Template vars | $0 full data, $1…$n groups, $wdir, $arg/$file |
| Built-in: URL | Opens https?://… in browser |
| Built-in: file:line(:col) | arg isfile guard → navigate to line (file must exist) |
See Section 14.
| Command | Action |
|---|---|
Wrap |
Toggle line wrap |
Indent |
Toggle auto-indent |
Tab n |
Set tab width |
Mark |
Bookmark current line |
Clean / Dirty |
Toggle dirty flag |
Clear |
Erase window content |
Scroll on/off |
Toggle auto-scroll |
Bind |
Toggle navigate-in-place |
Dir <path> |
Change root (affects relative path resolution and command working dir for relative/scratch windows; does not affect LSP workspace root) |
| Command | Action |
|---|---|
Font [name] [pt] |
Variable-width body font |
FONT [name] [pt] |
Fixed-width body font |
TagFont [name] [pt] |
Tag line font |
Fonts |
List available fonts |
Ln |
Toggle line numbers |
CLine |
Toggle current-line highlight |
Hilite [lang|off] |
Syntax highlighting |
Theme [name] |
Colour theme |
Color(Back|Fore|Caret|SelBack|SelFore) R G B |
Body colours (window level only) |
Color(TBack|TFore|TCaret|TSelBack|TSelFore) R G B |
Tag colour, current level only |
ColorAll(TBack|TFore|TCaret|TSelBack|TSelFore) R G B |
Tag colour, cascades to all children |
| Command | Action |
|---|---|
Lsp [root] |
Start language server |
Lsp off |
Stop language server |
Check |
Refresh diagnostics |
Complete |
Show completion popup |
| Flag | Action |
|---|---|
-c |
New column |
-! 'cmd' |
Run cmd in last window |
-c! 'cmd' |
Run cmd in current column |
-a! 'cmd' |
Run cmd app-wide |
-l 're' |
Search in last window |
-cl 're' |
Search in current column |
-al 're' |
Search app-wide |
-r |
Reset: treat rest as paths |
Any path containing + is a scratch buffer. Never written to disk. Examples: +scratch, ~/myproject+notes, +Results.
| File | Purpose |
|---|---|
~/.z/settings |
Window geometry (auto-saved) and config: tag.app, tag.col, tag.wnd, tag.cmd, history.limit |
~/.z/lsp.conf |
LSP server configuration |
z.dump (default) |
Saved session (Dump/Load) |
~/.z/scripts.conf |
User script directory configuration |
~/.z/scripts/ |
Global user scripts directory |
z is inspired by Plan 9 Acme. The philosophy is simple: text is the interface. Once that lands, everything else follows.