Skip to content

Latest commit

 

History

History
executable file
·
1365 lines (933 loc) · 53.5 KB

File metadata and controls

executable file
·
1365 lines (933 loc) · 53.5 KB

The z Editor Tutorial

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


Table of Contents

  1. Getting Started
  2. The Three Mouse Buttons
  3. Opening, Navigating & Closing Files
  4. Editing Essentials
  5. Running External Commands
  6. Working with Multiple Windows & Columns — including RotateView
  7. Scratch Buffers
  8. Appearance & Fonts
  9. Syntax Highlighting & Themes
  10. Language Server Protocol (LSP)
  11. Session Management
  12. Command-Line Flags (Advanced)
  13. User Scripts
  14. Plumbing Rules
  15. Putting It All Together

1. Getting Started

Installation

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

Then 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

Launching

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 files

Anatomy of the Editor

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

The Tag Line

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

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.


2. The Three Mouse Buttons

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 — Left Button: Select & Place

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 — Middle Button: Execute

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 — Right Button: Look & Act

B3 is the smart button. It does not just execute — it thinks first, working through a priority list:

  1. Plumbing rules → first matching rule in ~/.z/plumbing wins (see Section 14)
  2. Valid file or directory path → opens it in a new window
  3. :n → jumps to line n in the current file
  4. :/regexp → searches forward for regexp from the current position
  5. filename:n → opens filename at line n
  6. filename:/regexp → opens filename, then searches for regexp
  7. Anything else → searches forward for the text in the current window
  8. 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.

Relative Path Style Preservation

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

Forcing a Command with %

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.

Keyboard Capture Mode

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+Enter from a column tag runs the command across all windows in that column.
  • Ctrl+Enter from the app tag runs the command across all columns (which each cascade to their windows).
  • Ctrl+F from a column tag performs a look traversing all windows in that column.
  • Ctrl+F from the app tag performs a look across all columns.

Example — change the font without leaving the keyboard:

  1. Press Ctrl+Enter in any body → capture mode on.
  2. Type Font Hack 16 → text appears, highlighted as you type.
  3. Press Ctrl+Enter → font changes, typed text deleted.

Example — open a file by typing its path:

  1. Press Ctrl+Enter → capture mode on.
  2. Type src/main/z.scala → highlighted.
  3. Press Ctrl+F → file opens, path text deleted from body.

Putting It Together

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.


3. Opening, Navigating & Closing Files

Opening Files

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.

Navigating Within and Across Files

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

Fuzzy File Picker (Ctrl+P)

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.

Saving Files

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.

Closing Files

  • Close — closes the window, prompting if there are unsaved changes
  • CloseCol — closes an entire column and all its windows
  • CLOSE — closes without any confirmation, regardless of dirty state

Warning: CLOSE (all caps) is intentionally destructive. It discards unsaved changes silently.

Bind — Navigate in Place

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.

Dir — Changing the Root Directory

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 with Dir means 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.


4. Editing Essentials

Keyboard Shortcuts

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

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.

Line Behaviour

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.

Bookmarks with Mark

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.

The Dirty Flag

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.


5. Running External Commands

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.

< cmd — Replace Selection or Insert at Caret

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.

> cmd — Send Selection as Input

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.

| cmd — Pipe Through

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.

! cmd — Replace Window Content

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.

Working Directories and Dir

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.

Dir is not the same as the LSP workspace root. They are independent settings. See Section 10 for the distinction.

While a Command Is Running

The tag line shows <!> while an external command is active. To terminate it early, execute Kill from that window's tag line.

Interactive Processes with Input

When you start a long-running process (a REPL, a debugger, a build tool), you can talk to it interactively using Input mode.

  1. Start the process: < python3 or ! bash
  2. Execute Input from the tag line to toggle interactive mode
  3. z watches for prompt lines (lines ending in >, $, %, ?, or # by default)
  4. 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 $.

Batch Commands with X and Y

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

A Note on Quoting

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.

Environment Variables

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.


6. Working with Multiple Windows & Columns

Creating Windows and Columns

  • 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 from sbt run.
  • Zerox — clones the current window into a new window. Both windows show the same file; changes in one are reflected in the other.

Moving Things Around

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.

Rotating the Layout

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.

Sorting

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.


7. Scratch Buffers

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.

System Scratch Buffers

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

Controlling Scroll Behaviour

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: Scroll mode applies to >, |, and ! output only. The < operator replaces the selection (or inserts at the caret) and is unaffected by the scroll setting.


8. Appearance & Fonts

Fonts

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

Line Numbers

Ln toggles line numbers in the body gutter. Off by default.

Current Line Highlight

CLine toggles the highlight on the line containing the caret. On by default.

Colours

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.


9. Syntax Highlighting & Themes

Enabling Highlighting

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

Themes

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

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.

After Loading a Session

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.


10. Language Server Protocol (LSP)

LSP connects z to a language server for the file you are editing, enabling real-time diagnostics, hover documentation, and code completion.

Dir vs. the LSP Project Root

These are two independent settings that are easy to conflate:

  • Dir sets 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.

Starting the Server

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 +Diagnostics window listing all current issues, updated as you type
  • Code completion via Ctrl+Space or the Complete command

Forcing a Diagnostic Refresh

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.

Stopping the Server

Lsp off

This shuts down the language server for that window and clears the diagnostics.

Configuration

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.


11. Session Management

Saving and Restoring Sessions

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.

Customising Default Tag Lines

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.

Inspecting State with Props

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.


12. Command-Line Flags (Advanced)

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.

Opening Files and Columns

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.

Running Commands at Startup

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.

Searching at Startup

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.

Resetting Argument Parsing

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.

Combining Flags

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.


13. User Scripts

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.

Script Directories

z searches for scripts in this order:

  1. .z/scripts/ in the current working directory (project-local — different per project)
  2. ~/.z/scripts/ (global — available in every project)
  3. 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.

Invoking Scripts

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+Results window
  • From a column tag line → output goes to a +Cmd window in that column
  • From the app tag line → output goes to +Cmd in 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.

Environment Variables

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 test

Configuration

To 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 Editor-Agnostic

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.


14. Plumbing Rules

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.

Why Plumbing?

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.

The Plumbing File

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

Rule Format

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.

Template Variables

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.

Ports

  • plumb to edit — opens the data as a file or look target. If attr add addr=<n> was set, navigation jumps to that line.
  • plumb to exec — runs the command from plumb start in a +plumb scratch window.

Built-In Rules

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.

Example Plumbing File

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

Backward Compatibility

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.

Environment Variables for Exec Actions

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

15. Putting It All Together

Let us walk through a realistic workflow from scratch — opening a Scala project, using LSP, running tests, navigating errors, and saving the session.

1. Open the Project

z ~/myapp/src -c ~/myapp/src/test

Two columns: source files on the left, tests on the right.

2. Set Up Syntax Highlighting

From the app tag line:

Hilite scala
Theme monokai
Ln

All open windows are now highlighted, themed, and showing line numbers.

3. Start LSP

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.

4. Set the Working Directory and Run the Tests

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.

5. Fix the Bug

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.

6. Re-run with X

X '.*_test\.scala' ! sbt testOnly

Runs sbt testOnly in every test window. Watch all of them refresh simultaneously.

7. Bookmark Key Locations

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.

8. Save the Session

From the app tag line:

Dump ~/myapp/session

Tomorrow:

z

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


Quick Reference

Mouse

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

Keyboard

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

File Commands

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

Navigation

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

Window Management

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

External Commands

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

User Scripts

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.

Plumbing

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.

Editing

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)

Appearance

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

LSP

Command Action
Lsp [root] Start language server
Lsp off Stop language server
Check Refresh diagnostics
Complete Show completion popup

Command-Line Flags

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

Scratch Buffers

Any path containing + is a scratch buffer. Never written to disk. Examples: +scratch, ~/myproject+notes, +Results.

Session Files

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.