Features
This file lists what you can do with ductus 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. With that script, Tab afterdkcompletes command names and the names of files and directories, and Tab after a file completes a file name.-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 even without--debug. dk appends to the file and writes nothing to the terminal. With no--debugand no--log, dk writes no log.- Each line has a time, the event name and its values, such as
msg=key cmd=move-visual-line-down took=1.2ms. - dk times startup step by step, each key from the moment it is read until the draw after it ends, each draw, and each save.
- Each guest call into a Plugin gets a line with the Plugin’s name, the export and the time, and a failed call says whether its deadline passed, the guest trapped, or a host function panicked (A7).
Loading and unloading a Plugin log a line too.
A user Plugin shows as its file name without
.wasm. - Each request to a language server gets a line with the server, the method, and the time from the send to the reply.
An error reply logs its code, never its message.
A server start, up to its reply to
initialize, and a server exit get a line too. - Each grammar load and each parse gets a line with the grammar name and the time, and a parse also logs the size of the text.
- Each typed command gets 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 not a command logs
outcome=unknown, without the name. - A panic has one error line with the panic value, the stack, and the name of the crash file.
- A line contains names, paths, sizes and durations, never the text of a Document, a prompt line or a search (A4). A key shows only as the command its binding ran, so a key typed into the prompt or a search leaves no name. The error line of a panic is the one exception: it contains the panic value, and that value can quote the text of a Document.
- The log is 0600 and the state directory 0700, even when they already existed. A link or a non-regular file 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 logging after 16 MiB in one run, with one last line that says so (A2).
- If a write to the log fails, the last row says so once, and dk keeps running without a log.
- Each line has a time, the event name 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 wait in the Document list, for:bnandspace b, 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). If one of the files does not open, such as a FIFO or a device, dk stops before it draws anything and prints 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 are mutually exclusive, 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, like any other error. 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. 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, keeps a line typed twice in a row only once, and never writes them to disk (A2, A4). Register:contains 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 these 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, nor do Esc and any other key that is not a character, and the prompt stays open. A single line ending at the end of the value, LF or CR LF, is left out, so:o, a space, andC-r "after a line yank give a clean path. Any 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.
An alias still runs its command:
:xand Tab gives nox, but:xand Enter runswrite-quit. It matches fuzzily: 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. 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 visible rows include the cursor.
- The text after the command name splits into args at spaces and tabs, 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:%{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. 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 expands a value a second time, 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, so:echo %sh{echo {a}}shows{a}. stdin is/dev/null, and dk ignores stderr and the exit status. 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. dk puts each value of%{...}and%reg{...}in as one word of text, by the POSIX sh quote state at its place (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 with the value put in, 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. Anywhere else 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 even 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. 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 the focused View, as:helpopens[help], or in a new View above it with[editor] listing-split. It lists the version as:show-versionshows it, the linkhttps://ductus-editor.com/, 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 contains 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 or shows it again, and:bccloses it, as for[help]. No save writes it::wsays[about] is no file, so dk saves nothing(A6).:reload-configrereads the config and its theme, and reloads the plugins.: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, 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.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, like[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 or shows it again, and:bccloses it, as for[help].:helpopens the list of the keys and the commands as a Document named[help], in the focused View, or in a new View above it with[editor] listing-split. The Document that the View showed stays open as it was, and:bcon[help]shows it again, with its cursor and scroll position as they were. After the keys of the modes, it lists the keys of the command line, by name, such asC-w,C-u,tabandup, C-p, each with what it does.[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 never opens a second one. When[help]is open and no View shows it, the focused View shows it again.: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). Withlisting-spliton,: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.:hide-search-matches,:nohand:nohl, and the key commandhide-search-matches, hide the matches of the search pattern in each View. The key[editor.search] highlightstays as it is. The next/,?,n,Nor*, or a:setof the key, draws the matches again.:goto-line <n>,:g <n>, or:<n>, such as:20, goes to line n, 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.- 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 how long the command has run and its line, which shows as glyphs (A5). The picker has the columnstimeandcommandunder a header, and a query with no%matches the command.%timeand a space, such as%time 1m, match the time instead. 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.:show-messagesopens the messages that the last row showed as a Document named[messages], in the focused View, as:helpopens[help], or in a new View above it with[editor] listing-split. 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, even 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. Any 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]or shows it again, with the messages as they are then, and never opens a 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. The file you had stays open.:pipeor:|,:pipe-to,:insert-outputand:append-outputrun the rest of the line with the shell of:sh.:piperuns the command once for each selection, with its text as stdin, and puts the output in its place, such as:pipe sort.:pipe-toruns the command the same way 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 does not end in a line ending, dk drops one from the end of the output. 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. 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 each Range wider than one character, in Range order, each on its own line, or the whole Document when no Range is that wide.: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 partial 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.:quitor:qcloses the View, 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 goes for every 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.:reload,:rl, or:e!reads the file of the Document again and drops the unsaved changes. 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, including a file that no View shows. A file that fails to reload gives its error, and dk moves on to the next file.:get-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. 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), such as:sh jj new. It runs in the background, so keys still edit while it runs, and it has no timeout::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. Any 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 and drops 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 partial SGR. Only the visible text counts toward its size. 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, 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, but a key whose value is a string, such asline-number, takes the text as it is.:settrims the spaces at the ends of a text, so a text with a space at an end goes in double quotes, such as:set soft-wrap.wrap-indicator "foobar ": the value is the text between the quotes, with every space. A doubled quote inside the quotes is one quote, such as"a""b"fora"b, and a lone"is the text", such as:set default-yank-register ". A value that starts with a quote and does not end with an unescaped one gives the noteCould not parse fieldand a quoted copy of the value, and the old value stays. The last row and the info box show a text with a space at an end, or no text, in quotes, such assoft-wrap.wrap-indicator = "foobar ". For a true or false key,:set <key>sets it to true and:set no<key>sets it to false.:set <key>=<value>sets the value as:set <key> <value>does, such as:set line-number=relative. The key ends at the first=or space, so a value can contain=.:set <key>?shows the value on the last row, such asline-number = absolute.: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, whether or not the value changed. 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, even 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::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::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),date-format,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. 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. Another Document keeps its own values, and takes any other:setvalue at once.: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. 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, 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, and draws an escape in the name as a glyph (A5). The workspace and trust still use the real path (A9).:change-current-directory <directory>or:cd <directory>sets the working directory of dk, and shows it on the last row, such asCurrent working directory is now /tmp/x. With no name it goes to the home directory. A~at the start of the name is the home directory, a relative name starts at the working directory, and dk expands no environment variable (A9). A name that is not a directory, does not exist, or cannot be read gives an error on the last row, with no raw path, and the directory stays (A5). A Document keeps its file: a path that is relative to the old directory becomes absolute first, so a save never writes under the new directory (A6). After the change,:open, the file picker and its root, the search,:show-directory,%{current_working_directory},%{workspace_directory}and the commands that dk starts use the new directory. A language server that is already running keeps its directory. The config that dk loaded stays, and the.ductusof the new workspace loads only when it is trusted, at the next:reload-configor:trust(A9). A running save refuses the change. After:cdand a space, Tab lists the names of directories.:cd -goes to the directory that the last change left, and shows it as:cddoes. A second:cd -goes back, so:cd /tmp/a,:cd /tmp/b,:cd -and:cd -show/tmp/aand then/tmp/b. Before any change the last row saysNo previous working directory. A change that fails keeps the remembered directory. A remembered directory that is gone or cannot be read gives the error of:cd, with no raw path, and the directory stays (A5). dk expands no variable or~in a remembered path (A9), and a Document keeps its file as after:cd(A6).:push-directory <directory>or:pushd <directory>changes the directory as:cddoes, and puts the old directory on a stack.:pop-directoryor:popdgoes to the newest directory of the stack and removes it.:show-directory-stackshows the stack on the last row, newest first, with a space between the paths. An empty stack gives the errorStack is empty. The stack has room for 10 directories, and a push on a full stack drops the oldest (A2). A change that fails puts nothing on the stack and removes nothing from it.C-zin Normal and Select mode stops dk and gives the terminal back to the shell, 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). If the stop fails, dk keeps running 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, 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 on first use, and:reload-configdetects it again. On Linux it picks the first that fits from termux, tmux whenTMUXis set, termcode in WezTerm, Wayland whenWAYLAND_DISPLAYis set, xclip and then xsel whenDISPLAYis set, and win32yank, or else termcode. On macOS it picks tmux, then pasteboard, or 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] date-formatis the form of each date that dk draws:"iso", the default, as2026-11-10,"dmy"as10/11/2026,"mdy"as11/10/2026, or"locale". The blame variable{date}and the revision picker draw it (see Views). The file log picker,pick-changeand the annotate column draw how long ago a change was made, so they do not use it."locale"readsLC_ALL, thenLC_TIME, thenLANG, takes the first that is set and not empty, and cuts the encoding and the modifier, so thaten_US.UTF-8isen_US. A table from CLDR 49.0.0 maps the language and the region to one form:"mdy"for the United States and a few more regions,"dmy"for most of Europe, Latin America, Africa, Oceania and India, and"iso"for the regions whose short date starts with the year, such asja_JP,zh_CN,ko_KR,sv_SE,hu_HUandlt_LT.C,POSIX, an empty value, a malformed value and a locale that the table does not know give"iso", and dk never prints the value of the environment. Another value of the key gives a note on the last row, and dk uses"iso". A trusted.ductusmay set it, and:setand:set-document-optiontake it, such as:set date-format dmy.[editor] default-yank-registeris the register ofy,d,c,p,PandRwhen"picks none,"by default. 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 rereads the file 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:get-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 shows its error on the last row for each failed read, not on each draw, and gets 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. 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] listing-splitis false by default. With false, a listing opens in the focused View and replaces the Document that the View shows: the listing of:help,:show-about,:show-messagesand:check-health, and the patch that Enter opens in a picker of changes. The Document that the listing replaced stays open with its text, Selection, History and unsaved changes, and:bcon the listing shows it again, with its cursor and scroll position as they were. With true, a listing opens in a new View above the focused View. In a View of a diff session a listing always opens in a new View, so that the session goes on. A value that is not true or false gives a note on the last row, and dk uses false. A trusted.ductusmay set it, and:setand:set-document-optiontake it, such as:set listing-split true.[editor] picker-sizeis the percentage of the width and 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 get a picker’s frame,┌─┐,│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 room for 3 rows or fewer, or 3 columns or fewer, gets 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.tomlcan set it, because only that file 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] rulerslists the text columns, 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, even 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 how many rows and columns a View keeps between the cursor and its edges, 5 by default (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. 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.tomlcan set it, because only that file 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] splashis true by default, andsplash = falseshows the blank screen fordkwith no file (Opening files). A value that is not true or false gives a note on the last row, and dk uses true. A trusted.ductusmay set it, and:setand:set-document-optiontake it, such as:set splash false.[editor] tablinesays when the top row shows the tab line (Tab pages):"multiple", the default, with more than one tab page,"always", or"never". 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. 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-widthper language.[editor] titleis true by default, andtitle = falseleaves the title of the terminal window alone (Display). A value that is not true or false gives a note on the last row, and dk uses true. A trusted.ductusmay set it, and:setand:set-document-optiontake it, such as:set title false. The value of the focused Document is the one that counts, and a reload of the config applies it at the next draw.[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 usual. 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.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 follow a linked directory, and visit 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, 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 setting 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**. 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 stallsspace 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, andspace /lists them.[editor.search] smart-case = falsemakesspace /,/,?,nandNmatch case even for a pattern with no upper case letter.[editor.search] wrap-around = falsemakes/,?,nandNstop at the last match, andnandNthen sayNo more matches.[editor.search] highlight = falsestops drawing every match of the search pattern except the selected one, and:set nosearch.highlightdoes it for the session. 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 sets the title of the terminal window to the text
dk:, a space, and the file of the focused Document, such asdk: src/main.go, and keeps it current. The file is the path as the status line shows it, so a path outside the working directory is short, and a Document with no path shows its title, such asdk: [scratch]anddk: [help]. A Document with unsaved changes has a space and[+]after the file, the mark of the status line. The title changes when another Document, tab page or View is focused, when a Document is saved under another name, and when a Document opens or closes. A key that changes none of this writes nothing to the terminal. The path is text from outside, so a control character in it shows as a glyph, such as^[, and the title takes 255 cells at most, with the mark kept (A2, A5). When dk ends, even after a crash that it handles, a terminal with the xterm title stack puts back the title that it had before. A terminal with no title stack, such asst, shows an empty title instead, which it replaces with its own default title. A terminal with the stack never shows that empty title, because it restores over it.[editor] title = falseleaves the title alone (Config), anddk --helpsays the same. - 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, in the preview of a picker too, 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. [editor.soft-wrap] enable, default false, makes a View draw a line that is wider than its text on more rows, and:toggle soft-wrap.enableturns it on and off. A row ends between two words. A word that does not fit wraps whole when at most[editor.soft-wrap] max-wrapcells of it fit on the row, default 20, and otherwise it breaks at the edge.max-wrapis at most a quarter of the width of the text. A row after the first starts with the indent of the line, when that is at mostmax-indent-retaincells, default 40, and at most two fifths of the width, and then with the indicator.[editor.soft-wrap] wrap-indicator, default"↪ ", is the indicator, and the theme scopeui.virtual.wrapstyles it, asui.virtualdoes when the theme has no entry for it. The indicator is text from a config, so a control character in it shows as a glyph, such as^[, and it takes 8 cells at most: a longer value is cut at 8 (A2, A5). It also takes at most a quarter of the width of the text, so with a gutter of 4 cells an indicator of 7 cells shows whole from a view of 32 cells, and in a view of 30 cells it shows 6 (A2). The line number and the gutter marks of the diagnostics, the diff and annotate show on the first row of a line, and a later row has a blank gutter. The cursor, each Selection and each match of the search draw on the row they fall on. The end of line diagnostic and the inline blame draw after the last row of their line, and the cursor at the end of the line is on that row too. A View that wraps never scrolls to the side, and it scrolls by rows to keep the cursor in view withscrolloff. A View with 10 cells or less for its text does not wrap. The preview of a picker and the sticky rows draw one row per line. The two Views of a diff session each wrap at their own width. The lines of a changed Hunk draw one after the other on each side, with their own Visual lines. The shorter side gets the filler rows after the last line of the Hunk, as many as its Visual lines are fewer than those of the other side, so the sides line up after the Hunk. An equal line gets no filler rows, so after an equal line that wraps to a different height on each side, the sides stay misaligned below it. The filler of a Hunk counts only the rows of that Hunk. A changed word on a later Visual line keeps its style. The top of the session is a row and a Visual line of it, and the page keys, the scroll keys and the mouse count Visual lines in both Views. A session whose one side does not wrap, because soft wrap is off for its file or its View has 10 cells or less for text, draws one row per line.[editor.soft-wrap] wrap-at-text-width, default false, makes a row end at[editor] text-widthwhen that is less than the width of the text, and a word that ends at that width stays on its row when the line ends there. A value ofenableorwrap-at-text-widththat is not true or false, a size that is not a whole number of 0 or more, or an indicator that is not a string gives a note on the last row, and dk uses the default. A trusted.ductusmay set all five, the cap applies to its indicator, and:setand:set-document-optiontake them, such as:set soft-wrap.enable true.:set soft-wrap.wrap-indicator "foobar "gives the indicatorfoobarand four spaces, and the cap of 8 cells applies to it. A click and a drag land on the row under the pointer, at the column that the pointer names in that row. A click on the indicator is at the start of the text of its row, and a click after the text of a row is on the last character of the row, or at the end of the line after the last row.gwlabels the words that show on each row of a line that wraps. The page keys, the half page keys, the scroll keys,zt,zz,zb,H,MandLcount rows, and the top of the View can be a row inside a line.scrolloffkeeps that many rows above and below the cursor, and a jump puts the row of the cursor in the middle of the View. A line with more rows than the View shows the rows around the cursor. A step up into a line of more than 1000 rows lands on its 1000th row, so that a line of 10 MiB costs a bounded step (A2).jandk, Up and Down move the cursor one row down and up, to the column that the cursor had in its row. Afterjinto a shorter row,kgoes back to the same column.g jandg kstay on text lines, and keep the row and the column, sog jtheng kgoes back to the row that it left. Home, End,g h,g l,g s,x,Cand a Block selection use text lines and text columns. A count ofjmoves that many rows, and a move from the last row of a line goes to the first row of the next line.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+. 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 contains 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, 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 an unfocused View, the blank cells of the mode draw like the rest of its status line. Withcolor-modes = false, the mode draws like the rest of the status line in every 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, 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.spinnerspins while a shell command runs, with the frames⣾⣽⣻⢿⡿⣟⣯⣷, one each 80 ms, and otherwise it draws one blank cell, so the elements after it stay in place. It does not spin for a 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, falling back toui.statusline.separatorand thenui.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 in the short form that dk draws for a path. A file under the working directory shows its path from it, such asinternal/app/run.go, and the working directory itself shows.. Any other path under the home directory starts with~/, and every other absolute path keeps its leading/. In those two forms, each directory name shows as its first character, except the last two directory names and the file name, so/home/me/Development/git.sr.ht/~me/ductus/internal/app/run.goshows as~/D/g/~/d/internal/app/run.go. A name that starts with a dot keeps the dot and one character, so.configshows as.c. A character that cannot draw as itself, such as a control character, shows as�. A path that dk was given as relative shows as it was given. The Document keeps its whole path: saving, language servers, the Undofile andfile-absolute-pathuse it. A View too narrow for the whole line cuts the middle of the path with…first, so the file name and the elements after it stay. The title of the terminal, the buffer picker, the rows of the location and diagnostic pickers, and the notes of:wand:diff-diskshow the same short form. The%register and:pwddo not, as a user copies them. 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 sorted by 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, or 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).
▍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.
[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, like 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-short}, {time-ago} • {title} • {change}. Its variables are{author}, the full name of the author,{author-short}, the short name,{email},{date}, the date in the form ofdate-format(see Config),{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. For jj,{author-short}is the short name thatjj logshows: the part of the author email before the@, or(no email set)for an author with no email. Aformat_short_signature_onelinealias in the jj config changes it. A vcs plugin that gives no short name has the full name in{author-short}. Aformatthat names{author}keeps the full name. 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, including 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, a short 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 and falls back tonever. 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. 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 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, like 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, or else the colors that the plugin printed, or 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. 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, whether or not they opened from Insert mode. 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 default colors. The last row shows the first of these warnings and how many more there are, and:show-messageslists each. - The runtime directory contains 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 thenordtheme, which it ships. The binary also contains a copy ofnord, 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 218 themes in the runtime directory.
The binary contains 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. 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, even when it has no background of its own, and gets its own only when its colors give another mix. Otherwise 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 contains 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-configrereads the config, 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 falseblocks every change to the text of the focused Document, 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,:get-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.i,a,o,c, and the other keys that enter Insert mode refuse at once, so Insert mode does not start. A workspace edit from a server that names such a Document is refused as a 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. 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, 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 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, dk adds no line ending, 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, 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 the same goes 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 uses the language of the file path for this. The shippedlanguages.tomlsets theauto-pairsof each language, so Rust, HTML, and the other languages with pairs get them.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.
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. 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. 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).
- A double click selects the word under the pointer, as
miwdoes, and a triple click selects the whole text line with its line ending, even on the second row of a line that soft wrap spreads over several rows. Two presses of the left button at one cell less than 500 ms apart make a double click, and three make a triple click. A press at another cell, 500 ms or more after the press before, off the text, or after a triple click, starts the count again at one. Off the text is the tab line, a status line and a gutter. The new selection replaces the Ranges, even in Select mode, and a drag after it extends as a drag does after a click: the anchor stays. In Insert mode, and with Alt, each press is a single click. With no keymap plugin a double click is a single click. The release yanks the new Range, as after a drag. - 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. 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, even 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. 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 ".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 shows 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 never blocks a 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, so":ppastes the newest command line. This is the only key wait where: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 likey,p,PandRwith register+, in Normal and Select mode, whatever register"picked.rand then a key replaces each character of every Range with the key, 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 writes a tab, while 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, soHello WorldbecomeshELLO wORLD.`makes each Range lower case andA-`makes it upper case. dk changes each code point with the case tables of Go, so~turnsÄ1éintoä1É, and`turnsİ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, 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 every one of those lines starts with the token, the token comes off again. It comes off 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 comes off. 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. 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. Otherwise 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. With neither, 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. 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, in Normal and Select mode.A-uandA-Umove back and forward 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, 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. No option changes the limit of 2. The count covers 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, in the form ofdate-format(see Config). The picker has the columnsgraph,revisionandwhen, and a header row with their names. Typing in Insert mode filters by the number, and%whenand a space filters by the time, as%revisiondoes by the number. The graph is a column that no%graphfilters, and a graph wider than 32 cells shows cut with…. The other keys are those of the buffer picker, except thatC-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 narrows a wider mode to 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.
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, and extend in Select mode. They keep the column asjandkdo, and a count stops at the first or the last line. Without soft wrap they move asjandkdo.- 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.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 inside goes into 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, even 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”. Any 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 marks its values in the register as 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 contains what dk wrote. sopens the promptselect:, and each match of the regex inside each Range becomes a Range.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. 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-:as Esc and then:, so a[keys]binding or the command palette runs it. The first Range is primary, and afterA-_the Range that contains the old primary. These keys work in Select mode too.A-,removes the primary Range, 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.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 has 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.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, 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. Each line ending and the spaces and tabs after it become one space, or nothing when the next line contains 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, 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. 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 in 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 separately 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 line up in one column. 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. A Range of only whitespace is dropped, 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, 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 keeps its place counted 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. 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: 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.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 that many matches ahead, so ona(b, c)f,selectsa(b,.tstarts one character further on, so a secondt,moves on to just 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. 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. 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, for the next.too, 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, for the next.too, so after"ap,"b.and then.paste fromb. A typed command is not a change, even 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, 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 further.- A count before a motion repeats it:
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, and a command repeats at most 10,000 times (A2). gggoes to the first line, and12ggor12Gto line 12.gegoes to the last line, and so doesGwith no count. 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.H,MandLgo to the top, the middle and the bottom line of the View in Normal and Select mode. 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 have 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, 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, 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. 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, even when the View already shows that file.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. 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. 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 likei.g Iis a change, so.repeats it.space jin Normal or Select mode opens the jumplist picker. 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, 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, 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. The search of the prompt says nothing when it wraps. 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 contains 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.zn,zN,ZnandZNdo the same, and a count moves that many matches ahead, so3ngoes three ahead. 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 continues 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.^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, even 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).- Each View draws each match of the search pattern in its lines, in the scope
ui.highlight.search, or inui.highlightfor a theme with no such scope, and a theme with neither draws none. The selected match draws as a selection, and the matches ofvim_*andneovim*themes take the colors of the highlight groupSearch. The pattern is the one thatnreads, with its smart-case rule. While the/or?prompt is open, the Views draw the matches of the typed text, and Esc draws those of the old pattern again.[editor.search] highlight = falsedraws no match (Config), and:hide-search-matcheshides them until the next/,?,n,Nor*, or a:setof the key (Commands). dk searches only the lines of the View, so a large file with many matches draws in time linear in the lines that show (A2). A match that runs past the last line of the View draws up to that line. *puts the text of each Range in register/as a regex and saysregister '/' set to '\bfoo\b', 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-*. Go’s\bmatches 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, 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, 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, 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. 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: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.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. The object is the smallest node that contains 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. 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, 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 contains 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 contain 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, 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.ms(puts(before and)after each Range, and the Range grows to cover 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 restores 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. 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.oadds a line below the line of each Range, andOadds one above, in the line ending of the file, and both enter Insert mode. 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 its indent by the rules below. With a built grammar that has an indent query, the indent is the number of@indentscopes that contain the new line, less the@outdenttokens that start it. 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 with its own indent style keeps it. 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: 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. 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 stalls dk for 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: 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. 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 grammar order, 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 338 languages and 297 grammars, each grammar at the rev that its queries are written for. It ships the language servers with them: thelanguage-servers,rootsandlanguage-idof each language, and each[language-server]table. wren and gemini have no grammar, and their languages stay, as plain text. dk readsuse-grammars, withonlyorexcept, and thenameandsourceof each[[grammar]]oflanguages.toml, withgit,path,revandsubpathinsource. From 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. From a[language-server.<name>], it readsargs,command,config,environment,no-workspaceandtimeout, and it takesconfigwhole. The shippedlanguages.tomlhas no other key of these tables, exceptno-workspaceon six servers and theexcept-featuresof a fewlanguage-serversentries, which dk does not read yet.languages.tomlin the config directory adds languages and grammars with[[language]]and[[grammar]]tables. A[[language]]of a shipped name changes only the keys it sets, 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. With nouse-grammars, fetch and build pick every grammar, shipped or from your config. 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 downloads nothing. - 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. 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. A fetch of a newrev, or an edit of a path source, makes it build 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. Apathin a source wins over itsgit,revandsubpath.- A
[[grammar]]in a.ductus/languages.tomlnever fetches or builds, not even a path source, 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 sets mode 0700.
- A file of a language whose grammar is built shows its syntax in the colors of the theme.
dk picks the capture: 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 341
highlights.scm, for the grammar revs that dk pins. dk reads thelocals.scmof a language, 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, 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, applies 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 applies to its whole scope, even 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.tomlgoes back to 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, 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 232
injections.scmtoo, 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. 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. One highlight stops at the 500 ms budget, and keeps the spans of the layers that finished (A2). It finds at most 4096 injections, and layers nest at most 8 deep. A name over 256 bytes finds no language. - dk ships 158
textobjects.scmtoo, 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. - dk ships 77
tags.scmtoo, 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.[editor] rainbow-brackets = falseturns them off, andrainbow-bracketsin a[[language]]oflanguages.tomltakes its place for that language. The default istrue. dk ships 91rainbows.scmtoo, 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 continues 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 adds one level, 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: 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 theme scope names, sosetshows in thekeywordstyle of the theme, the text of a quoted string in thestringstyle, andmicin a comment selects its text. - dk ships 150
indents.scmtoo, andqueries/<language>/indents.scmin the config or runtime directory replaces it in the same way. An indent query takes the captures@align,@anchor,@indent,@indent.always,@opaque,@outdentand@outdent.always.@extendand@extend.prevent-oncedo nothing in dk, because they change no indent of a new line. A query with a predicate or a#set!that dk does not take gives one note, and the indent is the indent level. - A line
; inherits: a,bin a highlight query puts the queries ofaandbin its place. 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 comes to 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.jsonstores the hash of the source of each grammar, andtask hash-grammarsrewrites it 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 disappear, 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 by its file type: 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/. Otherwise the extension picks the language, and when two languages claim it, the later one inlanguages.tomlwins. 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. 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 two tarballs for each of darwin and linux on amd64 and arm64, and no bare binary.
dk-<os>-<arch>-<tag>-full.tar.gzhas the grammars of the shippedlanguages.tomlinshare/ductus/runtime/grammars/<language>.so, so a file shows its syntax with nodk grammar build.dk-<os>-<arch>-<tag>.tar.gzhas no grammars:dk grammar fetchanddk grammar buildmake them, as they make a grammar that you add. Each hasbin/dk, the themes inshare/ductus/runtime/themesand the license texts inshare/doc/ductus/LICENSES, under the top directory of its own name. Sobin/dk, or a link to it, finds the themes and grammars in its runtime directory. - The completion scripts of bash, fish and zsh, as
dk completionprints them, are inshare/bash-completion/completions/dk,share/fish/vendor_completions.d/dk.fishandshare/zsh/site-functions/_dkof the nix package, offinalPackageof the home-manager module and of each release tarball, so that a new shell completesdkwith no setup on your part. PowerShell has no such file, because nixpkgs has no place for one. - 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 contains 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.packagecontains 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. - To load the completion script of a shell (see Command line) in the current session, and in each new session, use the step for that shell:
dk completion bash: runsource <(dk completion bash)for the current session. The script needs thebash-completionpackage. For each new session, rundk completion bash > /etc/bash_completion.d/dkonce on Linux, ordk completion bash > $(brew --prefix)/etc/bash_completion.d/dkon macOS, and start a new shell.dk completion fish: rundk completion fish | sourcefor the current session. For each new session, rundk completion fish > ~/.config/fish/completions/dk.fishonce, and start a new shell.dk completion powershell: rundk completion powershell | Out-String | Invoke-Expressionfor the current session. For each new session, add that line to your PowerShell profile.dk completion zsh: runsource <(dk completion zsh)for the current session. If shell completion is not on, runecho "autoload -U compinit; compinit" >> ~/.zshrconce. For each new session, rundk completion zsh > "${fpath[1]}/_dk"once on Linux, ordk completion zsh > $(brew --prefix)/share/zsh/site-functions/_dkon macOS, and start a new shell.dk completion <shell> --helpgives the same steps.
Keys
- The keymap is a Default plugin. With no config it binds the default 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. 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. 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. A count moves 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. 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 moves 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, 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 it does whenever dk waits 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 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 matches a modified key that has no binding of its own. - A table binds a key sequence:
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 continue 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. Its title is the 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 the order of the default table, 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, 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, and overrides the default name of a 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. 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. 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 if you pressed Enter on the command line, 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, not even 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 contain%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 contains 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 contains 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-configrereads the config and its theme and reloads the plugin. 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 either, 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. 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 for a file in a workspace only when the workspace is trusted, 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 a workspace that is not trusted gets no server. - A file in no workspace, such as a temp file of another tool, gets only the servers of its language that set
no-workspace = truein their[language-server.<name>]table, and they need no:trust. The shippedlanguages.tomlsets it onbash-language-server,markdown-oxide,marksman,taplo,vscode-json-language-serverandyaml-language-server, for shell, TOML, Markdown, JSON and YAML. The other default servers of those five languages are off:ansible-language-serverwalks up from the file for.ansible-lintand runsansible-linton the file.rumdlsearches up from the file for its config.tombisearches fortombi.tomlfrom the directory of the file up to the root.golangci-lint-lspandgoplsare off too:golangci-lintreads.golangci.ymlup the tree, andgoplslooks forgo.modfrom the file and may download a toolchain. The last row then says nothing, and:start-language-serverssays<server> does not start for a file in no workspace.:show-language-serversshowsstate: needs a workspacefor it. A user turns a shipped server off for such files withno-workspace = falsein the[language-server.<name>]table of the userlanguages.toml. The user config also setsno-workspacefor a server of the user, and a user config is trusted (A9). What the six do on their own with such a file:bash-language-serverrunsshellcheckon the text of the file, soshellchecklooks for.shellcheckrcfrom the working directory of the server, and reads the files that the filesources. When it formats, it reads.editorconfigup the tree of the file for theshfmtflags only. It fetches nothing, unless the user config gives anexplainshellEndpoint.markdown-oxidereads its config from its root, which is the private directory of the server, and reads~/.config/moxide/settings.marksmanopens the file alone, reads~/.config/marksman/config.toml, and reads no project config. dk did not read the part of its source that builds a single-file folder.taplolooks for.taplo.tomlin its working directory or the root of the workspace, so it finds none. It fetches a schema that the file names with a#:schemaline or a$schemakey, and reads a schema file by its path or by afileURL. dk did not verify that it searches no further.vscode-json-language-serverreads no config file. It loads the schema that a$schemaproperty of the file names: it downloads an http or https URL and reads afileURL.yaml-language-servertakes its settings from dk, fetches a schema URL that a$schemamodeline of the file names, reads a relative schema path from the directory of the file, and downloads the SchemaStore catalog. A server that starts has no root and no workspace folder, soinitializesends a nullrootUri. It runs in the private directorylsp-cwdof the state directory, so it reads no config from its working directory. One server of each name runs for all the files in no workspace of a language, whatever their directories.:stop-language-servers,:restart-language-serversand:start-language-serversact on it, and:reload-configkeeps it.:show-language-serversshowsroot: nonefor it. With no state directory, such a file gets no server. dk refuses aWorkspaceEditfrom such a server, because it has no workspace (A9).
- A Document with no path, such as the base side of
:diff-base, gets no server, even 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.
One that does not start for another reason, or that exits, gives one note, such as
Language server exited: gopls, and dk keeps running. - 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 server message over 32 MiB, a header line over 4 KiB, and more than 64 header lines. The write queue takes 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: 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 theme. - An edit moves the diagnostics with the text, and a diagnostic whose text the edit deletes disappears. 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 disappear.
- An answer that two servers both give shows once, from the server whose name sorts first. 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. 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 as the status line shows it, 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 any 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, binary files included (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, even for 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 have the line showspath:linetoo (A1, A2). The rows sort 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. No location gives an 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. 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, even 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. The Range that contains 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 applies to 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 dk uses 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. 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 is 32 cells wide at most, and a cell that is wider shows cut to 32 cells with
…, so the next columns line up. The query still reads the whole cell, and a match in the cut part marks nothing. 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 or to their name, 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. 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. 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 without the Markdown 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 keep working 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. 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, even withauto-signature-help = false. The popup then shows in Normal and Select mode too, whereEsckeeps it andC-ccloses it. 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.
C-xin Insert mode asks the servers of the file for completions, and a menu of the items shows at the cursor. 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. Any other 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 undoes 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. 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 waiting request, 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 undoes the preview, and any other key, Esc included, accepts it and then does its own job. 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. 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, or 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.
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. 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. Tab inserts a tab when only whitespace is before each cursor. 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. [editor.lsp] snippets, default true, says whether a server that starts gets snippet items.[editor.smart-tab] enable, default true, turns on smart tab for Tab in Insert mode: when it is 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. 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 dk uses 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.
- 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 each make dk refuse it.
- 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, even 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).
space rin Normal and Select mode renames the symbol at the cursor with a server of the file. 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. The actions show in a picker. A disabled action does not show, and the rest go in this order: 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 the server whose name sorts first 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. 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. A server is attached when it has answeredinitializeand has the file open, so a server that is still starting is not. Its diagnostics disappear, and the last row gives no exit note. The server stays stopped in its LSP root until:start-language-serversor:restart-language-servers, even when another file of the language opens there.:start-language-servers [<name>...]starts each named server of the file that is not running, 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 is running, or each one, and starts it again.- A start works as it does at open: only in a trusted workspace, or else the last row says
<server> needs trust: :trust allows it, each time (A9). A stop works as it does 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. - 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 running server 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).:run-language-server-commandopens a picker of the commands that the servers of the file list. A row shows the command and the name of its server, and a pick runs the command on that server. The picker has the columnsnameandserver, with a header row, and its query takes%nameand%server. A control character in a command or a server name shows as a glyph (A5).: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. The status line and the buffer picker name it[scratch], and it keeps no Undofile.dkwith no file also draws a splash over the empty[scratch]Document, as a framed box in the middle of the focused View. The frame and the fill use the theme scopeui.popup, like a popup, so a ruler, the cursor line, the gutter and any text under the box do not show through it, and the rest of the View stays as it is. The box is as wide as its widest line with a cell of padding on each side, at most 72 cells. A line that is wider is cut and ends with…. The box shows the namedkin large letters when the View has room, then the version, a one-line tagline, the linkhttps://ductus-editor.com/, and four key hints. The name, the version, the tagline and the link are centered. The hints are two aligned columns. The hints are:help,:quit, the keys of the Normal mode keymap that runpick-file(at most 3), and those that runpick-command. A rebound key shows its new label, and a command with no key shows its:name. A key press, a mouse press, a paste or a change of the Document removes the splash for the rest of the run, and the key does its work as usual. A resize draws it centered again. A View too small for the frame shows the rows that fit, name first, with no frame. The splash is not part of the text of the Document, so[scratch]stays empty with no changes.dk <file>,dk <directory>,dk diff,:n, and closing the last file show no splash, and[editor] splash = falseturns it off (Config). A key label draws each control character as a glyph (A5).dk diff <a> <b>opens file a and file b in a diff session, as:diff-filesdoes (see Views). 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, 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. If the file is already open, 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. 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, and a count such as2gnskips that many files.g fin Normal and Select mode opens the file named at the cursor, andg Falso goes to the line after the name. With one Range of one Grapheme, the name is the first path on the line that contains the cursor or ends next to it, at most 1000 bytes from it (A2), or else the text of the Range. A path contains 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 already open 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 otherwise dk runs nothing and gives the trust note (A9). For a file in no workspace, the last row says<program> runs only for a file in a trusted workspace, because:trustcannot allow it. 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. 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 has 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 is running.: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 rereads it.
A file whose text is that 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. 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 keeps running 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, even 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 least recently used file 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 least recently used files. 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 stores 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 least recently used files (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 restores no place (A3). space bin Normal or Select mode opens the buffer picker. 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. 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. 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, 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 stale. 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, and dk reads at most the first 64 KiB. The read runs beside the editor, so a slow disk never blocks 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 follows a linked directory, and it stops at 100,000 files.[editor.file-picker]changes which names it skips, whether it follows a linked directory, and how deep it goes (Config). It skips the files that the ignore files name, such as.gitignore(Config). Enter opens the file as:opendoes, by its path from the working directory when it is under 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. 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.space gin Normal or Select mode opens the changed file picker. 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: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. The jj provider gives each row in the columnschange,date,author,bookmarksanddescription, with a header. Thechangecolumn shows the first 6 characters of the change id, and a pick still opens the whole change. A column whose cell is empty in every row, such asbookmarksin a repository with no local bookmark, is not drawn, and its header cell is not drawn, so the description gets its room. A query on a column that is not drawn is still accepted, and it matches no row. Theauthorcolumn shows the short name of the author, asjj logdoes: the part of the email before the@, or(no email set)for an author with no email. Thedatecolumn shows how long before now the change was made, in the shortest form:nowunder a minute, then5m,3h,3d,2w,4moand2y. A day is 24 hours, a week is 7 days, a month is 30 days and a year is 365 days, and a change from the future isnow. Each age pads on its left to the widest age of the rows, so the units line up, and dk counts the age when the picker opens.%datematches the age that the column shows and the ISO date of the change, such as%date 3dor%date 2026-11. Thebookmarkscolumn lists the local bookmarks of the change, and it is empty for a change with none. The description is its first line, and a change with no description has an empty cell. The cells draw in theme scopes: the change id inui.picker.change, the author inui.picker.author, the bookmarks inui.picker.bookmarks, and in the description the type of a conventional commit inui.picker.type. The type isfeat,fix,docs,refactor,test,ci,chore,build,perforstyle, with a scope in parentheses and a!if there are any, up to the colon. The date draws in the scope of its age, as the annotate column does:ui.virtual.annotate.age.day,.week,.month,.yearor.older, thenui.virtual.annotate.age, thenui.virtual.annotate, with no fade. The jj provider gives the length of the shortest unique prefix of the change id. The prefix draws inui.picker.change.prefixand the rest of the id inui.picker.change. A theme with noui.picker.change.prefixdraws the whole id inui.picker.change, and so does a provider that gives no length or a length that does not fit the id. A scope gives the foreground and the attributes of the cell, so the selected row keeps its background. A theme with none of the scopes of a cell draws the cell in the text color, and a scope of a parent, such asui.virtual, colors no cell. The characters that the query matches keep their own style over these colors. The themenordsets the fourui.pickerscopes andui.picker.change.prefix. Each other theme that ships with dk sets the fourui.pickerscopes too, marked# dk addition, from its own colors:ui.picker.authortakes the color ofstring,ui.picker.bookmarksoffunction,ui.picker.changeofcomment, andui.picker.typeofkeyword. A theme with no color for one of those four scopes draws that cell in the text color. Those themes do not setui.picker.change.prefix, so they draw the whole change id inui.picker.change.%authoror%bookmarksand a space make the query read that column, and the header marks the column of the query.%authormatches the short name and the full name of the author, such as%author kaior%author lindqvist. A word with a capital letter matches case, and a word without one ignores case, as in the other filters. A query with no%reads the description. A control character in a field draws as a glyph, and dk keeps 256 characters of a field (A5, A2). A provider that prints a line for each row gives one column and no header, and each row is that line in its colors and attributes. The line keeps no other escape sequence or control character, dk keeps 1,024 bytes of it, and the query matches the plain text of the row. A provider is one kind for the whole list: when any row has a line, the list has one column and no header. With a row highlighted, the pane on the right shows the diff of that change for the file: the lines that the diff session shows differ, with three lines of context,-before a removed line and+before an added one, in the colors of the diff. The pane runs the same commands as a pick, in the same trust gate, one run for the highlighted row at a time. A move to another row stops the run of the row before it, and a row that was shown stays cached while the picker is open, up to 64 rows. The pane shows the first 256 KiB of a diff and a note when it cut it, and no escape sequence of the file reaches the screen (A5). The row of@shows the diff of the file in dk against its base, with no run. A workspace that you did not trust, a provider that gives no diff, and a run that fails give a one-line note in the pane, and the picker keeps working. 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 close. 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.space Lin Normal or Select mode, or the key commandpick-change, opens a picker of the changes of the whole repository:@and each change before it, newest first, asjj log -r '::@ ~ root()'lists them. It reads the working directory, so a Document with no path gets the list too. A change here is a change of the version control system, and a Revision is a state of the History of one Document (space u). The columns, the header, the short change id, the hidden empty columns, the filters%change,%date,%authorand%bookmarks, and the glyphs for control characters are those of the file log picker (space l). The list has nokindand no line, so every row shows its fields. It stops at 1,000 changes, and the title says so (A2). With a row highlighted, the pane on the right shows the description and the patch of that change, asjj show --gitprints them. A line that starts with+,-or@@has the color of the diff, and a control character draws as a glyph (A5). The pane runs one jj command for the highlighted row at a time, and a move to another row stops the run of the row before it. A row that was shown stays cached while the picker is open, up to 64 rows. The pane shows the first 256 KiB of a patch and a note when it cut it (A2). Enter opens the patch in a Document with no path, named[change <id>]with the first 8 characters of the change id, in the focused View, or in a View above it with[editor] listing-split. The Document has readonly on and modifiable off, so no save writes it, and it keeps the first 1 MiB of the patch and a last line that says when it cut it (A2, A6). The commit goes into the jj command only when it is 1 to 64 lowercase hex digits, and jj reads it as a commit id and never as a bookmark (A1). The pane and Enter check the trust again and run nothing in a workspace that you did not trust (A9). The run of jj stops after 10 s. A workspace that you did not trust gives the trust note, a directory outside a workspace gives a note, and a run that fails gives a note, each in place of the picker. A vcs plugin from before this key says that it gives no list of changes.space 'in Normal or Select mode opens the picker that closed last, as it was: the same query, selection, and top row. 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, even 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. 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: 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, even 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. 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 several 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 like 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. 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 a picker with columns the find reads the whole text of each cell, including the part that a long cell hides, one cell after the other, so a regex does not match across two cells. It reads the first 64 KiB of a cell, and 8 MiB of text in all. A find that runs out of that selects no row and showsfind: cut at 8 MiB. In Insert mode the keys edit the query as usual, and Esc goes to Normal mode, so two Esc keys always close a picker (A7). - A picker takes the percentage of the width and height that
[editor] picker-sizesets, 80 by default (Config). It is centered over the screen minus its last 2 rows, and the Views stay drawn around it. The picker is in a box. The first row in the box shows 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. The mouse wheel over the preview pane scrolls its text by 3 lines, as the wheel scrolls a View, and stops at the first line and at the last page. The wheel over the list does nothing. A new selection shows the pane from where it starts, and so does a return to a row. Tab moves the focus from the query to the preview pane, and Tab or Esc there moves it back. Esc does not close the picker from the pane. With the focus in the pane, it has a double border, the query shows no cursor, and the last row sayspreview: Tab or Esc to go back, / ? n N to search. There the arrow keys andjandkmove the text by a line,C-dandC-uby half the rows of the pane,C-f,C-b,PageDown, Space,PageUpand Backspace by the rows of the pane, andg,G,HomeandEndto the first line and the last page./and?there open a find line for a regex that searches the text of the pane forward and back, and Enter selects the next or the previous match.nandNgo to the next and the previous match, and the search wraps at the ends with the noteWrapped around document, unless[editor.search] wrap-aroundis false (Config). A search with no match saysNo more matchesand moves nothing, and a regex that does not compile gives the note of the search of the list. The regex has the smart-case rule of/(Config), and its length has a cap. The pane draws each match in its lines inui.highlight.search, or elseui.highlight, and the selected match as a selection. The search reads only the text of the pane, never the list, and at most 8 MiB of it. No other key does anything there, and none edits the query or reaches the list. In a picker with a pane, Tab no longer selects the next row: Down andC-ndo. 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 file picker, such as
space f, shows at once, with no rows and the labelfile (walking). It fills as the walk finds files, and you can type, move, preview and pick while the walk runs. A pick opens the file and stops the walk, and so does a key that closes the picker or opens another. The query that you typed matches each batch of files that comes later. The selection stays on its row when files come. While the walk runs, the rows of a query show in the order of the batches, so the best rows of a later batch come after the rows of an earlier one. When the walk ends, the label goes back tofile, or tofile (the first 100000)when the walk hit its cap (A2). A directory that does not read gives its error on the last row, with no path, and the picker closes (A5). - 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. Among rows with the same score, the one with the shorter name, as it is drawn, comes first, and rows of the same length 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 contains 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. - The query of a picker with named columns can match one column.
%path foomatchesfooagainst the columnpathonly. The name can be the start of a column name, so%pis%path, and the shortest column with that start wins. Text before the first%matches the primary column. The text after%nameand a space goes to that column, up to the next%. A column used twice has its parts joined with a space. A row shows when each column that has a query matches, and its score is the sum of the scores of the columns. A name that is not a column sends its text to the primary column. A column that does not filter ignores its query.\%is a plain%. A picker with one column, such as the file picker, reads%path fooasfoo. A query of over 1024 bytes, or with over 16%, shows no rows (A2). The characters that each column matches show in its cell. A picker with two or more columns draws a header row above its rows, with the name of each column over its cell. A column is at least as wide as its name, so a name shows in full unless the room ends, and a control character in a name shows as a glyph (A5). The width of a cell is the number of cells that it draws, so a wide character or a control character leaves the next column and its name in line (A5). The name of the column that the cursor of the query is in draws in the theme scopeui.picker.header.column.active, and the other names inui.picker.header.column. A theme with neither scope draws the names inui.picker.header, and then inui.text. The header takes one row of the room of the rows, stays in place when the rows scroll, and shows when no row matches. A picker with one column, such as the file picker, has no header. The pickers have these columns, and the primary column is marked:- Buffer:
flags(+for unsaved changes,*for the focused Document),path(primary). - Changed file:
change,path(primary). - Diagnostic:
severity,source,code,message(primary). The workspace diagnostic picker haspathbetweencodeandmessage. - File:
path. - Goto and references:
location. - Jumplist:
path(primary),flags,contents. - Search (
space /):path,contents(primary). The text ofcontentsis the regex, so it has no%contentsfilter, and the header names no cell for it. - Symbol:
kind,name(primary),container. The symbol picker of the syntax tree haskindandnameonly. - Workspace command (
:run-language-server-command):name(primary),server. - Workspace symbol (
space S):kind,name(primary),container,path. The workspace symbol picker of the syntax tree haskind,nameandpath. The text ofnamegoes to the server, or to the tags run, so it has no%namefilter. The search and workspace symbol pickers send the text of the primary column to the regex or the server, and a part for another column, such asfoo %path src/in the search, filters the rows as they arrive. Only a change of the primary text starts the regex or the request again, and any other part filters the rows that the picker already has. A query of column parts only sends no text: the search and the tags run list nothing, and the server gets the empty query.\%is a plain%in the query, so\%dsearches for%d, and a%that no\guards starts a part. Text after a part belongs to that column up to the next%, so put the text for the primary column first:foo %path src/. A query over the bounds of the parts shows no row and starts no search. The other pickers read no%name, so%is a plain character there.
- Buffer:
space ?in Normal or Select mode opens the command palette. 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. The palette has the columnsname,keysanddesc, with a header row, and its query takes%name,%keysand%desc.%keys space fkeeps the commands whose keys match, and%desc yankkeeps the commands whose desc matches. Thekeyscolumn reads all the keys of a command, joined by a comma and a space, whether or not the row cuts or wraps them. 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. 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 any 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 working 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 keep running.
- The vcs plugin names the marker directories of a repository
and the commands that give the base text of a file, tell whether 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 line after it 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 contain 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 any 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,authorShort,change,commit,content,description,email, andtimein RFC 3339, then the change id as the vcs shows it, for the annotate column. A plugin that prints noauthorShort, or an empty one, has the fullauthorin{author-short}. The change id after the object can contain the SGR sequences that dk takes from the change command, and dk drops any 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,authorShort,change,commit,current,description,kind, andtimein RFC 3339, then the line that the picker shows, then a NUL byte. A plugin that prints noauthorShort, or an empty one, has the fullauthorin theauthorcolumn. 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 contain the SGR sequences that dk takes from the change command, and dk drops any 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 short 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. The plugin can also name the command that lists the changes of the whole repository, forpick-change(Opening files). It takes no name and prints a row for each change as the file log does, with nokind, then a NUL byte, and dk reads 1,000 rows at most. A plugin built before this command gives no list of changes, andpick-changesays so. The plugin can also name the command that prints the description and the patch of one change, for the pane and Enter ofpick-change. It takes the commit of a row and prints text, and dk reads 1 MiB of it at most. dk passes only 1 to 64 lowercase hex digits as the commit, and a plugin refuses any other value. A plugin built before this command lists the changes and gives no patch, and the pane and Enter say 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 have to 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 default picker keys, 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!. 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 shows a 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. A language of:set filetypestays. It saves nothing when that file is open in another Document, 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.- A Document is readonly when its file exists and dk cannot write it, and the status line then shows
[readonly].:set readonlyand:set-document-option readonlyturn it on for the focused Document, and:set noreadonlyturns it off. 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). So do:w <path>,:wq <path>, and:x <path>when path names the file of the Document itself, even 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, even 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. It also saves a readonly Document and a file that dk cannot write, and turns readonly off. The file keeps its mode, so it stays read only on disk. It still saves nothing to a file that is open in another Document.:wand:w!format the file first, as:formatdoes, whenauto-formatis true in[editor], the default, and in the[[language]]of the file. The shippedlanguages.tomlsets theauto-formatand theformatterkeys: 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, 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.: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. A quit or a close during the run waits for the current save, and the rest do not save.:write-all!or:wa!does the same as:w!does for each file, and says nothing about a changed[scratch]Document.:write-quit [<file>],:wq, or:xsaves as:w [<file>]does, then closes the View as:qdoes. 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. You can type while it formats and saves, and dk quits when the save ends; a key that would start a second save meanwhile says that a save is running.: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. 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. 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. 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. It does not quit while a changed[scratch]Document has no file name either, and the last row sayscannot 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.- A quit waits for a running save to end, so it never stops a write partway.
- 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 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 contains the time of the panic. The file contains 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 has its own Views, 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. 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. Closing the last View of the last tab page quits, as usual. :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 already shows the listing.- 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 contains 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 the way 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, 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, like
: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. 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: 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 set 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:diagnostics,spacer,line-numbers,diffandcode-action-hint, which draws blank.[editor.gutters.line-numbers] min-widthsets the minimum 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, key by key over the user config.[editor] line-number = "relative"makes each number the distance to the line of the cursor. 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 contain 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. 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 like 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 disappears. 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 maximum number of 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 maximum number of 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 separates the two.C-w sorC-w C-s, andC-w vorC-w C-v, do the same in Normal and Select mode.- 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 the other Views 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. 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 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 aremove-view-far-up,move-view-far-down,move-view-far-leftandmove-view-far-right.- In Normal and Select mode,
C-w +makes the focused View one row taller, andC-w -one row shorter.[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 aregrow-view-height,shrink-view-height,grow-view-width,shrink-view-width,set-view-height,set-view-widthandequalize-views. - 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.
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, 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. 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, and the last row saysDiff: "a" vs "b". A path can end in:lineor:line:col, counted from 1, 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, or elsediff.delta.view, with no white space at their ends. 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 any 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, sideways too, 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,:get-diff-changeor:diffgtakes the lines of the other file for each change under a Range, as one undo step. 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, like[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 keeps the base as it was at the time of:diff-base, even 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 closes too. 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. 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, 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, like 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 keeps the file as it was at 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 closes too, 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 is running, and while the read of an earlier:diff-diskis running. A save that ends during the read starts the read again, and a file that lost the focus during the read gets no session.: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.: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. 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, 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.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: 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.- 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 (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 is dropped, 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 contains 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.