Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Features

This file lists what a ductus user can do now. CHANGELOG.md lists the commits of each release.

Command line

  • dk --help or dk -h lists the commands of dk: completion, default-config, diff, grammar, health, help and trust. dk <command> --help and dk help <command> say what a command does.
  • dk --version or dk -v prints dk version <version>, such as dk version 0.62.0.
  • dk completion bash, fish, powershell or zsh prints a completion script for that shell.
  • -d and --debug write debug lines and timings to dk.log in the state directory, $XDG_STATE_HOME/ductus or ~/.local/state/ductus, while dk runs. --log <file> names another file, and turns on the debug log also with no --debug. dk adds to the end of the file, and writes nothing to the terminal. With no --debug and no --log, dk writes no log.
    • Each line has a time, the name of the event, and its values, such as msg=key cmd=move-visual-line-down took=1.2ms.
    • dk times the start step by step, each key from its read to the end of the draw after it, each draw, and each save.
    • Each guest call of a Plugin has a line with the name of the Plugin, the export, and the time, and a call that fails says if its deadline passed, the guest trapped, or a host function panicked (A7). A Plugin load and unload write a line too. A user Plugin has the name of its file without .wasm.
    • Each request to a language server has a line with the server, the method, and the time from the send to the reply. An error reply shows its code, and never its message. A server start, up to its reply to initialize, and a server exit write a line too.
    • Each grammar load and each parse has a line with the name of the grammar and the time, and a parse has the size of the text.
    • Each typed command has a line with its name in the command table and the time. A command line that does not expand says so, and a name that is no command shows as outcome=unknown, with no name.
    • A panic has one error line with the panic value, the stack, and the name of the crash file.
    • A line holds names, paths, sizes, and durations, and never the text of a Document, a prompt line, or a search (A4). A key shows only as the command that its binding ran, so a key that types into the prompt or a search shows no name. The error line of a panic is the one exception: it holds the panic value, and that value can quote the text of a Document.
    • The log is 0600 and the state directory 0700, also when they were there before. A link or a file that is not regular at the log path is an error, and dk writes nothing through it (A4).
    • A name with a control character or a bidi mark shows quoted, so cat of the log cannot take over the terminal (A5).
    • dk stops at 16 MiB of log in a run, with one last line that says so (A2).
    • When a write of the log fails, the last row says so once, and dk goes on with no log.
  • A file whose name is a command, such as trust, opens with dk ./trust or dk -- trust. -- also opens a file whose name starts with a dash.
  • dk a.txt b.txt c.txt opens each file as a Document, in the order of the names, and shows the first. The others are in the Document list, for :bn and space b, and open with no View, so the jumplist gets no jump into them. dk takes each name as the shell gives it, so a quoted '~/x' is the file ~/x under the working directory. A file named twice, or through a link, opens one Document (A6). When one of the files does not open, such as a FIFO or a device, dk stops before it draws, with the error on stderr.
  • dk <directory>, or a link to a directory, opens [scratch] and the file picker at that directory. The working directory, the workspace and its .ductus stay those of the shell, and only the picker walks the directory (A9). A directory with any other name is an error, and so are two directories.
  • --tabs or -p opens each file in a tab page of its own, in the order of the names, and the first tab page is the current one.
  • --split-right or -O opens each file in a View to the right of the one before, as split-view-right does, and --split-below or -o opens each in a View below, as split-view-below does. The first View has the focus. When a split does not fit, the last row says not enough room for a split, and the files with no View stay in the Document list.
  • The three flags exclude each other, and a flag with a directory is an error. A flag with no file opens [scratch] and does nothing else. They are flags of dk only, so dk diff --tabs a b is an error.
  • Each error quotes the name, so a name with terminal escapes never prints raw (A5).
  • A usage error, such as a flag that dk does not have, prints dk: <error> on stderr and exits 1, as any other error does. The flags -d for a diff, --diff, --trust, --grammar and --default-config are commands now, with no alias.

Commands

  • : opens the prompt on the last row. Enter runs the command, and Esc closes the prompt.
  • Up and C-p go to an older command line, and Down and C-n to a newer one, with the cursor at the end, as in Helix. Enter on an empty prompt runs the newest command line again, and the empty prompt shows that line in the theme scope ui.text.inactive. dk keeps the last 100 command lines of the session, a line typed twice in a row once, and writes none of them to disk (A2, A4). Register : holds these lines, the newest first, so C-r : on a prompt inserts the newest command line, ":p pastes it, and the " box shows it on the row of :.
  • The prompt line takes the Helix prompt keys, and each step is one Grapheme:
    • Backspace and C-h delete before the cursor, and on an empty line they close the prompt.
    • C-r and a character insert the first value of that register at the cursor, so C-r " inserts the yanked text. This works on each prompt, such as : and /, and the search of / updates as for a typed key. In the query of a picker in Insert mode it works too, and the rows filter as for a typed key, such as space f, i, and C-r a. The box shows over the picker, and Esc after C-r keeps the picker open. A box titled Insert register lists the registers as the " box does, and shows a control as a glyph (A5). An empty or unknown register inserts nothing, and so do Esc and each other key that is not a character, and the prompt stays open. One line ending at the end of the value, LF or CR LF, does not go in, so :o, a space, and C-r " after a line yank give a clean path. Each other line ending goes in, and the prompt shows LF as ^J and CR LF as �. A value that would make the line over 64 KiB inserts nothing and says so, as a paste does (A2).
    • C-u deletes to the start, and C-k deletes to the end.
    • C-w and A-backspace delete the word before the cursor, up to a space or a /.
    • Delete and C-d delete under the cursor.
    • Home and C-a move to the start, End and C-e to the end.
    • Left and C-b, Right and C-f move the cursor.
    • Tab completes the full name of a command, never an alias, as Helix does. An alias still runs its command: :x and Tab gives no x, but :x and Enter runs write-quit. It matches fuzzily, as Helix does: the typed characters must come in order in a name, so :wq finds write-quit. A typed capital letter makes the match case sensitive, and e also matches é. When more names match, Tab and S-Tab step through them best first, with ties in the order of :help, and the row above the prompt lists them, a page at a time. On that row the characters that the typed part matches show in the theme scope special and bold, as in the Helix picker. While the first word of the line is a name or an alias of a command, a box above that row, or above the prompt, shows what the command does. When the command has aliases, a second line lists them: :o shows Aliases: o, edit, e. The box stays while you type the args, and :op shows none. It draws in the theme scope ui.help with a border, at most 90 columns wide, and a long text wraps in it. It shows only the rows that fit above the prompt. After :open, :split-view-right, :split-view-below and their short names, Tab completes the file path one directory at a time. A directory, or a symlink to one, shows with a trailing /. A blank in a name gets a backslash before it, so the name stays one arg, as :o my\ dir/a\ b.txt, and Tab reads such a path back. Tab completes the whole text after the command as one path, so it does not complete the second file of :open a.txt b. Names that start with . show only when the typed part starts with ., and ~/ reads the home directory. Tab only lists names: it opens no file.
  • Text typed after a move goes in at the cursor. A line wider than the screen wraps onto more rows, up to half the screen, and the rows that show hold the cursor.
  • The text after the command name splits into args at spaces and tabs, as in Helix, in the prompt and in a [keys] binding. '...' and `...` keep their text as one arg, as it is, and two quotes in a row stand for one, so :o 'a b.txt' opens one file named a b.txt. "..." also keeps its spaces, and its expansions run, so :echo '%{cursor_line}' shows %{cursor_line} and :echo "line %{cursor_line}" shows line 1. A backslash before a space keeps the space in the arg, so :o a\ b.txt opens the same file. At the start of an arg, a backslash before a quote or a % makes it text. The next arg starts right after a closing quote, so 'a'b is two args.
  • An arg that starts with % is an expansion, as in Helix: %{name}, %u{...}, %sh{...} or %reg{...}, with (), [], <>, '', "" or || also good in place of {}. The expansions run when Enter runs the line, and also in the text of "...", where %% stands for one %. In a word that does not start with %, a % is text, as in Helix. An unknown kind or variable, a % with no delimiter after it, and an expansion with no end each stop the command before any shell runs, and the last row names the problem with the expansion quoted, such as echo: unknown variable "nope" (A5). dk never reads a value again for expansions, so a file named %sh{touch x} gives its name (A9).
    • %reg{r} is the values of register r with nothing between them, or empty text.
    • %sh{...} is the stdout of [editor] shell with the text, after the expansions in the text, without one line ending at its end, as in Helix, so :echo %sh{echo {a}} shows {a}. stdin is /dev/null, and stderr and the exit status do not count. It runs as :sh runs: in the background in a new session, with no timeout, and the spinner element spins. :pick-job lists the typed command line, a pick stops the run, and the typed command does not run. A quit stops it too. The typed command runs when its expansions end, on the View and the Document that have the focus then, so :w %sh{...} after a switch saves the Document you switched to. An output over 64 KiB stops the command with a note, such as echo: "%sh{cat big}": output over 64 KiB (A2). A %sh{...} before it in the line has run by then.
    • %u{hex} is the char of a Unicode code point, so :echo %u{25CF} shows ●.
    • %{name} is the value of a variable: buffer_name, the title of the Document, such as [scratch] or [help]; current_working_directory; cursor_column and cursor_line, of the primary cursor from 1, with the column in Graphemes; file_path_absolute, which is the working directory for a Document with no path; language, or text; line_ending; selection, the text of the primary Range; selection_line_end and selection_line_start, from 1; and workspace_directory, the directory of the .ductus of the working directory, or else the working directory.
  • In the line of :run-shell-command, :sh, :!, :pipe, :pipe-to, :insert-output and :append-output, the quotes stay as typed and the expansions run, as in Helix. dk puts each value of %{...} and %reg{...} in as one word of text, by the POSIX sh quote state at its place, where Helix puts it in as it is (A9). Outside quotes the value goes in '...' with each ' as '\'', inside '...' each ' becomes '\'', and inside "..." a \ goes before each $, `, " and \. So with a file named a;touch pwned, :sh echo %{buffer_name} shows the name and runs no touch. In quotes, a value with no sh syntax gives the line of Helix, such as echo "README.md:1" for :sh echo "%{buffer_name}:%{cursor_column}". Outside quotes the line differs, and sh gets the same words: :sh echo %{buffer_name} runs echo 'README.md'. A value with a space stays one word, so with a b selected, :sh rm %{selection} removes the file a b, where Helix removes a and b. In each other place the command does not run, and the last row says where, such as run-shell-command: cannot quote "%{buffer_name}" inside $(...). A %sh{...} before that place in the line has run by then. These places are inside $(...), (...), backticks, ${...} or $'...', also within "...", right after \ or $ outside '...', in a comment, after <<, and after a ) that closes nothing. dk quotes for POSIX sh also when [editor] shell is not POSIX, such as fish. The text of a %sh{...} is a shell line too, and its values go in by the same rules. The output of %sh{...} and the value of %u{...} go in as they are, as in Helix. So take care: :sh echo %sh{echo %{buffer_name}} puts a name such as a;touch pwned into the outer line as it is, and the shell runs touch.
  • :show-about opens a Document named [about] in a new View above the focused View, as :help opens [help]. It lists the version as :show-version shows it, the project URL, the license with its copyright line, the Go version and the versions of the wasmtime-go and go-tree-sitter modules of the build, the runtime directory, and the path of each user config file that dk reads, such as config.toml or config.yaml, or none. A replaced module shows the path that go.mod requires with the version of the replacement, such as the fork of wasmtime-go. When the trust store holds the .ductus of the workspace, it also names that .ductus. It draws each control character of a path as a glyph (A5). [about] has readonly on and modifiable off, a second :show-about focuses its View, and it closes with its last View, as [help] does. No save writes it: :w says [about] is no file, so dk saves nothing (A6).
  • :reload-config reads the config and its theme again, and loads the plugins again. Helix gives the command the same name, with no alias.
  • :diff-base shows the vcs base of the file and the file in a diff session (see Views).
  • :diff-disk shows the file on disk and the file in a diff session (see Views). [editor] gutter-diff makes the diff gutter compare with the file on disk (see Config).
  • :diff-files, :diffs and :end-diff compare two files in a diff session (see Views).
  • :echo shows its args on the last row with one space between two, as in Helix, so :echo %{cursor_line} shows the line of the cursor.
  • dk default-config prints a config.toml with every key that dk reads at its default, and exits. Each key has a one-line comment, and [keys] and [plugins.<name>] exec are commented-out examples. It takes no other argument, and it runs no plugin and loads no workspace. dk default-config > config.toml gives a config that changes nothing. Helix has no such command.
  • dk health prints what dk finds for each language, and exits 0. First come the config file, the languages file, the runtime directories, the .ductus of the workspace with trusted or not trusted, and the clipboard provider, such as x-sel (xsel). A file that does not exist shows default, and a runtime directory that does not exist, is empty, or is a link gets a line of its own. Then a table has a row for each language whose grammar, or name when it names no grammar, use-grammars picks, sorted, with the columns Language servers, Formatter, Grammar, Highlight, Textobject, Indent, Tags, Rainbow, Context and Comment tokens, and a note on how to see each language. So use-grammars = { only = ["proto"] } shows protobuf. With no use-grammars, dk grammar builds every grammar, so the table has each language and no note. A server or a formatter shows ✓ <name> when dk finds its command on PATH, ✘ <name> when not, and None for none, and a second server gets a row of its own. Grammar shows ✓ when the built grammar file is there, and a query column shows ✓ when dk reads the query, shipped or in the config or runtime directory. Each column is as wide as its widest cell. dk health all shows each language, languages and all-languages show only the table, filtered or not, and clipboard shows only the clipboard provider. dk health go shows the full path of each server and of the formatter, or '<command>' not found in $PATH, then Tree-sitter parser: ✓, None or the load error quoted, and a line for each query and for the comment tokens. An unknown language gives Language '<name>' not found and the languages that start with its first letter. dk reads the .ductus only when it is trusted (A9), runs no server, formatter or clipboard program to make the report, and shows each control character of a name, command or path as a glyph (A5). The report has no colors. :check-health opens the same text in a Document named [health], and :check-health go that of dk health go. [health] has readonly on and modifiable off, as [help], and a save of it says [health] is no file, so dk saves nothing (A6). A second :check-health puts its text in the open [health] and focuses it, and [health] closes with its last View.
  • :help opens the list of the keys and the commands as a Document named [help], in a new View above the focused View, as Vim’s :help does. Helix has no :help. After the keys of the modes, it lists the keys of the command line, by their Helix names, such as C-w, C-u, tab and up, C-p, each with what it does, as Vim’s help lists them under “Command-line editing”. [help] has readonly on and modifiable off, so each Normal and Select mode key moves and selects in it, such as gg, G and x, and each edit gives Cannot make changes, 'modifiable' is off. A second :help focuses the View of [help], with the keys as they are then, and opens no second one. :help <word> opens or focuses [help] in the same way, puts the cursor on the entry of the word, and scrolls the View to show it. The word is a command name or alias, with or without its :, such as write, w or :w. When no command has that name, the word is a key as [help] shows it, such as C-w, C-p or g g, and gg names g g too. A key that [help] lists more than once goes to its first line. A word with no entry opens nothing and says No help for <word>, with each control character of the word drawn as a glyph (A5). :q closes the View of [help] and shows the other View as it was. When the last View of [help] closes, or goes to another file, such as with :bn, :open or C-o, [help] closes too, so :bn and space b do not show it, and the next :help opens it again. [help] has no path, no language, and no language server, it never counts as unsaved, and no save writes it: :w, :w <path> and :w! say [help] is no file, so dk saves nothing.
  • :goto-line <n>, :g <n>, or :<n>, such as :20, goes to line n, as in Helix, and in Select mode every Range extends to it. A line past the end goes to the last line, and :0 does nothing. A word that is not a line number is an error on the last row, and the word shows quoted (A5). dk moves on Enter: it does not move while you type the number, as Helix does.
  • A command that dk does not have gives no such command on the last row, with its name quoted (A5).
  • :split-view-below, :hs, :split-view-right, and :vs split the View (see Views).
  • :pick-job opens a picker of the shell commands that run, oldest first, such as 1m15s sleep 100. Each row shows the time that the command has run and its line, which shows as glyphs (A5), and the query matches the line. A pick stops that command with its children, as a quit does, and the last row says stopped "<command>". A stopped command shows no error when it ends, and a stopped :pipe, :insert-output or :append-output changes nothing. space ! in Normal or Select mode opens it too, and the Space box lists it. Helix has no such command.
  • :show-messages opens the messages that the last row showed as a Document named [messages], in a new View above the focused View, as :help opens [help]. Helix has no such command: its :log-open opens its log file. Each message is one line, oldest first, with the time, the kind and the text, such as 14:43:09 error no such command: "foo". A message with more than one line goes on the lines below it, indented, and the cursor goes to the last line. Each note has a line of its own, also when the last row joins the notes and cuts them off, as at start. A command that dk does not have, args that do not expand, a save that fails or cannot start, a change while modifiable is off, and a language server that exits are errors. So is each error that a command shows itself, such as No more matches or Diff is not available in the current buffer. Emergency mode is an error too, and its line has the whole error, with the backtrace of a trap. A note, such as a key in config.toml that dk does not read or too many ranges, is a warning, and a server message is an error or a warning when its type says so. Each other message is info, such as wrote "a.txt" or Wrapped around document. dk keeps the last 1000 messages of the run, and the first 4 KiB of each, in memory only, so no message goes to the log or to disk (A2, A4). The text shows as glyphs, so an escape in a server message cannot change the screen (A5). [messages] has readonly on and modifiable off, and no path, no language and no language server, as [help] has, and no save writes it: :w says [messages] is no file, so dk saves nothing (A6). A second :show-messages focuses the View of [messages], with the messages as they are then, and opens no second one. When the last View of [messages] closes, or goes to another file, [messages] closes too.
  • :show-options opens a Document named [options] in the View, with the text of dk default-config and the toml language, so it highlights when the toml grammar is built. It is a scratch Document: :w <path> saves it like any other, and :bc or :q closes it. Helix has no such command, so it follows Vim’s :show-options. The file you had stays open.
  • :pipe or :|, :pipe-to, :insert-output and :append-output run the rest of the line with the shell of :sh, with Helix’s names. :pipe runs the command once for each selection, with its text as stdin, and puts the output in its place, such as :pipe sort. :pipe-to makes the same runs, and changes nothing. :insert-output and :append-output run the command once with no stdin, and put its output before or after each selection. When the text of the selection has no line ending at its end, the output loses one at its end, as in Helix. For :insert-output and :append-output, that is the text of the first selection. Each selection then covers its output, and the change is one undo step. A command that fails changes nothing, and the last row shows its stderr, or Shell command failed: status N. The command runs in the background, as :sh does, so keys still edit while it runs. Its output applies to the text that the run saw: after an edit of the file, dk changes nothing and says Edit refused: document has changed (A1). When the file is no longer in the focused View, the output still goes into it. An output over 16 MiB, or one that makes the change over 16 MiB, stops the command, changes nothing, and says the output is over 16 MiB, so dk changes nothing (A2).
  • |, A-|, ! and A-! in Normal and Select mode open the prompt with pipe, pipe-to, insert-output or append-output and a space typed, as Helix’s shell keys. So |, then sort and Enter, sorts the text of each selection. Enter runs the line as if you typed all of it after :, with the same completion and history, and Esc closes the prompt with no change. Enter on the line as the key typed it runs nothing, so a key that a .ductus binds to one of them runs no command that you did not type (A9). In a terminal without the kitty keyboard protocol or Win32 input mode, a fast Esc and | or ! reach dk as A-| or A-!, so in Normal and Select mode they open the prompt with pipe-to or append-output. Insert mode binds neither, so there they open it with pipe or insert-output, as Esc and then the key do.
  • :print-document sends the text to [editor] print-command on stdin (see Config), so with the default enscript it goes to the default printer. The text is that of each Range wider than one character, in the order of the Ranges, each on its own line, or the whole Document when no Range is. :print-document <file> writes a PDF to the file, with a leading ~ as the home directory. dk runs print-command with -o - after its args, so that it writes PostScript to stdout, and pipes that into ps2pdf - -. dk writes the PDF as :w writes a file, so it takes the place of the file only when both commands succeed, and a failure or a stop leaves the file as it was (A6). In the cases where :w writes in place, a file with more than one hard link and a dir that refuses the temp file or the rename, a failure can leave a part of a PDF. The command runs in the background, as :sh does, and :pick-job stops it. At the end, the last row says Printed N lines, or Wrote "<file>" with the name quoted (A5). A command that fails changes nothing, and the last row shows its stderr, or Print command failed: status N when stderr is empty. A text over 16 MiB starts no command, and the last row says the text is over 16 MiB, so dk prints nothing (A2). It has no default key, and the [keys] of your config can bind it. Helix has no such command.
  • :quit or :q closes the View, as in Helix, and at the last View it quits. The file of a closed View stays open. At the last View it refuses while any open file has unsaved changes, and the last row says how many, such as 2 files are unsaved. When the file in the View has no unsaved changes, the View shows the first file that has them. An undo back to the saved text leaves no unsaved changes. While a quit waits, the last row says what for: quitting: saving "<file>" for a save, quitting: waiting for the vcs query, quitting: waiting for the changed file list, quitting: waiting for the search, quitting: stopping "<command>" with each shell command that runs, or quitting: stopping gopls with the name of each language server that runs. This is so for each way to quit, :wq, :x, and a stop signal too, and a quit with nothing to wait for draws nothing new. The file name shows quoted, as in wrote "<file>", and a server name with a space or a control character shows quoted, as in the other server notes (A5).
  • :quit! or :q! closes the View, and at the last View it quits and drops the unsaved changes.
  • :quit-all or :qa quits from any View, but not while any open file has unsaved changes, as :q refuses. :quit-all! or :qa! quits and drops them.
  • :refresh-vcs, and the key command refresh-vcs, ask the vcs plugin again for the change id and the base text of the file of each View, so a jj command in another pane shows. The focused View comes first, then the others, one query at a time. refresh-vcs has no default key, and [keys] can bind it. Helix has neither.
  • :reload, :rl, or :e! reads the file of the Document again and drops the unsaved changes, as Helix’s :reload and Vim’s :e!. The reload is one undo step, so u gives the text back, and a file with the same text adds no step. After it the Document has no unsaved changes, and the next :w does not say that another process changed the file. The read-only flag, the line ending, and the indent come from the file again, as at an open. A Document with no path does nothing. A missing file gives can't find file to reload from "<file>", with the name quoted (A5), and the text stays. A Document with modifiable off does not reload, and the Document of a running save says a save is running (A6). The command takes no argument.
  • :reload-all or :rla reloads each open file as :reload does, also a file that no View shows. A file that does not reload gives its error, and the next file reloads.
  • :reset-diff-change or :diffg puts back the base text of each change under a Range, as one undo step. A change counts when its gutter sign is on a line of a Range. Helix also takes a change that ends on the line above a Range, and dk does not. In a diff session it takes the lines of the other file instead (see Views).
  • :run-shell-command, :sh or :! runs the rest of the line with sh -c, or the [editor] shell of config.toml (see Config), as in Helix, such as :sh jj new. It runs in the background, so keys still edit while it runs, and it has no timeout, as in Helix: :pick-job stops it. A nonblank output shows in a popup, which has a frame by default (see popup-border in Config), and the last row says Command run. The output is stdout, or stderr when the command prints to it. A command that fails shows its stderr on the last row, or Shell command failed: status N when stderr is empty. :sh with nothing after it gives expected at least 1 argument, got 0. The command gets no stdin, and it runs in a new session with no terminal, so it cannot draw over dk or read its keys (A5). So :sh echo x >/dev/tty fails with No such device or address, and a command that needs a terminal fails too, such as jj describe with no -m. The popup draws the colors that the output prints, as a terminal does: an SGR sequence sets the style of the text after it, and does not show. It takes the 16 and the 256 colors of the terminal, RGB colors, bold, dim, italic, underline, reverse, and their resets, for the text and its background. 0, an empty SGR, 39 and 49 go back to the style of the popup. Most programs print colors only to a terminal, and the command has none, so you ask for them with a flag of the program, such as :sh jj log --color=always. Each other escape, an SGR with any other code, and an SGR with more than 32 parameters show as glyphs, so :sh printf '\033[2J' shows ^[[2J, and the screen stays (A2, A5). :insert-output, :append-output and :pipe put each byte of the output into the Document, escapes too. dk keeps at most 64 KiB of stdout and of stderr, and reads the rest, so the command runs to its end (A2). The popup keeps at most 64 KiB of the output, escapes too, then …, and a cut never shows a part of an SGR. Its size counts only the text that shows. When the command ends, dk asks the vcs plugin again for the file of each View, as :refresh-vcs does, so the change id and the diff gutter show what jj new did. A quit stops each command that runs, with its children (A2).
  • :set-option or :set with a key and a value changes an [editor] key of config.toml until dk quits, as in Helix, such as :set line-number relative. A dot names a key of a table, as in :set lsp.auto-signature-help false or :set statusline.left ["mode"], and the other keys of the table stay. The key takes any letter case. The value is JSON, as in Helix, but a key whose value is a string, such as line-number, takes the text as it is. For a true or false key, :set <key> sets it to true and :set no<key> sets it to false, as in Vim. :set <key>=<value> sets the value as :set <key> <value> does, as in Vim, such as :set line-number=relative. The key ends at the first = or space, so a value can hold =. :set <key>? shows the value on the last row, such as line-number = absolute, as in Vim. :set <key> with no value does the same for a key that is not true or false. Each form of :set that sets a value then shows the key and its new value on the last row, as :set <key>? shows them, such as auto-completion = false after :set noauto-completion, also when the value was the same before. It takes each key that :set takes, filetype too, and a key that only config.toml sets gives the note of a :set of it. :set with no key shows an info box with a row <key> = <value> for each key whose value differs from the default of dk default-config, sorted by key, whatever set it: config.toml, a trusted .ductus, :set or :set-document-option. With no such key, the last row says every option is at its default. The box shows until the next key, also when auto-info is false. The box and the last row show the values of the focused Document as the text that :set takes, with a control as a glyph (A5). They show at most 1 KiB of a value, and a row wider than the box ends in … (A2). An unknown key gives Unknown key `<key>` , a value that is not JSON, or null, gives Could not parse field `<value>` , and a value that config.toml would not take gives its note, such as mouse: 5 is not true or false, so the setting stays as it was. The setting then stays as it was, so the note names no default, and the last row shows the key and the value as glyphs (A5). :set writes no file. :reload-config and :trust read the config again, and that ends each :set value. :set undofile-exclude does nothing and says that only config.toml sets it (A4). After :set or :set-document-option and a space, Tab completes the key, fuzzily, as Tab after : does, as in Helix: :set auto-sig gives :set lsp.auto-signature-help. After a key and a space, Tab lists the current value, of the focused Document for :set-document-option, and then the other fixed values of the key, and Tab and S-Tab move through them, as in Vim: :set line-number, a space and Tab list absolute and relative. The keys with fixed values are each true or false key, clipboard-provider (the built-in names, never custom), filetype, gutter-diff, indent-heuristic, inline-blame.show, line-number, picker-start-mode, popup-border and tabline. A key with no fixed values, such as picker-size, gets its current value only. After a typed part, Tab completes the fixed values that match it, such as relative after :set line-number r. A key that :set does not take gets no completion. :set takes only the keys that dk default-config lists, so a whole table such as statusline is an unknown key, and Tab lists only those keys.
  • :set-document-option or :setl takes the keys, the values and the forms of :set, and sets the value for the Document of the focused View only, as in Vim. The last row then shows the key and the new value of the focused Document, as after :set. Each place that reads a setting reads the value of its Document: a View draws its gutter and status line with the values of its Document, and the mouse follows the focused Document. :set also changes the value of the focused Document when it has its own, as in Vim. Another Document keeps its own values, and takes each other :set value at once, as in Helix. Vim gives a buffer a copy of the values when it opens. :reload-config and :trust end the :set-document-option values too.
  • :set filetype markdown and :set-document-option filetype markdown give the Document of the focused View the language markdown of languages.toml, as Vim’s ‘filetype’. The grammar, the highlights, the injections, the indent rules, the comment tokens and the formatter of that language then apply, and dk detects the indent and the line ending of the text again. The language servers of the old language close the Document, and those of the new language open it, but only in a trusted workspace (A9). :set filetype text gives the Document no language, and :set filetype with no value shows its language, such as filetype = markdown, or filetype = text. A name shows the new language on the last row in the same way, such as filetype = markdown after :set filetype markdown. A name that languages.toml does not have gives invalid language id: `<name>` , with the name as glyphs, and the language stays (A5). After :set filetype and a space, Tab lists the language of the Document first, and then the other language names and text. The language stays until the Document closes, so :reload-config, :trust and :w <path> keep it. config.toml and a .ductus do not take filetype: there the path picks the language.
  • :show-directory or :pwd shows the working directory on the last row, as in Helix, such as Current working directory is /tmp/x. When that path no longer leads to a directory, a space and (deleted) follow the text, and the row draws as an error. dk shows the directory where it started as the shell names it in $PWD, with its links, as in Helix, and draws an escape in the name as a glyph (A5). The workspace and trust still use the real path (A9).
  • C-z in Normal and Select mode stops dk and gives the terminal back to the shell, as Helix’s suspend, and fg brings dk back as it was. dk stops its whole process group, so a parent that waits for dk, such as jj describe, stops too. A kill -TSTP stops dk in the same way. As the session leader, dk has nothing to stop to, and C-z does nothing. A running save ends first (A6). When the stop fails, dk goes on and the last row says why (A5). When dk cannot take the terminal back after 10 tries, it stops again, so the next fg tries again.
  • :toggle-option or :toggle with a key flips a true or false key, as in Helix, and the last row shows the new value, such as mouse = false. It changes the key as :set does, so :set shows the key and :reload-config ends the value, and a key of the Document, such as readonly, flips too. A key that is not true or false gives <key> is not true or false, and the setting stays as it was. dk takes no list of values to step through. An unknown key gives Unknown key `<key>` , and a key that only config.toml sets gives the note of a :set of it. After :toggle-option and a space, Tab completes only the keys that are true or false, fuzzily.
  • :trust trusts the .ductus of the workspace as it is now, and loads it (see Workspaces).
  • :show-version shows dk version <version> on the last row, the same text that dk --version prints.

Config

  • A key of config.toml or languages.toml that dk does not read gives a note on the last row, and the other keys of the file still load. One note for each file counts the keys and names the first 3, each cut after 40 characters (A2). A key in a table of [[language]] or [[grammar]] names its table by its name, or else by its place from 1, such as language[go].scope and grammar[2].foo. The name of a table shows as a TOML key part, quoted when it needs that and with its C0 controls escaped, and the last row draws the rest as glyphs (A5). A key that belongs in the other file names it: use-grammars in config.toml shows as use-grammars (a key of languages.toml). The files of a trusted .ductus give the same notes, as .ductus/config.toml and .ductus/languages.toml (A9). The note does not look inside a value that dk takes whole, such as [editor.gutters], so a key there gets only the checks of that value (Q126.1).
  • [editor] clipboard-provider picks how + and * reach the system clipboard and the primary selection: pasteboard, wayland, x-clip, x-sel, win32-yank, tmux, termux, termcode, none, or a custom table. With no value, dk detects the provider at the first use, and :reload-config detects it again. On Linux it takes the first that fits of termux, tmux when TMUX is set, termcode in WezTerm, Wayland when WAYLAND_DISPLAY is set, xclip and then xsel when DISPLAY is set, and win32yank, and else termcode. On macOS it takes tmux, then pasteboard, and else termcode. dk runs a provider program with no shell, in a session of its own, with stderr thrown away, and stops it after 2 s (A2). A provider with no program for the primary selection, such as tmux, reads * as empty and writes it nowhere. termcode sends the text to the terminal as OSC 52, base64 encoded, so no byte of the text reaches the terminal raw (A5). A custom table names the program that reads the clipboard, yank, and the one that writes it, paste, each with a command and its args, and yank-primary and paste-primary do the same for *. So [editor.clipboard-provider.custom] with yank = { command = "xsel", args = ["-o", "-b"] } and paste = { command = "xsel", args = ["-i", "-b"] } uses xsel. A command is a program name, which PATH finds, or an absolute path, so a cloned repo cannot place it (A9). Another value gives a note on the last row, and dk detects a provider. :set and :set-document-option take a built-in name, such as :set clipboard-provider tmux, and only the user config sets a custom table (A9). A .ductus that sets it gives the warning .ductus cannot set clipboard-provider, and nothing changes (A9).
  • [editor] default-yank-register is the register of y, d, c, p, P and R when " picks none, " by default, as in Helix. So with default-yank-register = "a", y says yanked 1 selection to register a, and "ap pastes the yank. A value that is not one printable character gives a note on the last row, and dk uses ". :set and :set-document-option take it, such as :set default-yank-register b, until the next load of the config. A .ductus that sets it gives the warning .ductus cannot set default-yank-register, and nothing changes, because + would send each yank to the system clipboard (A9).
  • [editor] gutter-diff picks what the diff gutter compares the text with (see Views): "vcs", the default, the vcs base, "disk", the file on disk, or "both", in one column. With "disk", the gutter shows the unsaved changes, and dk reads the file again after a save and when the terminal gets the focus back, so a change from another program shows. With "both", a line that differs from the file on disk takes the sign of that diff, and another line that differs from the vcs base takes the sign of the vcs diff. The jumps and :reset-diff-change use the same comparison, so with "both" a reset puts back the lines of the file on disk where they differ, and the lines of the vcs base elsewhere. When a change since the last save cuts a vcs change, the other lines of the vcs change keep their sign, and a reset keeps a changed line, which has no line of the vcs base of its own, and deletes an added line. With "both" and only one of the two, the gutter compares with that one. A file that is not on disk shows every line as added. A file that is not a regular file or is over 8 MiB gives its error on the last row on each read that fails, not on each draw, and no signs. Another value gives a note on the last row, and dk uses "vcs". A trusted .ductus may set it, and :set and :set-document-option take it, such as :set gutter-diff disk.
  • [editor] jump-label-alphabet gives the label characters of gw, "abcdefghijklmnopqrstuvwxyz" by default, as in Helix. With "jk" the labels are jj, jk, kj and kk, so gw labels at most 4 words, and with the default at most 676. A value with fewer than two characters, a repeated character, or a character that is not one printable column, such as an escape or 日, gives a note on the last row, and dk uses the default (A2, A5). A trusted .ductus may set it, and :set and :set-document-option take it, such as :set jump-label-alphabet jk.
  • [editor] picker-size is the percent of the width and of the height that a picker takes, 80 by default. A whole number from 10 to 100 is valid, and another value gives a note on the last row, and dk uses 80. A trusted .ductus may set it, and :set and :set-document-option take it, such as :set picker-size 100.
  • [editor] picker-start-mode is the mode that a new picker opens in: "normal", the default, or "insert", which types into the query at once. Another value gives a note on the last row, and dk uses "normal". A trusted .ductus may set it, and :set and :set-document-option take it, such as :set picker-start-mode insert.
  • [editor] popup-border says which boxes have the frame of a picker, ┌─┐, │ and └─┘. "popup", the default, frames the popups and the docs pane of the completion menu, "menu" frames the completion menu, "all" frames both, and "none" frames nothing. The frame draws in the style of its box, ui.popup or ui.menu, and takes one row and one column on each side of the room that the box has. So the text wraps to the width inside the frame, and C-d and C-u scroll by the rows inside it. A box with 3 rows or fewer, or 3 columns or fewer, of room has no frame. Another value gives a note on the last row, and dk uses "popup". A trusted .ductus may set it, and :set and :set-document-option take it, such as :set popup-border none.
  • [editor] print-command is the program and the first args that :print-document runs, ["enscript"] by default. dk runs it with no shell and gives it the text on stdin, so print-command = ["lp", "-d", "office"] prints on the printer office. For :print-document <file>, dk adds -o - after the args, so a print-command that is not enscript must write PostScript to stdout when it gets -o -, as enscript does. A value that is not a list of one or more strings gives a note on the last row, and dk keeps the default. Only the user config.toml sets it, as only it grants exec (A9). A .ductus that sets it gives one warning, .ductus cannot set print-command, and the command stays. :set print-command and :set-document-option print-command change nothing, and say print-command: only config.toml sets it (A9).
  • [editor] rulers is a list of the columns of the text, from 1, that show a ruler, [] by default, so no ruler draws. With rulers = [80, 110], each View draws its 80th and 110th cells of the text in ui.virtual.ruler, on each row. The ruler is under the text, so the text, the selection and the cursor draw over it. A theme with no ui.virtual.ruler gives the style of its parent scope, ui.virtual, as for any scope. The gutter does not count, and a ruler counts from the start of the line, so it moves with a View that is scrolled to the side, and a ruler that is not in the View does not draw. The sticky rows, the diff background of a diff session and an inline diagnostic draw over the ruler. rulers in a [[language]] of languages.toml takes the place of [editor] rulers for a file of that language, also when it is []. A value that is not a whole number from 1 to 65535 gives a note on the last row, and dk skips it (A5). dk reads the first 16 values, and a note gives the count of the rest (A2). A trusted .ductus may set it in config.toml and languages.toml, and :set and :set-document-option take it, such as :set rulers [80].
  • [editor] scrolloff is the rows and columns that a View keeps between the cursor and its edges, 5 by default, as in Helix (Views). A whole number from 0 to 1000 is valid, and another value, such as -1, gives a note on the last row, and dk uses 5 (A2). A trusted .ductus may set it, and :set and :set-document-option take it, such as :set scrolloff 0, which lets the cursor reach the edge.
  • [editor] shell is the program and the first args that run a shell command, ["sh", "-c"] by default, as in Helix. The command line goes to it as one last argument, so shell = ["bash", "-c"] runs :sh with bash. A value that is not a list of one or more strings gives a note on the last row, and dk keeps the default. Only the user config.toml sets it, as only it grants exec (A9). A .ductus that sets it gives one warning, .ductus cannot set shell, and the shell stays. :set shell and :set-document-option shell change nothing, and say shell: only config.toml sets it (A9).
  • [editor] tabline says when the top row shows the tab line (Tab pages): "multiple", the default, with more than one tab page, "always", or "never", as Vim’s showtabline. Another value gives a note on the last row, and dk uses "multiple". A trusted .ductus may set it, and :set and :set-document-option take it, such as :set tabline always.
  • [editor] text-width is the width of g q in display columns, 80 by default, as Helix’s text-width. A value that is not a whole number of 1 or more gives a note on the last row with the value as glyphs, and dk uses 80 (A5). A trusted .ductus may set it, and :set takes it, such as :set text-width 72. dk has no text-width for each language.
  • [editor] url-opener is the program and the first args that open a URL of g f, ["xdg-open"] by default, or ["open"] on macOS. The URL goes to it as one last argument, with no shell. Only config.toml sets it, as for shell: a .ductus that sets it gives one warning and the opener stays, and :set refuses it (A9).
  • Each of config.toml, languages.toml and themes/<name>.toml in the user config directory may also be .yaml, .yml or .json, and so may config and languages in a trusted .ductus (A9). The files of one name merge key by key, at any depth, and each [[language]] merges with the one of the same name. Two files may set a key to the same value. Two values, or two arrays that differ, are a conflict. At start, dk prints the conflict, such as config.toml and config.yaml set editor.mouse to two values, and exits with status 1 before it draws. :reload-config shows the conflict on the last row and keeps the config that is loaded. The theme that the config names follows the same rules, so a conflict in its files stops dk too. A conflict in a theme that :set-theme or inherits names gives a note, and that theme does not load. json takes // and /* */ comments and trailing commas. In yaml, an unquoted yes, no, on, off, true or false, in any letter case, is true or false, as Ruby reads it, and y, n and a quoted value are text. dk refuses a yaml merge key <<, a yaml tag outside the core schema, a second yaml document, and in yaml and json a key twice in one table and null. A yaml or json file that does not read, or that is over 1 MiB in the user config directory, or with values more than 64 deep, or with more than 100,000 values, where an alias counts each value that it expands to, loads nothing and gives a note (A2). The other files of the name still load. With only the .toml file, dk reads it as before. The notes name the real files, such as config.yaml, and dk default-config and :show-options still print TOML.
  • A syntax error in a TOML or yaml file of config, languages or a theme shows on the last row as <file>:<line>: <message>, such as config.toml:3: expected value but found '=' instead. <file> is the name that the other notes use, such as config.yaml or .ductus/config.toml, and never a path (A5). A theme note starts with theme:, as in theme: themes/x.yaml:3: mapping values are not allowed in this context, and a theme name with a character other than a letter, a digit or +-._ shows quoted (A5). An error with no line, such as a json syntax error or a yaml file over 1 MiB, keeps its note.
  • [editor.file-picker] sets how space f and space / walk the files, as in Helix. hidden = false lists the names that start with ., but never a .git, .hg, .jj, .pijul or .svn directory. max-depth = 1 lists only the files of the root, and by default there is no limit. By default the walks go into a linked directory, and into each link target once, so a link loop ends (A2). A second link to a target lists nothing, and a link to an ancestor that the walk reached with no link walks that ancestor one more time. follow-symlinks = false keeps them out of a linked directory. So a link in a cloned repo, such as x -> ~, takes them out of the workspace, as in Helix, and the caps below bound what they list and read (A9). By default the walks skip each link that resolves inside the root, and deduplicate-links = false lists it. No value raises the caps of 100,000 files in space f, and of 100,000 matches and 8 MiB of each file in space / (A2). A value of another type, or a negative max-depth, gives a note on the last row, and dk uses the default. :set and :set-document-option take each key, such as :set file-picker.max-depth 2. By default the walks skip what the ignore files name, with the rules of .gitignore, such as ! and **, as in Helix. They read .ignore in any directory, ignore in the config dir of dk, and .gitignore, .git/info/exclude and git’s core.excludesFile only in a .git or .jj repo. The ignore files of the directories above the root count too. git’s global ignore file matches from the root of the walk, not from the working directory. Only a line with a / can differ. git-exclude, git-global, git-ignore, ignore and parents are true by default, and false turns off the files that each names. An ignore file over 1 MiB, with over 10,000 rules, or with a line over 1,024 bytes or 32 ** gives a note, and dk uses none of its rules. One walk reads at most 10,000 ignore files, and then gives a note (A2). One walk matches the rules at most 10,000,000 times. Then it uses no more of them and gives a note, so a hostile ignore file holds space f for about 1 s (A2). A segment of an ignore line with an unclosed [ matches only as a whole literal. space f also skips compressed archives, such as .zip and .gz, as the file picker of Helix does, and space / lists them.
  • [editor.search] smart-case = false makes space /, /, ?, n and N match case also for a pattern with no upper case letter. [editor.search] wrap-around = false makes /, ?, n and N stop at the last match, and n and N then say No more matches. A value of another type for a key of [editor.search] gives a note on the last row, and dk uses the default.

Display

  • dk shows control characters in caret notation, such as ^A, and expands tabs. A tab in a file goes to the next stop of the tab width of the file, as below, also in the preview of a picker, and j, k and a click count the columns at that width. A tab in the prompt, the rows of a picker, a popup or the status line goes to the next stop of 8.
  • g 8 in Normal and Select mode shows the Grapheme at the cursor on the last row, and then for each code point its U+ value and its UTF-8 bytes in hex, joined with +, as Vim’s ga and g8 in one key. So é as one code point shows é U+00E9 c3 a9, and e with U+0301 shows é U+0065 65 + U+0301 cc 81. A control character shows in caret notation, such as ^[ U+001B 1b, and a line ending of \r\n shows ^M^J U+000D 0d + U+000A 0a (A5). A Grapheme that holds a space or a bidi mark shows quoted, such as " " U+0020 20, so it cannot change how the last row reads (A5). A byte that is not UTF-8 shows as not UTF-8: ff, a Grapheme of more than 16 code points shows the first 16 and then + … (A2), and at the end of the text the last row says end of text. g 8 changes no text, no Selection and no mode.
  • dk turns on grapheme clustering in the terminal while it runs, so an emoji sequence shows as one character.
  • Each View ends in a status line of Helix’s elements, with the left side from the left and the right side up to the end of the View. By default the left side shows the mode, NOR, INS, or SEL, in the focused View, then the file name, [+] while the text has unsaved changes, and the change id and the closest bookmarks after one space, as in [+] nlxksqks main⇡2. While [editor] color-modes is true, the default, the mode draws in ui.statusline.normal, ui.statusline.insert, or ui.statusline.select, and in ui.statusline when the theme does not set that scope. In another View, the blank cells of the mode draw as the rest of its status line. With color-modes = false, the mode draws as the rest of the status line in each mode. A color-modes that is not true or false gives a note, and dk uses true. A trusted .ductus can set it, and :set and :set-document-option take it, such as :set color-modes false. The right side shows the warnings and errors of the language servers, the selections, the line and column of the primary cursor, the line count, and the file type, such as go, or text for a file with no language. The column counts the Graphemes of at most 1 MiB before the cursor, and shows + past that. A narrow View cuts the status line at its width.
  • [editor.statusline] in config.toml sets the left, center, and right lists, the separator, and the mode names, as in Helix, and a trusted .ductus can set them too. The elements are code-action-hint, current-working-directory, diagnostics, file-absolute-path, file-base-name, file-encoding, file-indent-style, file-line-ending, file-modification-indicator, file-name, file-type, mode, position, position-percentage, primary-selection-length, read-only-indicator, register, selections, separator, spacer, spinner, total-line-numbers, version-control, and workspace-diagnostics. code-action-hint, register, and workspace-diagnostics draw nothing yet, and file-encoding draws nothing for UTF-8, as in Helix. spinner spins while a shell command runs, with the frames of Helix, ⣾⣽⣻⢿⡿⣟⣯⣷, one each 80 ms, and else it draws one blank cell, so the elements after it stay in place. It spins for no language server. read-only-indicator draws [readonly] while the Document is readonly (see Saving). The separator element draws the separator text in ui.statusline.separator, and by default dk puts no text between other elements. An item of a list can be a text of your own, as in center = [{ text = "[" }, "version-control", { text = "]" }]. The text draws in ui.statusline.text, else in ui.statusline.separator, else in ui.statusline. A control character in it shows as a glyph, and a text over 256 bytes gives a warning and draws nothing. file-name shows the path as it was given, not relative to the working directory. A name that dk does not know, or a table other than { text = "..." }, gives a warning, and the rest of the list stays. A value of another shape gives a note, and the rest of the config stays.
  • In a trusted workspace, the status line shows the change id of the working copy after the file name, such as txwwzpou for jj. After the id, it shows each closest bookmark below the working copy, with ⇡ and the count of changes from the working copy down to the bookmark, such as rzopszyw t243/tab-width-cursor⇡1. A merge with bookmarks on its parents shows each name, as in a⇡1 b⇡1, and a bookmark on the working copy shows with no ⇡, as in txwwzpou main. The names show in order of name. The names fit in the room that the status line leaves: the width of the View, less the other elements and one space before the right side. In the center, the room also keeps one space from the wider side on each of its sides. A name shows only when it fits whole in that room and in 127 characters. The names after a name that does not fit still show when they fit, so a long b gives kkwtqnup a⇡1 c⇡1 …+1. …+N at the end counts the names that do not show, and its room comes first, so that no name takes its place. The id always shows, cut at the room when the room is smaller than the id. A bookmark more than 100 changes below the working copy does not show, and below a merge the count includes the changes of each branch. The id has the length that jj log shows, which the jj alias format_short_id(id) sets, such as id.shortest(3), and 8 by default. The parts take the colors of jj log: by default the shortest unique prefix of the id is bold magenta, the rest of it bright black, and a bookmark name magenta, and [colors] of the jj config changes them. An alias that is not shortest, such as id.short(5), shows the id in ui.statusline.version-control, else in the color that jj gives the whole id. A bookmark that moved since the last push shows * after its name, in the color of the name, as in t243/tab-width-cursor*⇡1. A theme can set the colors (see Themes), and ⇡n and …+N take the style of the status line. dk asks for it again after a save and when the terminal gets the focus back, and for the file of each View after a shell command and on :refresh-vcs. In tmux, set focus-events on so that tmux passes the focus on.
  • In a trusted workspace, a View of a file with a base text shows diff signs in the last column of the gutter (see Views), as in Helix. ▍ in diff.plus.gutter marks an added line, ▍ in diff.delta.gutter a changed line, and ▔ in diff.minus.gutter the line after removed lines. The base text is the file in the parent of the working copy, and in its parents merged when the working copy is a merge, as jj diff compares them. [editor] gutter-diff can make the gutter compare with the file on disk instead, or with both (see Config). A file that is not in the parents shows each line as added, and a binary file shows no signs. The signs follow each edit before a save. A file or a base text over 8 MiB gets no signs.
  • In a trusted workspace, inline blame shows the change of a line after the end of the line, as Helix’s inline blame does. [editor.inline-blame] show picks the lines: never, the default, shows none, cursor-line shows the cursor line of each View, and all-lines each line of the View. The blame starts 6 cells after the end of the line, in ui.virtual.inline-blame, and a blame that does not fit ends in …. The default theme draws the blame in gray italics, as inlay hints. An empty line, a line with an end of line diagnostic, and a line that is not committed yet show none. format sets the text, by default {author}, {time-ago} • {title} • {change}. Its variables are {author}, {email}, {date} as YYYY-MM-DD, {time-ago} such as 3 years ago, {title}, the first line of the description or No description for an empty one, {body}, the rest of the description, {commit}, the first 7 hex digits of the commit id, and {change}, the first 8 characters of the id of the change in the vcs. For jj, {change} is the change id, which stays the same when jj rewrites the commit, and for a vcs with no other id, such as git, it is the commit id. An unknown variable shows as typed. An empty variable drops the text before it, or the text after it when it comes first, and the text after the last variable does not show. A control character of a field, also a line break of {body}, shows as a glyph. dk annotates the file as jj file annotate -r @ gives it at the last snapshot, so a merge working copy gets a blame too. dk compares the annotated text with the text of the Document, so an edit or a save does not move a blame off its line, and a new or changed line gets none. With auto-fetch = true, dk annotates a file when it opens in a View, and each open file when :reload-config or :set inline-blame.auto-fetch true turns auto-fetch on. With auto-fetch = true, :trust annotates the open files of the workspace that it trusts. With auto-fetch = false, the default, only space B annotates a file, and dk does not annotate again on a save or on the terminal focus. An annotate run stops after 10 s, a file over 8 MiB gets no annotation, and dk keeps 256 characters of an author or an email and 1,024 of a description. A trusted .ductus can set the three keys, and a show that dk does not know gives a note, and never.
  • space B in Normal and Select mode shows the blame of the cursor line on the last row, in format, or Not committed yet. With no annotation yet, it annotates the file, and the last row says Annotating <file name> until the blame shows. The keys keep working while it runs. In a workspace that is not trusted, it gives the trust note, and a vcs plugin with no annotate gives plugin "vcs" gives no annotation.
  • The last row is the command line. It shows the prompt, and after a command it shows the result until the next key. A result that :show-messages lists as error, such as no such command: "foo" or a failed save, draws in the theme scope error, and any other result in ui.text, as in Helix. The Emergency mode notice draws in error too. A theme with no error scope draws an error in ui.text. The scope does not change the text, which draws as glyphs (A5).
  • theme = "<name>" in config.toml draws dk in the Helix theme themes/<name>.toml. dk looks in the config directory first, then in the runtime directory, so a theme in the config directory replaces a shipped theme of the same name. inherits finds its theme in the same order. The theme can use a palette, inherits, and the light and dark table, where dk takes fallback or dark. dk draws ui.background, ui.text, ui.statusline, ui.statusline.separator, ui.statusline.text, ui.virtual.whitespace for ^A and the like, and special for the matched characters on the match row and in the pickers. The rows of a picker draw in ui.text on ui.background, as the box under them, and the selected row in ui.menu.selected. A theme with no ui.menu.selected draws the selected row of a picker in ui.text.focus, which falls back to ui.text. On the status line, ui.statusline.version-control.prefix, .rest and .bookmark set the style of the prefix of the change id, the rest of it, and the bookmark names. Each one falls back to ui.statusline.version-control, and when the theme has neither, the part takes the colors of the jj config. An id or a name that is not one of these parts, such as the plain text of another vcs plugin, takes ui.statusline.version-control, else the colors that the plugin printed, else the style of the status line.
  • dk draws each Range of the Selection in ui.selection, the primary Range in ui.selection.primary, and the cursor of each Range that is not primary in ui.cursor.normal, ui.cursor.insert or ui.cursor.select by the mode, else in ui.cursor, or else in ui.selection, as in Helix. With the prompt open, the cursor takes the mode that the prompt opened from. In Normal and Select mode, dk hides the terminal cursor and draws the primary cursor as a block in ui.cursor.primary.normal, ui.cursor.primary.insert or ui.cursor.primary.select by the mode, else in ui.cursor.primary, else as the other cursors, or else reversed. On a tab, the block covers each cell of the tab, and on a line break or at the end of the text it takes one cell. In Insert mode, the terminal cursor shows the primary cursor, so the shape that the terminal gives it, such as a bar, shows the mode, and its cell gets no cursor style. With the prompt or a picker open, the terminal cursor is in the text that you type, and the View draws the block, also when they opened from Insert mode. As in Helix, the cell of a cursor is not in the selection, and a cursor at the end selects nothing. A theme that does not load, such as a file with a TOML syntax error, gives one warning on the last row, and dk draws in the default style. A bad scope, palette color or rainbow does not stop the theme: dk leaves it out and loads the rest. A scope that dk leaves out takes its parent scope, a scope that names a bad palette color is left out too, and a bad rainbow gives the six colors of Helix. The last row shows the first of these warnings and how many more there are, and :show-messages lists each.
  • The runtime directory holds the themes that dk ships, and the grammars that the nix package installs. For dk at <prefix>/bin/dk, it is <prefix>/share/ductus/runtime, after dk follows the links to the binary. DUCTUS_RUNTIME replaces it when the value is an absolute path.
  • With no theme in the config, dk draws in Helix’s nord theme, which it ships. The binary holds a copy of nord too, so nord loads with no runtime directory. themes/nord.toml in the config directory or the runtime directory replaces that copy. The nord theme sets ui.virtual to the gray nord3, so a ui.virtual scope that nord does not set draws gray.
  • dk ships the 218 themes of Helix d5d88b62, with their Helix names, in the runtime directory. The binary holds only nord, so with no runtime directory no other theme loads. A theme with a license of its own, such as sonokai or zenburn, names its file in LICENSES/ in its first lines. The other themes are under the Mozilla Public License 2.0 of Helix. The nix package puts the license texts in <prefix>/share/doc/ductus/LICENSES. Each theme that does not set them gets diff.plus.view, diff.minus.view and diff.delta.view, marked # dk addition: a background with 25% of the color of diff.plus, diff.minus or diff.delta over the background of ui.background. diff.delta.view.text gets the same mix with 45%, so the changed words show stronger. A theme with no such color, or with a color name of the terminal such as green, gets no scope for it. A diff scope that sets a background keeps it for its lines and gets no scope, such as in zenburn. A theme that inherits keeps the scopes of the theme it inherits, also when it has no background of its own, and gets its own only when its colors give another mix. Else the diff session falls back as before.
  • dk ships the colorschemes of Vim 451793fc as vim_<name> in the runtime directory, such as vim_habamax, vim_desert and vim_retrobox, 33 themes in all. A colorscheme with a dark and a light branch, such as retrobox, gives vim_<name> from the dark one and vim_<name>_light from the light one. default sets no colors, so dk has no vim_default. dk also ships the default colors of Neovim 1502d4a9 as neovim and neovim_light. Each theme has the gui colors and attributes of the highlight groups that dk has a scope for, such as Normal for ui.background and ui.text, Visual for ui.selection, and DiffText for diff.delta.view.text. The Vim themes are under the Vim license, in LICENSES/Vim, and the Neovim themes under the license of Neovim, in LICENSES/Apache-2.0-neovim. The binary holds none of them.
  • :set-theme <name> draws dk in another theme at once, found as theme = "<name>" finds it. The theme lasts until dk quits or :reload-config reads the config again, and the config does not change. A theme that does not load gives one warning, and dk keeps the theme it has. A theme with a bad scope loads, with the warnings of a config theme. :set-theme with no name shows the name of the theme.
  • Tab after :set-theme and a space completes the theme name from the config directory, the runtime directory, and the themes dk ships, in sorted order. Each Tab and S-Tab step draws dk in that theme. Esc, or Backspace on an empty line, gives back the theme you had, and Enter keeps a theme only when the line names one that loads.

Editing

  • dk starts in Normal mode. i enters Insert mode at the start of each Range, and Esc goes back to Normal mode.
  • :set nomodifiable or :set modifiable false stops each change of the text of the focused Document, as Vim’s ‘modifiable’, and :set modifiable allows them again. :set-document-option does the same, and neither has a session value or a config.toml key. Typed text, deletes, undo and redo, :reset-diff-change, :put-diff-change into it, auto pairs, plugins, and the edits of a language server all refuse, and the last row says Cannot make changes, 'modifiable' is off, as Vim’s E21. i, a, o, c, and the other keys that enter Insert mode refuse at once, so Insert mode does not start. A workspace edit of a server that names such a Document is refused whole. :w still saves the text as it is, and runs no formatter and asks no language server to format it.
  • In Insert mode, you type text at the cursor of every Range. Enter or C-j adds a line break in the line ending of the file, LF or CRLF, as Helix’s insert_newline. The new line gets the indent of a new line, as below, and dk deletes the spaces before the cursor. When the line is blank before the cursor, Enter puts the line break at the start of the line, so the line keeps its indent. Backspace deletes the Grapheme before each cursor, so CR LF and an emoji go as one. Each Insert session, from i to Esc, is one Revision of the History. A completion that you accept ends the Revision before it (see Language servers).
  • Insert mode has the other edit keys of Helix, and each acts at every cursor:
    • A-d and A-Delete delete to the end of the word, as e finds it.
    • C-d and Delete delete the Grapheme under the cursor.
    • C-h and S-Backspace delete before the cursor, as Backspace does.
    • C-k deletes to the end of the line, and on the line ending it joins the next line.
    • C-r and a character paste that register at each cursor, so C-r a inserts register a, and the cursor goes after the text. The paste is one Revision, and the text typed before it one more. A box titled Insert register lists the registers as the " box does, with a control as a glyph (A5), and Esc or another key that is not a character pastes nothing.
    • C-s ends the Revision so far, so u undoes only the text typed after it.
    • C-u deletes back to the first character of the line that is not a space, then to the line start, and at the line start it joins the line above.
    • C-w and A-Backspace delete back to the start of the word, as b finds it, so C-w after foo bar leaves foo and the space after it.
    • Home goes to the line start, and End to the line ending.
    • S-Tab inserts a tab, or with spaces the spaces to the next indent column.
    • Tab runs Helix’s smart_tab: after text it goes to the next tab stop of an active snippet, and with no snippet past the end of the syntax node at the cursor. When only whitespace is before each cursor, it inserts a tab, as S-Tab does.
  • Where a delete reaches the end of the text, Helix adds a line ending, and dk does not, as with a. The keymap plugin finds the words of C-w and A-d through move-word, so they do nothing in Emergency mode.
  • In Insert mode, dk pairs brackets and quotes as Helix’s auto-pairs, at each cursor on its own. Typing (, [, {, ', ", or a backtick also types its closer, with the cursor between, unless a letter or a digit follows. A quote also needs no letter or digit before it, so don't types one '. Typing the closer that is under the cursor moves over it. A space between a pair types two spaces. Backspace between a pair deletes both, and so it does between the two spaces in a pair. Enter between a pair puts the closer on a new line, and the cursor on a line between them, one indent unit in. auto-pairs in [editor] of config.toml is true, false, or a table such as { '(' = ')', '<' = '>' }, and a trusted .ductus can set it too. The auto-pairs of a [[language]] in languages.toml sets the pairs of that language, and false or {} turns them off there. dk takes the language of the file path for this. The shipped languages.toml holds the auto-pairs of Helix, so Rust, HTML, and the other languages that Helix gives pairs get the same pairs. auto-pairs = false in [editor] turns them off for every language. A value of another shape gives a warning, and dk uses the default pairs.
  • A paste from the terminal goes in as text at the cursor of every Range in Insert mode, and at the cursor of the prompt, as in Helix. In Normal mode it goes in before each Range and becomes the Selection, and a paste that ends with a line break goes in at the start of the line, as P does. In Select mode it goes in at the cursor of each Range, and Select mode ends. No pasted character runs as a key, and an escape in the text shows as ^[. Each line break becomes the line ending of the file, auto-pairs do not act, and the paste is one Revision of its own. In a picker a paste inserts nothing and says so, and so does a paste over 16 MiB in all, or one that makes the prompt line longer than 64 KiB.
  • A click puts the cursor at the character under the pointer and focuses its View, and a drag selects from the press to the pointer, as in Helix. Alt and a click add a cursor, and in Select mode a click extends the primary Range. A click in another View first goes back to Normal mode, as in Helix. A click on the command line or a gutter does nothing, and a click on a status line focuses its View (Views). A click on the tab line goes to a tab page (Tab pages).
  • The release of the left button yanks the primary Range to register * and says yanked primary selection to register *, when the Range covers more than one Grapheme, so a click alone yanks nothing. In Select mode the release yanks only when a drag changed the primary Range after the press. The release after a press on the tab line yanks nothing. A middle click on text puts one cursor there, focuses its View, and pastes * before it, and off the text it does nothing. Alt and a middle click put * in place of each Range, wherever the pointer is, except on the tab line. A middle click on the tab line pastes nothing and closes a tab page (Tab pages). Each paste of the middle button is one Revision.
  • The mouse wheel scrolls the View under the pointer by three lines, focused or not, as in Helix. When the View leaves the primary cursor behind, the cursor moves to the start of the line 5 rows inside the View, or fewer in a small View, and in Select mode the primary Range extends to it. The other Ranges stay, and the focus stays where it was. In a diff session the wheel scrolls both Views by three rows of the session, also over a filler row, while the cursor of the focused View stays in its View. When the cursor must move past filler rows, it goes to the line after them, and the Views scroll further to show it. A scroll up onto a filler row puts the cursor at the start of the line above the fillers.
  • middle-click-paste = false in [editor] turns off the yank of a release and the middle click, and a trusted .ductus can set it too. A middle click on the tab line still closes a tab page.
  • mouse = false in [editor] of config.toml turns the mouse off, so the terminal selects text itself, and a trusted .ductus can set it too.
  • mouse-yank-register in [editor] of config.toml picks the register of the release and the middle click, * by default. Only config.toml sets it: :set refuses it, and a .ductus that sets it gives the warning .ductus cannot set mouse-yank-register (A9). A value that is not one printable character gives a note, and dk uses *.
  • d deletes the text of every Range, and c deletes it and enters Insert mode, as in Helix. Both yank the text to the register first, and A-d and A-c do the same with no yank. a enters Insert mode after each Range, so at the end of the Document the text appends. c and the text typed after it are one Revision, and so is each d. In Select mode the same keys act, then go back to Normal mode.
  • y yanks the text of each Range to the register and says yanked 2 selections to register ", as in Helix. p pastes the register after each Range and P before it, the first value at the first Range, and the last value at each Range after the last. A value that ends with a line ending pastes by lines: p after the line of the Range and P at its start, so xyjp copies a line below the next one. A count pastes each value that many times, and in Normal mode the pasted text becomes the Selection. R puts the register in place of each Range, with the same rules. A paste takes the line ending of the file, is one Revision, and leaves Select mode, and one over 16 MiB inserts nothing and says so (A2).
  • " and a character pick the register of the next command, so "ay yanks to a and "ap pastes it, and with none picked a command uses default-yank-register, " by default (see Config). A box titled Select register lists each register with the first line of its value, 51 columns wide, and shows a control as a glyph (A5). A row that is wider than an info box ends in … where the box ends. The list holds the registers in the order of their names, then _, #, ., %, + and *. Each of these keeps its label and then shows the first line of the value that a paste gives at the primary Range, such as % <document path> cmd/dk/main.go, and _ shows <empty>. The box shows at once, and + and * show their values when the read of the provider ends, so a slow provider holds no key. A read that fails or stops after 2 s, and a provider that cannot read, such as termcode, leave the label alone with no note (A2). The registers last for the session. _ takes nothing and reads as empty, so "_d deletes with no yank. # reads the number of each Range from 1, . the text of each Range, and % the path of the file from the working directory. A write to #, ., % or : says Register % does not support writing, and d and c then delete nothing. " takes : as the name of a register, as in Vim, so ":p pastes the newest command line. This is the only wait for a key in which : does not open the prompt, so f: and mi: still open it. + is the system clipboard and * the primary selection, through [editor] clipboard-provider (see Config), and their labels in the box are <system clipboard> and <primary clipboard>. A write to one joins the values with line endings, and a paste of the same text gives each Range its own value again, so "+y and "+p on three Ranges paste three values. When a provider command fails, or runs longer than 2 s and stops, the last row says clipboard provider command failed, a yank keeps nothing, and a paste inserts nothing (A2). termcode and none cannot read, so a paste with them says This clipboard provider does not support reading and inserts nothing.
  • space y yanks the text of each Range to the system clipboard, and space Y the text of the primary Range only. space p and space P paste the system clipboard after and before each Range, and space R puts it in place of each Range. They work as y, p, P and R with register +, in Normal and Select mode, whatever register " picked.
  • r and then a key replaces each character of every Range with the key, as in Helix, so r- on three characters gives ---. On a bare cursor it replaces the character under it, and a character of more than one code point, such as an é with a combining accent, counts as one. Enter writes the line ending of the file, and Tab a tab, and Esc or another key such as an arrow changes nothing. The key is text, so rd writes a d. The replace is one Revision, a count changes nothing, and in Select mode r goes back to Normal mode.
  • ~ in Normal and Select mode switches the case of each Range, as Helix’s switch_case, so Hello World becomes hELLO wORLD. ` makes each Range lower case and A-` makes it upper case, as Helix’s switch_to_lowercase and switch_to_uppercase. dk changes each code point with the case tables of Go, so ~ makes Ä1é into ä1É, and ` makes İ into i. One code point gives one code point, so A-` keeps ß. A byte that is not UTF-8 stays as it is (A2). The change is one Revision, each Range covers its new text, . repeats it, and in Select mode the key goes back to Normal mode. With modifiable off it changes nothing, and the last row says Cannot make changes, 'modifiable' is off. In a terminal without the kitty keyboard protocol or Win32 input mode, a fast Esc and ` reach dk as A-`, so they run switch-to-uppercase in Normal and Select mode.
  • C-c and space c comment or uncomment the lines of each Range in Normal and Select mode, as Helix’s toggle_comments, and leave Select mode. The line token and a space go before the text of each line that is not blank, at the smallest indent of the lines. When each of those lines starts with the token, the token goes away again. It goes away at the indent of its own line, so // a at an indent of 2 and // b at an indent of 4 become a and b. The token is the longest one of the language that the cursor line starts with, so /// wins over // in Rust, and # in a Document with no language. A language with only block tokens, such as CSS, gets them around each line, and a block comment around each line or around the whole Range goes away. The tokens are the comment-tokens and block-comment-tokens of the language in languages.toml, not those of an injected language yet. One C-c over many Ranges is one undo step.
  • :format, or :fmt, formats the file, as in Helix. The formatter = { command, args } of its [[language]] in languages.toml runs in the directory of the file, with the text on stdin, and the text changes to its stdout. Else dk asks the first language server of the file that formats, with the tab width and the indent of the file. The request sends no properties from the format key of the server config. Else the last row says A formatter isn't available, and no language server provides formatting capabilities. The change is one Revision, and a format that changes nothing adds none. A result that comes after an edit changes nothing. A result that comes after a move to another file applies to its own Document, while that Document is open and has not changed since the request. The change replaces each whole line that differs, so a cursor in a changed line can move in that line.
  • A formatter runs only in a trusted workspace, as a language server does, and a trusted .ductus can set it (A9). A file in no workspace gets no formatter. With modifiable off, :format runs no formatter, and the last row says Cannot make changes, 'modifiable' is off. Its command must be a program name on PATH or an absolute path, with no control character, or dk does not run it (A5, A9). It runs with no shell, and stops after 20 s, with its children (A2). Output over 32 MiB, or output that is not UTF-8, changes nothing (A2). dk keeps 1 KiB of its stderr (A2). A formatter that fails shows its stderr quoted on the last row, as Formatter error: "..." (A5).
  • = in Normal and Select mode formats the selection with the first language server of the file that formats a range, as Helix’s format_selections. The change is one Revision, as with :format. With more than one Range it says format-selection only supports a single selection for now, and with no such server No configured language server supports range formatting.
  • u undoes the last change and U redoes it, as in Helix, in Normal and Select mode. A-u and A-U move back and on through the changes in the order they were made, across the branches that an edit after an undo starts. g - and g + in Normal mode do the same as A-u and A-U, as Vim’s g- and g+, so 3g- moves back three changes. Each puts back the Document and the Selection, a count repeats it, and the last row says when nothing is left to undo or redo. In a terminal without the kitty keyboard protocol or Win32 input mode, a fast Esc and u reach dk as A-u, so they run goto-earlier-revision in Normal mode.
  • When a key command or a typed command adds or removes more than 2 lines, the last row says how many, such as 3 fewer lines after 3xd and 3 more lines after its u. The limit of 2 has no option. The count is of the whole command over all selections. Each key command and typed command that edits outside Insert mode reports, such as a delete, a paste, u and U, and so do :pipe, :insert-output and :append-output when their output comes in. When :pipe changes the text and keeps the line count, the last row says how many lines it replaced, such as 5 lines filtered. A format and an edit of a language server report too when they come in, such as :format, =, a rename and a code action, and they count only the lines of the focused file. After an edit of a language server, the report follows the note of the changed files, such as 2 files changed, not saved; 3 more lines, so the last row still says that other files changed and are not saved. An edit in Insert mode gives no report, and neither does a key that shows another file, a :pipe into a file that is no longer in the focused View, or the format of a :w, which shows the note of the write. The report is an info message: it does not replace another message of the same key, and a newer report replaces it.
  • space u in Normal or Select mode opens the revision picker. It lists each Revision of the History, newest first, with a graph of the branches, @ on the current one, its number, its time, and [saved] on one that a save wrote. A time of today shows only the clock, and an older time shows the date too. Typing in Insert mode filters by the number and the time, and the other keys are the keys of the buffer picker, but C-s and C-v pick as Enter does. Enter moves the Document to the Revision, with the Selection from when it was made, and adds no Revision. The picker shows the newest 100,000 Revisions and 32 lanes, and its title says when a cap cuts the tree (A2). When the picker is over 72 columns wide, a pane shows the text of the selected Revision, with the line of its first change from your text in the middle row, and the History and the Document stay as they are. The current Revision shows with the line of the cursor in the middle row. A Revision whose path from the current one is too costly to compose, about 60 ms of work, shows a note in place of its text (A2).
  • Undo works after a restart. After each :w, dk writes the History to an Undofile in $XDG_STATE_HOME/ductus/undo, or ~/.local/state/ductus/undo. The directory is mode 0700 and each Undofile 0600, and dk never writes through a symlink there (A4). The ductus state directory gets the same check: dk refuses a symlink there or a directory of another user, and gives a wider mode 0700. dk then keeps and loads no Undofile, the file still saves, and the last row says why (A3, A4, A6). A state directory that others can write loads no Undofile until the next :w sets mode 0700 (A3). An Undofile that does not read, or one for text that changed after the save, gives one warning, and history starts fresh (A3).
  • A file under /dev/shm, /tmp, or $TMPDIR keeps no Undofile, so a pass secret never reaches disk (A4). [editor] undofile-exclude in the user config.toml lists globs of files that keep none. A glob with no / matches the base name, and one with a / matches the whole path, as in a .gitignore. The defaults are *.env and **/.git/COMMIT_EDITMSG, and a list replaces them. A workspace .ductus cannot set it.
  • The arrow keys move the cursor one Grapheme or one line, in Normal and Insert mode. Up and Down keep the display column past a shorter line, as in Helix. Each Range keeps the column of its last vertical move, and Up, Down, j, k, C-d, and C-u go back to it. Another motion or an edit forgets the column.
  • g j and g k move every Range down and up by text line, as Helix’s move_line_down and move_line_up, and extend in Select mode. They keep the column as j and k do, and a count stops at the first or the last line. Views do not soft-wrap, so today they move as j and k do, also on a line wider than the View.
  • A motion moves the cursor of every Range in the Selection. Ranges that meet merge into one, and the primary Range stays primary.
  • C adds a Range on the line below each Range, at the same display column, as in Helix. A-C adds one on the line above. A line that is too short for the column gets no Range, and a Range over two or more lines is copied whole. The last copy of the primary Range becomes primary. A count adds that many copies, and the Selection stops at 10,000 Ranges with the note “too many ranges”.
  • , keeps only the primary Range. C, A-C, and , work in Select mode too.
  • C-v in Normal and Select mode enters Block mode, which the status line names BLK. The anchor and the cursor of the primary Range are the corners of a rectangle, and each line of it that reaches its left side gets one Range over the text between the display columns of the corners. A tab or a wide character that is only partly in it is in its Range whole. So on three lines abcd, C-v l j j selects ab on each line. The motions of Select mode move the cursor corner, and j and k keep its display column past a short line. The cursor corner draws in ui.cursor.primary, also past the end of a short or empty line, and the View scrolls to it. The cursors of the Ranges draw in the style of the other cursors, so the corner is the one primary cursor. $ and End take the right side to the end of each line, so each Range has the length of its line, until a motion that is not j or k. o swaps the two corners, and O swaps only their columns, so the cursor goes to the other corner and the block stays. The Range on the line of the cursor corner is primary, or else the nearest one, and the block stops at 10,000 lines with the note “too many ranges”. Each other key of Select mode acts on the Ranges, such as i, c, d, y and r, and dk then goes back to Normal mode. C-v or Esc goes back to Normal mode and keeps the Ranges.
  • A yank in Block mode keeps in the register that its values are a block, and so do d and c, which yank first. p or P of that register at one cursor puts the values as a rectangle: the first value after or at the cursor, and each next value on the next line at the same display column. A short line gets spaces up to the column, the paste adds lines at the end of the Document when it needs them, and the pasted values become the Selection. A paste on as many Ranges as values puts one value on each Range, as any paste does. The paste is one Revision, and one whose values, spaces and new lines are over 16 MiB inserts nothing and says so (A2). A later write outside Block mode makes the register plain, and "+ and "* paste as a block only while the clipboard still holds what dk wrote.
  • s opens the prompt select:, and each match of the regex inside each Range becomes a Range, as Helix’s select_regex. S opens split: and splits each Range at the matches, so the text between them becomes the Ranges. K opens keep: and keeps the Ranges whose text matches, and A-K opens remove: and removes them. Each key in the prompt shows the new Selection, and Esc puts the old one back. The regex has the smart-case rule of /, and Enter puts it first in register /, or in the register that " picked, so n then searches for it. Enter with no match says nothing selected for s and no selections remaining for K and A-K, and the Selection stays. A regex that does not compile leaves the Selection, and Enter shows its error on the last row. The first new Range is primary. ^, $, and \b see the edge of a Range as the edge of the text, so ^a matches an a at the start of a Range. s and S stop at 10,000 Ranges with the note “too many ranges” (A2). The four keys work in Select mode too.
  • A-s splits each Range into its lines, without the line endings, as Helix’s split_selection_on_newline. It stops at 10,000 Ranges with the note “too many ranges” (A2). A-minus merges the Ranges into one, from the start of the first to the end of the last. A-_ merges each Range with the next one where it ends where the next one starts. A merged Range is backward only when both Ranges were backward. ; makes each Range a cursor on the Grapheme of its cursor. ensure-ranges-forward turns each backward Range forward, with the same text. It has no key, because dk reads A-:, its Helix key, as Esc and then :, so a [keys] binding or the command palette runs it. The first Range is primary, and after A-_ the Range that holds the old primary. These keys work in Select mode too.
  • A-, removes the primary Range, as Helix’s remove_primary_selection, and the Range after it becomes primary, or the one before it when it was the last. With one Range it says no selections remaining. ) makes the next Range primary and ( the one before, and both wrap around. A count moves that many Ranges, and ( with a count over the Range count moves none, as in Helix. A-) moves the text of each Range to the next Range, and the text of the last to the first, and A-( moves it back. A count moves the text that many Ranges, up to the Range count, and the primary Range moves with its text. Each Range then covers its new text, and u undoes the change in one step. The end cursor holds no text, so the text that moves to it goes at the end. These keys work in Select mode too.
  • X makes each Range cover its whole lines with the line ending of the last, as Helix’s extend_to_line_bounds. A-x shrinks a Range over more than one line to the whole lines inside it, and leaves a Range inside one line as it is. Both keep the direction of each Range.
  • > indents and < unindents the lines of each Range in Normal and Select mode, as Helix’s indent and unindent, and both leave Select mode. > puts one indent unit before each line that is not blank, and 2> puts two. With spaces, the first unit only goes to the next indent column, so with a 4-space unit a line at 2 spaces goes to 4. A tab before the text goes to its tab stop there, so with a tab width of 4 a line that starts with a tab gets 4 spaces. < takes away the spaces and tabs at the start of each line up to the width of one unit, or of two for 2<, and a line with less indent loses all of it. A tab goes to the next tab stop. The unit and the tab width are those of the Document, as for a new line. A line with two Ranges on it changes once, and u undoes the key in one step. An indent over 1 MiB changes nothing, and the last row says so (A2).
  • J joins the lines of each Range, or a Range on one line with the next line, as Helix’s join_selections. Each line ending and the spaces and tabs after it become one space, or nothing when the next line holds only spaces. A next line that starts with the comment token of the line before loses the token, with the tokens of the language of the Document. A-J joins in the same way and selects the spaces that it puts in, and u undoes either join in one step.
  • g J in Normal and Select mode joins the same lines as J, as Vim’s gJ, but removes only each line ending between them, \n or \r\n. The spaces and tabs at the start of the next line and its comment token stay, and dk puts in no space, so foo and bar give foobar. On the last line it changes nothing, u undoes it in one step, and . repeats it.
  • g q in Normal and Select mode fills each paragraph of each selection to [editor] text-width, as Vim’s gq and Helix’s :reflow. The prefix of a line is its indent, then one or more markers, each with the spaces and tabs after it. A marker is a line comment token of the language of the Document, the longest first, so /// is one marker in Rust, or > in any language, as a Markdown or mail quote. The tokens are those of the language of the Document, not those of an injected language, and a comment after code on the same line is not a prefix. A paragraph is a run of lines with the same markers and text after them, so a comment above code fills apart from the code. A line with only its prefix, such as // alone or a blank line, ends the paragraph and stays as it is. dk splits words only at ASCII spaces and never hyphenates, so well-known stays whole, and a word wider than text-width stays whole on a line of its own. The prefix of the first line of a paragraph starts each of its new lines and counts in the width, the new lines end with the line ending of the Document, and a line ending at the end of the selection stays. A paragraph whose prefix has more bytes than half of text-width stays as it is (A2). dk does not take the * of the middle lines of a block comment as a marker. All Ranges change in one Revision, so u undoes it in one step, each Range then covers its new text, Select mode stays, and . repeats it. It runs in time linear in the size of each selection (A2), and dk has no :reflow typed command.
  • & puts spaces before the Ranges of each line, so the heads of the Ranges with the same index on their lines get one column, as Helix’s align_selections. dk then leaves Select mode, and a Range over two lines makes & say align cannot work with multi line selections and change nothing.
  • _ takes the whitespace off both ends of each Range, as Helix’s trim_selections. A Range of only whitespace goes, and with none left the primary Range becomes a cursor.
  • C-a adds 1 to the integer of each Range and C-x subtracts 1, in Normal and Select mode, as Helix’s increment and decrement, and dk leaves Select mode when a Range changes. The whole text of a Range must be the integer, so the cursor on the 2 of 123 gives 133, and a Range that is not an integer stays as it is. A count adds that much, so 5C-a on 10 gives 15. With register # the amount grows by 1 for each Range after the first, so "#C-a on three Ranges of 0 gives 1, 2, and 3. An integer is base 10, or base 16, 8, or 2 after 0x, 0o, or 0b, and only base 10 goes below 0. _ can split the digits, such as 1_000, and stays at its place from the right. Leading zeros keep the width, so 007 gives 008, and base 16 keeps upper case digits when most of them are upper case. The sum stops at the bounds of a signed 128-bit integer for base 10, and of an unsigned one for the others. A Range over 64 bytes stays as it is (A2), and u undoes the key in one step.
  • X, A-x, J, A-J, &, and _ work in Select mode too.
  • w, b, and e select the text to the start of the next word, the start of the word before, and the end of the next word, as in Helix. Letters, digits, and _ make a word, and a run of punctuation is a word too. The motions step over whole Graphemes and over line endings.
  • W, B, and E do the same for the long word, as in Helix: letters, digits, and punctuation next to each other make one long word, so W on foo/bar.go baz goes to baz. A space or a line ending ends a long word, and so does a symbol that is not punctuation, such as an emoji. The keymap plugin gives these motions through move-word, so a keymap.wasm in the config directory that does not know their names makes them do nothing.
  • f and then a key selects from the cursor to the next match of the key, and t to the character before it, as in Helix. F and T go back, and in Select mode the four keys extend each Range. The search goes past the end of the line, and a count finds the match that many times on, so on a(b, c) f, selects a(b,. t starts one character further on, so t, again goes on to before the next ,. Tab finds a tab, and Enter finds the end of a line. A Range with no match stays as it was, and Esc or any other key ends the wait, which the last row shows. A key matches a whole Grapheme, so fe does not stop inside an é of two code points, where Helix matches one character. The finds of one key read at most 8 Mi characters over all Ranges (A2).
  • . in Normal and Select mode repeats the last change on each selection that exists when you press it, as Vim’s . does. The keys that made the selections do not run again. A change is a command that changes the text, such as d, A-d, r, R, p or P, a command that enters Insert mode, such as i, a, I, A, o, O, c or A-c, or a bracketed paste in Normal or Select mode, which . pastes again. A change that entered Insert mode repeats with the keys that you typed in it and the text that you pasted in it, and a completion that you accepted puts its text in again from the cursor. A count replaces the count of the change, also for the next ., and with no count the change uses its own count again. So after 2p, . pastes two times, and 3. pastes three times. A change that named a register, such as "ap, uses that register again. A register before . replaces it, also for the next ., so after "ap, "b. and then . paste from b. A typed command is not a change, also when a key runs it, and neither is a change that ran one. A yank, a motion, a selection change, undo, and redo are not changes either. One . is one undo step, and with no change yet . does nothing. The keys and the pasted text of one change stop at 16 MiB, where each key or paste counts its text and 64 bytes, about what dk keeps of it, and . after a longer change repeats nothing (A2). . is the command repeat-last-edit, and . after a prefix key, such as g, does not run it.
  • A-. runs the last motion again, as in Helix, and a count runs it that many times: the finds, ]p and [p, ]g and [g, the tree-sitter jumps such as ]f, the syntax tree keys such as A-o, the objects of mi and ma, and the move of Tab past a syntax node. Each motion keeps the count it ran with, so after 2f,, A-. goes two more on.
  • A count before a motion repeats it, as in Helix: 3w selects the third word, and 2j moves down two lines. The last row shows the count and the keys so far. A digit that a binding starts with runs that binding. A count stops at 100,000,000, as in Helix, and a command repeats at most 10,000 times (A2).
  • gg goes to the first line, and 12gg or 12G to line 12, as in Helix. ge goes to the last line, and so does G with no count, where Helix does nothing. The cursor of every Range goes to the start of the line, and a line past the end is the last line. When the text ends in a line break, the last line is the one before the empty line after it. In Select mode gg and ge extend every Range, and G moves it, as in Helix.
  • H, M and L go to the top, the middle and the bottom line of the View in Normal and Select mode, as in Vim. The cursor of every Range goes to the first character of the line that is not a space or a tab, and in Select mode every Range extends. H and L stay scrolloff rows inside the edge of the View, 5 by default, or less in a small View, and H stays under the sticky rows, so neither one scrolls, except in a diff session when the line after a run of filler rows is in the bottom margin. When the View shows the first line, H goes to it, and when the last line shows above the bottom margin, L goes to it. A count goes to line N from the top for H and from the bottom for L, but never into those rows, so with the default 3H goes to the 6th row and 8H to the 8th. M takes no count, and when the end of the text shows, M and L count only the rows that hold text. None of them scrolls the View. In a diff session a filler row counts as a row, and a target on a filler row takes the line after its fillers.
  • The line keys go to a place on the line of the cursor of each Range, as in Helix, and in Select mode each Range extends to it. g h and Home go to the start of the line, and g l and End to the last Grapheme before the line ending, or to the start of an empty line. g s goes to the first character that is not whitespace, and on a line of only whitespace the Range stays. g | goes to Grapheme N of the line, where N is the count, or to the line ending past the last Grapheme, and saves the place on the jumplist first. g M goes to the Grapheme at half the Graphemes of the line, as Vim’s gM, and a count of 1 to 100 goes to that percent of them, so 25gM goes a quarter in, and a count over 100 to the last one. A count stops at 100,000,000, and no step runs once for each count (A2). In Select mode End keeps extend-to-line-end, and the commands are extend-to-line-start, extend-to-line-end, extend-to-first-non-whitespace, extend-to-column and extend-to-line-middle.
  • C-o goes back to the place before a jump, and C-i or Tab goes forward again, in Normal and Select mode, as Helix’s jumplist. A count goes that many jumps, so 3 then C-o goes back three. [g, ]g, [G, ]G, G, ge, gg, g ;, g ,, g ., g |, :goto-line, H, L, M, the tree-sitter jumps such as ]f, a label of gw, and the goto keys of the language servers such as gd save the place before they move. So do a pick in the diagnostic and symbol pickers, and a move to a file, such as :open, gn, or a pick in the file picker, also when the View shows that file already. C-s saves the place and says Selection saved to jumplist. Each View keeps the newest 30 places (A2), and a split of the View starts with a copy of them. A split to another file, such as :split-view-right <file> or C-v in a picker, starts with none, and a split saves no place. A jump into another file shows that file in the View, and a cursor line off screen goes to the middle of the View. dk counts lines to find that, not the rows of wrapped lines. An edit moves each saved place with its text, and :bc of a file drops its places (A1).
  • g ; and g , in Normal and Select mode go to an older and a newer revision position, as Vim’s g; and g,. Each Document keeps the revision position of each edit that makes a Revision: after the change at the primary Range from before the edit, or else after the first change. An edit on the line of the newest position and less than 79 bytes from it takes its place, so five d side by side keep one position. Just after an edit, g ; goes to the newest position, and a count goes that many positions and stops at the oldest or the newest. The cursor goes to the position as the only Range, and in Select mode the primary Range extends to it as the only Range. With no older position g ; says At the oldest revision position, with no newer one g , says At the newest revision position, and with none each says No revision position. u, U, A-u and A-U add no position and remove none, and a server edit to a file that no View shows adds none. Each Document keeps the newest 30 positions (A2), an edit moves each one with its text (A1), and dk writes none of them to disk (A4).
  • g I in Normal and Select mode enters Insert mode with the Selection as it was when Insert mode last ended in the Document, with all its Ranges, as Vim’s gi. Esc and a click in another View end Insert mode. An edit moves that Selection with its text (A1), and in a Document where Insert mode has not ended yet, g I works as i. g I is a change, so . repeats it.
  • space j in Normal or Select mode opens the jumplist picker, as in Helix. It lists the saved places of every View, newest first in each View. A row shows the path and the line, (*) when that View shows the file, and the text of the selection, up to 200 characters (A2). Control characters in the text show as glyphs (A5). Typing in Insert mode filters on the path and the line, and the pane marks the line of the place. Enter goes to the place, with its line in the middle of the View, and C-s and C-v go there in a new View. A place in a file that closed after the picker opened gives an error, and opens nothing (A1).
  • / opens the prompt search:, and ? opens it for a search back, as in Helix, in Normal and Select mode and in the z and Z menus. Each key in the prompt selects the next match of the line from the Selection from before the prompt, and the View shows the match. Esc puts the Selection and the View back. Enter keeps the match, saves the place from before the prompt on the jumplist, and puts the line first in register /, so n and N go on from there. Enter on an empty line searches for the newest pattern of register /, and with none it does nothing. Up and C-p put an older pattern of register / on the line, and Down and C-n a newer one, as in Helix, and each step searches as a typed key does. A pattern over 64 KiB, which "/y of a large selection can give, does not go on the line (A2). In Select mode the match is a new primary Range. "a/ keeps the line in register a, Up and Down step through register a, and n and N then read register a. A pattern that does not compile leaves the Selection, and Enter shows the error of the regex on the last row, where Helix shows a popup. The search of the prompt says nothing when it wraps, as in Helix. The pattern has the syntax and the rules of the pattern of n, below. If the text changes while the prompt is open, and no longer holds the Selection from before the prompt, Esc keeps the Selection as it is (A1).
  • n selects the next match of the search pattern after the primary Range, in its place, and N the match before it, as in Helix. zn, zN, Zn and ZN do the same, and a count goes that many matches on, so 3n goes three on. In Select mode n and N add the match as a new primary Range, and the keys of the z menu move it. The match keeps the direction of the primary Range, and a match off screen goes to the middle of the View. Past the last match the search goes on from the other end and says Wrapped around document (wrap-around, Config), and with no match it says No more matches. The pattern is the first value of register /, or of the register of the last /, ? or *, and "a before n reads register a once. The pattern uses the regexp syntax of Go, which differs from the Rust regex syntax of Helix in some details. ^ and $ match at each line. Go’s regexp runs in time linear in the text, so no pattern makes dk hang (A2). A pattern with no upper case char matches in any case, and any upper case char, also in a class such as [A-Z] or an escape such as \S, makes it match case (smart-case, Config). A pattern that does not compile says Invalid regex: (, with control characters as glyphs (A5).
  • * puts the text of each Range in register / as a regex and says register '/' set to '\bfoo\b', as in Helix, so n then goes to the next foo that is a whole word. * adds \b at a Range edge that is the edge of a word, and A-* adds none. g * in Normal and Select mode does the same as A-*, as Vim’s g*. Go’s \b holds only next to an ASCII letter, digit or _, so dk adds no \b next to another letter, and * on café gives \bcafé, which also matches the start of cafés. Each text has its regex characters escaped, and the texts join with |, each text once, in the order of the Ranges. "a* puts the regex in register a, and n and N then read register a. Register / keeps the newest 100 patterns (A2).
  • C-f and PageDown scroll the View down by its text rows, and C-b and PageUp scroll it up, as in Helix, in Normal, Select and Insert mode. When the View leaves the primary cursor behind, only the primary Range moves, to the start of the line 5 rows inside the View, or fewer in a short View. The other Ranges stay, and in Select mode the primary Range keeps its anchor. A page puts the top row of the View at most on the last line that ge goes to, so the empty line after a trailing line break is never alone in the View, and the primary Range moves at most to that line. So after C-f to the end, H, M and L go to a line in the View, and the View does not scroll. The line scrolls and the wheel stop at the same line.
  • C-d and C-u scroll the View by half its text rows and move every Range by as many lines, at the same column, in Normal and Select mode, where each Range extends. A Range stops at the first and the last line.
  • Each View scrolls by its own height, and a page key uses no count. In a diff session a page moves the rows of both Views, filler rows too, so they stay lined up.
  • % selects the whole text as one Range, in Normal and Select mode, so %d empties it in one undo step.
  • x selects the lines of each Range, from the start of the first through the line break of the last, as Helix’s extend_line_below, in Normal and Select mode. On a Range that is those lines already, x adds the line below, so a second x takes one more line. 3x on a part of a line selects three lines, and on whole lines it adds three. The Range stops at the end of the text. Ranges merge only when they overlap: two Ranges on lines next to each other touch after one x, and merge after the second.
  • mm moves the cursor of each Range to the bracket that matches the one under it, as in Helix, and in Select mode each Range extends. With no grammar, the cursor must be on one of (), {}, [], <>, ‘’, “”, «», 「」 and (), and dk counts the brackets of that kind for at most 10,000 characters (A2). With a built grammar, mm uses the syntax tree, so it works from inside a pair too, and quotes pair as well, and a bracket inside a string never pairs with one outside it. Off a bracket, or with no match, the Range stays. The syntax tree comes from a parse when you press the keys, which stops after 100 ms, and a file over 8 MiB uses the plain text (A2).
  • In the focused View, the bracket or quote that matches the one under the primary cursor draws in the theme scope ui.cursor.match, over its other styles, as in Helix. It is the pair of the syntax tree that mm finds from a bracket, so it needs a built grammar. Nothing draws off a pair, with no match, or when the theme does not set ui.cursor.match itself. The syntax tree comes from a parse after the text changes, only while the cursor is on a pair. The parse stops after 100 ms, and a file over 8 MiB shows no match (A2).
  • mi and ma and a pair character select inside and around the pair around each Range, as in Helix: mi( selects the text between ( and ), and ma( selects the pair too. The open or the close names a pair, and any other character that is not an ASCII letter or digit is its own pair, as in mi" or mi*. mim takes the closest pair: a bracket pair with no grammar, and a pair of the syntax tree, quotes too, with a built grammar. A count takes the pair that many levels out, so 2mi( selects inside the second ( around the Range. A Range with no pair stays. While dk waits for the character, the last row shows the keys so far, Esc cancels, and : still opens the prompt. So : is never the pair of mi, ma, ms, mr or md. miw and maw select the word at the cursor, miW and maW the WORD, which only spaces end, and mip and map the paragraph, as in Helix. maw takes the spaces after the word, or with none there the spaces before it, and map takes the blank lines after the paragraph. A count takes that many paragraphs. In Select mode the Range does not extend: it becomes the object. The word and paragraph objects walk Graphemes, as the word motions do. The keymap plugin gives these objects through move-word, so a keymap.wasm in the config directory that does not know their names makes them do nothing.
  • mi and ma with f, t, a, c, T, e or x select the function, type, argument, comment, test, entry or (X)HTML element at each cursor, as in Helix. The object is the smallest node that holds the cursor, of the capture function.inside or function.around, class.inside or class.around, and so for the others, in the textobjects.scm of the language. A match of more than one node, such as (comment)+, is one object. With no grammar, no textobjects.scm or no such node the Range stays, and an object that ends at the end of the text is not selected, as in Helix. One query runs for all Ranges of a key, on the language of the Document, not on an injected layer. The query stops after 500 ms, and then every Range stays and the last row says so (A2).
  • A-o or A-up selects the parent node of each Range in the syntax tree, as in Helix, and A-i or A-down goes back the same steps. With no step to go back to, A-i selects the first child node. A-n or A-right and A-p or A-left select the next and the previous named sibling node, so they go from one argument to the next and step over the commas. A-a selects each named sibling node as a Range, and A-I or A-S-down selects each named child node, such as each statement of a statement list. In Go a block holds a statement list, so A-I on a block selects the list, and a second A-I selects the statements. The keys work in Normal and Select mode, and in Select mode the Range does not extend. Each View keeps its own steps for A-i, and a new split starts with none. An edit drops the steps, and so does A-i on a Selection that does not hold the last step. With no grammar the keys do nothing. They use the tree of the language of the Document, not an injected layer. The parse stops after 100 ms and the walk of the tree after 500 ms, and a key that makes more than 10,000 Ranges keeps the Selection and says so (A2).
  • mig and mag select the whole lines of the diff change at the cursor line of each Range, toward the Range, as in Helix, so migd deletes a change in one undo step. A line in no change keeps its Range, the line of a removal puts the cursor at the start of that line, and with no diff the last row says Diff is not available in current buffer.
  • ms, mr and md add, replace and delete a pair around each Range, as in Helix. ms( puts ( before and ) after each Range, and the Range grows to hold them. ms and Enter put a line ending on each side. mr( puts a cursor on each end of the pair, and the next character replaces the pair, so mr([ changes (a) to [a]. A key that is not a character gives back the Ranges. The second key of mr does nothing when the Document changed after the first key (A2). md( deletes the pair. mrm and mdm take the closest pair, and a count takes the pair that many levels out. When one Range has no pair, or two Ranges have the same pair, the command changes nothing and the last row shows the error.
  • One of these keys reads at most 8 MiB of text over all Ranges. Past that, every Range stays and the last row says “the pair search stopped at its limit” (A2).
  • I enters Insert mode at the first character of the line that is not a space or a tab, and A at the end of the line, as in Helix. Each Range uses its cursor line, and in Select mode each Range keeps its anchor. On a line of spaces, I goes to the start of the line. On an empty line, I and A put the indent of a new line there, as Helix does.
  • o adds a line below the line of each Range, and O adds one above, in the line ending of the file, and both enter Insert mode, as in Helix. A count adds that many lines, each with its own cursor, and the new cursors stop at 10,000 with the note “too many ranges” (A2). The new line, the line break and the text typed after it are one undo step. dk does not continue a line comment on the new line yet.
  • A new line from Enter, o or O gets the indent that Helix gives it. With a built grammar that has an indent query, the indent is the number of @indent scopes that hold the new line, less the @outdent tokens that start it. That is Helix’s tree-sitter heuristic. The default, hybrid, adds the difference that the tree gives to the real indent of an earlier line, from at most 4 lines that are not blank. So a file indented in its own way keeps its way. A new line inside a Go raw string keeps the leading spaces of the line before it. indent-heuristic in [editor] of config.toml picks simple, tree-sitter or hybrid, and a trusted .ductus can set it too. Another value gives a note, and dk uses hybrid.
  • With no grammar, with no indent query, or with simple, the new line gets the indent level of its line, in the indent unit of the file. dk finds the unit when the file opens, as Helix does: the increase of the indent that the first 1,000 lines show most, in tabs or in spaces. When the text shows none, the unit comes from indent = { tab-width, unit } of the language in languages.toml, or else it is a tab. The tab width comes from the language too, or else it is 4, and the shipped go entry has a tab of width 4, as in Helix. A tab in the file draws at that width. A tab width that is not 1 to 16 (A2), or a unit that is not a tab or 1 to 16 spaces, is a languages.toml error. The indent level reads at most 256 characters of a line (A2).
  • The indent queries of one key stop after 500 ms, the highlight budget, and a file over 8 MiB uses the indent level, so a key never holds dk long (A2). An indent never fetches or builds a grammar (A10).
  • v enters Select mode, and the status line shows SELECT. There h, j, k, l, the arrows, w, b, e, W, B, and E extend every Range instead of moving it, as in Helix: each Range keeps its anchor. Esc or v goes back to Normal mode, i enters Insert mode, and : opens the prompt, which returns to Select mode.
  • g v in Normal mode selects the last Selection of the View again, with all its Ranges and the same primary Range, as Vim’s gv. The last Selection is the newer of two: the Selection from just before Select mode last ended, and the Selection after x or X in Normal mode. g v enters Select mode for the first, and stays in Normal mode for the second, so x x j g v selects the two lines again. Select mode ends with v, Esc, or a key that leaves it, such as d or c. Other keys that select, such as w, and x or X in Select mode, make no last Selection. In Select mode g v swaps the Selection and the last one and stays in Select mode, and a second g v swaps them back. With none, g v says No last selection and changes nothing. Each View keeps its own for each file, a split starts with a copy, an edit moves it with its text, and :bc of the file drops it (A1).

Grammars

  • dk grammar fetch gets the source of each grammar that use-grammars in languages.toml picks, at its rev, into $XDG_STATE_HOME/ductus/grammars/sources, or ~/.local/state/ductus/grammars/sources. It fetches up to 8 grammars at once, and prints the line of each grammar in the order of the grammars, so the output is the same from run to run (A2). A fetch that fails does not stop the others, and an interrupt stops each fetch and starts no new one. dk grammar build builds one grammar at a time. dk ships the 338 languages and 297 of the 299 grammars of Helix d5d88b62, each grammar at the rev that Helix pins, which its queries are written for. It ships the language servers of Helix with them: the language-servers, roots and language-id of each language, and each [language-server] table. wren and gemini stay out, as Helix builds neither, and their languages stay, with plain text. dk reads use-grammars, with only or except, and the name and source of each [[grammar]] of languages.toml, with git, path, rev and subpath in source. Of a [[language]], it reads auto-format, auto-pairs, block-comment-tokens, comment-token, comment-tokens, file-types, formatter, grammar, indent, injection-regex, language-id, language-servers, name, roots, rulers and shebangs. Inside those keys it reads the start and end of a block comment token, the glob of a file type, args and command in formatter, tab-width and unit in indent, and the name of a language-servers entry. Of a [language-server.<name>], it reads args, command, config, environment and timeout, and it takes config whole. The shipped languages.toml holds no other key of these tables, but for the except-features of a few language-servers entries, which dk does not read yet. languages.toml in the config directory adds languages and grammars in Helix format. A [[language]] of a shipped name changes only the keys it sets, as Helix merges the two, and a key it sets replaces the shipped value whole. A [[grammar]] of a shipped name replaces its source. A comment-token or comment-tokens of a user table replaces both names in the shipped table. A table that sets both reads comment-tokens.
  • use-grammars = { only = [...] } or { except = [...] } before the first [[language]] picks the grammars, in Helix format. With no use-grammars, fetch and build pick every grammar, shipped and of your config, as Helix does. The yaml grammar has a C++ scanner, so a build of every grammar needs a C++ compiler. { only = ["go"] } picks go only, and { except = ["go"] } picks every grammar but go. A name there that no grammar has gives a warning, and when a language or file type has that name, the warning names its grammar: tsq gives query.
  • dk grammar fetch rust go and dk grammar build rust go act on the named grammars only, whatever use-grammars picks. A name that languages.toml does not have is an error, and nothing runs.
  • fetch and build are the commands of dk grammar, so dk grammar with no command shows its help, and another word, such as dk grammar pull, gives unknown command "pull" for "dk grammar" and exits 1.
  • use-grammars in a .ductus/languages.toml changes nothing, trusted or not, and dk says so (A9, A10).
  • A git source must be one commit of an https repository. dk refuses a source with no rev, a branch, a tag name, a short commit id, or another URL scheme, and runs no git for it (A10). A rev can be the full id of an annotated tag, such as the pin of embedded-template, and the checkout is then at the commit of the tag. A checkout that is not at the rev goes, and the fetch fails. A checkout at the rev stays, so a second fetch gets nothing again.
  • git runs with no shell and none of your GIT_ variables, takes only https, and never asks for a password. What git prints shows quoted, so it cannot move the terminal cursor (A5).
  • dk grammar build compiles each fetched grammar with $CC, or cc, into grammars/<name>.so, mode 0600. It builds only a checkout at its rev, src must stay inside the checkout, and parser.c and scanner.c or scanner.cc must be regular files, not links. A build runs code of the grammar, so only a pinned source or a path source from your config builds (A10). A scanner.cc builds with $CXX, or c++, which then also compiles parser.c, as C, and links the grammar, so the C++ library loads with it. A grammar with both scanner.c and scanner.cc builds scanner.c, as Helix does. What a compiler prints shows quoted (A5). After these checks, a build skips a grammar whose .so is not older than parser.c and its scanner, and prints <name>: already built, as Helix does. A fetch of a new rev, or an edit of a path source, builds again, but a change of $CC or $CXX does not.
  • source = { path = "/home/you/tree-sitter-mine" } in your languages.toml names a grammar in a directory of yours. dk grammar build compiles its src and runs no git, and dk grammar fetch skips it and says so. The path must be absolute, so it never names a directory in the working directory (A9). The directory, its src and the C and C++ files must be yours, not links, and no other user can write them (A10). The error on stderr names the file that fails, quoted (A5), and says why, and for a write bit of the group or other users it adds chmod -R go-w with the directory. As in Helix, a path in a source wins over its git, rev and subpath.
  • A [[grammar]] in a .ductus/languages.toml never fetches or builds, a path source too, trusted or not, and dk says so (A9, A10).
  • The grammars directory gets the checks of the state directory: dk refuses a symlink there or a directory of another user, and gives mode 0700.
  • A file of a language whose grammar is built shows its syntax in the colors of the theme. dk picks the capture as Helix does: the innermost node wins, and on the same bytes the last pattern of the query wins. dk queries the colors of the rows that show: the rows of each View with one page above and below, the sticky rows, and the rows of a picker preview. So a key in a Go file of 2000 lines gets its colors in less than one frame. After a jump or a scroll past those rows, the new rows show without color until their highlight ends. dk ships the 341 highlights.scm of Helix d5d88b62, for the grammar revs that dk pins. dk reads the locals.scm of a language, as Helix does, so a pattern with #is-not? local skips a name that a parameter or a variable in its scope declares, such as a parameter named console in TypeScript. A use of a name after its definition in scope takes the highlight of that definition, as in Helix, so a Go or Python parameter in the function body shows as variable.parameter. dk adds definitions of its own to the shipped locals.scm of JavaScript, TypeScript and Python, and of the languages whose locals.scm inherits theirs, such as JSX, TSX and Mojo. In JavaScript and TypeScript, a let, const or var declarator, and a function or class declaration, are definitions. In Python, the target of an assignment, of a for and of a with ... as, and a function or class definition, are definitions. A definition on the name of a node that is a scope of locals.scm, such as a function or a TypeScript class, holds in the scope around that node, so a call of the function after its body, or above it when the language hoists, names the definition. In the body, a parameter or a local of the same name comes before it. A user locals.scm of a language replaces the shipped one and these definitions of that language. A name that is defined again takes the highlight of its latest definition before the use, in its scope or in a scope around it: in a Go function with a parameter x, a use of x above x := 2 in an if block shows as variable.parameter, and a use below it as variable. With hoist-locals = true in a [[language]] of languages.toml, a definition holds for its whole scope, also above it: a use takes the latest definition before it in its scope, or else the first one after it, and only with neither does it look in the scope around it. The shipped languages.toml sets hoist-locals = true for JavaScript, JSX, Python, TSX and TypeScript, and other languages do not hoist. So with the shipped files, the x of console.log(x) in function f(x) { { console.log(x); let x = 2; } } takes the highlight of the let x, not of the parameter. A var hoists only in its block, not in its whole function. hoist-locals = false in a user languages.toml gives back the latest definition before the use. Another locals.scm capture on the same node, such as the @_ on the key of a Go struct literal, keeps the highlight of highlights.scm. A locals.scm that does not compile gives a note, and the highlights work without it. A layer with more than 100,000 scopes and definitions in its locals.scm gets none, so its #is-not? local patterns apply to every name (A2). A use past the first 100,000 uses in a layer keeps the highlight of highlights.scm (A2). dk counts each local.definition.<name> capture, whether or not the theme has a scope of that name. dk ignores the #has-ancestor? predicate, which no shipped query has. queries/<language>/highlights.scm in the config directory, or else in the runtime directory, replaces it. The directory has the name of the language, as in Helix, so two languages of one grammar, such as hcl and tfvars, have their own queries. A language name with / or \, or one that is empty, . or .., is a languages.toml error, so its queries stay under queries/ (A9).
  • dk ships the 232 injections.scm of Helix d5d88b62 too, and queries/<language>/injections.scm in the config or runtime directory replaces it in the same way, with inherits. Text that an injection query names highlights with the grammar and the queries of its own language, as in Helix. Markdown shows emphasis, links and inline code with markdown.inline, the code of a fence with the language of its info string. A language is found by its name, or else by the longest match of its injection-regex, so a fence named ts finds typescript. The injection-regex is a Go regular expression, and one that Go refuses is a languages.toml error. A #set! injection.language, an @injection.shebang and an @injection.filename find a language too. injection.combined, injection.include-children and injection.include-unnamed-children work. When two injections overlap, the outer node wins, and then the last pattern. An injected language whose grammar is not built shows plain text and no note, and an injection never fetches or builds a grammar (A10). An injection query that does not compile gives the language no highlight and one note, as in Helix. One highlight stops at the 500 ms budget, and keeps the spans of the layers that ended (A2). It finds at most 4096 injections, and layers inside layers go 8 deep. A name over 256 bytes finds no language.
  • dk ships the 158 textobjects.scm of Helix d5d88b62 too, and queries/<language>/textobjects.scm in the config or runtime directory replaces it in the same way, with inherits. dk ignores the #make-range! and #offset! directives of a textobject query, as Helix does.
  • dk ships the 77 tags.scm of Helix d5d88b62 too, and queries/<language>/tags.scm in the config or runtime directory replaces it in the same way, with inherits. A tags.scm that does not read or compile gives one note, with its error quoted (A5). dk ignores the #strip!, #select-adjacent! and #is-not? predicates of a tags query, because they touch only captures that dk does not use.
  • Brackets draw in the colors of the rainbow list of the theme, by how deep they nest, as in Helix. [editor] rainbow-brackets = false turns them off, and rainbow-brackets in a [[language]] of languages.toml takes its place for that language. The default is true. dk ships the 91 rainbows.scm of Helix d5d88b62 too, and queries/<language>/rainbows.scm in the config or runtime directory replaces it in the same way, with inherits. A language with no rainbows.scm shows no rainbow, and the text of an injection takes the brackets of its own language. The text of an injection goes on from the level of the scopes around it, so the parens of println!(...) in the body of a Rust function take the second color. Each @rainbow.scope node holds one level more, and a @rainbow.bracket takes the color of its level, modulo the length of rainbow, when its parent is the scope node or the scope has rainbow.include-children. So in the Go line func m() { f(a[g(b)]) } the braces take the first color, the parens of f the second, [ and ] the third, and the parens of g the fourth. rainbow is a list of styles, with the names of the palette and modifiers, such as rainbow = ["red", { fg = "blue", modifiers = ["bold"] }]. A theme with no rainbow gets red, yellow, green, blue, cyan and magenta, and a bad entry is an error of the theme. The rainbow draws over the syntax color, and a diagnostic draws over the rainbow. The brackets come with the highlight, in its 500 ms budget, so a file over 8 MiB shows none (A2). An edit keeps each color on its bracket until the next highlight ends. A rainbows.scm that does not compile gives one note, with its error quoted (A5), and the highlights work without it.
  • dk ships the 76 context.scm of nvim-treesitter-context 8decd734 whose language dk has, under the MIT License in LICENSES/MIT-nvim-treesitter-context. The svelte, tact and tcl queries do not compile with the grammars that dk pins, so dk does not ship them.
  • dk ships muttrc, which Helix does not have: the grammar of tree-sitter-muttrc at 748101a3, and its highlight, injection and textobject queries, under the MIT License in LICENSES/MIT-tree-sitter-muttrc. A file named muttrc, neomuttrc, .muttrc or .neomuttrc, or one that ends in .muttrc, is muttrc, and dk grammar fetch muttrc and dk grammar build muttrc get its grammar. The queries use the capture names of Helix, so set shows in the keyword style of the theme, the text of a quoted string in the string style, and mic in a comment selects its text.
  • dk ships the 150 indents.scm of Helix d5d88b62 too, and queries/<language>/indents.scm in the config or runtime directory replaces it in the same way. An indent query takes the captures and predicates of the Helix indent guide. @extend and @extend.prevent-once do nothing in dk: in the Helix d5d88b62 indent they change no indent of a new line. A query with a predicate or a #set! that Helix refuses gives one note, and the indent is the indent level.
  • A line ; inherits: a,b in a highlight query puts the queries of a and b in its place, as in Helix. dk finds each one in the config directory, then the runtime directory, then the queries it ships, and a missing one is empty. A query that inherits itself, reads more than 256 files, or holds more than 1 MiB does not load, and dk gives a note (A2).
  • Opening a file never fetches or builds a grammar. A grammar that is not built yet and is not in the runtime directory gives plain text and no note.
  • The nix package installs each grammar of the shipped languages.toml that builds under nix in the runtime directory, read-only, at its rev and subpath, so a Go, Python or Rust file shows its syntax after a nix install with no dk grammar run. brokenGrammars in default.nix lists each grammar that does not build, with the reason. grammar-hashes.json holds the hash of the source of each grammar, and task hash-grammars writes it again from languages.toml. A rev with no new hash fails the nix build with the name of its grammar.
  • dk loads grammars/<name>.so from the state directory, or else from the runtime directory, so a grammar that you build replaces the one in the runtime directory. A grammar in the state directory that fails a check gives a note, and dk does not load the runtime one in its place.
  • dk loads a grammar only from a grammars directory and a file of yours that no other user can write, and only one built for the tree-sitter of dk (A3, A9, A10). In the runtime directory, root can own the directories and the file too, as root owns the nix store (A10).
  • One highlight stops after 500 ms, and a file over 8 MiB gets no highlight (A2). An edit highlights the new text. Until that highlight ends, each color stays on its text. Text that the edit adds inside a colored token takes its color, and other added text has none. A reload by :reload, :reload-all or a change on disk does the same for each line that it keeps. When the first line of the file now gives another language, the old colors go, and the next highlight gives the new ones.
  • A trusted .ductus/languages.toml can map a file type to a grammar of your config, and an untrusted one maps nothing (A9).
  • A file gets its language as in Helix: a { glob = "Makefile" } file type matches the whole path first, and the longest glob that matches wins. A glob that does not start with / or */ matches in any directory. A * in a glob stays in one directory and never matches a /. Else the extension picks the language, and a later language in languages.toml wins it. A string file type is an extension only, so ["Makefile"] matches no Makefile, and a dotfile such as .bashrc has no extension. A file type table with no glob, another key, or a glob that does not parse is a languages.toml error.
  • With no file type match, the first line picks the language by shebangs, with the regex of Helix. It skips env and its flags and drops digits, so #!/usr/bin/env python3 names python. dk reads the first line when the file opens and when languages.toml loads again, so a shebang typed later counts from the next load.

Install

  • Each release has dk-<os>-<arch>-<tag>.tar.gz for darwin and linux on amd64 and arm64, and no bare binary. It holds bin/dk, the themes in share/ductus/runtime/themes and the license texts in share/doc/ductus/LICENSES, under the top directory dk-<os>-<arch>-<tag>/. So bin/dk, or a link to it, finds the themes in its runtime directory. It holds no grammars, because each grammar is native code for one OS and CPU: dk grammar fetch and dk grammar build make them.
  • The flake gives homeModules.default, a home-manager module that sets programs.ductus:
    • defaultEditor = true sets EDITOR to dk.
    • enable = true installs finalPackage.
    • extraPackages, such as [ pkgs.gopls ], go on the PATH of dk, after the PATH of the shell.
    • finalPackage is package when extraPackages and grammars are empty. With grammars, it has a runtime directory of its own, which holds the runtime directory of package and the grammars, and its bin/dk sets DUCTUS_RUNTIME to it when your environment does not set DUCTUS_RUNTIME. The runtime directory, its grammars directory and each .so are copies, because dk refuses a link there (A10).
    • grammars.<language> is a grammar whose parser file dk loads as <language>.so, so the language shows its syntax with no dk grammar build. package holds each grammar of the shipped languages.toml that builds under nix, so grammars is for a grammar that it does not have, such as grammars.readline = pkgs.tree-sitter-grammars.tree-sitter-readline for a readline language that languages adds. A name that dk cannot load, such as one with a /, fails the home-manager assertions (A10).
    • ignores, languages, plugins.<name>, queries.<language>.<name>, settings and themes.<name> write ignore, languages.toml, plugins/<name>.wasm, queries/<language>/<name>.scm, config.toml and themes/<name>.toml in the config directory, as links into the nix store. languages is an attrset, a Plugin is a .wasm file, a query is text or a file, settings is an attrset, and a theme is an attrset or a TOML file. A list in languages, settings or themes.<name> fails evaluation, with an error that names the option and “attribute set”. plugins.keymap, plugins.picker and plugins.vcs replace the shipped Plugins.
    • package is the package of the flake by default.
  • The flake gives each grammar that the shipped queries are written for, at the rev of the shipped languages.toml, as packages.<system>.grammar-<language> (Q19.5). They are go, hcl, markdown, markdown_inline, python, ruby, rust, typescript and yaml.

Keys

  • The keymap is a Default plugin. With no config it binds the Helix keys of the commands that dk has, and the keys that dk adds. docs/manual.md lists them under Default keys, for each mode, with the command of each key and what it does.
  • [keys.normal], [keys.insert], [keys.select], and [keys.block] in ~/.config/ductus/config.toml change the keys, in Helix format. Block mode takes each key that [keys.block] does not bind from Select mode. dk reads $XDG_CONFIG_HOME/ductus/config.toml instead when XDG_CONFIG_HOME is set. Each key takes one command name, such as x = "move-character-right".
  • docs/manual.md lists each command name that a key can take, with what it does, under Key commands.
  • ]g and [g select the next and the previous change, and ]G and [G the last and the first, in Normal and Select mode, as in Helix. A count skips changes, and stops at the last one. A removal selects one character where the lines were. Without a diff, ]g and [g say Diff is not available in current buffer.
  • ]p and [p select to the start of the next and the previous paragraph in Normal mode, and extend to it in Select mode, as in Helix. A count goes over that many paragraphs, and they need no grammar.
  • ]f and [f select the next and the previous function, and ]t, ]a, ]c, ]e, ]T and ]x and their [ keys the next and the previous type, argument, comment, entry, test and (X)HTML element, as in Helix. A jump takes the nodes of the capture function.movement, or else function.around, or else function.inside, of the textobjects.scm of the language, and so for the others. The Range covers the node, toward the jump, and in Select mode the anchor stays and the Range extends. A count goes over that many objects, and a jump stops at the first and the last one. With no syntax tree the last row says Syntax-tree is not available in current buffer, and with no textobjects.scm the Range stays in place, and in Normal mode it turns toward the jump. The query stops after 500 ms, and then every Range stays and the last row says so (A2).
  • gw in Normal mode puts a two-character label over the first two characters of each word in the View, as Helix’s goto_word, and the next two keys pick a label. A word has two or more word characters, so =< and a one-letter word get no label, and neither does the word under the cursor. The nearest word after the cursor gets aa, the nearest one before it ab, the next one after it ac, and so on out from the cursor, up to 676 labels with the default [editor] jump-label-alphabet (Config). Only the focused View gets labels, and only on words that show in it, so a word scrolled out to the left or the right, or under a sticky row, gets none. A label covers the first characters of its word, and the text does not move. A label selects its word with the cursor at its end, as the only Range, and saves the place on the jumplist first. In Select mode gw extends the primary Range to the word, and the end of the Range away from the word stays. Esc, a key with a modifier, such as an Alt key, a key that is in no label, or a first key that starts no label removes the labels and does nothing else. : removes the labels and opens the prompt, as in every wait for a key, so :w and :q stay one key away. A resize, or a change of the text or the scroll of the View, such as a reload, also removes them, so a label never selects a stale word (A2). The labels draw in the theme scope ui.virtual.jump-label, or in error when the theme does not set it. A count does nothing.
  • A key can have the Helix modifiers C-, A-, and S-, such as C-s, A-x, or S-tab. S- goes with a named key, such as tab or left. For a capital letter, write the letter. A named key with no modifier, such as left, also takes a modified key that no binding has.
  • A table binds a key sequence, as in Helix: g = { g = "move-character-left" } runs on g, then g. The last row shows the keys so far until the sequence ends. A key that does not go on with the sequence drops the keys so far and does nothing.
  • After a key that starts a sequence, such as g, m, [, ], C-w or space, a box at the bottom right shows the keys that can come next, as Helix’s info box does. Its title is the Helix name of the group, such as Goto or Window, or the keys so far for a group that only user config makes. Each row has the keys and the desc that :help shows. Keys with the same desc share one row, in Helix’s key order, such as left, h, and a key that starts a deeper group shows that group. The rows of each of these boxes are sorted by the key that can come next, in every mode, for the default keys and for the keys of user config: in byte order with case ignored, and a lower case key first, such as d, D, e, g. A named key sorts by its name, so minus comes after m. A row sorts by its shortest key name with the modifiers C-, A- and S- dropped, so left, h sorts as h, o, C-o as o and pageup, C-b as b. When two rows sort as the same key, the row whose key has no modifier comes first, so b comes before pageup, C-b. The next key closes the box, and Esc closes it and drops the keys so far. mi, ma, ms, mr and md show the box of their command until the next key, with Helix’s titles and texts, and mr shows a second box until its second key. auto-info = false in [editor] of config.toml turns the boxes off, and a trusted .ductus can set it too. A box fits above the status line and the command line: the rows that fit, and a last row that counts the rest, each cut at the border (A2). A key name from user config shows as glyphs, so a control character never reaches the terminal (A5).
  • [keys] takes descriptions, a dk extension: description = "..." in the table of a group names it, also over the Helix name of a default group, and a binding can be { command = "...", description = "..." }. A plain string stays a plain binding. The box and :help show the text as glyphs (A5), and a trusted .ductus can describe its keys too. A description is text of at most 80 characters (A2). Another one, one in a mode table, or a binding table with another field gives a warning, and the default stays. Helix has no such field: it refuses a config that holds one, as it refuses the [plugins] table of dk. A sequence in the config replaces a default key that starts it, such as h for h = { h = ... }.
  • A [keys] value is a command, a list of commands, or a table of keys, as in Helix. A string that starts with : is a typed command, such as C-s = ":write", and its args stay text. A list runs its commands in order, such as C-t = [":write", "goto-document-start"]. A typed command with %sh{...} runs when its shell ends, so the commands after it in the list run first. A typed command runs as Enter on the command line runs it, and an error goes to the status line. { command = ..., description = ... } takes the same string or list. The box and :help show the desc of a typed command, or :name "args" with args, and [Multiple commands] for a list. A [keys] description wins. An unknown typed command, an empty list, an item that is not text, and an unknown item each give one warning, and the key keeps its old binding.
  • A .ductus/config.toml, trusted or not, cannot bind some commands, also as an item of a list (A9). They are :run-shell-command, :sh, :!, :insert-output, :append-output, :pipe, :|, :pipe-to, :print-document and :trust, a typed command whose args hold %sh and then one of (, [, {, <, ', " or |, and :write, :w, :write-quit, :wq, :x, and their ! forms, with a path. dk drops such a binding with one warning, as it drops an exec grant there, and the key keeps its binding from your config. The same commands with no args, such as ":write", bind from a trusted .ductus. The key commands pipe, pipe-to, insert-output and append-output bind from it too, because they only open the prompt, and nothing runs until you type a command and press Enter.
  • A key stays unbound when it names a key command that dk does not have, S- and one character, a key name that holds a space, or a mode other than Normal, Insert, Select, and Block. A key that starts another binding, or that another binding starts, stays unbound too. dk lists each one in a single warning on the last row when the keymap loads.
  • : in Normal and Select mode and Esc in Insert mode always keep their default, so :w and :q stay one key away. A config or a plugin that binds them to another command gets a warning. They run while keys are pending, and a key sequence that holds one stays unbound.
  • A-: stays unbound in every mode and gets a warning, because a terminal without the kitty keyboard protocol or Win32 input mode sends a fast Esc and : as A-:.
  • plugins/keymap.wasm in the config directory replaces the Default keymap plugin. The plugin names its sticky menus in the sticky list of the resolved record, and a plugin built before that list has no sticky menu.
  • If the keymap plugin does not load or traps, dk uses its Emergency keymap. :reload-config reads the config and its theme again and loads the plugin again. There u and U still undo and redo, and A-u is Esc, then u.
  • A capital letter binds the same key whether the terminal sends it as the capital or as Shift and the letter, as the kitty keyboard protocol does.
  • Alt, Shift and a digit or a sign read as the sign that the layout gives, under the kitty keyboard protocol, xterm’s modifyOtherKeys and Win32 input mode. So on a German layout Alt, Shift and 7 give A-/, and on a US layout Alt, Shift and / give A-?. A kitty terminal that sends no shifted key gets the sign that Shift gives on a US layout, such as A-( for Alt, Shift and 9.
  • Shift types the shifted text in a kitty terminal that sends the key with no text, in Insert mode, on the prompt line and in the picker query, and the completion menu filters by it. So Shift and a types A, and Shift and 1 types the shifted key that the terminal sends, or else ! as on a US layout.
  • dk acts on a key press and on a held key that repeats. A key release does nothing. A modifier or a lock key alone, such as Shift or AltGr, does nothing too, so it does not end a key sequence or a wait for a key.

Language servers

  • Opening a file starts the servers that language-servers of its language in languages.toml names, as Helix does. dk ships gopls and golangci-lint-lsp for Go. One server runs for each LSP root, and each file of the language in it opens in that server. The LSP root is the topmost directory from the file up to its workspace with a marker of roots, such as go.mod, or else the workspace.
  • Each edit, undo and redo sends the full text to the servers of the file, a save tells each server that asks for it, and :bc closes the file in them.
  • dk starts a server only in a trusted workspace, because a server reads config from it, such as go.mod (A9). Until then the last row says <server> needs trust: :trust allows it once for each workspace, and :trust starts the server. A file in no workspace gets no server. A Document with no path, such as the base side of :diff-base, gets no server, also when it has a language.
  • When the .ductus of a workspace changes, for example after a pull, :reload-config stops the servers of that workspace, and the last row says <server> needs trust: :trust allows it once (A9). :trust then starts them again for each file of the workspace. The servers of another workspace that is still trusted keep running. A server that does not exit is killed after 1 s, as at quit.
  • A trusted .ductus/languages.toml can define a server with its command, args, environment, and config, and an untrusted one defines none (A9).
  • A server command must be a program name on PATH or an absolute path, with no control character, or dk says so and does not start it (A5, A9).
  • A server whose program is not installed gives no note, as in Helix. One that does not start for another reason, or that exits, gives one note, such as Language server exited: gopls, and dk goes on.
  • A message that a server shows goes to the last row, and its log goes nowhere. A server that does not answer initialize within its timeout, 20 s by default, stops with one note.
  • dk refuses a message of a server over 32 MiB, a header line over 4 KiB, and more than 64 header lines. The write queue holds at most 1024 messages and 64 MiB, and dk stops a server that does not read its stdin (A2).
  • Each line shows a diagnostic of a server after its text, as Helix does: of those that start on the line, the last one of the highest severity, in <severity>.diagnostic.inline. The lines of the message are two spaces apart, and a message that does not fit ends in …. A control character in it shows as a glyph (A5).
  • The text of a diagnostic is in diagnostic.<severity>, or else diagnostic, which sets the underline in a Helix theme.
  • An edit moves the diagnostics with the text, and one whose text it deletes goes. A diagnostic for an older version of the file, or for a file that is not open, is dropped, and dk keeps at most 1000 for each server and file (A2). The diagnostics of a server that exits go.
  • An answer that two servers both give shows once, the one of the server that is first by name. Two locations are equal with the same path and start line, whatever the column. So are two symbols with the same name and start line, two completion items with the same label and text to insert, two code actions with the same title, and two hovers with the same text. The answers of one server all stay, so two references on one line both show. Goto with one location from each of two servers jumps, with no picker.
  • gd, gD, gy, gr and gi in Normal and Select mode ask the servers of the file for the definition, declaration, type definition, references or implementation at the cursor, as Helix does. One location opens its file, as :open does, selects the range with the cursor at its start, and puts the line in the middle of the View. More than one opens a picker of path:line:column rows, such as flake.nix:56:21, with the path from the working directory, and the first 100000 at most. The column is the start of the location, in Graphemes from 1, as %{cursor_column} counts it. To count it, dk takes the text of an open file, with its unsaved changes, and reads each other file of the rows once, whole, in the order of the rows, when the picker opens. These reads take at most 16 MiB and 1,000 files together, also the reads of binary files (A2). They stop after the 1,000th file, and at the first file that does not fit in what is left of the 16 MiB, and the row of each file after it shows path:line, also of a small file. The stop and the cap apply to the files that dk reads, and an open Document always gives its column. A file over 10 MiB is not read and does not stop the reads. A row whose file does not read, is binary, is over 10 MiB, or does not hold the line shows path:line too (A1, A2). The rows go by path, then by line as a number, then by column, so flake.nix:80 comes before flake.nix:159, and flake.nix:56:21 before flake.nix:56:30. Two rows at the same column keep the order of the replies. The matched characters of each row show in special and bold, as in the file picker. C-s and C-v jump as Enter does, where Helix jumps in a new View. No location gives the Helix error, such as No definition found.. A reply that comes after an edit, or after a move to another file, is dropped. A reply to an older goto is dropped too, when a newer goto came after it.
  • [editor.lsp] goto-reference-include-declaration, default true, says whether gr lists the declaration too.
  • space s in Normal and Select mode asks the servers of the file for its document symbols and opens a picker of them, as Helix does. A row shows the kind, the name, and the container of a flat symbol, and typing in Insert mode filters by the name. A pick selects the symbol with the cursor at its start and puts its line in the middle of the View, as gd does. A reply that comes after an edit, or after a move to another file, is dropped. dk drops a flat symbol of another file, because its range is not in the text of the Document (A1). With no server with document symbols, space s lists the symbols of the syntax tree, from the tags.scm of the language, when the file has a built grammar. A symbol of the syntax tree is a @definition.<kind> capture of the kinds class, constant, enum, field, function, interface, macro, method, module, section, struct and type, so a Go method shows. Its name is the @name capture, or else the whole definition, cut at 1 KiB (A2). The symbols come from the text with its unsaved changes, and a pick maps through the edits made after the picker opened. A text over 8 MiB, or a parse or a query past its budget, gives a note and opens no picker (A2). The picker keeps the first 10,000 symbols of the syntax tree, and its title then says so (A2). With no grammar or no tags.scm either, space s gives No language server supporting document symbols or syntax info available.
  • space S in Normal and Select mode opens a picker of the symbols of the workspace. With a running server of the file that has workspace symbols, it asks the servers, first with the empty query, which asks for all symbols. Typing asks again 100 ms after the last key, and at most one request is out at a time, so a key during a request waits for its reply. The server matches the name, so dk shows the rows in the order of the replies and filters nothing. A row shows the kind, the name, the container, and the path from the working directory. A pick opens the file as :open does and selects the symbol with the cursor at its start, as gd does. A symbol that a server sends with no range gets its range from workspaceSymbol/resolve at the pick, and with no range found dk says No location found for the symbol. dk drops a symbol whose URI is not a local file, keeps the first 10,000 symbols, and cuts a name or a container at 1 KiB (A1, A2). A symbol that two servers both give shows once. space ' reopens the picker, and typing then asks the servers again. With no such server, as in a workspace that is not trusted, space S lists the symbols of the tags.scm queries in the files of the workspace, when the language of the file has a built grammar and a tags.scm. Its query is a regex with smart case, as in space /, and it matches the name of each symbol, so a query under 3 bytes lists nothing. The search starts 275 ms after the last key, walks the root of space f with its ignore files and [editor.file-picker] keys, and reads each file on disk, also an open file with unsaved changes. A row shows the kind, the name, and the path, and the rows come while the search runs. A pick opens the file and selects the symbol with the cursor at its start, and a symbol past the end of a file that changed gives The location you jumped to does not exist anymore because the file has changed.. The search walks at most 100,000 files, reads at most 8 MiB of a file, parses a file for at most 500 ms, and stops at 10,000 symbols or after 10 s, and the title then says which cap stopped it (A2). With no grammar or no tags.scm for the file either, space S gives No configured language server supports workspace symbols.
  • space h in Normal and Select mode selects each use of the symbol at the cursor. It asks the servers of the file for the document highlights at the cursor and makes one Range for each, as Helix does. The Range that holds the cursor is the primary, and a highlight that two servers both give makes one Range. With no server with document highlight, dk uses the locals.scm of the language. On a definition, or on a reference that names one, it selects that definition and each reference in its scope that names it. So in Go, space h on a local variable selects it in its function, and not a variable with the same name in another function. Each definition of a name is its own local, with the references after it up to the next definition of that name: after a, err := f() and b, err := g(), space h on the first err selects it and the uses of err above the second :=. With hoist-locals = true for the language, a definition also holds for the references above it in its scope, as in the highlights. dk does not join two definitions of one name, as Go’s := on a name that it defines again would need, because in other languages, such as Rust’s let, the second definition is a new variable. With neither source, space h gives No configured language server supports document-highlight. No highlight, or no local at the cursor, changes nothing. A reply that comes after an edit, or after a move to another file, is dropped, and so is a highlight that starts past the text (A1). More than 10,000 Ranges change nothing, and the last row says too many ranges (A2).
  • When the cursor rests for [editor.lsp] document-highlight-timeout ms, default 1000, the focused View shows each use of the symbol at the cursor, from the same two sources as space h. The uses draw in ui.highlight of the theme, or else ui.selection, or else ui.cursor, under the selection and the tab stops, and a theme with none of these draws nothing. The Ranges do not change. A key, a cursor move or an edit clears the uses and starts the wait again. At most one request waits at a time, and a reply for an older version of the file or for another place of the cursor is dropped (A1). At most 10,000 uses draw (A2), and the locals query of a rest stops at its 500 ms budget with no note. A server that starts while the cursor rests shows the uses after the next key or move. [editor.lsp] auto-document-highlight = false turns this off, and :set lsp.auto-document-highlight false clears the uses at once. The default is true. config.toml, a trusted .ductus and :set change both keys, and a value of another type or a negative timeout gives a note and the default.
  • space d in Normal and Select mode opens a picker of the diagnostics of the file, and space D a picker of the diagnostics of each open file, with its path, as Helix does. A row shows the severity, the source, the code and the first line of the message, errors first, and typing in Insert mode filters by every line of the message. A pick shows the file and selects the diagnostic with the cursor at its start. space D lists only open files, because dk keeps no diagnostics of other files.
  • A column of these pickers pads to 32 characters at most, the diagnostic pickers keep the first 100,000 rows, and a control character in a row shows as a glyph (A2, A5). The columns pad to the widest cell of all rows, with no header row, and the severity has no color.
  • The space s, space S, space d, and space D pickers and the location picker of the goto keys have a preview pane, as Helix does. The pane shows the file of the selected row, with the lines of the symbol, the diagnostic, or the location in the middle and marked. The pane of the location picker also marks the range of the location in ui.selection.primary, over its lines, and an empty range marks the one Grapheme at its start. An open file shows as it is in dk, with unsaved changes, and another file as the file picker shows it. A file that closed after the picker opened shows nothing in the pane, and after an edit the pane marks no line and no range (A1).
  • space k in Normal and Select mode shows the hover text of the servers of the file at the cursor in a popup, as Helix does. With more than one answer, A-n and A-p show the next and the previous one, each under [1/2] and the name of its server. C-d and C-u scroll the popup, Esc and C-c close it, and any other key closes it and runs. The popup has a frame by default (see popup-border in Config). No answer gives No hover results available., and no server with hover gives No configured language server supports hover. Hover asks for Markdown only, and the popup draws it as Helix does, with no marks. A server can still send an answer of kind plaintext, and the popup draws it as it is, with no Markdown. A heading is in markup.heading.1 to markup.heading.6, emphasis is italic, strong is bold, and strikethrough is crossed out. Inline code and raw HTML are in markup.raw.inline, and a link shows its text only. A fenced code block shows its code with no fences, in the highlights of its language, or in markup.raw.inline for a language with no Grammar. The popup shows at once with its code blocks in markup.raw.inline, and they take their highlights when the highlight ends, so keys go on while it runs. A list item starts with • in markup.list.unnumbered or its number in markup.list.numbered, two spaces in for each level, and a rule is ─── in punctuation.special. A blank line follows a heading, a paragraph, a code block and a list. Signature help asks for plain text first, then Markdown, because dk shows its docs as plain text.
  • In Insert mode a popup above the cursor shows the signature help of the first server of the file that has it, with the active parameter in ui.selection, as Helix does. dk asks when Insert mode starts, when you type a trigger character of the server, such as (, and after each edit or cursor move while the popup shows. dk asks with no wait, and keeps one request in flight. A reply for an older text asks again. A-n and A-p show the other signatures, with (1/2) at the right of the first row. Esc leaves Insert mode, and leaving Insert mode closes the popup. The popup has a frame by default (see popup-border in Config).
  • [editor.lsp] auto-signature-help, default true, turns off the requests that dk makes by itself, and display-signature-help-docs, default true, says whether the popup shows the docs of the signature.
  • C-g in Insert mode and space K in Normal and Select mode ask for signature help at once, also with auto-signature-help = false. This is a dk extension: Helix has the signature_help command but binds it to no key. The popup then shows in Normal and Select mode too, where Esc keeps it and C-c closes it, as in Helix. No server with signature help gives No configured language server supports signature-help.
  • For each of these calls, an answer with no signatures closes the popup, and a request that fails or times out leaves it as it is, as in Helix.
  • C-x in Insert mode asks the servers of the file for completions, and a menu of the items shows at the cursor, as Helix does. Each row has the label and the kind of an item, and the label of a deprecated item is struck through. The menu matches the word before the cursor fuzzily, in any case. Typing a word char filters the menu again, and so does Backspace. When a server marks its list incomplete, each such key asks that server again, and its new items take the place of its old ones, as in Helix. Another char, a filter that matches nothing, or a command such as Left closes it. dk asks the servers in the order of their names, and ranks no server over another. It waits for the reply of every server, each with its timeout, and then shows the menu. When a new reply comes for a request that asks again, the menu takes back the preview and selects no item. The menu has no scroll bar, and it follows the cursor. It has a frame only when popup-border is "menu" or "all" (see Config).
  • With [editor] auto-completion, default true, a request starts on its own, as in Helix. It starts 5 ms after you type a trigger character of a server, such as . for gopls. It starts completion-timeout ms, default 250, after you type completion-trigger-len word chars, default 2, and each word char waits that long again. Insert mode that starts after word chars, and an accepted item that ends with a trigger character, start one too. A key other than a typed char or Backspace drops the request that waits, and so do leaving Insert mode and a Backspace before the place where it started.
  • Tab, Down, and C-n select the next item, S-Tab, Up, and C-p the one before, and PageDown, C-d, PageUp, and C-u move half the menu. The selected item shows in the text in place of the word before the cursor. The preview sends didChange to the servers. Enter accepts it, C-c closes the menu and takes it back, and any other key, Esc too, accepts it and then acts. With no item selected, Enter adds a line break, and Esc leaves Insert mode.
  • Accepting an item applies its text edit at each cursor, then its additional text edits, such as an import, and asks for signature help. The text typed before the request is one undo step, and the completion and the text typed after it are the next.
  • While the selection rests on an item for 150 ms, dk asks its server for the rest of it with completionItem/resolve, as Helix does. The detail and the docs of the item then show as plain text next to the menu: right of it when more than 30 columns are free there, and else above or below, in at most 15 rows. The docs pane has a frame by default (see popup-border in Config).
  • Accepting an item that the server can resolve, and that is not resolved yet, asks for it on a worker. Its additional text edits apply when the answer comes, in the undo step of the item. An answer after a key that changes the text, or after Insert mode ends, is dropped. A server that does not answer blocks no key, and its timeout ends the request. An item that has additional text edits of its own applies them at once, and dk does not resolve it.
  • An item that is a snippet goes in at each cursor and selects each place of its first tab stop, as in Helix. So Println(${1:a})$0 puts in Println(a) with a selected. A placeholder puts in its text, and a choice and an empty tab stop put in nothing. A variable puts in its default, because dk resolves no variable, as Helix. Each line after the first gets the indent of the line where the snippet starts.
  • While a snippet is active, Tab in Insert mode selects its next tab stop, and at $0 the snippet ends, as in Helix. Tab inserts a tab when only whitespace is before each cursor, as in Helix. The first char typed after the completion or a move takes the place of the placeholder text at each place of the tab stop. goto-next-tab-stop and goto-previous-tab-stop have no default key, and [keys] can bind them. The snippet ends when an edit removes it, when the Selection leaves it, or when its Document loses the focus. A cursor one Grapheme wide at the end of the snippet is in it. The theme scope tabstop draws the text of each tab stop, as in Helix.
  • [editor.lsp] snippets, default true, says whether a server that starts gets snippet items.
  • [editor.smart-tab] enable, default true, turns on Helix’s smart_tab for Tab in Insert mode: off, Tab always inserts a tab. supersede-menu, default false, makes the completion menu leave Tab and S-Tab to smart_tab, after it accepts the selected item, as in Helix. A trusted .ductus can set them, and :set smart-tab.enable false does too.
  • A snippet over 64 KiB, with placeholders more than 32 deep, or with more than 1000 tab stops puts in nothing, and so does a snippet whose text at all cursors is over 16 MiB (A2). A tab stop that is mirrored puts in its placeholder text again at each place, so the text can be much larger than the snippet. A snippet completion while a snippet is active takes its place.
  • [editor] preview-completion-insert, default true, says whether the selected item shows in the text. completion-replace, default false, says whether an item replaces the word after the cursor too. A trusted .ductus can set them and the keys of the requests that start on their own too. A value of another type, a completion-trigger-len over 255, or a negative completion-timeout gives a note, and the default.
  • A reply keeps its first 100,000 items, and a key scores the first 256 bytes of each (A2). A label, a detail, and docs draw as glyphs, so a control character never reaches the terminal (A5). A text edit with a line past the end of the text is dropped, and so is an additional text edit that overlaps the one before it or ends before it starts. The rest of the item still applies (A1).
  • A popup keeps at most 64 KiB of each answer and 1000 rows, and shows at most 26 rows of 120 columns (A2). A control character in its text shows as a glyph (A5). It keeps at most 100 signatures, and takes at most 32 trigger characters of at most 8 bytes from a server (A2). A popup pads one column on each side and no row, inside its frame when it has one. A line wider than the popup breaks after the last word that fits, and a word wider than the popup breaks at its edge, as in Helix.
  • An edit that a server sends applies only while a command that you started waits on that server (A9). At any other time dk answers that it did not apply it, and the last row says so. The command of a code action that you pick with space a is such a command. An edit that comes after the server answered the command is refused too.
  • dk answers window/showDocument with success: false, so a server cannot move a View (A9).
  • dk checks the whole edit first, and changes nothing when one part fails (A1). A range that is not in the text, two edits that overlap, an edit for another version of a file, and a file that the edit names twice refuse it. Helix applies it change by change.
  • Each file of an edit is one undo step. A file that is not open opens as a Document, and dk never saves it. The last row says how many files changed and are not saved (A6).
  • A path outside the trusted workspace of the server, also through a link, refuses the whole edit, and the last row quotes the path (A5, A9). An edit with a create, a rename, or a delete changes nothing, and neither does one over 2,048 files, 10,000 edits, or 16 MiB (A2, A6). Helix runs a create, a rename, and a delete on disk.
  • space r in Normal and Select mode renames the symbol at the cursor with a server of the file, as Helix’s rename_symbol does. The rename-to: prompt starts with the name that the server gives back for textDocument/prepareRename, or else with the selection, or with the word at the cursor. Enter asks the server for the rename, and the edit applies as the server edits above do: one undo step for each file, no save, and no file rename (A6, A9). Tab completes nothing on this prompt.
  • You can type while dk waits for the answer to a rename. An answer that comes after an edit of the file applies nothing, and the last row says Edit refused: document has changed. An error of the server goes to the last row. A failed request shows server error <code>: <message>. A prepareRename answer after an edit, a second space r, or a key that leaves Normal and Select mode opens no prompt (A1). gopls refuses to rename a package with can't rename package: LSP client does not support file renaming, because dk renames no files. No server that renames gives No configured language server supports symbol renaming.
  • When the start of the rename prompt would be over 64 KiB, the prompt does not open (A2). A control character in it shows as a glyph (A5).
  • space a in Normal and Select mode asks the servers of the file for the code actions of the selection, with the diagnostics over it, as Helix’s code_action does. The actions show in a picker, where Helix shows a menu. A disabled action does not show, and the rest go in the order of Helix: quick fixes, then refactors, then source actions, with fixes of a diagnostic and preferred actions first among equals. Among actions of equal priority, those of a server earlier by name come first. No server with code actions gives No configured language server supports code actions, and no action gives No code actions available.
  • A pick applies the edit of the action as the server edits above do, and then runs its command on the same server. An action with no edit asks the server for it with codeAction/resolve first, so Fill S of gopls works. When dk refuses the edit, the command does not run, where Helix runs it. A server with no commands gives Language server does not support executing commands.
  • A pick after an edit of the file, as space ' can reopen the picker, applies nothing and says Edit refused: document has changed. A pick after the server stopped gives Language Server disappeared. A reply keeps its first 1,000 actions (A2), and a control character in a title shows as a glyph (A5).
  • gopls writes go.mod and go.sum with os.WriteFile when you have no Document of them open. No rule of dk stops that.
  • :stop-language-servers [<name>...] stops each named server attached to the file, or each one with no name, as Helix does. A server is attached when it has answered initialize and has the file open, so a server that still starts is not. Its diagnostics go, and the last row gives no exit note. The server stays stopped in its LSP root until :start-language-servers or :restart-language-servers, also when another file of the language opens there.
  • :start-language-servers [<name>...] starts each named server of the file that does not run, or each one, and opens in it each file in its LSP root whose language names it, such as go.mod for gopls. :restart-language-servers [<name>...] stops each named server of the file that runs, or each one, and starts it again, as Helix does.
  • A start works as at open: only in a trusted workspace, and else the last row says <server> needs trust: :trust allows it, each time (A9). A stop works as at quit: a server that does not exit is killed after 1 s. A server whose program is not installed gives a note only when the command names it, as in Helix.
  • After :start-language-servers and :restart-language-servers, a name that the language of the file does not name gives Unknown language server: <name>, or Unknown language servers: <a>, <b>, and the command does nothing. After :stop-language-servers, so does a name of a server that is not attached to the file. A file with no language, or a language with no servers, gives LSP not defined for the current document. Tab completes the names: the servers of the language after :restart-language-servers and :start-language-servers, and the servers attached to the file after :stop-language-servers.
  • :show-language-servers shows a popup with one block for each server of the file: its name, its LSP root with the home directory as ~, and its state. The state is running, starting, stopped, needs trust, not installed, failed to start, or exited. not installed is a program name that is not on PATH, or an absolute path that is missing. In a trusted workspace, a command that is not a program name, and a server that ends before it answers initialize, show failed to start. A server that runs also shows its position encoding and the requests that it answers, such as definition, hover, completion. The popup scrolls and closes as the hover popup does, and a control character in a name or a path shows as a glyph (A5). Helix has no such command.
  • :run-language-server-command opens a picker of the commands that the servers of the file list, as Helix does. A row shows the command and the name of its server, and a pick runs the command on that server. :run-language-server-command <command> [<json>...] runs the command with the rest of the line as a list of JSON values for its arguments.
  • Bad JSON gives failed to parse arguments: <error> and sends nothing. A command that no server lists gives `<command>` is not supported for any language server, and one that two servers list gives `<command>` supported by multiple language servers. A pick after the server stopped gives Language Server disappeared.
  • You start the command, so the server can edit while it waits (A9). dk keeps at most 1,000 commands of a server and drops a name over 1 KiB (A2). Tab completes the command.
  • Quit sends shutdown and exit to each server, and kills one that has not exited after 1 s, with its children.

Manual

  • docs/manual.md lists the text of dk --help, each key of config.toml with its default and doc, the typed commands with their aliases, the default keys of Normal, Insert, Select and Block mode, the keys of the : prompt, and the command names that [keys] binds, each with its desc.

Opening files

  • dk <file> opens the file. A file that does not exist yet opens empty.
  • dk with no file opens an empty Document, as Helix does. The status line and the buffer picker name it [scratch], and it keeps no Undofile.
  • dk diff <a> <b> opens file a and file b in a diff session, as :diff-files does (see Views), and as hx -d does in Helix. It takes exactly two files. When one of the two files does not open, dk stops with the error.
  • :open-empty-document or :n opens another empty [scratch] Document in the View, as in Helix, and the Document you had stays open.
  • :open <file> shows another file in the View, and the file you had stays open with its unsaved changes. With more files, as in :open a.txt 'b c.txt', it shows the first and opens the others with no View, as dk a.txt 'b c.txt' does, so the jumplist gets no jump into them. :open <directory> shows the file picker at that directory, over the Document in the View. A directory with any other name is an error on the last row, and nothing opens. :o and :e are the same command. A leading ~ in the path of :open, :split-view-right, or :split-view-below is the home directory, as in Helix. If the file is open already, through any path, a symlink, or a hard link, the View shows it, so that two saves never write one file. While a :w <path> save runs, :open of that path says a save is running, because the saved Document takes the path when the save ends. An empty Document with no file and no changes closes when :open replaces it, as in Helix. If the file does not open, you keep the text you have.
  • :goto-next-document or :bn shows the next open file in the View, and :goto-previous-document or :bp the one before, in the order they opened. Each file comes back with its text, its Selection, and its undo history. gn and gp in Normal and Select mode do the same, as in Helix, and a count such as 2gn skips that many files.
  • g f in Normal and Select mode opens the file named at the cursor, as Helix’s goto_file, and g F also goes to the line after the name, as Vim’s gF. With one Range of one Grapheme, the name is the first path on the line that holds the cursor or ends next to it, at most 1000 bytes from it (A2), or else the text of the Range. A path holds no :, so main.go:10:5 gives main.go, and g F goes to line 10. A line past the end goes to the last line. With more Ranges, or a wider one, each Range is a name, with its whitespace trimmed. A relative name is from the directory of the file, a leading ~ is the home directory, and dk expands no environment variable, so $HOME/x stays as it is (A9). A file:// URL opens its path. dk opens the names as :open does: it shows the first and opens the others with no View, a file that is open already shows that file, one directory opens the file picker in it, and a FIFO opens nothing (A2, A6). A URL with another scheme, such as https://, goes to [editor] url-opener (Config), in a session of its own, and dk stops it after 2 s (A2). It runs only for a file in a trusted workspace, and else dk runs nothing and gives the trust note (A9). A URL with a control character opens nothing and shows quoted (A5). dk asks no language server for document links.
  • g a, g m and g . in Normal and Select mode go back to the last file and the last revision position, as in Helix. Each View keeps the files that it showed, each one once, and the file that it leaves, by :open, gn, a picker or a jump, goes to the end. g a takes the newest file off that list and shows it, so g a twice gives back the first file, and with none it says no last accessed document. Each View also keeps the last two files that it left with a change made while it showed them, and g m shows the first of them that is not the current file, or says no last modified document. g . puts the cursor of each Range at the revision position of the current Revision: after the change at the primary Range from before the edit, or else after its first change, and saves the place on the jumplist first. In Select mode each Range extends, and at the first Revision, such as in a file with no edits or after u back to the start, g . does nothing. A file that closes, such as with :bc, leaves the lists of each View, so g a and g m never show it (A1), and each list holds each file once, so it grows only with the open files (A2).
  • :close-document or :bc closes the file, and each View that shows it shows the file before it in the list. It refuses while the file has unsaved changes or a save of it runs. :close-document! or :bc! drops the unsaved changes, and waits for a running save of the file. Closing the last file leaves an empty Document with no file, as when dk starts with no file.
  • When another program changes an open file, dk reads it again, as Vim’s 'autoread' does. A file that holds the text of the last save does nothing, so a :w of dk never reloads. A Document with no unsaved changes reloads as :reload does, and u gives back the text from before. A Document with unsaved changes keeps its text, and the last row says "<name>" changed on disk and in dk: :reload takes the file, :w! keeps yours, :diff-disk compares. A removed file keeps its text, and the last row says "<name>" was deleted on disk. A Document with modifiable off keeps its text, and the last row says so. While a save of dk writes the file, the file does not reload (A6). auto-reload = false in [editor] of config.toml turns the reload off, and the notes of unsaved changes and of a removed file still show. A Document with no unsaved changes then keeps its text, and the last row says "<name>" changed on disk: :reload takes the file, as Vim’s W11 does. Only config.toml and :set set it: a .ductus that sets it gives the warning .ductus cannot set auto-reload (A9). A value that is not true or false gives a note, and dk uses true. dk watches the directory of each open file, one watch for each directory, so the save of an editor that renames a new file over the old one reloads too. A watch that the system refuses, such as at the inotify limit, gives one note, and dk goes on without it (A2). The names in the notes show quoted (A5).
  • A file opens where its cursor was when you last closed it or quit, as Vim’s '" mark and restore-cursor do, also in a new dk run. dk keeps the line and the column of the primary cursor, and the View shows the line in its middle, as after a jump. If the line is no longer in the file, the cursor is at the start, and a column past the end of the line stops at its end. A file that keeps no Undofile, such as one under /dev/shm, /tmp, or $TMPDIR, or one that undofile-exclude names, keeps no place (A4). Neither does a commit or rebase message, such as COMMIT_EDITMSG or git-rebase-todo, or a file of a diff session. A stop from SIGTERM or SIGHUP, such as when the terminal window closes, keeps the places as a quit does. restore-cursor = false in [editor] of config.toml opens each file at the start. dk still keeps the places then, so a later restore-cursor = true restores them.
  • The places of at most restore-cursor-files files, 100 by default, stay in $XDG_STATE_HOME/ductus/positions, or ~/.local/state/ductus/positions, and the file used longest ago goes first. restore-cursor-files in [editor] of config.toml is a whole number from 1 to 10000, and with a lower value the next write drops the files used longest ago. Only config.toml sets restore-cursor and restore-cursor-files: :set refuses them, and a .ductus that sets one gives the warning .ductus cannot set <key> (A4, A9). A value that is not true or false, or not a whole number from 1 to 10000, gives a note, and dk uses the default. The file is mode 0600 in a 0700 directory, it holds the path, the line, and the column of each file and no text, and dk never writes through a symlink there (A4). Two dk runs that quit at once each keep their places, and the file is never half written. dk never writes a positions file over 1 MiB: when the rows do not fit, it drops the files used longest ago (A3). A positions file over 1 MiB, or with rows that do not parse, gives one warning, and those files open at the start (A3). A state directory that others can write gives no place (A3).
  • space b in Normal or Select mode opens the buffer picker, as in Helix. It lists each open file in the order they opened, with * on the focused one and [+] on one with unsaved changes. Typing in Insert mode filters the list fuzzily, best first, as the query of each picker matches (Pickers). The matched characters of each path show in special and bold, as in the Helix picker. Up, Down, Tab, S-Tab, C-n, and C-p move the selection, PageUp, PageDown, C-u, and C-d move it a page, and Home and End go to the ends. When the picker is over 72 columns wide, the list takes the left half, and a pane on the right shows the selected file with the line of its cursor in the middle, as in Helix. Enter shows the file in the View, and C-c, or Esc in Normal mode, closes the picker. C-s and C-v show it in a new View below or to the right, as :split-view-below and :split-view-right do, so they do not edit the query. A pick of the file in the View gives two Views of it, and a screen too small for a new View refuses, as :split-view-right does.
  • space f in Normal or Select mode opens the file picker, as in Helix, with the same keys and the same marks on the matched characters. When the picker is over 72 columns wide, the pane shows the first rows of the selected file, as on disk. The pane draws the file in the colors of its Grammar, as the editor does, for the file pickers, the changed file picker, and the search. A file with no language or no built Grammar, and a Grammar that does not load, shows plain text with no note. The highlight covers the first 64 KiB that the pane reads, and it stops after 500 ms. The buffer picker pane shows the colors of the open file, until an edit makes them older than the text. The revision picker pane draws a Revision in the colors of the language of the Document, as the editor does, and the current Revision in the colors of the open file. A Revision over 8 MiB, or one whose highlight passes 500 ms, shows plain text with no note. A binary file, a file over 10 MiB, or a path that is no longer a file shows a note there in place of the text, as in Helix, and dk reads at most the first 64 KiB. The read runs beside the editor, so a slow disk never holds a key. It lists the files under the workspace root, the nearest directory with .ductus or a vcs marker such as .jj, or else under the working directory. It skips each name that starts with . and each link that resolves inside the root, it goes into a linked directory, and it stops at 100,000 files. [editor.file-picker] changes which names it skips, whether it goes into a linked directory, and how deep it goes (Config). It skips the files that the ignore files name, such as .gitignore, as in Helix (Config). Enter opens the file as :open does, by its path from the working directory when it is under it, as Helix shows it. C-s and C-v open it in a new View below or to the right, as :split-view-below and :split-view-right with the path do, and an open file shows its Document there.
  • space F in Normal or Select mode opens the file picker at the directory of the focused file, as Helix’s file_picker_in_current_buffer_directory. It lists the files under that directory and below it, with the walk, the [editor.file-picker] keys, the limit, the rows, and the pick of space f. A Document with no path opens it at the working directory, and says Current buffer has no parent, opening file picker in current working directory, as Helix does. Helix binds space F to a picker of the working directory, which dk does not have.
  • space g in Normal or Select mode opens the changed file picker, as in Helix. It lists each file that the jj working copy changes, with the kind first: + untracked, ~ modified, x conflict, - deleted, or > renamed. A renamed file shows as old -> new, and a path shows control characters as glyphs (A5). Each kind draws in its theme scope, as in Helix: diff.plus for untracked, diff.delta for modified, diff.delta.conflict, diff.minus for deleted, and diff.delta.moved for renamed. dk joins each path that jj prints to the repository root, so a pick from a subdirectory opens the right file. It has the keys and the pane of the file picker, and Enter opens the file as :open does. dk runs the commands of the vcs plugin with its exec grant, and in a workspace that you did not trust, the key runs nothing and gives the trust note. The list shows the working copy as jj last saw it, because dk runs jj with --ignore-working-copy. Run a jj command such as jj status after an edit to update it. It stops at 100,000 files, and the title says so. The picker opens when both jj commands end, and a command that fails gives a note in place of the picker. It opens only in Normal or Select mode with no pending keys, so it never replaces another mode or picker. A vcs plugin from before this key says that it gives no list.
  • space l in Normal or Select mode, or the key command pick-file-change, opens the file log picker: @ and each change before it that changes the focused file, newest first, as jj log -r '::@ & files(<file>)' lists them. Each row shows the line of jj log -T builtin_log_oneline for the change in the colors of the jj config: the change id, the author, the time, the bookmarks, the commit id, and the description, as jj log shows them. The line keeps only its colors and attributes, and no other escape sequence or control character reaches the screen (A5). dk keeps 1,024 bytes of the line, and the query matches the plain text of the row. It stops at 10,000 changes, and the title says so. The run of jj stops after 10 s, as for annotate, because jj diffs each change of a long history. Enter compares the file before and after the change in a diff session. The focused View shows the file in the parents of the change, merged, and a split to its right shows the file after the change, with the focus. Each side is a Document with no path, readonly on and modifiable off, named [<change id>-] <file name> or [<change id>] <file name>. An added file has an empty left side, and a removed file an empty right side, and dk runs no command for that side. When the session ends, the right View closes, the left View shows the file again, and both sides go. The row of @ runs :diff-base, so the right side is the file as it is in dk. A run that fails gives a note, and no View changes. jj matches the path, so the changes from before a rename of the file do not show. dk runs the commands of the vcs plugin with its exec grant, and in a workspace that you did not trust, the key runs nothing and gives the trust note. A vcs plugin from before this key says that it gives no file log. Helix has no such picker and no space l.
  • space ' in Normal or Select mode opens the picker that closed last, as it was: the same query, selection, and top row, as in Helix. dk keeps a picker when it closes, after a pick or after Esc. With no kept picker, the key gives “no last picker”. Opening a picker drops the kept one, so dk keeps one picker at most. A reopened picker is not kept again until it closes. A picker with more than 1,000,000 rows is not kept. A pick of a row whose open file closed in the meantime gives “cannot switch to document that does not exist (anymore)”, and shows nothing. A symbol or goto row whose open file closed in the meantime opens the file again and reads the position against it. A diagnostic, symbol, or goto row of an open file selects the text that the row named when the picker opened, also after edits since then, as dk maps the position through the undo history. When the edits since then are too many to map, the pick gives “too many edits since the picker opened” and moves nothing. A row of a file that is gone acts as :open does for that path. A kept revision picker lists the Revisions of the file it opened for, and a pick shows that file first.
  • space / in Normal or Select mode opens the search picker, as in Helix. The query is a regex, and each change of it searches the working directory again. A row is path:line: text, with the first line of the match, in the order of the walk. A row has colors, as the path column of Helix has them: the directory in ui.text.directory, the colons in punctuation, and the line number in constant.numeric.integer. The match in the text shows in special and bold, as a matched character does in the other pickers. The nord theme defines punctuation and constant.numeric, and ui.text.directory falls back to ui.text. The matches stream in while the search runs, and a new query stops the old search. A query with no upper case char ignores case, and any upper case char, also in a class such as [A-Z] or an escape such as \S, makes it match case, unless [editor.search] smart-case is false (Config). A query that does not compile gives “Failed to compile regex” in the title, and it clears when the query compiles. A file with a NUL byte is skipped. The preview pane shows the match in the middle of the pane, and draws its lines in ui.highlight, or else ui.selection, as in Helix. The pane reads the first 64 KiB of a file, so a match after that shows the start of the file, with no mark. A match over more lines selects all of them. Enter opens the file as :open does, and selects the lines of the match. If the file lost the line, you get “The line you jumped to does not exist anymore because the file has changed.” A row shows control characters as glyphs (A5). The search stops at 100,000 matches, and the title says so. It reads the first 8 MiB of a file, and a row shows the first 200 characters of its line. It lists the files as the file picker does, with the same ignore files. It searches the files on disk, so an unsaved change does not match. C-s and C-v open the match as Enter does.

Pickers

  • A picker opens in Normal mode, or in Insert mode with [editor] picker-start-mode = "insert" (Config). The row of the query starts with the mode, NOR or INS, or the names that [editor.statusline] mode sets. While [editor] color-modes is true, the mode draws in the scope of the status line mode, ui.statusline.normal or ui.statusline.insert, and with color-modes = false it draws as the rest of the row. In Normal mode a letter does not edit the query: j and k move to the next and the previous row, and i goes to Insert mode. Enter, C-s, C-v, the arrows, and the paging keys work as in Insert mode, and Esc closes the picker. / opens a find line for a regex in place of the query, and Enter on it selects the next row whose text matches, and ? the previous one. n and N go to the next and the previous match of the last find, and wrap at the ends, as in Helix. The find reads only the rows that show, at most 100,000 (A2), and a regex that does not compile gives a note in place of the query (A5). In Insert mode the keys edit the query as before, and Esc goes to Normal mode, so two Esc keys always close a picker (A7).
  • A picker takes the percent of the width and of the height that [editor] picker-size sets, 80 by default (Config). It is centered over the screen less its last 2 rows, as in Helix, and the Views stay drawn around it. The picker is in a box, as in Helix. The first row in the box holds the label and the query, and at its right end the count of shown and all rows, such as 12/340. A line of ─ is under that row, and the rows of the picker are below it. The preview pane shows only when the box also fits beside it, so a short screen drops the pane first. A screen too short or too narrow for the box shows the rows with no box, and the label, the query, and the count on the last row.
  • The query of a picker matches each word on its own, in any order, so jujutsu default and default jujutsu both find jujutsu/default. A space cuts the query into words, and two spaces count as one. A backslash before a space puts the space in the word, so jujutsu\ default finds jujutsu default and not jujutsu/default. A row shows when each word matches its name, and the best rows come first: the score of a row is the sum of the scores of its words, and rows with the same score keep their order. A word matches fuzzily, as Tab on the prompt does: its characters must come in order in the name. A word with a capital letter matches case, and a word without one ignores case. 'word matches the exact text, ^word the start of the name, word$ its end, and ^word$ or 'word$ the whole name, so .go$ finds the Go files. !word hides each row whose name holds the exact text, so go !test hides each _test.go file. \!, \^, \' and \$ are the plain characters. The characters that each word matches show in special and bold. Tab on the prompt and the completion menu do not cut words: there a space is a character to find.
  • space ? in Normal or Select mode opens the command palette, as in Helix. It lists each command that a key can run, by name, and then each prompt command, such as :write-quit. Each row has the keys that the mode binds to the command, with the labels of :help, and its desc, or the [keys] description of one of those keys. Typing in Insert mode filters by the name, so wq puts :write-quit first. Enter runs the command as its key or its prompt line would, with the count typed before space ?, and a prompt command gets no argument. C-s and C-v run it as Enter does, as Helix’s palette does. The keys and the descs show control characters as glyphs (A5).

Plugins

dk loads the keymap plugin (see Keys), the vcs plugin, the picker plugin, and each user plugin in plugins/*.wasm in the config directory. The editor package is the plugin host.

  • A plugin is a WebAssembly component that implements a world of ductus:editor@0.1.1 in wit/. The plugin world is the keymap, the hooks world is the Document lifecycle Hooks, and the lsp-client world is the LSP Hooks. The plugin world imports history and exports command, so the keymap plugin runs undo, redo, earlier, and later on the History that dk lends it for the call. A plugin with more than one role includes the world of each role in its own world. You can write it in TinyGo.
  • dk loads a plugin only when each ductus:editor name that it imports or exports has the minor of dk and a patch that is not above the patch of dk. So a plugin built for an older patch, such as 0.1.0, loads. dk refuses each other plugin before it runs any of its code, with one warning such as plugin "x": built for ductus:editor@"0.2.0", dk has 0.1.1. The warning shows at most 64 bytes of the version.
  • The Hooks run on each plugin that exports them, the keymap plugin first. document-opened runs for each open Document when the plugins load, and for each new Document: a file that :open, :hs <file>, or :vs <file> opens, and the empty Document that the last :bc leaves. document-focused runs when the plugins load, when a command shows another Document in the View, and when the focus moves to a View of another Document. document-will-save runs before a save, and document-saved runs after a save that succeeds. document-closed runs after :bc or :bc! closes a Document, before the Hooks of the Document that the View shows next. A plugin that does not export document-closed still loads. A Hook gets the Document and the path of its file, and it cannot stop or change the save or the close.
  • A user plugin that exports the LSP Hooks gets lsp-attached once for each language server and Document, when the server opens the Document, and again for each of them on :reload-config. It gets diagnostics-changed after each publish of diagnostics that dk keeps, and when a server exits and a Document loses its diagnostics. The keymap plugin gets no LSP Hook.
  • The Document of a Hook, of symbols-reply and of move-word reads its text by Grapheme, as the text was when dk made the handle. next-grapheme and prev-grapheme give the Position of the next and the last Grapheme boundary, and grapheme-at gives the Grapheme that starts at a Position. graphemes gives up to max Graphemes after a Position, or before it, nearest first, and none when the Position is not a Grapheme boundary. It gives at most 256, and it stops before 64 KiB of text, but that stop never drops the first Grapheme (A2). A back other than 0 or 1 traps the plugin (A1).
  • In each Hook of a user plugin, the Document gives the diagnostics of each server, with their Positions in its text. A message or a source comes as the server sent it, cut at 1 KiB, with each run of bytes that are not UTF-8 as U+FFFD. A Document of an older text, such as the one of document-saved after you typed during the save, gives none.
  • In the same Hooks and in symbols-reply, request-symbols asks each language server of the Document for its document symbols, and gives the id of the request. The reply comes through symbols-reply once for each id, on the text of the request, with the symbols in pre-order and each name cut at 1 KiB. A request that fails or times out gives a reply with no symbols, and the Document of a reply gives no diagnostics. A plugin has at most 8 requests with no reply, a reply keeps at most 10,000 symbols, and dk drops a flat symbol of another file. It needs no grant, and it reaches no server outside a trusted workspace.
  • dk never loads a plugin from a .ductus directory, trusted or not.
  • To install a plugin, copy its .wasm file into plugins/ in the config directory. For a plugin that needs a grant, run sha256sum on the file, and write the hash as sha256 in the grant. When the author publishes a hash, compare it with yours first. To update a plugin, copy the new file: its grant stops until you write the new hash. dk downloads no plugin and never updates one.
  • A plugin gets no filesystem or network access.
  • The host stops a plugin call after 100 ms, and a plugin cannot grow past 64 MiB of memory. A plugin that traps is unloaded, and its later calls fail at once. A user plugin that traps or does not load gives one warning on the last row, and the others go on.
  • The vcs plugin names the marker directories of a repository and the commands that give the base text of a file, tell if the file is tracked, and print the change id. dk asks the plugin for them when a file opens, on :reload-config, on :trust, after a save, on the terminal focus, and for the file of each View after a shell command and on :refresh-vcs. The change command prints the change id on its first line, and on each other line a change id and the bookmark names on that change. dk reads at most 64 KiB and 101 lines, and keeps at most 127 characters of the id and the names. A line that the 64 KiB cut does not take whole does not show, and the names after the cut do not count in …+N. The output can hold SGR sequences and the <<labels::text>> segments of jj --color=debug, and plain text also works. dk takes only the SGR sequences of 32 bytes or less for reset, bold, dim, italic, underline, reverse, their resets, the 16 colors, the default colors, and the 256 and RGB colors. A sequence with any other code, or with a 256 or RGB color that is not whole or over 255, does not apply, and the text after it keeps the colors from before it. It drops each other escape sequence and control character, so no byte of the output reaches the terminal as it is. The plugin can also name the command that annotates a file, for the inline blame and space B (Display). That command prints a line for each line of the file: one JSON object with author, change, commit, content, description, email, and time in RFC 3339, then the change id as the vcs shows it, for the annotate column. The change id after the object can hold the SGR sequences that dk takes from the change command, and dk drops each other escape sequence and control character, and keeps 128 bytes. A plugin that prints nothing after the object gets the plain change in the column, and only the fields name the change. A plugin built before annotate gives no annotation, and space B says so. The plugin can also name the command that lists the changes of a file, and the commands that print the file before and after a change, for pick-file-change (Opening files). The list prints a row for each change: one JSON object with author, change, commit, current, description, kind, and time in RFC 3339, then the line that the picker shows, then a NUL byte. The plugin gives a line that a user can read, such as the line of jj log -T builtin_log_oneline, or for git the line of git log --oneline --color=always. The line can hold the SGR sequences that dk takes from the change command, and dk drops each other escape sequence and control character, and keeps 1,024 bytes. A plugin that prints an empty line gets a row of the fields: the change id, the date, the author, and the description. Only the fields name the change: dk passes only the commit of the fields, of 1 to 64 lowercase hex digits, to the plugin, and drops a row with another commit. A plugin built before these commands gives no file log, and pick-file-change says so.
  • dk ships a vcs plugin for jj, a Default plugin. Its marker is .jj, and the base text is the file in @-. It runs jj --ignore-working-copy with diff, file annotate, file list, file show, and log, so jj never snapshots the working copy. log prints @ and each change back to the closest changes with a local bookmark, at most 100 changes below @. It runs with --color=debug and the template alias format_short_change_id, so the id and the colors follow the jj config. plugins/vcs.wasm in the config directory replaces it.
  • dk runs a command of the vcs plugin only when the grant names its program. The shipped plugin has exec = ["jj"] with no config. [plugins.vcs] exec in your config.toml replaces that grant, and a plugins/vcs.wasm gets no grant until you write one with a pin. The pin is sha256 = "<hash>" in the same table, the SHA-256 of the file as sha256sum prints it. dk hashes the bytes that it loads, and the grant applies to the file only when the pin is their hash. When the pin is missing or is not the hash, the plugin runs nothing, and one warning says that the grant of “vcs” does not apply and how to write the pin. The warning does not show the hash, so you check the file before you write one. A sha256 that is not 64 hex digits gives a warning, and a workspace .ductus cannot set it. The shipped plugin needs no pin. A program is a plain name on PATH or an absolute path. exec = [] turns the plugin off, and a workspace .ductus cannot grant a program. An exec grant for jj is full trust: jj util exec and aliases run any program. The command runs in the directory of the file with no shell, and dk stops it after 2 s, 10 s for the annotate command, or 32 MiB of output (A2).
  • The picker plugin implements the picker-ui world: for each key it says where the selection goes, and whether the key picks, picks in a new View below or to the right, closes, or edits the query. dk keeps the list, filters it, and draws it, so no file name reaches the plugin. dk ships a picker plugin with the keys of Helix’s picker, and plugins/picker.wasm in the config directory replaces it. Esc always closes a picker, and a picker plugin that traps closes the picker with one warning.

Saving

  • :write or :w saves the file. The save runs on a worker, so you can type while it runs. On a [scratch] Document it saves nothing and says Can't save with no path set!, as Helix does. It saves nothing when the directory of the file is gone, or when another process changed the file after dk read or saved it, and says Helix’s message, such as file modified by an external process, use :w! to overwrite. The text then stays unsaved, and :wq and :x do not quit.
  • :w <path> saves the text to path, and the Document then has that path, its language, and its language servers, as in Helix. A language of :set filetype stays, as Vim’s :saveas keeps a ‘filetype’ that is set. It saves nothing when another open Document holds that file, when the directory of path does not exist, or when the file changed after the Document was last saved or opened. Tab completes the path, as it does for :open. A path in quotes keeps its spaces (see Commands), and a second path gives expected at most 1 argument, got 2, as in Helix.
  • A Document is readonly when its file exists and dk cannot write it, as in Helix, and the status line then shows [readonly]. :set readonly and :set-document-option readonly turn it on for the focused Document, and :set noreadonly turns it off, as Vim’s ‘readonly’. It has no session value and no config.toml key, and :reload-config keeps it. On a readonly Document, :w, :wq, and :x save nothing and say 'readonly' option is set (add ! to override), as Vim’s E45. So do :w <path>, :wq <path>, and :x <path> when path names the file of the Document itself, also through another spelling or a link. :w <path> to another file saves to path, and the Document then takes readonly from path.
  • :w to a file that dk cannot write saves nothing and says Path is read only, as Helix does, also when readonly is off.
  • :write! or :w!, with or without a path, saves as :w does, but makes a missing directory and writes over a file that changed on disk, as Helix does. It also saves a readonly Document and a file that dk cannot write, and turns readonly off, as Vim does. Helix refuses that file. The file keeps its mode, so it stays read only on disk. It still saves nothing to a file that another open Document holds.
  • :w and :w! format the file first, as :format does, when auto-format is true in [editor], the default, and in the [[language]] of the file, as in Helix. The shipped languages.toml holds the auto-format and the formatter keys of Helix: 43 languages, Go among them, auto-format, and 36 have a formatter. An auto-format in [editor] that is not true or false gives a note, and dk uses true. The save starts when the format ends, and it saves the file even when the format fails. When you move to another file in the meantime, the file still gets the format and the save, as in Helix, unless it closed or changed. :w --no-format saves the text as it is, and another flag, such as -x, gives unknown flag '-x' and saves nothing. -- ends the flags, so :w -- -x saves to the file -x.
  • :write-all or :wa formats and saves each file with unsaved changes, one file at a time, as :w does, and you can type while it runs, as in Helix. :wa --no-format saves each file as it is. A save that fails does not stop the others, and the last row names each file that did not save, quoted (A5). A changed [scratch] Document gets no save, and the last row then says cannot write a buffer without a filename, as in Helix. A quit or a close while it runs waits for the save that runs, and the rest does not save.
  • :write-all! or :wa! does the same as :w! does for each file, and says nothing about a changed [scratch] Document, as in Helix.
  • :write-quit [<file>], :wq, or :x saves as :w [<file>] does, then closes the View as :q does, as in Helix. With a path it makes every check of :w <path>, and Tab completes the path, as it does for :w. It formats first as :w does, and --no-format saves the text as it is, as in Helix. You can type while it formats and saves, and dk quits when the save ends, where Helix waits; a key that would start a second save meanwhile says that a save runs. :wq closes its View at the end only when that View is still open. If the save fails, dk stays open and shows the error, and the text stays unsaved.
  • :write-quit! [<file>], :wq!, or :x! saves as :w! [<file>] does, then closes the View as :q! does, as in Helix. So it makes a missing directory, writes over a file that changed on disk, and quits even while another file has unsaved changes. If the save fails, dk stays open with the error and the unsaved text, as Helix does. Tab completes the path, as it does for :w. On a [scratch] Document with no path given, neither :wq nor :wq! quits, because nothing was saved.
  • :write-quit-all, :wqa, or :xa formats and saves each file with unsaved changes as :wa does, then quits when the last save ends, as in Helix. A save that fails does not stop the others, and the last row names each file that did not save, quoted (A5). Then dk does not quit, and neither does it while a changed [scratch] Document has no file name, as Helix says: cannot write a buffer without a filename. It takes --no-format and no path: :wqa a.txt says expected no arguments, got 1.
  • :write-quit-all!, :wqa!, or :xa! saves each file as :w! does, then quits even when a save fails, dropping what did not save, as in Helix.
  • A quit waits for a running save to end, so it never stops a write part way.
  • The save writes a temp file, syncs it, and renames it over the file. After a crash, the disk has the old file or the new one, never a part of either.
  • The save keeps the owner and the mode of the file, and a symlink stays a symlink.
  • A file with more than one hard link is written in place, so every link sees the new text. The last row then says “not atomic”.
  • When dk panics, it writes a backup of each Document with unsaved changes to a new file in $XDG_STATE_HOME/ductus/backup, or ~/.local/state/ductus/backup, before it ends. The directory is mode 0700 and each backup 0600, and a later panic keeps the backups of an earlier one (A4). A Document that keeps no Undofile, such as a file under /dev/shm, an excluded file, or a [scratch] Document, gets no backup (A4). After the backups, dk writes a crash file to a new file in $XDG_STATE_HOME/ductus/crash, or ~/.local/state/ductus/crash, whether or not the debug log is on. The directory is mode 0700 and each crash file 0600, dk never writes through a symlink there, and a later panic keeps the crash files of an earlier one (A4). The name of the file holds the time of the panic. The file holds the dk version, the time, the panic value, quoted so that a control character in it shows escaped (A5), and the stack of the goroutine that panicked, which for a panic of a worker is the stack of the worker. dk never deletes a crash file. With -d or --log, the debug log gets one error line for the panic, with the value, the stack, and the name of the crash file. dk restores the terminal, then stderr names each backup with the quoted path of its Document, and each changed Document with no backup with the reason (A5). Then stderr names the crash file, or says why dk wrote none. Then dk ends as a Go panic does: the panic value and the stack on stderr, and exit status 2. Go marks the value [recovered, repanicked], because dk takes the panic to write the crash file and then panics again with the same value. A panic of a worker that a quit waits for, such as the vcs query, the changed-file list or the search, ends dk in the same way. To get the text back, open or copy the backup.

Tab pages

  • A tab page holds its own Views, as in Vim, and each tab page keeps its layout and its focused View. dk starts with one tab page, and a split in one tab page does not change the others. The files belong to dk, not to a tab page, so two tab pages can show one file, and each View keeps its own Selection and jumplist.
  • :open-tab opens a tab page after the current one with a new [scratch] file. :open-tab <file> or :tabe <file> shows the file there, and with more files they open one tab page for each file in turn. If the file does not open, no tab page is made.
  • :close-tab or :tabc closes the tab page and its Views, and the next tab page becomes the current one, or the one before when it was the last. :close-tab N closes tab page N, counted from 1. The files stay open with their changes, so no text is lost, and the buffer picker still lists them. The last tab page does not close, and the last row says cannot close the last tab page.
  • :close-other-tabs or :tabo closes every other tab page, and their files stay open too.
  • :goto-next-tab or :tabn goes to the next tab page, and from the last to the first. :goto-next-tab N goes to tab page N. :goto-previous-tab, :tabp or :tabN goes to the tab page before, and from the first to the last, and :goto-previous-tab N goes N tab pages back.
  • In Normal and Select mode, gt and C-PageDown go to the next tab page, and gT and C-PageUp to the one before, as in Vim. With a count, 3gt goes to tab page 3, and 3gT goes 3 tab pages back. A count that is not a tab page gives a note, and nothing changes. Their commands are goto-next-tab and goto-previous-tab, so a [keys] entry can bind other keys to them.
  • :move-tab N or :tabm N moves the tab page to after tab page N, counted before the move, and :move-tab 0 makes it the first. :move-tab with no N makes it the last, and :move-tab +N and :move-tab -N move it N places, 1 with no N.
  • A number that is not a tab page, or a place past the ends, gives a note that quotes it, and nothing changes.
  • Closing the last View of a tab page, with :q, :wq or C-w q, closes the tab page, as in Vim. Closing the last View of the last tab page quits as before.
  • :quit-all, :write-all, :write-quit-all and the other -all commands act on the files of every tab page.
  • :help, :show-messages, :show-about and :check-health go to the tab page that shows the listing already.
  • A diff session belongs to no tab page. :diff-this in one tab page and then in another starts a session whose two Views never show together, and a session keeps its Views when you go to another tab page. Closing a tab page that holds one of its Views ends the session.
  • When a :diff-base session ends, its base View in another tab page closes when that tab page shows again.
  • With more than one tab page, the top row is the tab line, and the Views start on the row below; [editor] tabline changes when it shows (Config). Each label is the number of the tab page and the file name of the file in its focused View, such as 1 [scratch] and 2 a.txt, with a space and + after it when a file of a View of the tab page has unsaved changes. When two or more labels show the same file name, each shows the shortest end of its path that the others do not end with, such as dk/main.go and keymap/main.go, or cmd/dk/main.go and plugins/dk/main.go when the parent folders match too. Two tab pages of one file keep the same label, and a file with no path, such as [scratch], keeps its name. The labels draw in ui.bufferline, the current one in ui.bufferline.active, and the rest of the row in ui.bufferline.background, and each scope falls back to its parent, as for any scope. When the labels do not fit, the current label shows, and as many labels next to it as fit, so 40 tab pages on 80 columns show the current label and its neighbors. A name shows as the Views show text, so a control character shows as a glyph (A5). A popup, the completion menu and its docs stay below the tab line.
  • A click on a label goes to its tab page, as in Vim, and in Insert mode the edits commit and Normal mode starts first, as for a click in another View. A press on a label and a drag over another label move that tab page to the place of that label, and a drag past the last label moves it to the end. A middle click on a label closes that tab page, as :close-tab N, and its files stay open. The press and the release must both be on that label, so a middle press somewhere else that ends on a label closes nothing. With one tab page left, it closes nothing, and the last row says cannot close the last tab page. A double click past the last label opens a tab page with a new [scratch] file at the end: two presses there less than 500 ms apart, Vim’s default mousetime. One click there does nothing. While the prompt, help or a picker is open, the tab line does nothing with the mouse, and with mouse = false the terminal sends no clicks.

Views

  • Each View has a gutter on its left, as in Helix: a blank column for diagnostics, a space, the line numbers, a space, and the diff signs. A number is right-aligned, at least 3 wide, and as wide as the last number, so 10,000 lines take 5. An empty last line shows ~. The gutter draws on ui.gutter, and on ui.gutter.selected on the cursor line of each Range. A number is in ui.linenr, and in ui.linenr.selected on such a line in the focused View. A View that is too narrow for its gutter shows none. :annotate, or the key command annotate, turns on an annotate column at the left of the gutter of the focused View, and turns it off again. On the first line of each block of lines of one change, the column shows the change id and the age of the change, such as 3 days ago, as the inline blame shows it, and the other lines of the block are blank. The count of the age pads on its left with a space to two cells, so 3 hours ago lines up with 12 days ago, and a count of three digits takes three cells. dk compares the annotated text with the text of the Document, as for the inline blame, so a new or changed line is blank. When an edit blanks the first line of a block, the next line of the block shows the id. The column is the 8 cells of the id, a space, and the widest age of the file, and each age pads on its right, so the text of each line starts in one column. It draws in ui.gutter, and in ui.gutter.selected on the cursor line. The change id has the colors of the jj config, as jj log shows it, with the shortest prefix apart from the rest. It keeps only the colors and attributes of jj, shows a control character of a plain id as a glyph (A5), and cuts the id at 8 cells. The date draws in the scope of the age of the change: ui.virtual.annotate.age.day for the last 24 hours, .week, .month (30 days), .year (365 days), and .older, each of which falls back to ui.virtual.annotate. The default theme draws them from warm to cold, so the column shows how recent the file is. In each file the date of the newest change has the full color, and older dates fade toward the gutter background on a log scale of age, down to 70% for the oldest, so that each date stays readable. A date that takes ui.virtual.annotate, because the theme has no age scope for it, does not fade. dk reads the clock once for each draw, so each age stays current while dk runs, and the ages, colors and fades of one screen agree. It is not a name in [editor] gutters, because a run of annotate can take seconds. With no annotation of the file yet, :annotate annotates the file as space B does, and the column shows when the run ends. A run that fails, as for a file that jj does not track, gives a note on the last row, because you asked for it, and a run that auto-fetch starts stays silent. In a workspace that you did not trust, :annotate gives the trust note, and dk runs nothing. dk binds no key to it, but a [keys] entry can bind annotate.
  • [editor] gutters = [...], or layout = [...] in [editor.gutters], picks the gutter columns, as in Helix: diagnostics, spacer, line-numbers, diff and code-action-hint, which draws blank. [editor.gutters.line-numbers] min-width sets the least width of the numbers, from 0 to 32. A name matches in any letter case, in both forms. A name that dk does not know, or a bad value, gives a note on the last row, and the rest of the config loads. An unknown key in [editor.gutters] is ignored. A trusted .ductus may set these keys too, one by one over the user config.
  • [editor] line-number = "relative" makes each number the distance to the line of the cursor, as in Helix. The line of the cursor keeps its number, and Insert mode and a View without the focus show the numbers from 1. The default is "absolute", and a trusted .ductus may set it. A value that is not absolute or relative gives a note, and dk uses absolute numbers.
  • The scopes that hold the primary cursor, such as a function, a class, an if or a loop, show at the top of the View when their first line is above it, as in nvim-treesitter-context. A scope whose first line is under the row of an outer scope gets a row too, so no row hides the header of an inner scope. Each such scope draws its first line on a row over the text, outer scope first, with the line number in the gutter and the highlight of the line, in the theme scope ui.sticky-context, or else ui.popup, so that the rows stand out from the text in any theme. The nord theme gives ui.sticky-context the background nord2, one step lighter than the cursor line. A theme that sets ui.sticky-context.bottom gives it to the last row, gutter included, with no row of its own, and with no such scope the last row draws as the others. The scopes come from queries/<language>/context.scm, or else from the function.around and class.around nodes of textobjects.scm, or else from the document symbols of the language servers of the file. dk asks the servers for the symbols again after a change, and the rows show when the reply comes, so a key never waits for it. Until then the rows of the last reply stay and move with the text, as the diagnostics do, and a scope whose first line an edit deletes goes. A language that ships a query but has no built grammar takes the symbols of its server too, and the last row then says once in the run, for each language, to run dk grammar fetch and dk grammar build with the name of its grammar. A context.scm in the config or runtime directory replaces the shipped one, with inherits, and a pattern with #lua-match? in it never matches. [editor.sticky-context] max-lines, 3 by default, is the most rows, and the outer scopes go first. The rows never cover the line of the cursor: the top margin of the View is the larger of scrolloff and the number of rows, so the View scrolls and all the rows show. In a View too short for that margin, the rows give way to the cursor. [editor.sticky-context] max-symbols, 10000 by default, is the most document symbols of a server reply that dk reads for the rows, a whole number from 1 to 100000 (A2). [editor.sticky-context] enable = false turns the rows off, and config.toml, a trusted .ductus and :set change the three keys. A file with no built grammar and no server with document symbols, and a View in a diff session, show no rows.
  • :split-view-below or :hs splits the View, and the new View goes below. :split-view-right or :vs puts the new View to the right, past a column that parts the two. C-w s or C-w C-s, and C-w v or C-w C-v, do the same in Normal and Select mode, as in Helix.
  • The new View shows the same file from the same place and takes the focus. Each View keeps its own Selection and scroll position.
  • An edit or an undo in one View keeps the Selection of each other View on the same text. When the edit deletes that text, the Selection moves to where the text was.
  • A split refuses when a View would get less than 2 rows of text or 10 columns, and the last row says so.
  • :split-view-below <file> and :split-view-right <file> show the file in the new View, as :open shows it, and the View you split keeps its file. With more files, as in :split-view-right a.txt 'b c.txt', they open one split for each file in turn, and the last one has the focus, as in Helix. If the file does not open, no View is made. :bn and :bp in one View also leave the other Views as they are, so two Views can show two files.
  • C-w w focuses the next View. C-w h, j, k, and l, or C-w and an arrow, focus the nearest View on that side that overlaps the focused View. Of two equally near Views, the one at the top row or left column of the focused View gets the focus.
  • C-w K moves the View to the top, with the full width of the screen, and C-w J moves it to the bottom, in Normal and Select mode. C-w H moves it to the far left, with the full height, and C-w L to the far right. The four keys follow Vim’s C-w K, C-w J, C-w H and C-w L. In Helix they swap the View with its neighbor. The View keeps its file, its Selection, its scroll position and the focus, and the other Views keep their order. With one View the keys do nothing, and a move that would give a View less than 2 rows of text or 10 columns refuses, as a split does. Their commands, move-view-far-up, move-view-far-down, move-view-far-left and move-view-far-right, are dk names that Helix does not have.
  • In Normal and Select mode, C-w + makes the focused View one row taller, and C-w - one row shorter, as in Vim. [help] names the - key minus. C-w > makes the View one column wider, and C-w < one column narrower. A count sets the step, so 5 C-w + adds 5 rows. C-w _ makes the View as tall as it can be, and C-w | as wide, and with a count, as in 10 C-w _, they make it that many rows of text or columns. C-w = gives the Views equal shares again. The rows or columns come from the Views after the View in its split, the nearest first, and then from the Views before it, and the rows that a View gives up go to the View after it. No View gets less than 2 rows of text or 10 columns, so a step that does not fit goes as far as it can. A View with no neighbor that way does not change, and the last row shows nothing. Their commands, grow-view-height, shrink-view-height, grow-view-width, shrink-view-width, set-view-height, set-view-width and equalize-views, are dk names that Helix does not have.
  • A drag of the status line between two Views moves it up or down, and a drag of the column between two Views moves it left or right, as in Vim. No View gets less than 2 rows of text or 10 columns. The press on the line moves no cursor, and the release yanks nothing. The status line of a View at the bottom of the screen does not drag, and with mouse = false nothing drags.
  • A click or a drag on the status line of a View, or on the column to its right, ends with that View focused, as in Vim, and moves no cursor. The column belongs to the View on its left at the row of the press, so with several Views on that side, the row of the press picks one. A click on a status line at the bottom of the screen focuses its View too.
  • A split or a close of a View gives the Views of the tab page equal shares again, as Vim does with its default equalalways. A split that refuses keeps the sizes. A change of the terminal size keeps the share of each View, and each tab page keeps the sizes of its own Views.
  • :diff-files <a> <b> or :diffs <a> <b> shows file a in the View and file b in a split to its right, as in Helix, and the last row says Diff: "a" vs "b". A path can end in :line or :line:col, counted from 1, as in Helix, and the cursor of its View goes there, in the middle of the View, or to the start with no line. A line past the end goes to the last line, and a file that exists with such a name opens whole. The rows line up: a change gets empty rows on its shorter side, so the next equal line is on the same row in both Views. Removed lines draw on diff.minus, added lines on diff.plus, and changed lines on diff.delta, or on the .view scope of each when the theme has one. In changed lines, the words that changed draw in diff.delta.view.text, else diff.delta.view, as in Helix, with no white space at their ends. Unlike Helix, they never use the bare diff.delta, so that a theme with only a foreground there keeps the words readable. The nord theme sets the .view scope of removed, added, and changed lines, and diff.delta.view.text for the words, each with a background. Most of the other themes that dk ships set them as a mix of the diff color over the background of the theme. A word is a run of letters, digits, and _, and each other character is a word of its own. A change over 64 KiB, or after the first 1 MiB of changes, shows no changed words, and a change of more than 256 words marks all of its words. The focused View scrolls both Views, also to the side, and a move to the other View puts its cursor on the paired line. ]g, [g, ]G, and [G move between the changes on the side of the View. In a diff session, :reset-diff-change or :diffg takes the lines of the other file for each change under a Range, as one undo step, as in Helix. A change with no lines on the side of the View is under a Range on the line after its empty rows. :end-diff ends the session, and so does closing one of its Views or showing another file in it. Two files over 8 MiB show side by side with no lined-up rows.
  • :diff-base shows the vcs base of the file in the View and the file in a split to its right, in a diff session, and the file keeps its cursor and the focus. The base side is a Document named [base] and the file name, with no path, readonly on and modifiable off, as [help], so no save writes it. It has the language of the file, which its path or :set filetype gives, so it shows the syntax colors of that language, and its status line names it. No language server opens the base side, and :format in it runs no formatter (A9). It holds the base of the time of :diff-base, also with gutter-diff = "disk", and a later vcs query changes only the gutter. The keys of a diff session work as for :diff-files, so :diffg in the file puts back the base lines. :diffp into the base side and :diffg in it give Cannot make changes, 'modifiable' is off. When the session ends, the base View closes and the base side goes. As the last View, the base View shows the file, or, when the file is closed, the Document before the base side in the list, as after :bc!. A file with no base text, such as an untracked file or a jj working copy that is a merge, gives Diff is not available in current buffer, and no View changes. In a workspace that you did not trust, it gives the trust note, as space g does. Helix has no such command, and dk binds no key to it, but a [keys] entry can bind diff-base.
  • :diff-disk shows the file on disk of the file in the View and the file in a split to its right, in a diff session, as Vim’s :DiffOrig, and the file keeps its cursor and the focus. So after a change on disk and in dk, it shows what differs before :reload or :w!. dk reads the file in the background, as an open reads it. The disk side is a Document named [disk] and the file name, with no path, readonly on and modifiable off, as the base side of :diff-base. :w in it says [disk] is no file, so dk saves nothing (A6), and its status line draws each control character of the name as a glyph (A5). It holds the file of the time of :diff-disk, and a later change on disk does not change it. :diffg in the file takes the lines on disk, as one undo step, and :diffp into the disk side gives Cannot make changes, 'modifiable' is off. When the session ends, the disk View closes and the disk side goes, as for :diff-base. A file that is the same as the text gives "<name>" is the same as on disk, a file that is not on disk gives "<name>" is not on disk, and a file that is not a regular file gives its error, and no View changes. dk refuses while a save of the file runs, and while the read of an earlier :diff-disk runs. A save that ends during the read starts the read again, and a file that lost the focus during the read gets no session. Helix has no such command.
  • :diff-this marks the View, and :diff-this in another View then compares the two in a diff session, with the marked View as the old side, as in Helix. :diff-this again in the marked View ends the mark, and a View in a session refuses it.
  • :put-diff-change or :diffp puts each change under a Range into the other file of the diff session, as one undo step there, as in Helix. A change with no lines on the side of the View is under a Range on the line after its empty rows.
  • C-w q closes the View, but not the last one of the last tab page, and C-w o closes every other View of the tab page. :q closes the View too (see Commands).
  • z opens the View menu in Normal and Select mode, as in Helix, and its box lists the keys. zt, zz or zc, and zb put the cursor line on the first, the middle and the last row of the View, and the cursor stays. The View then keeps its scrolloff margin, so with the default zt leaves the cursor line 5 rows below the top, as in Helix. zm scrolls the View sideways so that the cursor column is in its middle. zj or z Down scrolls the View down one line, and zk or z Up scrolls it up, so 3zj scrolls three lines. The cursor moves only when the View leaves it behind, as with the page keys, and in Select mode its Range extends. z with C-b, C-f, C-u, C-d, PageUp, PageDown, Backspace or Space runs the page keys. z/, z?, zn and zN search as /, ?, n and N do (Editing), and in Select mode zn and zN move the primary Range.
  • Z opens the same menu as a sticky menu, as in Helix: it stays open after each key, so Zjjj scrolls three lines, and its box shows until Esc closes it. A key that the menu does not have does nothing, and the menu stays, and : still opens the prompt.
  • C-e scrolls the View down a count of lines, and C-y up, in Normal and Select mode, as zj and zk do. Helix binds neither key, so dk follows Vim’s C-e and C-y.
  • After each key, each View scrolls so that the cursor is at least [editor] scrolloff rows from its top and its bottom, and as many columns from its left and its right edge, as in Helix (Config). The margin is at most half the rows or columns of the View, so in a View of 6 rows the cursor stays 2 rows from the top and 3 from the bottom. The page keys, the line scrolls and the wheel move the cursor to the margin when the View leaves it behind, so the View does not scroll back, except in a diff session when the line after a run of filler rows is in the bottom margin. At the end of the text the View scrolls past the last line, so the margin stays.

Workspaces

  • dk looks for a .ductus directory in the working directory and in each directory above it, up to $HOME, or up to / when the working directory is outside $HOME. The nearest one is the workspace.
  • dk ignores the .ductus of a workspace until you trust it. The last row then says .ductus is not trusted: :trust loads it, and your own config still applies.
  • :trust or dk trust trusts the .ductus as it is now. dk stores its path and a hash of its files in $XDG_STATE_HOME/ductus/trusted, or in ~/.local/state/ductus/trusted. A store in a state directory that is a symlink, belongs to another user, or that others can write trusts nothing (A9). When dk fixes the mode of a state directory that others could write, the store goes, so each workspace asks for :trust again.
  • After a change to any file in .ductus, dk asks for trust again before the next load, at start or on :reload-config.
  • For the vcs plugin, the workspace of a file is the nearest directory above it that holds a .ductus or a marker of the plugin. dk follows links to the real file first, and a marker that is a link does not count.
  • dk runs the commands of the vcs plugin only in a trusted workspace, because the program can read config from it that runs more code. Until then the last row says <program> needs trust: :trust allows it once for each workspace, with the first program in the grant, quoted when it is not a plain name, such as jj needs trust. In a repo with no .jj/repo/config-id, jj 0.45 and 0.46 adopt the .jj/repo/config.toml of the repo, so :trust accepts that config too.
  • :trust trusts the workspace of the open file, or the workspace of the working directory when the file is in none. When the two differ, the .ductus note says dk trust loads it.
  • A trusted .ductus/config.toml applies on top of your config. Its [keys] win for the keys it binds, and its theme replaces yours. Themes and plugins still load only from your config directory and the themes dk ships.
  • dk follows a link to a file in .ductus and hashes the file it points to. It refuses a link to a directory, a FIFO, a device, more than 256 files, a file over 1 MiB, and more than 4 MiB in all.