Features
This file lists what a ductus user can do now. CHANGELOG.md lists the commits of each release.
Command line
dk --helpordk -hlists the commands of dk:completion,default-config,diff,grammar,health,helpandtrust.dk <command> --helpanddk help <command>say what a command does.dk --versionordk -vprintsdk version <version>, such asdk version 0.62.0.dk completion bash,fish,powershellorzshprints a completion script for that shell.-dand--debugwrite debug lines and timings todk.login the state directory,$XDG_STATE_HOME/ductusor~/.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--debugand 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
catof 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.
- Each line has a time, the name of the event, and its values, such as
- A file whose name is a command, such as
trust, opens withdk ./trustordk -- trust.--also opens a file whose name starts with a dash. dk a.txt b.txt c.txtopens each file as a Document, in the order of the names, and shows the first. The others are in the Document list, for:bnandspace 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~/xunder 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.ductusstay 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.--tabsor-popens 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-rightor-Oopens each file in a View to the right of the one before, assplit-view-rightdoes, and--split-belowor-oopens each in a View below, assplit-view-belowdoes. The first View has the focus. When a split does not fit, the last row saysnot 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 ofdkonly, sodk diff --tabs a bis 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-dfor a diff,--diff,--trust,--grammarand--default-configare 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-pgo to an older command line, and Down andC-nto 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 scopeui.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, soC-r :on a prompt inserts the newest command line,":ppastes 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-hdelete before the cursor, and on an empty line they close the prompt. C-rand a character insert the first value of that register at the cursor, soC-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 asspace f,i, andC-r a. The box shows over the picker, and Esc afterC-rkeeps 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, andC-r "after a line yank give a clean path. Each other line ending goes in, and the prompt shows LF as^Jand CR LF as�. A value that would make the line over 64 KiB inserts nothing and says so, as a paste does (A2).C-udeletes to the start, andC-kdeletes to the end.C-wandA-backspacedelete the word before the cursor, up to a space or a/.- Delete and
C-ddelete under the cursor. - Home and
C-amove to the start, End andC-eto the end. - Left and
C-b, Right andC-fmove the cursor. - Tab completes the full name of a command, never an alias, as Helix does.
An alias still runs its command:
:xand Tab gives nox, but:xand Enter runswrite-quit. It matches fuzzily, as Helix does: the typed characters must come in order in a name, so:wqfindswrite-quit. A typed capital letter makes the match case sensitive, andealso matchesé. When more names match, Tab andS-Tabstep 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 scopespecialand 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::oshowsAliases: o, edit, e. The box stays while you type the args, and:opshows none. It draws in the theme scopeui.helpwith 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-belowand 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.
- Backspace and
- 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 nameda b.txt."..."also keeps its spaces, and its expansions run, so:echo '%{cursor_line}'shows%{cursor_line}and:echo "line %{cursor_line}"showsline 1. A backslash before a space keeps the space in the arg, so:o a\ b.txtopens 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'bis 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 asecho: 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] shellwith 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:shruns: in the background in a new session, with no timeout, and thespinnerelement spins.:pick-joblists 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 asecho: "%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_columnandcursor_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, ortext;line_ending;selection, the text of the primary Range;selection_line_endandselection_line_start, from 1; andworkspace_directory, the directory of the.ductusof the working directory, or else the working directory.
- In the line of
:run-shell-command,:sh,:!,:pipe,:pipe-to,:insert-outputand: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 nameda;touch pwned,:sh echo %{buffer_name}shows the name and runs notouch. In quotes, a value with no sh syntax gives the line of Helix, such asecho "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}runsecho 'README.md'. A value with a space stays one word, so witha bselected,:sh rm %{selection}removes the filea b, where Helix removesaandb. In each other place the command does not run, and the last row says where, such asrun-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] shellis 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 asa;touch pwnedinto the outer line as it is, and the shell runstouch. :show-aboutopens a Document named[about]in a new View above the focused View, as:helpopens[help]. It lists the version as:show-versionshows 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 asconfig.tomlorconfig.yaml, ornone. 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.ductusof the workspace, it also names that.ductus. It draws each control character of a path as a glyph (A5).[about]hasreadonlyon andmodifiableoff, a second:show-aboutfocuses its View, and it closes with its last View, as[help]does. No save writes it::wsays[about] is no file, so dk saves nothing(A6).:reload-configreads the config and its theme again, and loads the plugins again. Helix gives the command the same name, with no alias.:diff-baseshows the vcs base of the file and the file in a diff session (see Views).:diff-diskshows the file on disk and the file in a diff session (see Views).[editor] gutter-diffmakes the diff gutter compare with the file on disk (see Config).:diff-files,:diffsand:end-diffcompare two files in a diff session (see Views).:echoshows 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-configprints aconfig.tomlwith every key that dk reads at its default, and exits. Each key has a one-line comment, and[keys]and[plugins.<name>] execare commented-out examples. It takes no other argument, and it runs no plugin and loads no workspace.dk default-config > config.tomlgives a config that changes nothing. Helix has no such command.dk healthprints what dk finds for each language, and exits 0. First come the config file, the languages file, the runtime directories, the.ductusof the workspace withtrustedornot trusted, and the clipboard provider, such asx-sel (xsel). A file that does not exist showsdefault, 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-grammarspicks, 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. Souse-grammars = { only = ["proto"] }showsprotobuf. With nouse-grammars,dk grammarbuilds every grammar, so the table has each language and no note. A server or a formatter shows✓ <name>when dk finds its command onPATH,✘ <name>when not, andNonefor 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 allshows each language,languagesandall-languagesshow only the table, filtered or not, andclipboardshows only the clipboard provider.dk health goshows the full path of each server and of the formatter, or'<command>' not found in $PATH, thenTree-sitter parser: ✓,Noneor the load error quoted, and a line for each query and for the comment tokens. An unknown language givesLanguage '<name>' not foundand the languages that start with its first letter. dk reads the.ductusonly 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-healthopens the same text in a Document named[health], and:check-health gothat ofdk health go.[health]hasreadonlyon andmodifiableoff, as[help], and a save of it says[health] is no file, so dk saves nothing(A6). A second:check-healthputs its text in the open[health]and focuses it, and[health]closes with its last View.:helpopens the list of the keys and the commands as a Document named[help], in a new View above the focused View, as Vim’s:helpdoes. Helix has no:help. After the keys of the modes, it lists the keys of the command line, by their Helix names, such asC-w,C-u,tabandup, C-p, each with what it does, as Vim’s help lists them under “Command-line editing”.[help]hasreadonlyon andmodifiableoff, so each Normal and Select mode key moves and selects in it, such asgg,Gandx, and each edit givesCannot make changes, 'modifiable' is off. A second:helpfocuses 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 aswrite,wor:w. When no command has that name, the word is a key as[help]shows it, such asC-w,C-porg g, andggnamesg gtoo. A key that[help]lists more than once goes to its first line. A word with no entry opens nothing and saysNo help for <word>, with each control character of the word drawn as a glyph (A5).:qcloses 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,:openorC-o,[help]closes too, so:bnandspace bdo not show it, and the next:helpopens 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:0does 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 commandon the last row, with its name quoted (A5). :split-view-below,:hs,:split-view-right, and:vssplit the View (see Views).:pick-jobopens a picker of the shell commands that run, oldest first, such as1m15s 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 saysstopped "<command>". A stopped command shows no error when it ends, and a stopped:pipe,:insert-outputor:append-outputchanges nothing.space !in Normal or Select mode opens it too, and the Space box lists it. Helix has no such command.:show-messagesopens the messages that the last row showed as a Document named[messages], in a new View above the focused View, as:helpopens[help]. Helix has no such command: its:log-openopens its log file. Each message is one line, oldest first, with the time, the kind and the text, such as14: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 whilemodifiableis off, and a language server that exits areerrors. So is each error that a command shows itself, such asNo more matchesorDiff is not available in the current buffer. Emergency mode is anerrortoo, and its line has the whole error, with the backtrace of a trap. A note, such as a key inconfig.tomlthat dk does not read ortoo many ranges, is awarning, and a server message is anerroror awarningwhen its type says so. Each other message isinfo, such aswrote "a.txt"orWrapped 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]hasreadonlyon andmodifiableoff, and no path, no language and no language server, as[help]has, and no save writes it::wsays[messages] is no file, so dk saves nothing(A6). A second:show-messagesfocuses 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-optionsopens a Document named[options]in the View, with the text ofdk default-configand 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:bcor:qcloses it. Helix has no such command, so it follows Vim’s:show-options. The file you had stays open.:pipeor:|,:pipe-to,:insert-outputand:append-outputrun the rest of the line with the shell of:sh, with Helix’s names.:piperuns the command once for each selection, with its text as stdin, and puts the output in its place, such as:pipe sort.:pipe-tomakes the same runs, and changes nothing.:insert-outputand:append-outputrun 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-outputand: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, orShell command failed: status N. The command runs in the background, as:shdoes, 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 saysEdit 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 saysthe output is over 16 MiB, so dk changes nothing(A2).|,A-|,!andA-!in Normal and Select mode open the prompt withpipe,pipe-to,insert-outputorappend-outputand a space typed, as Helix’s shell keys. So|, thensortand 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.ductusbinds 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 asA-|orA-!, so in Normal and Select mode they open the prompt withpipe-toorappend-output. Insert mode binds neither, so there they open it withpipeorinsert-output, as Esc and then the key do.:print-documentsends the text to[editor] print-commandon stdin (see Config), so with the defaultenscriptit 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 runsprint-commandwith-o -after its args, so that it writes PostScript to stdout, and pipes that intops2pdf - -. dk writes the PDF as:wwrites 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:wwrites 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:shdoes, and:pick-jobstops it. At the end, the last row saysPrinted N lines, orWrote "<file>"with the name quoted (A5). A command that fails changes nothing, and the last row shows its stderr, orPrint command failed: status Nwhen stderr is empty. A text over 16 MiB starts no command, and the last row saysthe 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.:quitor:qcloses 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 as2 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, orquitting: stopping goplswith 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 inwrote "<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-allor:qaquits from any View, but not while any open file has unsaved changes, as:qrefuses.:quit-all!or:qa!quits and drops them.:refresh-vcs, and the key commandrefresh-vcs, ask the vcs plugin again for the change id and the base text of the file of each View, so ajjcommand in another pane shows. The focused View comes first, then the others, one query at a time.refresh-vcshas 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:reloadand Vim’s:e!. The reload is one undo step, sougives the text back, and a file with the same text adds no step. After it the Document has no unsaved changes, and the next:wdoes 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 givescan'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 saysa save is running(A6). The command takes no argument.:reload-allor:rlareloads each open file as:reloaddoes, also a file that no View shows. A file that does not reload gives its error, and the next file reloads.:reset-diff-changeor:diffgputs 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,:shor:!runs the rest of the line withsh -c, or the[editor] shellofconfig.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-jobstops it. A nonblank output shows in a popup, which has a frame by default (seepopup-borderin Config), and the last row saysCommand run. The output is stdout, or stderr when the command prints to it. A command that fails shows its stderr on the last row, orShell command failed: status Nwhen stderr is empty.:shwith nothing after it givesexpected 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/ttyfails withNo such device or address, and a command that needs a terminal fails too, such asjj describewith 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,39and49go 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-outputand:pipeput 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-vcsdoes, so the change id and the diff gutter show whatjj newdid. A quit stops each command that runs, with its children (A2).:set-optionor:setwith a key and a value changes an[editor]key ofconfig.tomluntil 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 falseor: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 asline-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 asline-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:setthat sets a value then shows the key and its new value on the last row, as:set <key>?shows them, such asauto-completion = falseafter:set noauto-completion, also when the value was the same before. It takes each key that:settakes,filetypetoo, and a key that onlyconfig.tomlsets gives the note of a:setof it.:setwith no key shows an info box with a row<key> = <value>for each key whose value differs from the default ofdk default-config, sorted by key, whatever set it:config.toml, a trusted.ductus,:setor:set-document-option. With no such key, the last row saysevery option is at its default. The box shows until the next key, also whenauto-infois false. The box and the last row show the values of the focused Document as the text that:settakes, 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 givesUnknown key `<key>`, a value that is not JSON, ornull, givesCould not parse field `<value>`, and a value thatconfig.tomlwould not take gives its note, such asmouse: 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).:setwrites no file.:reload-configand:trustread the config again, and that ends each:setvalue.:set undofile-excludedoes nothing and says that onlyconfig.tomlsets it (A4). After:setor:set-document-optionand a space, Tab completes the key, fuzzily, as Tab after:does, as in Helix::set auto-siggives: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 listabsoluteandrelative. The keys with fixed values are each true or false key,clipboard-provider(the built-in names, nevercustom),filetype,gutter-diff,indent-heuristic,inline-blame.show,line-number,picker-start-mode,popup-borderandtabline. A key with no fixed values, such aspicker-size, gets its current value only. After a typed part, Tab completes the fixed values that match it, such asrelativeafter:set line-number r. A key that:setdoes not take gets no completion.:settakes only the keys thatdk default-configlists, so a whole table such asstatuslineis an unknown key, and Tab lists only those keys.:set-document-optionor:setltakes 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.:setalso 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:setvalue at once, as in Helix. Vim gives a buffer a copy of the values when it opens.:reload-configand:trustend the:set-document-optionvalues too.:set filetype markdownand:set-document-option filetype markdowngive the Document of the focused View the languagemarkdownoflanguages.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 textgives the Document no language, and:set filetypewith no value shows its language, such asfiletype = markdown, orfiletype = text. A name shows the new language on the last row in the same way, such asfiletype = markdownafter:set filetype markdown. A name thatlanguages.tomldoes not have givesinvalid language id: `<name>`, with the name as glyphs, and the language stays (A5). After:set filetypeand a space, Tab lists the language of the Document first, and then the other language names andtext. The language stays until the Document closes, so:reload-config,:trustand:w <path>keep it.config.tomland a.ductusdo not takefiletype: there the path picks the language.:show-directoryor:pwdshows the working directory on the last row, as in Helix, such asCurrent 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-zin Normal and Select mode stops dk and gives the terminal back to the shell, as Helix’ssuspend, andfgbrings dk back as it was. dk stops its whole process group, so a parent that waits for dk, such asjj describe, stops too. Akill -TSTPstops dk in the same way. As the session leader, dk has nothing to stop to, andC-zdoes 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 nextfgtries again.:toggle-optionor:togglewith a key flips a true or false key, as in Helix, and the last row shows the new value, such asmouse = false. It changes the key as:setdoes, so:setshows the key and:reload-configends the value, and a key of the Document, such asreadonly, 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 givesUnknown key `<key>`, and a key that onlyconfig.tomlsets gives the note of a:setof it. After:toggle-optionand a space, Tab completes only the keys that are true or false, fuzzily.:trusttrusts the.ductusof the workspace as it is now, and loads it (see Workspaces).:show-versionshowsdk version <version>on the last row, the same text thatdk --versionprints.
Config
- A key of
config.tomlorlanguages.tomlthat 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 itsname, or else by its place from 1, such aslanguage[go].scopeandgrammar[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-grammarsinconfig.tomlshows asuse-grammars (a key of languages.toml). The files of a trusted.ductusgive the same notes, as.ductus/config.tomland.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-providerpicks how+and*reach the system clipboard and the primary selection:pasteboard,wayland,x-clip,x-sel,win32-yank,tmux,termux,termcode,none, or acustomtable. With no value, dk detects the provider at the first use, and:reload-configdetects it again. On Linux it takes the first that fits of termux, tmux whenTMUXis set, termcode in WezTerm, Wayland whenWAYLAND_DISPLAYis set, xclip and then xsel whenDISPLAYis 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 astmux, reads*as empty and writes it nowhere.termcodesends the text to the terminal as OSC 52, base64 encoded, so no byte of the text reaches the terminal raw (A5). Acustomtable names the program that reads the clipboard,yank, and the one that writes it,paste, each with acommandand itsargs, andyank-primaryandpaste-primarydo the same for*. So[editor.clipboard-provider.custom]withyank = { command = "xsel", args = ["-o", "-b"] }andpaste = { command = "xsel", args = ["-i", "-b"] }uses xsel. A command is a program name, whichPATHfinds, 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.:setand:set-document-optiontake a built-in name, such as:set clipboard-provider tmux, and only the user config sets acustomtable (A9). A.ductusthat sets it gives the warning.ductus cannot set clipboard-provider, and nothing changes (A9).[editor] default-yank-registeris the register ofy,d,c,p,PandRwhen"picks none,"by default, as in Helix. So withdefault-yank-register = "a",ysaysyanked 1 selection to register a, and"appastes the yank. A value that is not one printable character gives a note on the last row, and dk uses".:setand:set-document-optiontake it, such as:set default-yank-register b, until the next load of the config. A.ductusthat 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-diffpicks 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-changeuse 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set gutter-diff disk.[editor] jump-label-alphabetgives the label characters ofgw,"abcdefghijklmnopqrstuvwxyz"by default, as in Helix. With"jk"the labels arejj,jk,kjandkk, sogwlabels 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set jump-label-alphabet jk.[editor] picker-sizeis 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set picker-size 100.[editor] picker-start-modeis 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set picker-start-mode insert.[editor] popup-bordersays 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.popuporui.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, andC-dandC-uscroll 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set popup-border none.[editor] print-commandis the program and the first args that:print-documentruns,["enscript"]by default. dk runs it with no shell and gives it the text on stdin, soprint-command = ["lp", "-d", "office"]prints on the printeroffice. For:print-document <file>, dk adds-o -after the args, so aprint-commandthat 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 userconfig.tomlsets it, as only it grantsexec(A9). A.ductusthat sets it gives one warning,.ductus cannot set print-command, and the command stays.:set print-commandand:set-document-option print-commandchange nothing, and sayprint-command: only config.toml sets it (A9).[editor] rulersis a list of the columns of the text, from 1, that show a ruler,[]by default, so no ruler draws. Withrulers = [80, 110], each View draws its 80th and 110th cells of the text inui.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 noui.virtual.rulergives 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.rulersin a[[language]]oflanguages.tomltakes the place of[editor] rulersfor 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.ductusmay set it inconfig.tomlandlanguages.toml, and:setand:set-document-optiontake it, such as:set rulers [80].[editor] scrolloffis 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set scrolloff 0, which lets the cursor reach the edge.[editor] shellis 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, soshell = ["bash", "-c"]runs:shwith 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 userconfig.tomlsets it, as only it grantsexec(A9). A.ductusthat sets it gives one warning,.ductus cannot set shell, and the shell stays.:set shelland:set-document-option shellchange nothing, and sayshell: only config.toml sets it (A9).[editor] tablinesays 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.ductusmay set it, and:setand:set-document-optiontake it, such as:set tabline always.[editor] text-widthis the width ofg qin display columns, 80 by default, as Helix’stext-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.ductusmay set it, and:settakes it, such as:set text-width 72. dk has notext-widthfor each language.[editor] url-openeris the program and the first args that open a URL ofg f,["xdg-open"]by default, or["open"]on macOS. The URL goes to it as one last argument, with no shell. Onlyconfig.tomlsets it, as forshell: a.ductusthat sets it gives one warning and the opener stays, and:setrefuses it (A9).- Each of
config.toml,languages.tomlandthemes/<name>.tomlin the user config directory may also be.yaml,.ymlor.json, and so mayconfigandlanguagesin 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 samename. 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 asconfig.toml and config.yaml set editor.mouse to two values, and exits with status 1 before it draws.:reload-configshows 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-themeorinheritsnames gives a note, and that theme does not load. json takes//and/* */comments and trailing commas. In yaml, an unquotedyes,no,on,off,trueorfalse, in any letter case, is true or false, as Ruby reads it, andy,nand 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 andnull. 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.tomlfile, dk reads it as before. The notes name the real files, such asconfig.yaml, anddk default-configand:show-optionsstill print TOML. - A syntax error in a TOML or yaml file of
config,languagesor a theme shows on the last row as<file>:<line>: <message>, such asconfig.toml:3: expected value but found '=' instead.<file>is the name that the other notes use, such asconfig.yamlor.ductus/config.toml, and never a path (A5). A theme note starts withtheme:, as intheme: 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 howspace fandspace /walk the files, as in Helix.hidden = falselists the names that start with., but never a.git,.hg,.jj,.pijulor.svndirectory.max-depth = 1lists 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 = falsekeeps them out of a linked directory. So a link in a cloned repo, such asx -> ~, 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, anddeduplicate-links = falselists it. No value raises the caps of 100,000 files inspace f, and of 100,000 matches and 8 MiB of each file inspace /(A2). A value of another type, or a negativemax-depth, gives a note on the last row, and dk uses the default.:setand:set-document-optiontake 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.ignorein any directory,ignorein the config dir of dk, and.gitignore,.git/info/excludeand git’score.excludesFileonly in a.gitor.jjrepo. 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,ignoreandparentsaretrueby default, andfalseturns 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 holdsspace ffor about 1 s (A2). A segment of an ignore line with an unclosed[matches only as a whole literal.space falso skips compressed archives, such as.zipand.gz, as the file picker of Helix does, andspace /lists them.[editor.search] smart-case = falsemakesspace /,/,?,nandNmatch case also for a pattern with no upper case letter.[editor.search] wrap-around = falsemakes/,?,nandNstop at the last match, andnandNthen sayNo 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, andj,kand 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 8in Normal and Select mode shows the Grapheme at the cursor on the last row, and then for each code point itsU+value and its UTF-8 bytes in hex, joined with+, as Vim’sgaandg8in one key. Soéas one code point showsé U+00E9 c3 a9, andewith 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\nshows^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 asnot 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 saysend of text.g 8changes 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, orSEL, 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-modesis true, the default, the mode draws inui.statusline.normal,ui.statusline.insert, orui.statusline.select, and inui.statuslinewhen the theme does not set that scope. In another View, the blank cells of the mode draw as the rest of its status line. Withcolor-modes = false, the mode draws as the rest of the status line in each mode. Acolor-modesthat is not true or false gives a note, and dk uses true. A trusted.ductuscan set it, and:setand:set-document-optiontake 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 asgo, ortextfor 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]inconfig.tomlsets theleft,center, andrightlists, theseparator, and themodenames, as in Helix, and a trusted.ductuscan set them too. The elements arecode-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, andworkspace-diagnostics.code-action-hint,register, andworkspace-diagnosticsdraw nothing yet, andfile-encodingdraws nothing for UTF-8, as in Helix.spinnerspins 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-indicatordraws[readonly]while the Document is readonly (see Saving). Theseparatorelement draws theseparatortext inui.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 incenter = [{ text = "[" }, "version-control", { text = "]" }]. The text draws inui.statusline.text, else inui.statusline.separator, else inui.statusline. A control character in it shows as a glyph, and a text over 256 bytes gives a warning and draws nothing.file-nameshows 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
txwwzpoufor 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 asrzopszyw t243/tab-width-cursor⇡1. A merge with bookmarks on its parents shows each name, as ina⇡1 b⇡1, and a bookmark on the working copy shows with no⇡, as intxwwzpou 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 longbgiveskkwtqnup a⇡1 c⇡1 …+1.…+Nat 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 thatjj logshows, which the jj aliasformat_short_id(id)sets, such asid.shortest(3), and 8 by default. The parts take the colors ofjj 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 notshortest, such asid.short(5), shows the id inui.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 int243/tab-width-cursor*⇡1. A theme can set the colors (see Themes), and⇡nand…+Ntake 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, setfocus-events onso 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.
▍indiff.plus.guttermarks an added line,▍indiff.delta.guttera changed line, and▔indiff.minus.gutterthe 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, asjj diffcompares them.[editor] gutter-diffcan 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] showpicks the lines:never, the default, shows none,cursor-lineshows the cursor line of each View, andall-lineseach line of the View. The blame starts 6 cells after the end of the line, inui.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.formatsets the text, by default{author}, {time-ago} • {title} • {change}. Its variables are{author},{email},{date}asYYYY-MM-DD,{time-ago}such as3 years ago,{title}, the first line of the description orNo descriptionfor 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 asjj 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. Withauto-fetch = true, dk annotates a file when it opens in a View, and each open file when:reload-configor:set inline-blame.auto-fetch trueturnsauto-fetchon. Withauto-fetch = true,:trustannotates the open files of the workspace that it trusts. Withauto-fetch = false, the default, onlyspace Bannotates 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.ductuscan set the three keys, and ashowthat dk does not know gives a note, andnever. space Bin Normal and Select mode shows the blame of the cursor line on the last row, informat, orNot committed yet. With no annotation yet, it annotates the file, and the last row saysAnnotating <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 givesplugin "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-messageslists aserror, such asno such command: "foo"or a failed save, draws in the theme scopeerror, and any other result inui.text, as in Helix. The Emergency mode notice draws inerrortoo. A theme with noerrorscope draws an error inui.text. The scope does not change the text, which draws as glyphs (A5). theme = "<name>"inconfig.tomldraws dk in the Helix themethemes/<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.inheritsfinds its theme in the same order. The theme can use a palette,inherits, and the light and dark table, where dk takesfallbackordark. dk drawsui.background,ui.text,ui.statusline,ui.statusline.separator,ui.statusline.text,ui.virtual.whitespacefor^Aand the like, andspecialfor the matched characters on the match row and in the pickers. The rows of a picker draw inui.textonui.background, as the box under them, and the selected row inui.menu.selected. A theme with noui.menu.selecteddraws the selected row of a picker inui.text.focus, which falls back toui.text. On the status line,ui.statusline.version-control.prefix,.restand.bookmarkset the style of the prefix of the change id, the rest of it, and the bookmark names. Each one falls back toui.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, takesui.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 inui.selection.primary, and the cursor of each Range that is not primary inui.cursor.normal,ui.cursor.insertorui.cursor.selectby the mode, else inui.cursor, or else inui.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 inui.cursor.primary.normal,ui.cursor.primary.insertorui.cursor.primary.selectby the mode, else inui.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-messageslists 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_RUNTIMEreplaces it when the value is an absolute path. - With no
themein the config, dk draws in Helix’snordtheme, which it ships. The binary holds a copy ofnordtoo, sonordloads with no runtime directory.themes/nord.tomlin the config directory or the runtime directory replaces that copy. Thenordtheme setsui.virtualto the graynord3, so aui.virtualscope thatnorddoes 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 assonokaiorzenburn, names its file inLICENSES/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 getsdiff.plus.view,diff.minus.viewanddiff.delta.view, marked# dk addition: a background with 25% of the color ofdiff.plus,diff.minusordiff.deltaover the background ofui.background.diff.delta.view.textgets 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 asgreen, gets no scope for it. A diff scope that sets a background keeps it for its lines and gets no scope, such as inzenburn. 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 asvim_habamax,vim_desertandvim_retrobox, 33 themes in all. A colorscheme with a dark and a light branch, such as retrobox, givesvim_<name>from the dark one andvim_<name>_lightfrom the light one.defaultsets no colors, so dk has novim_default. dk also ships the default colors of Neovim 1502d4a9 asneovimandneovim_light. Each theme has the gui colors and attributes of the highlight groups that dk has a scope for, such asNormalforui.backgroundandui.text,Visualforui.selection, andDiffTextfordiff.delta.view.text. The Vim themes are under the Vim license, inLICENSES/Vim, and the Neovim themes under the license of Neovim, inLICENSES/Apache-2.0-neovim. The binary holds none of them. :set-theme <name>draws dk in another theme at once, found astheme = "<name>"finds it. The theme lasts until dk quits or:reload-configreads 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-themewith no name shows the name of the theme.- Tab after
:set-themeand a space completes the theme name from the config directory, the runtime directory, and the themes dk ships, in sorted order. Each Tab andS-Tabstep 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.
ienters Insert mode at the start of each Range, and Esc goes back to Normal mode. :set nomodifiableor:set modifiable falsestops each change of the text of the focused Document, as Vim’s ‘modifiable’, and:set modifiableallows them again.:set-document-optiondoes the same, and neither has a session value or aconfig.tomlkey. Typed text, deletes, undo and redo,:reset-diff-change,:put-diff-changeinto it, auto pairs, plugins, and the edits of a language server all refuse, and the last row saysCannot 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.:wstill 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-jadds a line break in the line ending of the file, LF or CRLF, as Helix’sinsert_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, fromito 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-dandA-Deletedelete to the end of the word, asefinds it.C-dand Delete delete the Grapheme under the cursor.C-handS-Backspacedelete before the cursor, as Backspace does.C-kdeletes to the end of the line, and on the line ending it joins the next line.C-rand a character paste that register at each cursor, soC-r ainserts registera, 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-sends the Revision so far, souundoes only the text typed after it.C-udeletes 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-wandA-Backspacedelete back to the start of the word, asbfinds it, soC-wafterfoo barleavesfooand the space after it.- Home goes to the line start, and End to the line ending.
S-Tabinserts 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-Tabdoes.
- 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 ofC-wandA-dthroughmove-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, sodon'ttypes 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-pairsin[editor]ofconfig.tomlistrue,false, or a table such as{ '(' = ')', '<' = '>' }, and a trusted.ductuscan set it too. Theauto-pairsof a[[language]]inlanguages.tomlsets the pairs of that language, andfalseor{}turns them off there. dk takes the language of the file path for this. The shippedlanguages.tomlholds theauto-pairsof Helix, so Rust, HTML, and the other languages that Helix gives pairs get the same pairs.auto-pairs = falsein[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
Pdoes. 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 saysyanked 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 = falsein[editor]turns off the yank of a release and the middle click, and a trusted.ductuscan set it too. A middle click on the tab line still closes a tab page.mouse = falsein[editor]ofconfig.tomlturns the mouse off, so the terminal selects text itself, and a trusted.ductuscan set it too.mouse-yank-registerin[editor]ofconfig.tomlpicks the register of the release and the middle click,*by default. Onlyconfig.tomlsets it::setrefuses it, and a.ductusthat 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*.ddeletes the text of every Range, andcdeletes it and enters Insert mode, as in Helix. Both yank the text to the register first, andA-dandA-cdo the same with no yank.aenters Insert mode after each Range, so at the end of the Document the text appends.cand the text typed after it are one Revision, and so is eachd. In Select mode the same keys act, then go back to Normal mode.yyanks the text of each Range to the register and saysyanked 2 selections to register ", as in Helix.ppastes the register after each Range andPbefore 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:pafter the line of the Range andPat its start, soxyjpcopies a line below the next one. A count pastes each value that many times, and in Normal mode the pasted text becomes the Selection.Rputs 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"ayyanks toaand"appastes it, and with none picked a command usesdefault-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 astermcode, leave the label alone with no note (A2). The registers last for the session._takes nothing and reads as empty, so"_ddeletes 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:saysRegister % does not support writing, anddandcthen delete nothing."takes:as the name of a register, as in Vim, so":ppastes the newest command line. This is the only wait for a key in which:does not open the prompt, sof:andmi: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"+yand"+pon three Ranges paste three values. When a provider command fails, or runs longer than 2 s and stops, the last row saysclipboard provider command failed, a yank keeps nothing, and a paste inserts nothing (A2).termcodeandnonecannot read, so a paste with them saysThis clipboard provider does not support readingand inserts nothing.space yyanks the text of each Range to the system clipboard, andspace Ythe text of the primary Range only.space pandspace Ppaste the system clipboard after and before each Range, andspace Rputs it in place of each Range. They work asy,p,PandRwith register+, in Normal and Select mode, whatever register"picked.rand then a key replaces each character of every Range with the key, as in Helix, sor-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, sordwrites ad. The replace is one Revision, a count changes nothing, and in Select modergoes back to Normal mode.~in Normal and Select mode switches the case of each Range, as Helix’sswitch_case, soHello WorldbecomeshELLO wORLD.`makes each Range lower case andA-`makes it upper case, as Helix’sswitch_to_lowercaseandswitch_to_uppercase. dk changes each code point with the case tables of Go, so~makesÄ1éintoä1É, and`makesİintoi. One code point gives one code point, soA-`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. Withmodifiableoff it changes nothing, and the last row saysCannot make changes, 'modifiable' is off. In a terminal without the kitty keyboard protocol or Win32 input mode, a fast Esc and`reach dk asA-`, so they runswitch-to-uppercasein Normal and Select mode.C-candspace ccomment 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// aat an indent of 2 and// bat an indent of 4 becomeaandb. 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 thecomment-tokensandblock-comment-tokensof the language inlanguages.toml, not those of an injected language yet. OneC-cover many Ranges is one undo step.:format, or:fmt, formats the file, as in Helix. Theformatter = { command, args }of its[[language]]inlanguages.tomlruns 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 theformatkey of the server config. Else the last row saysA 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
.ductuscan set it (A9). A file in no workspace gets no formatter. Withmodifiableoff,:formatruns no formatter, and the last row saysCannot make changes, 'modifiable' is off. Its command must be a program name onPATHor 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, asFormatter error: "..."(A5). =in Normal and Select mode formats the selection with the first language server of the file that formats a range, as Helix’sformat_selections. The change is one Revision, as with:format. With more than one Range it saysformat-selection only supports a single selection for now, and with no such serverNo configured language server supports range formatting.uundoes the last change andUredoes it, as in Helix, in Normal and Select mode.A-uandA-Umove back and on through the changes in the order they were made, across the branches that an edit after an undo starts.g -andg +in Normal mode do the same asA-uandA-U, as Vim’sg-andg+, so3g-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 andureach dk asA-u, so they rungoto-earlier-revisionin 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 linesafter3xdand3 more linesafter itsu. 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,uandU, and so do:pipe,:insert-outputand:append-outputwhen their output comes in. When:pipechanges the text and keeps the line count, the last row says how many lines it replaced, such as5 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 as2 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:pipeinto 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 aninfomessage: it does not replace another message of the same key, and a newer report replaces it. space uin 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, butC-sandC-vpick 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). Theductusstate 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:wsets 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$TMPDIRkeeps no Undofile, so apasssecret never reaches disk (A4).[editor] undofile-excludein the userconfig.tomllists 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*.envand**/.git/COMMIT_EDITMSG, and a list replaces them. A workspace.ductuscannot 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, andC-ugo back to it. Another motion or an edit forgets the column. g jandg kmove every Range down and up by text line, as Helix’smove_line_downandmove_line_up, and extend in Select mode. They keep the column asjandkdo, and a count stops at the first or the last line. Views do not soft-wrap, so today they move asjandkdo, 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.
Cadds a Range on the line below each Range, at the same display column, as in Helix.A-Cadds 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-vin Normal and Select mode enters Block mode, which the status line namesBLK. 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 linesabcd,C-vljjselectsabon each line. The motions of Select mode move the cursor corner, andjandkkeep its display column past a short line. The cursor corner draws inui.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 notjork.oswaps the two corners, andOswaps 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 asi,c,d,yandr, and dk then goes back to Normal mode.C-vor 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
dandc, which yank first.porPof 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. sopens the promptselect:, and each match of the regex inside each Range becomes a Range, as Helix’sselect_regex.Sopenssplit:and splits each Range at the matches, so the text between them becomes the Ranges.Kopenskeep:and keeps the Ranges whose text matches, andA-Kopensremove: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, sonthen searches for it. Enter with no match saysnothing selectedforsandno selections remainingforKandA-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\bsee the edge of a Range as the edge of the text, so^amatches anaat the start of a Range.sandSstop at 10,000 Ranges with the note “too many ranges” (A2). The four keys work in Select mode too.A-ssplits each Range into its lines, without the line endings, as Helix’ssplit_selection_on_newline. It stops at 10,000 Ranges with the note “too many ranges” (A2).A-minusmerges 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-forwardturns each backward Range forward, with the same text. It has no key, because dk readsA-:, its Helix key, as Esc and then:, so a[keys]binding or the command palette runs it. The first Range is primary, and afterA-_the Range that holds the old primary. These keys work in Select mode too.A-,removes the primary Range, as Helix’sremove_primary_selection, and the Range after it becomes primary, or the one before it when it was the last. With one Range it saysno 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, andA-(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, anduundoes 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.Xmakes each Range cover its whole lines with the line ending of the last, as Helix’sextend_to_line_bounds.A-xshrinks 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’sindentandunindent, and both leave Select mode.>puts one indent unit before each line that is not blank, and2>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 for2<, 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, anduundoes the key in one step. An indent over 1 MiB changes nothing, and the last row says so (A2).Jjoins the lines of each Range, or a Range on one line with the next line, as Helix’sjoin_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-Jjoins in the same way and selects the spaces that it puts in, anduundoes either join in one step.g Jin Normal and Select mode joins the same lines asJ, as Vim’sgJ, but removes only each line ending between them,\nor\r\n. The spaces and tabs at the start of the next line and its comment token stay, and dk puts in no space, sofooandbargivefoobar. On the last line it changes nothing,uundoes it in one step, and.repeats it.g qin Normal and Select mode fills each paragraph of each selection to[editor] text-width, as Vim’sgqand 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, sowell-knownstays whole, and a word wider thantext-widthstays 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 oftext-widthstays 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, souundoes 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:reflowtyped 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’salign_selections. dk then leaves Select mode, and a Range over two lines makes&sayalign cannot work with multi line selectionsand change nothing._takes the whitespace off both ends of each Range, as Helix’strim_selections. A Range of only whitespace goes, and with none left the primary Range becomes a cursor.C-aadds 1 to the integer of each Range andC-xsubtracts 1, in Normal and Select mode, as Helix’sincrementanddecrement, and dk leaves Select mode when a Range changes. The whole text of a Range must be the integer, so the cursor on the2of123gives133, and a Range that is not an integer stays as it is. A count adds that much, so5C-aon10gives15. With register#the amount grows by 1 for each Range after the first, so"#C-aon three Ranges of0gives1,2, and3. An integer is base 10, or base 16, 8, or 2 after0x,0o, or0b, and only base 10 goes below 0._can split the digits, such as1_000, and stays at its place from the right. Leading zeros keep the width, so007gives008, 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), anduundoes the key in one step.X,A-x,J,A-J,&, and_work in Select mode too.w,b, andeselect 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, andEdo the same for the long word, as in Helix: letters, digits, and punctuation next to each other make one long word, soWonfoo/bar.go bazgoes tobaz. 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 throughmove-word, so akeymap.wasmin the config directory that does not know their names makes them do nothing.fand then a key selects from the cursor to the next match of the key, andtto the character before it, as in Helix.FandTgo 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 ona(b, c)f,selectsa(b,.tstarts one character further on, sot,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, sofedoes 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 asd,A-d,r,R,porP, a command that enters Insert mode, such asi,a,I,A,o,O,corA-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 after2p,.pastes two times, and3.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 fromb. 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 commandrepeat-last-edit, and.after a prefix key, such asg, does not run it.A-.runs the last motion again, as in Helix, and a count runs it that many times: the finds,]pand[p,]gand[g, the tree-sitter jumps such as]f, the syntax tree keys such asA-o, the objects ofmiandma, and the move of Tab past a syntax node. Each motion keeps the count it ran with, so after2f,,A-.goes two more on.- A count before a motion repeats it, as in Helix:
3wselects the third word, and2jmoves 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). gggoes to the first line, and12ggor12Gto line 12, as in Helix.gegoes to the last line, and so doesGwith 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 modeggandgeextend every Range, andGmoves it, as in Helix.H,MandLgo 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.HandLstayscrolloffrows inside the edge of the View, 5 by default, or less in a small View, andHstays 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,Hgoes to it, and when the last line shows above the bottom margin,Lgoes to it. A count goes to line N from the top forHand from the bottom forL, but never into those rows, so with the default3Hgoes to the 6th row and8Hto the 8th.Mtakes no count, and when the end of the text shows,MandLcount 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 hand Home go to the start of the line, andg land End to the last Grapheme before the line ending, or to the start of an empty line.g sgoes 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 Mgoes to the Grapheme at half the Graphemes of the line, as Vim’sgM, and a count of 1 to 100 goes to that percent of them, so25gMgoes 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 keepsextend-to-line-end, and the commands areextend-to-line-start,extend-to-line-end,extend-to-first-non-whitespace,extend-to-columnandextend-to-line-middle. C-ogoes back to the place before a jump, andC-ior Tab goes forward again, in Normal and Select mode, as Helix’s jumplist. A count goes that many jumps, so3thenC-ogoes 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 ofgw, and the goto keys of the language servers such asgdsave 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-ssaves the place and saysSelection 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>orC-vin 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:bcof a file drops its places (A1).g ;andg ,in Normal and Select mode go to an older and a newer revision position, as Vim’sg;andg,. 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 fivedside 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 positiong ;saysAt the oldest revision position, with no newer oneg ,saysAt the newest revision position, and with none each saysNo revision position.u,U,A-uandA-Uadd 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 Iin 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’sgi. 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 Iworks asi.g Iis a change, so.repeats it.space jin 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, andC-sandC-vgo 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 promptsearch:, and?opens it for a search back, as in Helix, in Normal and Select mode and in thezandZmenus. 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/, sonandNgo on from there. Enter on an empty line searches for the newest pattern of register/, and with none it does nothing. Up andC-pput an older pattern of register/on the line, and Down andC-na newer one, as in Helix, and each step searches as a typed key does. A pattern over 64 KiB, which"/yof 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 registera, Up and Down step through registera, andnandNthen read registera. 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 ofn, 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).nselects the next match of the search pattern after the primary Range, in its place, andNthe match before it, as in Helix.zn,zN,ZnandZNdo the same, and a count goes that many matches on, so3ngoes three on. In Select modenandNadd the match as a new primary Range, and the keys of thezmenu 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 saysWrapped around document(wrap-around, Config), and with no match it saysNo more matches. The pattern is the first value of register/, or of the register of the last/,?or*, and"abeforenreads registeraonce. 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 saysInvalid regex: (, with control characters as glyphs (A5).*puts the text of each Range in register/as a regex and saysregister '/' set to '\bfoo\b', as in Helix, sonthen goes to the nextfoothat is a whole word.*adds\bat a Range edge that is the edge of a word, andA-*adds none.g *in Normal and Select mode does the same asA-*, as Vim’sg*. Go’s\bholds only next to an ASCII letter, digit or_, so dk adds no\bnext to another letter, and*oncafégives\bcafé, which also matches the start ofcafé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 registera, andnandNthen read registera. Register/keeps the newest 100 patterns (A2).C-fand PageDown scroll the View down by its text rows, andC-band 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 thatgegoes 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 afterC-fto the end,H,MandLgo to a line in the View, and the View does not scroll. The line scrolls and the wheel stop at the same line.C-dandC-uscroll 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%dempties it in one undo step.xselects the lines of each Range, from the start of the first through the line break of the last, as Helix’sextend_line_below, in Normal and Select mode. On a Range that is those lines already,xadds the line below, so a secondxtakes one more line.3xon 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 onex, and merge after the second.mmmoves 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,mmuses 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 thatmmfinds from a bracket, so it needs a built grammar. Nothing draws off a pair, with no match, or when the theme does not setui.cursor.matchitself. 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). miandmaand a pair character select inside and around the pair around each Range, as in Helix:mi(selects the text between(and), andma(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 inmi"ormi*.mimtakes 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, so2mi(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 ofmi,ma,ms,mrormd.miwandmawselect the word at the cursor,miWandmaWthe WORD, which only spaces end, andmipandmapthe paragraph, as in Helix.mawtakes the spaces after the word, or with none there the spaces before it, andmaptakes 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 throughmove-word, so akeymap.wasmin the config directory that does not know their names makes them do nothing.miandmawithf,t,a,c,T,eorxselect 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 capturefunction.insideorfunction.around,class.insideorclass.around, and so for the others, in thetextobjects.scmof the language. A match of more than one node, such as(comment)+, is one object. With no grammar, notextobjects.scmor 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-oorA-upselects the parent node of each Range in the syntax tree, as in Helix, andA-iorA-downgoes back the same steps. With no step to go back to,A-iselects the first child node.A-norA-rightandA-porA-leftselect the next and the previous named sibling node, so they go from one argument to the next and step over the commas.A-aselects each named sibling node as a Range, andA-IorA-S-downselects each named child node, such as each statement of a statement list. In Go a block holds a statement list, soA-Ion a block selects the list, and a secondA-Iselects 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 forA-i, and a new split starts with none. An edit drops the steps, and so doesA-ion 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).migandmagselect the whole lines of the diff change at the cursor line of each Range, toward the Range, as in Helix, somigddeletes 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 saysDiff is not available in current buffer.ms,mrandmdadd, 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.msand 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, somr([changes(a)to[a]. A key that is not a character gives back the Ranges. The second key ofmrdoes nothing when the Document changed after the first key (A2).md(deletes the pair.mrmandmdmtake 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).
Ienters Insert mode at the first character of the line that is not a space or a tab, andAat 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,Igoes to the start of the line. On an empty line,IandAput the indent of a new line there, as Helix does.oadds a line below the line of each Range, andOadds 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,
oorOgets the indent that Helix gives it. With a built grammar that has an indent query, the indent is the number of@indentscopes that hold the new line, less the@outdenttokens that start it. That is Helix’stree-sitterheuristic. 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-heuristicin[editor]ofconfig.tomlpickssimple,tree-sitterorhybrid, and a trusted.ductuscan set it too. Another value gives a note, and dk useshybrid. - 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 fromindent = { tab-width, unit }of the language inlanguages.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 alanguages.tomlerror. 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).
venters Select mode, and the status line shows SELECT. Thereh,j,k,l, the arrows,w,b,e,W,B, andEextend every Range instead of moving it, as in Helix: each Range keeps its anchor. Esc orvgoes back to Normal mode,ienters Insert mode, and:opens the prompt, which returns to Select mode.g vin Normal mode selects the last Selection of the View again, with all its Ranges and the same primary Range, as Vim’sgv. The last Selection is the newer of two: the Selection from just before Select mode last ended, and the Selection afterxorXin Normal mode.g venters Select mode for the first, and stays in Normal mode for the second, sox x j g vselects the two lines again. Select mode ends withv, Esc, or a key that leaves it, such asdorc. Other keys that select, such asw, andxorXin Select mode, make no last Selection. In Select modeg vswaps the Selection and the last one and stays in Select mode, and a secondg vswaps them back. With none,g vsaysNo last selectionand changes nothing. Each View keeps its own for each file, a split starts with a copy, an edit moves it with its text, and:bcof the file drops it (A1).
Grammars
dk grammar fetchgets the source of each grammar thatuse-grammarsinlanguages.tomlpicks, at itsrev, 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 buildbuilds 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: thelanguage-servers,rootsandlanguage-idof each language, and each[language-server]table. wren and gemini stay out, as Helix builds neither, and their languages stay, with plain text. dk readsuse-grammars, withonlyorexcept, and thenameandsourceof each[[grammar]]oflanguages.toml, withgit,path,revandsubpathinsource. Of a[[language]], it readsauto-format,auto-pairs,block-comment-tokens,comment-token,comment-tokens,file-types,formatter,grammar,indent,injection-regex,language-id,language-servers,name,roots,rulersandshebangs. Inside those keys it reads thestartandendof a block comment token, theglobof a file type,argsandcommandinformatter,tab-widthandunitinindent, and thenameof alanguage-serversentry. Of a[language-server.<name>], it readsargs,command,config,environmentandtimeout, and it takesconfigwhole. The shippedlanguages.tomlholds no other key of these tables, but for theexcept-featuresof a fewlanguage-serversentries, which dk does not read yet.languages.tomlin 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 itssource. Acomment-tokenorcomment-tokensof a user table replaces both names in the shipped table. A table that sets both readscomment-tokens.use-grammars = { only = [...] }or{ except = [...] }before the first[[language]]picks the grammars, in Helix format. With nouse-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:tsqgives query.dk grammar fetch rust goanddk grammar build rust goact on the named grammars only, whateveruse-grammarspicks. A name thatlanguages.tomldoes not have is an error, and nothing runs.fetchandbuildare the commands ofdk grammar, sodk grammarwith no command shows its help, and another word, such asdk grammar pull, givesunknown command "pull" for "dk grammar"and exits 1.use-grammarsin a.ductus/languages.tomlchanges nothing, trusted or not, and dk says so (A9, A10).- A git source must be one commit of an
httpsrepository. dk refuses a source with norev, a branch, a tag name, a short commit id, or another URL scheme, and runs no git for it (A10). Arevcan 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 therevgoes, and the fetch fails. A checkout at therevstays, 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 buildcompiles each fetched grammar with$CC, orcc, intogrammars/<name>.so, mode 0600. It builds only a checkout at itsrev,srcmust stay inside the checkout, andparser.candscanner.corscanner.ccmust 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). Ascanner.ccbuilds with$CXX, orc++, which then also compilesparser.c, as C, and links the grammar, so the C++ library loads with it. A grammar with bothscanner.candscanner.ccbuildsscanner.c, as Helix does. What a compiler prints shows quoted (A5). After these checks, a build skips a grammar whose.sois not older thanparser.cand its scanner, and prints<name>: already built, as Helix does. A fetch of a newrev, or an edit of a path source, builds again, but a change of$CCor$CXXdoes not.source = { path = "/home/you/tree-sitter-mine" }in yourlanguages.tomlnames a grammar in a directory of yours.dk grammar buildcompiles itssrcand runs no git, anddk grammar fetchskips it and says so. The path must be absolute, so it never names a directory in the working directory (A9). The directory, itssrcand 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 addschmod -R go-wwith the directory. As in Helix, apathin a source wins over itsgit,revandsubpath.- A
[[grammar]]in a.ductus/languages.tomlnever 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.scmof Helix d5d88b62, for the grammar revs that dk pins. dk reads thelocals.scmof a language, as Helix does, so a pattern with#is-not? localskips a name that a parameter or a variable in its scope declares, such as a parameter namedconsolein 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 asvariable.parameter. dk adds definitions of its own to the shippedlocals.scmof JavaScript, TypeScript and Python, and of the languages whoselocals.scminherits theirs, such as JSX, TSX and Mojo. In JavaScript and TypeScript, alet,constorvardeclarator, and a function or class declaration, are definitions. In Python, the target of an assignment, of aforand of awith ... as, and a function or class definition, are definitions. A definition on thenameof a node that is a scope oflocals.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 userlocals.scmof 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 parameterx, a use ofxabovex := 2in anifblock shows asvariable.parameter, and a use below it asvariable. Withhoist-locals = truein a[[language]]oflanguages.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 shippedlanguages.tomlsetshoist-locals = truefor JavaScript, JSX, Python, TSX and TypeScript, and other languages do not hoist. So with the shipped files, thexofconsole.log(x)infunction f(x) { { console.log(x); let x = 2; } }takes the highlight of thelet x, not of the parameter. Avarhoists only in its block, not in its whole function.hoist-locals = falsein a userlanguages.tomlgives back the latest definition before the use. Anotherlocals.scmcapture on the same node, such as the@_on the key of a Go struct literal, keeps the highlight ofhighlights.scm. Alocals.scmthat does not compile gives a note, and the highlights work without it. A layer with more than 100,000 scopes and definitions in itslocals.scmgets none, so its#is-not? localpatterns apply to every name (A2). A use past the first 100,000 uses in a layer keeps the highlight ofhighlights.scm(A2). dk counts eachlocal.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.scmin 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 ashclandtfvars, have their own queries. A language name with/or\, or one that is empty,.or.., is alanguages.tomlerror, so its queries stay underqueries/(A9). - dk ships the 232
injections.scmof Helix d5d88b62 too, andqueries/<language>/injections.scmin the config or runtime directory replaces it in the same way, withinherits. 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 withmarkdown.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 itsinjection-regex, so a fence namedtsfinds typescript. Theinjection-regexis a Go regular expression, and one that Go refuses is alanguages.tomlerror. A#set! injection.language, an@injection.shebangand an@injection.filenamefind a language too.injection.combined,injection.include-childrenandinjection.include-unnamed-childrenwork. 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.scmof Helix d5d88b62 too, andqueries/<language>/textobjects.scmin the config or runtime directory replaces it in the same way, withinherits. dk ignores the#make-range!and#offset!directives of a textobject query, as Helix does. - dk ships the 77
tags.scmof Helix d5d88b62 too, andqueries/<language>/tags.scmin the config or runtime directory replaces it in the same way, withinherits. Atags.scmthat 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
rainbowlist of the theme, by how deep they nest, as in Helix.[editor] rainbow-brackets = falseturns them off, andrainbow-bracketsin a[[language]]oflanguages.tomltakes its place for that language. The default istrue. dk ships the 91rainbows.scmof Helix d5d88b62 too, andqueries/<language>/rainbows.scmin the config or runtime directory replaces it in the same way, withinherits. A language with norainbows.scmshows 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 ofprintln!(...)in the body of a Rust function take the second color. Each@rainbow.scopenode holds one level more, and a@rainbow.brackettakes the color of its level, modulo the length ofrainbow, when its parent is the scope node or the scope hasrainbow.include-children. So in the Go linefunc m() { f(a[g(b)]) }the braces take the first color, the parens offthe second,[and]the third, and the parens ofgthe fourth.rainbowis a list of styles, with the names of the palette and modifiers, such asrainbow = ["red", { fg = "blue", modifiers = ["bold"] }]. A theme with norainbowgets 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. Arainbows.scmthat does not compile gives one note, with its error quoted (A5), and the highlights work without it. - dk ships the 76
context.scmof nvim-treesitter-context 8decd734 whose language dk has, under the MIT License inLICENSES/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 namedmuttrc,neomuttrc,.muttrcor.neomuttrc, or one that ends in.muttrc, is muttrc, anddk grammar fetch muttrcanddk grammar build muttrcget its grammar. The queries use the capture names of Helix, sosetshows in thekeywordstyle of the theme, the text of a quoted string in thestringstyle, andmicin a comment selects its text. - dk ships the 150
indents.scmof Helix d5d88b62 too, andqueries/<language>/indents.scmin the config or runtime directory replaces it in the same way. An indent query takes the captures and predicates of the Helix indent guide.@extendand@extend.prevent-oncedo 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,bin a highlight query puts the queries ofaandbin 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.tomlthat 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 nodk grammarrun.brokenGrammarsindefault.nixlists each grammar that does not build, with the reason.grammar-hashes.jsonholds the hash of the source of each grammar, andtask hash-grammarswrites it again fromlanguages.toml. Arevwith no new hash fails the nix build with the name of its grammar. - dk loads
grammars/<name>.sofrom 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-allor 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.tomlcan 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 inlanguages.tomlwins it. A string file type is an extension only, so["Makefile"]matches noMakefile, and a dotfile such as.bashrchas no extension. A file type table with no glob, another key, or a glob that does not parse is alanguages.tomlerror. - With no file type match, the first line picks the language by
shebangs, with the regex of Helix. It skipsenvand its flags and drops digits, so#!/usr/bin/env python3namespython. dk reads the first line when the file opens and whenlanguages.tomlloads again, so a shebang typed later counts from the next load.
Install
- Each release has
dk-<os>-<arch>-<tag>.tar.gzfor darwin and linux on amd64 and arm64, and no bare binary. It holdsbin/dk, the themes inshare/ductus/runtime/themesand the license texts inshare/doc/ductus/LICENSES, under the top directorydk-<os>-<arch>-<tag>/. Sobin/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 fetchanddk grammar buildmake them. - The flake gives
homeModules.default, a home-manager module that setsprograms.ductus:defaultEditor = truesetsEDITORtodk.enable = trueinstallsfinalPackage.extraPackages, such as[ pkgs.gopls ], go on thePATHofdk, after thePATHof the shell.finalPackageispackagewhenextraPackagesandgrammarsare empty. Withgrammars, it has a runtime directory of its own, which holds the runtime directory ofpackageand the grammars, and itsbin/dksetsDUCTUS_RUNTIMEto it when your environment does not setDUCTUS_RUNTIME. The runtime directory, itsgrammarsdirectory and each.soare copies, because dk refuses a link there (A10).grammars.<language>is a grammar whoseparserfile dk loads as<language>.so, so the language shows its syntax with nodk grammar build.packageholds each grammar of the shippedlanguages.tomlthat builds under nix, sogrammarsis for a grammar that it does not have, such asgrammars.readline = pkgs.tree-sitter-grammars.tree-sitter-readlinefor a readline language thatlanguagesadds. A name that dk cannot load, such as one with a/, fails the home-manager assertions (A10).ignores,languages,plugins.<name>,queries.<language>.<name>,settingsandthemes.<name>writeignore,languages.toml,plugins/<name>.wasm,queries/<language>/<name>.scm,config.tomlandthemes/<name>.tomlin the config directory, as links into the nix store.languagesis an attrset, a Plugin is a.wasmfile, a query is text or a file,settingsis an attrset, and a theme is an attrset or a TOML file. A list inlanguages,settingsorthemes.<name>fails evaluation, with an error that names the option and “attribute set”.plugins.keymap,plugins.pickerandplugins.vcsreplace the shipped Plugins.packageis 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, aspackages.<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.tomlchange 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.tomlinstead whenXDG_CONFIG_HOMEis set. Each key takes one command name, such asx = "move-character-right".- docs/manual.md lists each command name that a key can take, with what it does, under Key commands.
]gand[gselect the next and the previous change, and]Gand[Gthe 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,]gand[gsayDiff is not available in current buffer.]pand[pselect 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.]fand[fselect the next and the previous function, and]t,]a,]c,]e,]Tand]xand 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 capturefunction.movement, or elsefunction.around, or elsefunction.inside, of thetextobjects.scmof 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 saysSyntax-tree is not available in current buffer, and with notextobjects.scmthe 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).gwin Normal mode puts a two-character label over the first two characters of each word in the View, as Helix’sgoto_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 getsaa, the nearest one before itab, the next one after itac, 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 modegwextends 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:wand:qstay 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 scopeui.virtual.jump-label, or inerrorwhen the theme does not set it. A count does nothing.- A key can have the Helix modifiers
C-,A-, andS-, such asC-s,A-x, orS-tab.S-goes with a named key, such astaborleft. For a capital letter, write the letter. A named key with no modifier, such asleft, 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-worspace, 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:helpshows. Keys with the same desc share one row, in Helix’s key order, such asleft, 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 asd,D,e,g. A named key sorts by its name, sominuscomes afterm. A row sorts by its shortest key name with the modifiersC-,A-andS-dropped, soleft, hsorts ash,o, C-oasoandpageup, C-basb. When two rows sort as the same key, the row whose key has no modifier comes first, sobcomes beforepageup, C-b. The next key closes the box, and Esc closes it and drops the keys so far.mi,ma,ms,mrandmdshow the box of their command until the next key, with Helix’s titles and texts, andmrshows a second box until its second key.auto-info = falsein[editor]ofconfig.tomlturns the boxes off, and a trusted.ductuscan 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:helpshow the text as glyphs (A5), and a trusted.ductuscan 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 ashforh = { 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 asC-s = ":write", and its args stay text. A list runs its commands in order, such asC-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:helpshow 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-documentand:trust, a typed command whose args hold%shand 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 anexecgrant 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 commandspipe,pipe-to,insert-outputandappend-outputbind 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:wand:qstay 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:asA-:.plugins/keymap.wasmin the config directory replaces the Default keymap plugin. The plugin names its sticky menus in thestickylist of theresolvedrecord, 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-configreads the config and its theme again and loads the plugin again. ThereuandUstill undo and redo, andA-uis Esc, thenu. - 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
7giveA-/, and on a US layout Alt, Shift and/giveA-?. A kitty terminal that sends no shifted key gets the sign that Shift gives on a US layout, such asA-(for Alt, Shift and9. - 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
atypesA, and Shift and1types 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-serversof its language inlanguages.tomlnames, as Helix does. dk shipsgoplsandgolangci-lint-lspfor 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 ofroots, such asgo.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
:bccloses 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 itonce for each workspace, and:truststarts 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
.ductusof a workspace changes, for example after a pull,:reload-configstops the servers of that workspace, and the last row says<server> needs trust: :trust allows itonce (A9).:trustthen 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.tomlcan 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
PATHor 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
initializewithin itstimeout, 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 elsediagnostic, 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,grandgiin 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:opendoes, 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 ofpath:line:columnrows, such asflake.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 showspath: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 showspath:linetoo (A1, A2). The rows go by path, then by line as a number, then by column, soflake.nix:80comes beforeflake.nix:159, andflake.nix:56:21beforeflake.nix:56:30. Two rows at the same column keep the order of the replies. The matched characters of each row show inspecialand bold, as in the file picker.C-sandC-vjump as Enter does, where Helix jumps in a new View. No location gives the Helix error, such asNo 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 whethergrlists the declaration too.space sin 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, asgddoes. 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 slists the symbols of the syntax tree, from thetags.scmof 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@namecapture, 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 notags.scmeither,space sgivesNo language server supporting document symbols or syntax info available.space Sin 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:opendoes and selects the symbol with the cursor at its start, asgddoes. A symbol that a server sends with no range gets its range fromworkspaceSymbol/resolveat the pick, and with no range found dk saysNo 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 Slists the symbols of thetags.scmqueries in the files of the workspace, when the language of the file has a built grammar and atags.scm. Its query is a regex with smart case, as inspace /, 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 ofspace fwith 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 givesThe 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 notags.scmfor the file either,space SgivesNo configured language server supports workspace symbols.space hin 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 thelocals.scmof 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 hon 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: aftera, err := f()andb, err := g(),space hon the firsterrselects it and the uses oferrabove the second:=. Withhoist-locals = truefor 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’slet, the second definition is a new variable. With neither source,space hgivesNo 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 saystoo many ranges(A2).- When the cursor rests for
[editor.lsp] document-highlight-timeoutms, default 1000, the focused View shows each use of the symbol at the cursor, from the same two sources asspace h. The uses draw inui.highlightof the theme, or elseui.selection, or elseui.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 = falseturns this off, and:set lsp.auto-document-highlight falseclears the uses at once. The default istrue. config.toml, a trusted.ductusand:setchange both keys, and a value of another type or a negative timeout gives a note and the default. space din Normal and Select mode opens a picker of the diagnostics of the file, andspace Da 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 Dlists 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, andspace Dpickers 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 inui.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 kin 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-nandA-pshow the next and the previous one, each under[1/2]and the name of its server.C-dandC-uscroll the popup,EscandC-cclose it, and any other key closes it and runs. The popup has a frame by default (seepopup-borderin Config). No answer givesNo hover results available., and no server with hover givesNo 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 kindplaintext, and the popup draws it as it is, with no Markdown. A heading is inmarkup.heading.1tomarkup.heading.6, emphasis is italic, strong is bold, and strikethrough is crossed out. Inline code and raw HTML are inmarkup.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 inmarkup.raw.inlinefor a language with no Grammar. The popup shows at once with its code blocks inmarkup.raw.inline, and they take their highlights when the highlight ends, so keys go on while it runs. A list item starts with•inmarkup.list.unnumberedor its number inmarkup.list.numbered, two spaces in for each level, and a rule is───inpunctuation.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-nandA-pshow the other signatures, with(1/2)at the right of the first row.Escleaves Insert mode, and leaving Insert mode closes the popup. The popup has a frame by default (seepopup-borderin Config). [editor.lsp] auto-signature-help, default true, turns off the requests that dk makes by itself, anddisplay-signature-help-docs, default true, says whether the popup shows the docs of the signature.C-gin Insert mode andspace Kin Normal and Select mode ask for signature help at once, also withauto-signature-help = false. This is a dk extension: Helix has thesignature_helpcommand but binds it to no key. The popup then shows in Normal and Select mode too, whereEsckeeps it andC-ccloses it, as in Helix. No server with signature help givesNo 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-xin 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 whenpopup-borderis"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 startscompletion-timeoutms, default 250, after you typecompletion-trigger-lenword 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, andC-nselect the next item,S-Tab, Up, andC-pthe one before, and PageDown,C-d, PageUp, andC-umove half the menu. The selected item shows in the text in place of the word before the cursor. The preview sendsdidChangeto the servers. Enter accepts it,C-ccloses 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 (seepopup-borderin 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})$0puts inPrintln(a)withaselected. 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
$0the 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-stopandgoto-previous-tab-stophave 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 scopetabstopdraws 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.ductuscan set them, and:set smart-tab.enable falsedoes 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.ductuscan set them and the keys of the requests that start on their own too. A value of another type, acompletion-trigger-lenover 255, or a negativecompletion-timeoutgives 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 ais such a command. An edit that comes after the server answered the command is refused too. - dk answers
window/showDocumentwithsuccess: 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 rin Normal and Select mode renames the symbol at the cursor with a server of the file, as Helix’srename_symboldoes. Therename-to:prompt starts with the name that the server gives back fortextDocument/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 showsserver error <code>: <message>. AprepareRenameanswer after an edit, a secondspace r, or a key that leaves Normal and Select mode opens no prompt (A1). gopls refuses to rename a package withcan't rename package: LSP client does not support file renaming, because dk renames no files. No server that renames givesNo 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 ain Normal and Select mode asks the servers of the file for the code actions of the selection, with the diagnostics over it, as Helix’scode_actiondoes. 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 givesNo configured language server supports code actions, and no action givesNo 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/resolvefirst, soFill Sof gopls works. When dk refuses the edit, the command does not run, where Helix runs it. A server with no commands givesLanguage server does not support executing commands. - A pick after an edit of the file, as
space 'can reopen the picker, applies nothing and saysEdit refused: document has changed. A pick after the server stopped givesLanguage 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.modandgo.sumwithos.WriteFilewhen 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 answeredinitializeand 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-serversor: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 asgo.modfor 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-serversand:restart-language-servers, a name that the language of the file does not name givesUnknown language server: <name>, orUnknown 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, givesLSP not defined for the current document. Tab completes the names: the servers of the language after:restart-language-serversand:start-language-servers, and the servers attached to the file after:stop-language-servers. :show-language-serversshows 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 isrunning,starting,stopped,needs trust,not installed,failed to start, orexited.not installedis a program name that is not onPATH, 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 answersinitialize, showfailed to start. A server that runs also shows its position encoding and the requests that it answers, such asdefinition, 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-commandopens 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 givesLanguage 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
shutdownandexitto 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.dkwith 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-filesdoes (see Views), and ashx -ddoes in Helix. It takes exactly two files. When one of the two files does not open, dk stops with the error.:open-empty-documentor:nopens 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, asdk 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.:oand:eare the same command. A leading~in the path of:open,:split-view-right, or:split-view-belowis 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,:openof that path saysa 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:openreplaces it, as in Helix. If the file does not open, you keep the text you have.:goto-next-documentor:bnshows the next open file in the View, and:goto-previous-documentor:bpthe one before, in the order they opened. Each file comes back with its text, its Selection, and its undo history.gnandgpin Normal and Select mode do the same, as in Helix, and a count such as2gnskips that many files.g fin Normal and Select mode opens the file named at the cursor, as Helix’sgoto_file, andg Falso goes to the line after the name, as Vim’sgF. 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:, somain.go:10:5givesmain.go, andg Fgoes 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/xstays as it is (A9). Afile://URL opens its path. dk opens the names as:opendoes: 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 ashttps://, 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 mandg .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 atakes the newest file off that list and shows it, sog atwice gives back the first file, and with none it saysno last accessed document. Each View also keeps the last two files that it left with a change made while it showed them, andg mshows the first of them that is not the current file, or saysno 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 afteruback to the start,g .does nothing. A file that closes, such as with:bc, leaves the lists of each View, sog aandg mnever show it (A1), and each list holds each file once, so it grows only with the open files (A2).:close-documentor:bccloses 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:wof dk never reloads. A Document with no unsaved changes reloads as:reloaddoes, andugives 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 withmodifiableoff 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 = falsein[editor]ofconfig.tomlturns 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. Onlyconfig.tomland:setset it: a.ductusthat 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 andrestore-cursordo, 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 thatundofile-excludenames, keeps no place (A4). Neither does a commit or rebase message, such asCOMMIT_EDITMSGorgit-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 = falsein[editor]ofconfig.tomlopens each file at the start. dk still keeps the places then, so a laterrestore-cursor = truerestores them. - The places of at most
restore-cursor-filesfiles, 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-filesin[editor]ofconfig.tomlis a whole number from 1 to 10000, and with a lower value the next write drops the files used longest ago. Onlyconfig.tomlsetsrestore-cursorandrestore-cursor-files::setrefuses them, and a.ductusthat 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 bin 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 inspecialand bold, as in the Helix picker. Up, Down, Tab, S-Tab,C-n, andC-pmove the selection, PageUp, PageDown,C-u, andC-dmove 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, andC-c, or Esc in Normal mode, closes the picker.C-sandC-vshow it in a new View below or to the right, as:split-view-belowand:split-view-rightdo, 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-rightdoes.space fin 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.ductusor 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:opendoes, by its path from the working directory when it is under it, as Helix shows it.C-sandC-vopen it in a new View below or to the right, as:split-view-belowand:split-view-rightwith the path do, and an open file shows its Document there.space Fin Normal or Select mode opens the file picker at the directory of the focused file, as Helix’sfile_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 ofspace f. A Document with no path opens it at the working directory, and saysCurrent buffer has no parent, opening file picker in current working directory, as Helix does. Helix bindsspace Fto a picker of the working directory, which dk does not have.space gin 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 asold -> new, and a path shows control characters as glyphs (A5). Each kind draws in its theme scope, as in Helix:diff.plusfor untracked,diff.deltafor modified,diff.delta.conflict,diff.minusfor deleted, anddiff.delta.movedfor 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:opendoes. 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 asjj statusafter 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 lin Normal or Select mode, or the key commandpick-file-change, opens the file log picker:@and each change before it that changes the focused file, newest first, asjj log -r '::@ & files(<file>)'lists them. Each row shows the line ofjj log -T builtin_log_onelinefor the change in the colors of the jj config: the change id, the author, the time, the bookmarks, the commit id, and the description, asjj logshows 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 nospace 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:opendoes 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 ispath: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 inui.text.directory, the colons inpunctuation, and the line number inconstant.numeric.integer. The match in the text shows inspecialand bold, as a matched character does in the other pickers. The nord theme definespunctuationandconstant.numeric, andui.text.directoryfalls back toui.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-caseis 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 inui.highlight, or elseui.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:opendoes, 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-sandC-vopen 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,NORorINS, or the names that[editor.statusline] modesets. While[editor] color-modesis true, the mode draws in the scope of the status line mode,ui.statusline.normalorui.statusline.insert, and withcolor-modes = falseit draws as the rest of the row. In Normal mode a letter does not edit the query:jandkmove to the next and the previous row, andigoes 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.nandNgo 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-sizesets, 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 as12/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 defaultanddefault jujutsuboth findjujutsu/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, sojujutsu\ defaultfindsjujutsu defaultand notjujutsu/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.'wordmatches the exact text,^wordthe start of the name,word$its end, and^word$or'word$the whole name, so.go$finds the Go files.!wordhides each row whose name holds the exact text, sogo !testhides each_test.gofile.\!,\^,\'and\$are the plain characters. The characters that each word matches show inspecialand 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, sowqputs:write-quitfirst. Enter runs the command as its key or its prompt line would, with the count typed beforespace ?, and a prompt command gets no argument.C-sandC-vrun 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.1inwit/. Thepluginworld is the keymap, thehooksworld is the Document lifecycle Hooks, and thelsp-clientworld is the LSP Hooks. Thepluginworld importshistoryand exportscommand, so the keymap plugin runsundo,redo,earlier, andlateron 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:editorname 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 asplugin "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-openedruns 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:bcleaves.document-focusedruns 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-saveruns before a save, anddocument-savedruns after a save that succeeds.document-closedruns after:bcor:bc!closes a Document, before the Hooks of the Document that the View shows next. A plugin that does not exportdocument-closedstill 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-attachedonce for each language server and Document, when the server opens the Document, and again for each of them on:reload-config. It getsdiagnostics-changedafter 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-replyand ofmove-wordreads its text by Grapheme, as the text was when dk made the handle.next-graphemeandprev-graphemegive the Position of the next and the last Grapheme boundary, andgrapheme-atgives the Grapheme that starts at a Position.graphemesgives up tomaxGraphemes 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). Abackother 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-savedafter you typed during the save, gives none. - In the same Hooks and in
symbols-reply,request-symbolsasks each language server of the Document for its document symbols, and gives the id of the request. The reply comes throughsymbols-replyonce 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
.ductusdirectory, trusted or not. - To install a plugin, copy its
.wasmfile intoplugins/in the config directory. For a plugin that needs a grant, runsha256sumon the file, and write the hash assha256in 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 ofjj --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 andspace B(Display). That command prints a line for each line of the file: one JSON object withauthor,change,commit,content,description,email, andtimein 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 plainchangein the column, and only the fields name the change. A plugin built before annotate gives no annotation, andspace Bsays 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, forpick-file-change(Opening files). The list prints a row for each change: one JSON object withauthor,change,commit,current,description,kind, andtimein 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 ofjj log -T builtin_log_oneline, or for git the line ofgit 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, andpick-file-changesays so. - dk ships a vcs plugin for jj, a Default plugin.
Its marker is
.jj, and the base text is the file in@-. It runsjj --ignore-working-copywithdiff,file annotate,file list,file show, andlog, so jj never snapshots the working copy.logprints@and each change back to the closest changes with a local bookmark, at most 100 changes below@. It runs with--color=debugand the template aliasformat_short_change_id, so the id and the colors follow the jj config.plugins/vcs.wasmin 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] execin yourconfig.tomlreplaces that grant, and aplugins/vcs.wasmgets no grant until you write one with a pin. The pin issha256 = "<hash>"in the same table, the SHA-256 of the file assha256sumprints 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. Asha256that is not 64 hex digits gives a warning, and a workspace.ductuscannot set it. The shipped plugin needs no pin. A program is a plain name onPATHor an absolute path.exec = []turns the plugin off, and a workspace.ductuscannot grant a program. An exec grant for jj is full trust:jj util execand 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-uiworld: 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, andplugins/picker.wasmin the config directory replaces it. Esc always closes a picker, and a picker plugin that traps closes the picker with one warning.
Saving
:writeor:wsaves the file. The save runs on a worker, so you can type while it runs. On a[scratch]Document it saves nothing and saysCan'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 asfile modified by an external process, use :w! to overwrite. The text then stays unsaved, and:wqand:xdo 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 filetypestays, as Vim’s:saveaskeeps 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 givesexpected 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 readonlyand:set-document-option readonlyturn it on for the focused Document, and:set noreadonlyturns it off, as Vim’s ‘readonly’. It has no session value and noconfig.tomlkey, and:reload-configkeeps it. On a readonly Document,:w,:wq, and:xsave 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. :wto a file that dk cannot write saves nothing and saysPath is read only, as Helix does, also when readonly is off.:write!or:w!, with or without a path, saves as:wdoes, 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.:wand:w!format the file first, as:formatdoes, whenauto-formatis true in[editor], the default, and in the[[language]]of the file, as in Helix. The shippedlanguages.tomlholds theauto-formatand theformatterkeys of Helix: 43 languages, Go among them, auto-format, and 36 have a formatter. Anauto-formatin[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-formatsaves the text as it is, and another flag, such as-x, givesunknown flag '-x'and saves nothing.--ends the flags, so:w -- -xsaves to the file-x.:write-allor:waformats and saves each file with unsaved changes, one file at a time, as:wdoes, and you can type while it runs, as in Helix.:wa --no-formatsaves 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 sayscannot 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:xsaves as:w [<file>]does, then closes the View as:qdoes, 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:wdoes, and--no-formatsaves 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.:wqcloses 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:wqnor:wq!quits, because nothing was saved.:write-quit-all,:wqa, or:xaformats and saves each file with unsaved changes as:wadoes, 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-formatand no path::wqa a.txtsaysexpected 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-dor--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-tabopens 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-tabor:tabccloses 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 Ncloses 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 sayscannot close the last tab page.:close-other-tabsor:tabocloses every other tab page, and their files stay open too.:goto-next-tabor:tabngoes to the next tab page, and from the last to the first.:goto-next-tab Ngoes to tab page N.:goto-previous-tab,:tabpor:tabNgoes to the tab page before, and from the first to the last, and:goto-previous-tab Ngoes N tab pages back.- In Normal and Select mode,
gtandC-PageDowngo to the next tab page, andgTandC-PageUpto the one before, as in Vim. With a count,3gtgoes to tab page 3, and3gTgoes 3 tab pages back. A count that is not a tab page gives a note, and nothing changes. Their commands aregoto-next-tabandgoto-previous-tab, so a[keys]entry can bind other keys to them. :move-tab Nor:tabm Nmoves the tab page to after tab page N, counted before the move, and:move-tab 0makes it the first.:move-tabwith no N makes it the last, and:move-tab +Nand:move-tab -Nmove 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,:wqorC-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-alland the other-allcommands act on the files of every tab page.:help,:show-messages,:show-aboutand:check-healthgo to the tab page that shows the listing already.- A diff session belongs to no tab page.
:diff-thisin 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-basesession 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] tablinechanges 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 as1 [scratch]and2 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 asdk/main.goandkeymap/main.go, orcmd/dk/main.goandplugins/dk/main.gowhen 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 inui.bufferline, the current one inui.bufferline.active, and the rest of the row inui.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 sayscannot 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 defaultmousetime. One click there does nothing. While the prompt, help or a picker is open, the tab line does nothing with the mouse, and withmouse = falsethe 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 onui.gutter, and onui.gutter.selectedon the cursor line of each Range. A number is inui.linenr, and inui.linenr.selectedon such a line in the focused View. A View that is too narrow for its gutter shows none.:annotate, or the key commandannotate, 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 as3 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, so3 hours agolines up with12 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 inui.gutter, and inui.gutter.selectedon the cursor line. The change id has the colors of the jj config, asjj logshows 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.dayfor the last 24 hours,.week,.month(30 days),.year(365 days), and.older, each of which falls back toui.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 takesui.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,:annotateannotates the file asspace Bdoes, 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 thatauto-fetchstarts stays silent. In a workspace that you did not trust,:annotategives the trust note, and dk runs nothing. dk binds no key to it, but a[keys]entry can bindannotate. [editor] gutters = [...], orlayout = [...]in[editor.gutters], picks the gutter columns, as in Helix:diagnostics,spacer,line-numbers,diffandcode-action-hint, which draws blank.[editor.gutters.line-numbers] min-widthsets 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.ductusmay 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.ductusmay set it. A value that is notabsoluteorrelativegives a note, and dk uses absolute numbers.- The scopes that hold the primary cursor, such as a function, a class, an
ifor 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 scopeui.sticky-context, or elseui.popup, so that the rows stand out from the text in any theme. The nord theme givesui.sticky-contextthe backgroundnord2, one step lighter than the cursor line. A theme that setsui.sticky-context.bottomgives 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 fromqueries/<language>/context.scm, or else from thefunction.aroundandclass.aroundnodes oftextobjects.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 rundk grammar fetchanddk grammar buildwith the name of its grammar. Acontext.scmin the config or runtime directory replaces the shipped one, withinherits, 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 ofscrolloffand 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 = falseturns the rows off, andconfig.toml, a trusted.ductusand:setchange 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-belowor:hssplits the View, and the new View goes below.:split-view-rightor:vsputs the new View to the right, past a column that parts the two.C-w sorC-w C-s, andC-w vorC-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:openshows 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.:bnand:bpin one View also leave the other Views as they are, so two Views can show two files.C-w wfocuses the next View.C-w h,j,k, andl, orC-wand 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 Kmoves the View to the top, with the full width of the screen, andC-w Jmoves it to the bottom, in Normal and Select mode.C-w Hmoves it to the far left, with the full height, andC-w Lto the far right. The four keys follow Vim’sC-w K,C-w J,C-w HandC-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-leftandmove-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, andC-w -one row shorter, as in Vim.[help]names the-keyminus.C-w >makes the View one column wider, andC-w <one column narrower. A count sets the step, so5 C-w +adds 5 rows.C-w _makes the View as tall as it can be, andC-w |as wide, and with a count, as in10 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-widthandequalize-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 = falsenothing 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 saysDiff: "a" vs "b". A path can end in:lineor: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 ondiff.minus, added lines ondiff.plus, and changed lines ondiff.delta, or on the.viewscope of each when the theme has one. In changed lines, the words that changed draw indiff.delta.view.text, elsediff.delta.view, as in Helix, with no white space at their ends. Unlike Helix, they never use the barediff.delta, so that a theme with only a foreground there keeps the words readable. The nord theme sets the.viewscope of removed, added, and changed lines, anddiff.delta.view.textfor 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[Gmove between the changes on the side of the View. In a diff session,:reset-diff-changeor:diffgtakes 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-diffends 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-baseshows 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 filetypegives, so it shows the syntax colors of that language, and its status line names it. No language server opens the base side, and:formatin it runs no formatter (A9). It holds the base of the time of:diff-base, also withgutter-diff = "disk", and a later vcs query changes only the gutter. The keys of a diff session work as for:diff-files, so:diffgin the file puts back the base lines.:diffpinto the base side and:diffgin it giveCannot 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, givesDiff is not available in current buffer, and no View changes. In a workspace that you did not trust, it gives the trust note, asspace gdoes. Helix has no such command, and dk binds no key to it, but a[keys]entry can binddiff-base.:diff-diskshows 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:reloador: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.:win 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.:diffgin the file takes the lines on disk, as one undo step, and:diffpinto the disk side givesCannot 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-diskruns. 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-thismarks the View, and:diff-thisin another View then compares the two in a diff session, with the marked View as the old side, as in Helix.:diff-thisagain in the marked View ends the mark, and a View in a session refuses it.:put-diff-changeor:diffpputs 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 qcloses the View, but not the last one of the last tab page, andC-w ocloses every other View of the tab page.:qcloses the View too (see Commands).zopens the View menu in Normal and Select mode, as in Helix, and its box lists the keys.zt,zzorzc, andzbput the cursor line on the first, the middle and the last row of the View, and the cursor stays. The View then keeps itsscrolloffmargin, so with the defaultztleaves the cursor line 5 rows below the top, as in Helix.zmscrolls the View sideways so that the cursor column is in its middle.zjorzDown scrolls the View down one line, andzkorzUp scrolls it up, so3zjscrolls three lines. The cursor moves only when the View leaves it behind, as with the page keys, and in Select mode its Range extends.zwithC-b,C-f,C-u,C-d, PageUp, PageDown, Backspace or Space runs the page keys.z/,z?,znandzNsearch as/,?,nandNdo (Editing), and in Select modeznandzNmove the primary Range.Zopens the same menu as a sticky menu, as in Helix: it stays open after each key, soZjjjscrolls 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-escrolls the View down a count of lines, andC-yup, in Normal and Select mode, aszjandzkdo. Helix binds neither key, so dk follows Vim’sC-eandC-y.- After each key, each View scrolls so that the cursor is at least
[editor] scrolloffrows 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
.ductusdirectory 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
.ductusof a workspace until you trust it. The last row then says.ductus is not trusted: :trust loads it, and your own config still applies. :trustordk trusttrusts the.ductusas 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:trustagain.- 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
.ductusor 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 itonce for each workspace, with the first program in the grant, quoted when it is not a plain name, such asjj needs trust. In a repo with no.jj/repo/config-id, jj 0.45 and 0.46 adopt the.jj/repo/config.tomlof the repo, so:trustaccepts that config too. :trusttrusts the workspace of the open file, or the workspace of the working directory when the file is in none. When the two differ, the.ductusnote saysdk trust loads it.- A trusted
.ductus/config.tomlapplies on top of your config. Its[keys]win for the keys it binds, and itsthemereplaces yours. Themes and plugins still load only from your config directory and the themes dk ships. - dk follows a link to a file in
.ductusand 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.