// The guide: pardes day to day, read once in about ten minutes. The // cheatsheet is the index of keys and words; the reference has every // 9P file; scripting has the recipes. #import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs pardes is a screen of columns. Each column holds panes, and each pane is a tag (a line of words) over a body: a file, a terminal, a PDF or an image. Above the columns runs the workspace tag, and each column has a tag of its own. Any text anywhere can be clicked: #btn("B2") runs it, #btn("B3") looks at it. The keys are helix's, in modes. = Modes Each pane has its own mode and keeps it while you are elsewhere: leave a terminal at its `$` and it is still `$` when you come back. The box at the left of a pane's tag shows the mode. #pairs( [normal (box blank)], [keys move and select; an edit acts on the selection. File and PDF panes start here, and so does a terminal from #key("Alt-n").], [insert (`^`)], [keys type. #keys("i", "a", "o") and the rest enter it.], [raw (`$`), terminals only], [keys go to the program. A terminal from #word("Tty"), the shell a bare `pardes` starts with, and a command pane start here.], ) #key("Ctrl-b") switches a terminal between raw and normal; in normal mode the shell's prompts are hidden and its text is a page to move over and copy from. #word("Mode") in the tag steps raw, normal, insert. Esc and Shift-Esc depend on the mode: #pairs( [normal], [#key("Esc"): back to the previous pane (#word("Last")). #key("Shift-Esc"): the same, except in a terminal, where it switches to raw.], [insert], [#key("Esc"): back to normal. #key("Shift-Esc"): the same, except in a terminal, where it switches to raw.], [raw `$`], [#key("Esc"): to the program, except at a shell prompt with nothing typed on it, where it goes back to the previous pane. #key("Shift-Esc"): always back to the previous pane. Either way the terminal stays `$`.], [a PDF], [#key("Esc"): clears the selection and the search highlights. #key("Shift-Esc"): back to the previous pane.], ) In short, #key("Shift-Esc") is as Esc, but always back to the previous pane from raw `$` or a PDF, and into raw from a terminal's normal or insert mode. "A shell prompt with nothing typed on it" is one pardes knows from the prompt marks it injects into bash and fish (and, for other shells, from no program holding the terminal). "The previous pane" is the last other pane on the jump list; with none, the next pane down its column (wrapping), else any other pane. In raw mode every key but #key("Ctrl-b"), Esc and the paste chords goes to the program, so #key("Ctrl-w") and #key("Alt-n") need you out of raw mode first (and #key("Ctrl-w") out of insert mode, where it deletes a word). = The mouse #pairs( btn("B1"), [select; a plain click puts the cursor there and gives the pane the keyboard. In a tag it starts typing at the click, in insert mode. A double click selects the word, the line (at a line's start or end), or up to the matching bracket or quote.], btn("B2"), [execute: a builtin word runs, anything else is a shell line.], btn("B3"), [look: open the file, address, directory or URL, or else find the word's next place.], ) A #btn("B2") or #btn("B3") click with no drag takes the word under it, where a word is the run of letters, digits, non-ASCII and `. - + / : @ _ ~` around the click; a trailing `:` is dropped. So #btn("B3") anywhere on `src/bar.c:12:5:` in a compiler's `src/bar.c:12:5: error` opens `src/bar.c` at line 12, byte column 5, while a click on `error` finds `error`. Drag instead to say exactly what you mean. #key("Enter") and #key("Tab") in normal mode look and execute the selection, or the word under the cursor. The chords are on the cheatsheet. = Tags There are three kinds, and which directory a command runs in depends on which one you click: #tag("Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit") #tag("New Tty Find Grep Joincol Delcol") #tag("Save Tty Collapse Del", path: "/home/me/notes.txt") - The workspace tag and the column tags run commands in the session's directory (where pardes started). A column's own words (#word("New"), #word("Tty"), #word("Delcol")) act on that column; a pane word (#word("Save"), #word("Del")) acts on the column's pane with the keyboard, or its first; from the workspace tag, on the pane with the keyboard. - A pane's tag runs commands in the pane's directory: a file's directory, a terminal's current directory. A tag is text with undo: type a word into it and click it, or delete the defaults. #key(":") moves the keyboard between a body and its tag. A pane tag may wrap onto several lines; column and workspace tags are one line. The path at the start of a pane's tag is computed: typing into it, or clicking it, drafts a new name, #key("Enter") confirms and the next #word("Save") writes there; #key("Esc") cancels. #word("Collapse") folds a pane to its tag. Unsaved text shows on the grip, the box left of the tag. Drag the grip up or down to resize, or onto another column to move the pane there. = The active column and the keyboard Two things are easy to confuse: - *The pane with the keyboard* is where your keys go. - *The active column* is where new panes go. It is the column you last typed in, #btn("B1")-clicked in, dropped a pane into or gave the keyboard to its tag, or the one that got the last new pane. A look does not move it. They usually agree. Here is how they split. You are typing in column 1, so column 1 is active. You click #word("New") in column 2's tag: a scratch pane opens in column 2 and takes the keyboard, and column 2 is now active. In it you #btn("B3") `x.txt`, which is already open in column 1: the keyboard jumps to that pane in column 1, but the active column stays 2, so the next pane a look opens lands in column 2, away from where you are. Type or click in column 1 and it is active again. == Where new panes go #word("Placement") picks the rules: `acme` (the default) or `pardes`. Under `acme`, a new pane goes into the column whose tag asked, else the active column, never into a new column. An empty column it takes whole; #word("New") and 9P's #file("pane/new") take the bottom half of the column's last pane; a pane opened from a pane's text (a look, #word("Tty"), #key("Alt-n")) goes under the pane with the most blank rows, or halves the biggest; a command pane goes to the last column. #word("New") in a pane's tag opens a `+New` scratch named in that pane's directory, in that pane's column; from a column tag, in that column and the session's directory. Every new pane takes the keyboard, except a command pane and a listing (`+Search`, #word("Recent"), #word("Jumplist"), #word("Themes"), `Fonts`), which opens below the pane that asked and leaves it the keyboard, so #keys("n", "N") walk the listing and #key("Tab") runs a row. No pane is made shorter than its tag and two rows; where there is no room the pane is refused. `Placement pardes` instead fills an empty column whose tag asked or has the keyboard, puts a scratch or a shell under the pane that asked, a document beside the last one read (or in a column of its own on a wide screen), and a command pane at the foot of the last column. A column can be empty, as in acme: closing its last pane leaves it, its tag over blank space. #word("Delcol") and #word("Joincol") take columns away. Closing the session's last pane quits pardes. = Looking #btn("B3") (or #key("Enter")) on a path opens it, or goes to the pane that already shows it; `file:12` goes to line 12. The address forms are on the cheatsheet. A directory types `ls` into a terminal idle there, else opens a terminal there. A URL opens in the browser. #word("Find") `name` lists the files below the pane's directory whose names hold it; #word("Grep") `text` lists the lines that contain that text, literally (ASCII case aside, no regular expression), in files under every pane's directory. Both land in `+Search`, a place per row for #btn("B3"). Grep reads at most 256 KiB of a file and stops at 512 hits, Find at 512 names; the walk stops at 20000 files or 100000 entries, 16 deep, and passes over `.git`, `.jj`, `target`, `node_modules`, `.venv`, `__pycache__`, `.zig-cache` and `zig-out`. A cap hit, or a directory it could not open, is said at the end (`cut at 512 hits`, `N files read only in part (first 256 KiB)`, `walk cut at N entries`, `N directories skipped: permission denied`). A search that finds nothing and skipped nothing fails, `Grep: pattern not found`, and leaves the `+Search` as it was. A relative path is looked for where the click was, then where you have been: first in the looking pane's own directory, then in the directory of each pane on the jump list, most recent first. The first that names a file, or a pane open on that path, wins. `./x` and `../x` look only in the pane's own directory. = Command panes #btn("B2") on a line that is no builtin (`make`, `git log`) runs it with the #word("Shell") setting's `-c`, in the directory of the tag or pane it came from. In a terminal at a shell prompt with nothing typed on it, a line from that terminal's own text is typed into its shell. Anywhere else it runs in a command pane, a terminal of its own: #tag("Kill Save Collapse Del", path: "/home/me/src (make) exit 0") Its tag says `running`, then `exit N`. Its directory is the one it started in, and relative looks in it resolve there, whatever the command did with `cd`. The next command for the same directory reuses a command pane that has finished (from a column tag, only one in that column), below what it showed; a pane still running, or one a background job still prints to, is never reused, so a second command meanwhile gets a pane of its own. #word("Kill") stops what pardes started (`Kill make`: those whose line starts with `make`); #word("Exit") quits pardes. = Unsaved panes A pane whose text is unsaved is marked on its grip. #word("Del") on it refuses once: a notice says `1 unsaved pane — Del again to discard` and the pane is listed in `+Unsaved`. The same #word("Del") again, with nothing edited since, discards it. #word("Exit") and #word("Restore") refuse once over unsaved panes the same way; #word("Delcol") refuses once too, but lists them only in its notice and the log, opening no `+Unsaved`. A `+New` scratch under 100 bytes and a command's output never hold anything up. From the keyboard (#key("SPC d")), #word("Del") on a pane with panes above and below it also asks which neighbour takes its rows: #key("k") above, #key("j") below, any other key keeps the pane. A click never asks, and gives the rows to the pane above. = Terminals A terminal is a pane like any other. #tag("Tty+bash Save Mode Filter Collapse Del", path: "/home/me/src") - `Tty+bash` opens another terminal on that shell; a bare #word("Tty") runs the #word("Shell") setting, `$SHELL` when it is executable, else `/bin/sh`. - #word("Save") asks for a path on the pane's notice band and writes the terminal's scrollback there as text; `Save path` writes it at once. - #word("Filter") maps the program's colours through the theme. - A terminal showing kitty graphics (yazi's previews) shows `petscii:on` or `petscii:off` in its tag. #word("Petscii") toggles it: on draws the program's images as glyph art instead of host pixels, off goes back to pixels. A terminal with no images shows the word only while it is on. - The `+New` scratch a bare `pardes` starts under its shell runs its commands in that terminal's current directory, following its `cd`. - A paging command in a terminal (`git log`, `man`) opens its text in a `+Pager` pane: a terminal's `PAGER` and `GIT_PAGER` are `pardes -` unless your environment sets them. `Pager off` keeps your own pager, for terminals started after it. A diff paged into `+Pager` (`git diff`, `git show`, `git log -p`) is drawn and looked at as a diff. - In raw mode Ctrl-V types what you yanked into the program (with nothing yanked, the program gets the key), and Ctrl-Shift-V the desktop clipboard. - A program that tracks the mouse (htop, vim with `mouse=a`) gets #btn("B1")'s clicks and drags and the wheel over its grid; #btn("B2") and #btn("B3") stay pardes's. Hold Shift to swap: #btn("B1", shift: true) selects and Shift-wheel scrolls pardes's scrollback, while #btn("B2", shift: true) and #btn("B3", shift: true) go to the program as its buttons 2 and 3. A full-screen program that does not track the mouse gets the wheel as arrow keys. Tags, grips and gutters stay pardes's. - `Repl python` in a terminal's tag makes #btn("B2") on a `.py` pane send the text to that REPL instead of running it. = Reviewing diffs Open a `.diff` or `.patch`, or run `git diff` (or `git show`, `diff -u`) as a command: once it has finished, output that starts as a diff is shown as one, each hunk coloured in its file's language and its added and removed rows tinted. Then #btn("B3") to jump, with the usual look resolution: - a `diff --git`, `---` or `+++` line opens the file (the new one, or the old one on a `---` line unless the `+++` under it names another); - a `@@ -a,b +c,d @@` line goes to line `c` of the new file; - the `+`, `-` or space in a hunk line's first column goes to that line in the new file (for a removed line, the line now standing where it was); - the code after it is ordinary words. git's `a/` and `b/` prefixes are dropped (and `c/ i/ w/ o/` with `diff.mnemonicPrefix`); a `--no-prefix` diff's paths are kept as written. = `pardes FILE` and `--wait` In a pane's shell, `pardes FILE` opens FILE in this session and returns at once, as acme's `B` does. A FILE not there yet opens an empty pane named for it; #word("Save") creates it, making its directories first. Something the session refuses (a bad name; a name under a directory you may not search or write, `permission denied`) is printed and the command exits 1. `--` ends the options: `pardes -- -name` opens a file named `-name`. `pardes --wait FILE` (`-w`) returns when the pane showing FILE is closed (exit 0) or the session goes away (exit 1), as acme's `E` does. With `$PARDES_9P` set but no pane of its own (an agent's `EDITOR`) it opens FILE in that session and waits there. Set `EDITOR='pardes --wait'` (`GIT_EDITOR` follows it), and `git commit`, `crontab -e` and fish's Alt-E open in a pane and read the file once you close it. Bare `pardes` inside a pane refuses and names `--nested`, which starts a separate session whose shells do not forward to it. = Keys Motions select what they cross, and an edit acts on the selection: #key("w") then #key("d") deletes a word. #key("x") selects lines, #key("v") extends characters, #key(";") collapses to the cursor. #key("s") makes a cursor per regex match inside the selection, and every edit then acts at each. #key("/") is a case-insensitive substring search, one hit a line; the regexes are on #key("s") and #key("S"). #keys("n", "N") step through everything a look would open, across panes. Line end is #key("g l"), and #key("$") is helix's keep-pipe. #key("SPC") starts the leader, #key("SPC ?") lists every path, and #word("Help") lists every key and builtin. #word("Tutor") (#key("SPC h t")) practises them. The language servers run as child processes, one per language when it is on `PATH`: `zls`, `rust-analyzer`, `clangd`, `gopls`, `typescript-language-server` and `pyright-langserver` (`PARDES_LSP_ZIG`, `_RS`, `_C`, `_GO`, `_TS`, `_PY` name another, empty turns one off): #pairs( [#keys("g d", "g D", "g y", "g i", "g r")], [definition, declaration, type, implementation, references; Ctrl-#btn("B1") is a definition too], [#keys("SPC l k", "SPC l r", "SPC l a", "SPC l h")], [hover, rename, code action, select the references], [#keys("SPC l s", "SPC l S", "SPC l d", "SPC l D")], [symbols and diagnostics, of the file and the workspace], [#keys("SPC l c", "SPC l C", "SPC l t", "SPC l T")], [incoming and outgoing calls, supertypes and subtypes], [#keys("] d", "[ d")], [next and previous diagnostic], [#key("=")], [format], [#keys("SPC l i", "SPC l w")], [#word("Lspinfo"): the servers' state; #word("Lspwhy"): why the last query found what it did], ) One answer jumps; several open a list where #key("Enter") on a row goes there. In insert mode #key("Tab") after a `.` lists the candidate declarations and inserts nothing. A format or a rename within the file is one undo step. A rename that reaches other files applies nothing: it opens a `+Search` preview of the edits. So does one the server answers in this file alone while another open file of its language still holds the old name, with a row per such file (zls does this renaming at a declaration; from a use it reaches every file). A server is `not started yet` until its first query. = Sessions ``` pardes --detach=work & a session with no screen of its own pardes --attach=work show it here pardes-gui --attach=work the SDL window can attach too ``` The session owns the panes, shells and files; frontends come and go. #word("Attach") `work` (#key("SPC s a")) switches this window to that session, and #word("Detach") (#key("SPC s D")) leaves it running, shells and all. Bare `--detach` names the session after its pid; bare `--attach` needs exactly one session; a second `--detach=NAME` while NAME runs says so and exits 1. Every attached frontend sees the same screen, at the smallest common size. With none attached, `size C R` on the root ctl sets the screen and messages clear by the clock, and a pane's pty reports its size in pixels at the last frontend's cell size (8×16 before any frontend attached), so programs like yazi still size their images; CSI 14, 16 and 18 t answer the same. A detached session ends when its last pane closes, as any session does. = Config #word("Config") (#key("SPC f c")) opens the startup file, `~/.config/pardes/init` (`$XDG_CONFIG_HOME/pardes/init` when that is absolute; on macOS `~/Library/Application Support/pardes/init`). Each line is one builtin, run at start as if executed; `#` starts a comment, and a line that fails is skipped silently. Text that is no builtin does not run as a shell command. ``` Theme atelier Shell zsh Placement pardes ``` #word("DumpConfig") opens every live setting as the line that sets it, so it pastes back into `init` as is. The settings are listed with the root ctl in the reference (#doc("fs", section: "settings")). Keys are compile-time, in `src/config.zig`. #word("Dump") writes the workspace to `pardes--