The Duat Text Editor
Duat is a text editor that is built and configured in the Rust programming language. It is meant to be very modular in its design, while still having great defaults for anyone used to TUI and modal text editors.
Despite having a rather complex language as its configuration language of choice, one of the goals of Duat is to have relatively straightforward configuration, and easy pluginability.
As a motivating example, here’s a small plugin, which highlights matches to the word underthe cursor:
use duat::prelude::*;
pub struct HighlightMatches;
impl Plugin for HighlightMatches {
fn plug(self, _: &Plugins) {
form::set_weak("same_word", Form::underlined());
let tagger = Tagger::new();
hook::add::<BufferUpdated>(move |pa, handle| {
let lines = handle.printed_line_ranges(pa);
handle.text_mut(pa).remove_tags(tagger, ..);
let caret = handle.text(pa).main_sel().caret();
let Some(range) = handle.text(pa).search(r"\A\w+").range(..caret).next_back() else {
return;
};
let start = handle
.edit_main(pa, |c| c.search(r"\w*\z").to_caret().next_back())
.map(|range| range.start)
.unwrap_or(range.start);
let mut parts = handle.text_parts(pa);
let pat = parts.bytes.strs(start..range.end);
let form_id = form::id_of!("same_word");
for range in lines.into_iter().flat_map(|r| parts.bytes.search(r"\w+").range(r)) {
if parts.bytes.strs(range.clone()) == pat {
parts.tags.insert(tagger, range, form_id.to_tag(50));
}
}
});
}
}
The goal of this book is to help you understand not only how to operate Duat, but hopefully how to do something like this as well.
Installation
In order to begin using Duat, you will need cargo installed in your system.
See cargo’s installation instructions for more info. After installing
cargo, you will want to make sure you’re using rust’s nighlty toolchain:
rustup install nightly
Now, you can go ahead and install duat using cargo. Duat will be installed
for the current user, in the ~/.cargo/bin/ directory:
cargo +nightly install duat
Or, if you want the master version of duat, from the latest commit, you can
do this (warning: unstable!):
cargo +nightly install --git https://github.com/AhoyISki/duat --features git-deps
If you don’t have ~/.cargo/bin/ in your $PATH variable, you should add that
in order to be able to call duat. Alternatively, you may add just
~/.cargo/bin/duat, if you don’t want to bring the other things in
~/.cargo/bin/ to global executable scope.
At this point, you should be able to use Duat by calling duat or duat some_file. When you first run duat, you will be prompted for the creation
of a new configuration crate at ~/.config/duat/. If you accept, this new
configuration will be created and compiled.
The first compilation will take a while, but subsequent compilations should be really speedy. In my 4.5 year old low level gaming laptop, it usually takes at most ~1.2 seconds to reload, but most reloads take far less than that.
If you already have a configuration in that directory, and wish to see what the new default config crate looks like, you can run:
duat --init-config ~/.config/duat
This will warn you about there already being a crate in there, and prompty you for its substitution. If you agree, a new default config crate will replace the old one.
This is just for the default crate however. The configuration of Duat is what the rest of this book is for.
How to use
When calling duat, with no arguments, you will open a scratch buffer. If you
pass files as arguments, then duat will open one window for each file.
duat also comes with various flags that you can pass:
--cfgwill open the~/.config/duat/src/lib.rs.--cfg-manifestwill open~/.config/duat/Cargo.toml. You can also pass--cfgto open both.--no-loadwill not load any config, running the default one.--loadwill pick a path to look for a config on. This lets you have.--profilewill pick a profile to load.--openwill openNwindows to place the files passed as arguments. multiple configurations, if you wish.--reloadwill recompile the config crate.--cleancallscargo cleanon the config crate.--updatewill update dependencies to their latest compatible version.--init-configwill clear the replace the config crate with the default version. If--loadwas passed, clears that path instead.--init-pluginwill initialize a crate for plugin development at the given path.--repositorywill pick a repository for the plugin.--authorwill pick an author for the plugin.--helpdisplays these flags.--versiondisplays Duat’s version.
Got to the next chapter to figure out how to control Duat once you’re in it.
Key bindings
Duat’s default mode is one heavily inspired by Kakoune. This means that it is modal, and follows “object action” semantics, unlike (neo)vim, which follows “action object” semantics.
Duat is extremely capable in the multi-cursor department, so Duat’s default mode makes heavy use of multiple cursors. Every action will be done on every cursor, unless specified otherwise.
Given that, here are the bindings for duatmode:
Keymaps
On every key, if the action involves selections, unless stated otherwise, it will take place in all selections.
Insert mode
Insert mode is the text editing mode of Duat, much like Vim’s. It
is also entered via various keys in Normal mode.
On insert mode, keys are sent normally, with the exception of the following:
<Tab> and <S-Tab> will do different things depending on your
[tab mode].
<C-n> and <C-p> go to the next and previous completion
entries.
<Esc> exits insert mode, returning to Normal mode`.
Normal mode
The keys in normal mode follow the following patterns:
wordcharacters follow Duat’s [word chars], which are normally used to define where lines wrap.WORDcharacters are just any non-whitespace character.
In normal mode, another factor is the param value, which is
incremented by typing digits. For example, if you type
10<Right>, the selections will move 10 times to the right.
Object selection
In Duat, there are various types of “objects” for selection. These
get used on Normal mode key sequences, most notably on <A-i>
and <A-a>. Each of them defines something to be selected:
b, (, )
Inside/around parenthesis.
B, {, }
Inside/around curly braces.
r, [, ]
Inside/around brackets.
a, <, >
Inside/around angle brackets.
q, '
Inside/around single quotes.
Q, "
Inside/around double quotes.
g, `
Inside/around graves.
w, <A-w>
Inside/around words and WORDs.
s
Inside/around sentences.
p
Inside/around paragraphs.
i
Inside/around lines of equal or greater indentation.
Inside/around whitespace.
Selection keys
h, <Left>
Move left. Wraps around lines.
j
Move down
<Down>
Move down to the next wrapped line (i.c vim’s gj).
k
Move up.
<Up>
Move up to the previous wrapped line (i.e. vim’s gk).
l, <Right>
Move right. Wraps around lines.
H, <S-Left>, J, <S-Down>, K, <S-Up>, L, <S-Right>
Same as the previous characters, but extends the selection
w
Selects the word and following space ahead of the selection.
b
Selects the word followed by spaces behind the selection.
e
Selects to the end of the next word ahead of the selection.
<(W|B|E)>
The same as (w|b|e), but extends the selection.
<A-(w|b|e)>
The same as (w|b|e), but over a WORD.
<A-(W|B|E)>
The same as <A-(w|b|e)>, but extends the selection.
f{char}
Selects to the next occurrence of the {char}.
t{char}
Selects until the next occurrence of the {char}.
<(F|T)>{char}
Same as (f|t), but extends the selection.
<A-(f|t)>{char}
Same as (f|t), but in the opposite direction.
<A-(F|T)>{char}
Same as <A-(f|t)>, but in extends the selection.
{param}g
Goes to the {param}th line. If param was not set, enters go to
mode.
{param}G
Extends to the {param}th line. If param was not set, enters go to mode, and actions will extend.
x
Extends selection to encompass full lines.
%
Selects the whole buffer.
<A-h>, <Home>
Selects to the start of the line.
<A-l>, <End>
Selects until the end of the line.
<A-H>, <S-Home>, <A-L>, <S-End>
Same as the previous two, but extends the selection.
m
Selects to the next pair of matching brackets.
<A-m>
Selects the previous pair of matching brackets.
M, <A-M>
Same as the previous two, but extends the selection.
<A-u>
Returns to the previous state for the selections.
<A-U>
Goes to the next state for the selections.
;
Reduces selections to just the [caret].
<A-;>
Flips the [caret] and [anchor] of selectionss around.
,
Removes extra selections.
C
Creates a selection on the column below the last one.
<A-C>
Creates a selection on the column above the first one.
<A-:>
Places the [caret] ahead of the [anchor] in all selections.
<A-s>
Divides selection into multiple selections, one per line.
<A-S>
Splits into two selections, one at each end of the selection.
<A-_>
Merges all adjacent selections.
Text modification
i
Enter insert mode before selections.
a
Enter insert mode after selection.
I
Moves to the beginning of the line (after indent) and enters
insert mode.
A
Moves to the end of the line and enters insert mode.
y
Yanks selections.
d
Deletes and yanks the selections.
c
Deletes, yanks, and enter insert mode.
p
Pastes after end of each selection (multi line selections are
placed on the next line).
P
Pastes at the start of each selection (multi line pastes are
placed on the previous line).
R
Replaces with the pasted text, without yanking.
<A-d>
Deletes selections without yanking.
<A-c>
Deletes selections without yanking, then enters insert mode.
o
Creates a new line below and enters insert mode in it.
O
Creates a new line above and enters insert mode in it.
<A-(o|O)>
Same as (o|O), but just adds the new line without moving.
r{key}
Replaces each character with {key}
u
[Undoes] the last moment
U
[Redoes] the next moment
>
Adds indentation to the selected lines.
<
Removes indentation to the selected lines.
<A-j>
Merges selected lines.
`
Changes selection to lowercase.
~
Changes selection to uppercase.
<A-`>
Swaps the case of each character.
<A-)>
Rotates each selection’s content forwards.
<A-(>
Rotates each selection’s content backwards.
|
Changes mode to [PipeSelections], letting you pipe each
selection to an external program.
Search
The searching in this plugin is done through the [IncSearch]
[Mode] from Duat, with some [IncSearcher]s defined in this
crate. This means that search will be done incrementally over a
Regex pattern.
/
Searches forward for the next pattern.
<A-/>
Searches backwards for the previous pattern.
?
Extends forward for the next pattern.
<A-?>
Extends backwards for the previous pattern.
s
Selects the pattern from within current selections.
S
Splits current selections by the pattern.
<A-k>
Keeps only the selections that match the pattern.
<A-K>
Keeps only the selections that don’t match the pattern.
n
Go to next match for pattern.
N
Create a new cursor on the next match for pattern.
<A-n>
Go to previous match for pattern.
<A-N>
Create a new cursor on the previous match for pattern.
*
Makes the main selection the searching pattern.
goto mode
Goto mode is accessed by the g and G keys in normal mode.
It serves a s a way to quickly move places, be it on selections or to other
Buffers and such.
Key bindings
goto mode is entered with the g or G keys in normal mode.
On every key that selects, G will have the same behavior, but
extending the selection instead.
h
Move to the beginning of the line (before indents, column 0).
l
Go to the end of the line.
i
Go to the beginning of the line, after indents.
g,k
Go to the first line.
j
Go to the last line.
a
Go to the last buffer. Repeating will return to this buffer
n
Go to the next buffer (includes other windows).
N
Go to the previous buffer (includes other windows).
User mode
In Duat, User mode is a “generalized mode”, which should be
used by Plugins for key maps. For example, you could map l
on User mode to do LSP related actions.
Other “monolithic modes” (Vim, Helix, Emacs, etc) should make use
of this User mode for the same purpose. Think of it like the
leader key in (neo)vim.
To enter User mode, you type <Space> in Normal mode.
Commands
In Duat, you can run commands by pressing the : key. This will focus on the
PromptLine Widget, where you can run commands.
You can press <Up> and <Down> to navigate the history of commands, and you
can also press <Tab> and <S-Tab> to scroll through the commands.
Here’s the list of commands:
Writing/quitting:
write,wwrites the currentBuffer. If there’s an path argument, saves to that path.quit,qquits the currentBuffer, fails if it isn’t written. If there’s an argument, quits thatBufferinstead.quit!,q!quits the currentBuffer, even if it isn’t written. If there’s an argument, quits thatBufferinstead.write-quit,wqwrites and quits the currentBuffer. If there’s an argument, quits thatBufferinstead.write-all,wawrites allBuffers.quit-all,qatries to quit allBuffers, failing if some aren’t written.quit-all!,qa!quits allBuffers, even if they aren’t written.write-all-quit,waqwrites to allBuffers and quits.write-all-quit!,waq!writes to allBuffers and quits, even if writing fails for some reason.
Switching Buffers
edit,eopens a newBufferon the current window.open,oopens a newBufferon another window.buffer,bswitches to anotherBuffer.next-bufferswitches to the nextBufferopened.prev-bufferswitches to the previousBufferopened.last-bufferswitches to the previously focusedBuffer.swapSwaps the positions of the currentBufferand another. If there are two arguments, swaps those two buffers instead.
Other
set-formtakes in a name and 0 to 3 colors (##rrggbb,rgb r g borhsl h s l) and sets that name’sfg,bgandulcolors. More settings coming in the future.colorschemesets the colorscheme.aliasAliases a word to a command call.
Quick settings
First of all, before you start anything, you should try to become at least somewhat accustomed to how rust works as a programming language. If you don’t know anything at all, the rust book is a great place to start learning.
One of the main aspects that Duat tries to accomplish is an emergent complexity for its features. That is, the more you learn about Duat, the easier it becomes to learn new things about it. Every feature should help you understand other features.
That being said, here are some quick settings, which should hopefully compose later on to help you better understand duat.
The setup function and setup_duat!
In order for Duat to be able to run your config crate in ~/.config/duat, it
needs to have at the very least this in it:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {}
That’s because Duat will call the setup function (after some initial setup,
and before some final setup) in order configure Duat. For all intents and
purposes, treat the setup function as if it were the init.lua on a neovim
configuration.
Of course, not everything needs to be placed inside of this function. For example, you could separate the plugins into a separate function, if you think that will look cleaner:
mod duat_catppuccin {
use duat::prelude::*;
#[derive(Default)]
pub struct Catppuccin;
impl Plugin for Catppuccin {
fn plug(self, _: &Plugins) { todo!() }
}
}
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
add_plugins();
}
fn add_plugins() {
plug(duat_catppuccin::Catppuccin::default());
}
You can add Plugins to duat by calling the plug function. By default, the
Treesitter MatchPairs plugins are added, providing syntax highlighting,
automatic indentation, and matching parenthesis highlighting. There is also the
DuatMode plugin, which provides the default control scheme for duat, heavily inspired by Kakoune.
The prelude module
At the top of your crate, you should be able to find this:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// The stuff inside your setup...
}
This will import everything in the prelude module of duat. This should have
everything you will need in order to configure Duat, not including things from
other crates that you may want to import (such as plugins).
When calling use duat::prelude::*, most imported things will be in the form
of modules, like this:
use duat::prelude::*;
use duat::opts;
This is importing the opts module, as opposed to importing its items
directly, like this:
use duat::prelude::*;
use duat::opts::*;
This means that, for most options, their path is made up of a
{module}::{function} combo. So the usual setup function should look
something like this:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
opts::set(|opts| {
opts.wrap_lines = true;
opts.wrapping_cap = Some(80);
});
form::set("caret.main", Form::yellow());
cmd::add("set-rel-lines", |pa: &mut Pass| {
let handles: Vec<_> = context::windows()
.handles(pa)
.filter_map(|handle| handle.try_downcast::<LineNumbers>())
.collect();
for handle in handles {
handle.write(pa).relative = true;
}
Ok(Some(txt!("Lines were set to [a]relative")))
});
map::<Insert>("jk", "<Esc>:w<Enter>");
}
The exceptions to this are the map, alias and plug functions and the
setup_duat! macro. These items are imported directly.
The following chapters should give a quick overview of these items imported from the prelude module.
The opts module
This module contains a bunch of commonly used options. It covers settings for
the various Widgets of Duat, most notably the Buffer widget, which is where
editing takes place.
Below are the available functions on this module, as well as their default values.
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// Default options for the Buffer widget
opts::set(|opts| {
// Buffer options:
opts.wrap_lines = false;
opts.wrap_on_word = false;
// Where to wrap, as opposed to at the rightmost edge.
opts.wrapping_cap = None::<u32>;
// Indent wrapped lines.
opts.indent_wraps = true;
opts.tabstop = 4;
// Wether to print new lines as space characters.
opts.print_new_line = true;
// Minimum cursor distance from the top and bottom edges.
opts.scrolloff.x = 3;
// Minimum cursor distance from the left and right edges.
opts.scrolloff.y = 3;
opts.extra_word_chars = &[];
// Forces scrolloff at the end of a line.
opts.force_scrolloff = false;
// Wether to allow showing ghosts (When LSP eventually comes, for example).
opts.show_ghosts = true;
// Allow scrolling until the Buffer shows only scrolloff.y lines.
opts.allow_overscroll = false;
// General settings:
// Place the bottom widgets on the top of the screen.
opts.footer_on_top = false;
// Make the bottom widgets take up only one line of space.
opts.one_line_footer = false;
// Shows available keybindings
opts.help_key = Some(KeyEvent::new(KeyCode::Char('?'), mode::KeyMod::CONTROL));
// duatmode settings:
// Inserts a \t instead of spaces when pressing Tab
opts.duatmode.insert_tabs = false;
// How to handle the Tab key
opts.duatmode.tab_mode = opts::TabMode::VerySmart;
// Auto indent new lines on tree-sitter Buffers
opts.duatmode.auto_indent = true;
// Characters that trigger a reindentation
opts.duatmode.indent_chars = &['\n', '(', ')', '{', '}', '[', ']'];
// Reindent when pressing 'I' in normal mode
opts.duatmode.indent_on_capital_i = true;
// Makes the 'f' and 't' keys set the search pattern
opts.duatmode.f_and_t_set_search = true;
// Bracket pairs to be considered by keys like 'm' and the 'u' object
opts.duatmode.set_brackets([["(", ")"], ["{", "}"], ["[", "]"]]);
// LineNumbers options:
opts.line_numbers.relative = false;
// Where to align the numbers
opts.line_numbers.align = std::fmt::Alignment::Left;
// Where to align the main line number
opts.line_numbers.main_align = std::fmt::Alignment::Right;
// Wether to show wrapped line's numbers
opts.line_numbers.show_wraps = false;
// Place the widget on the right, as opposed to on the left
opts.line_numbers.on_the_right = false;
// Notifications options:
// Reformat the notifications messages
opts.notifications.fmt(|rec| todo!("default fmt function"));
// Which mask to use to show the messages
opts.notifications.set_mask(|rec| todo!("error for error, info for info, etc"));
// Which log levels will actually show up on the notifications
opts.notifications.set_allowed_levels([
context::Level::Error,
context::Level::Warn,
context::Level::Info
]);
// WhichKey options:
// How to format each keybinding entry on the widget
opts.whichkey.fmt(|desc| todo!("default fmt function"));
// Disable the widget for the given Mode
// opts.whichkey.disable_for::<{Mode in question}>();
// Always show the widget for the given Mode
opts.whichkey.always_show::<User>();
// Removes the Mode from the disable_for and always_show lists
// opts.whichkey.show_normally::<{Mode in question}>();
// LogBook options:
// How to format each message
opts.logs.fmt(|rec| todo!("default log fmt"));
opts.logs.close_on_unfocus = true;
// It can be shown via the "logs" command
opts.logs.hidden = false;
// Where to place it
opts.logs.side = ui::Side::Below;
// Is ignored when the side is Left or Right
opts.logs.height = 8.0;
// Is ignored when the side is Above or Below
opts.logs.width = 50.0;
// Wether to show the source of the message (on the default fmt)
opts.logs.show_source = true;
});
// Default options for the StatusLine widget
opts::fmt_status(|pa| {
// If on one line footer mode:
let mode = mode_txt();
let param = duat_param_txt();
status!("{Spacer}{name_txt} {mode} {sels_txt} {param} {main_txt}");
// If on regular mode (default):
let mode = mode_txt();
let param = duat_param_txt();
status!("{mode} {name_txt}{Spacer}{sels_txt} {param} {main_txt}")
});
}
For more information about modification of the StatusLine, see the chapter on
modding the StatusLine. For information on modding the Notifications and
LogBook widgets, see the Text chapter
form: How text is colored
In duat, the way text is styled is through Forms. The Form struct,
alongside the form module, are imported by the prelude:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// Setting by Form
form::set("punctuation.bracket", Form::red());
form::set("default", Form::with("#575279").on("#faf4ed"));
form::set("matched_pair", Form::blue().underlined());
// Setting by reference
form::set("accent.debug", "default.debug");
}
The main function that you will use from this module is form::set. This
function sets the form on the left to the value on the right. This value can be
of two types:
- A
Formargument will be used to color the form directly. - A
&strargument will “reference” the form on the right. If the form on the right is altered, so will the one on the left. This reduces the need for setting a ton of forms in things like colorschemes.
How forms should be named
Every form in duat should be named like this: [a-z0-9]+(\.[a-z0-9]+)*. That
way, inheritance of forms becomes very predictable, and it’s much easier for
plugin writers to depend on that feature.
There is one exception to this rule however, that being the default form. The
default form, unlike other forms, can have Widget specific implementations,
like default.StatusLine, which will change the default form only on
StatusLines, and is set by default.
Colorschemes
The other main function that you will use from this module is the
form::set_colorscheme function. This function will change the colorscheme to
a previously named one:
mod duat_catppuccin {
use duat::prelude::*;
#[derive(Default)]
pub struct Catppuccin;
impl Plugin for Catppuccin {
fn plug(self, _: &Plugins) { todo!() }
}
}
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// Adds four colorschemes, "catppuccin-latte" among them.
plug(duat_catppuccin::Catppuccin::default());
form::set_colorscheme("catppuccin-latte");
}
Form inheritance
Another aspect of duat’s forms that can save a lot of typing is the concept of Form inheritance. In Duat, forms follow the following structure:
- If
form.subformis unset, it will referenceform; - If
form.subformis set toForm::green(), it won’t be changed whenformchanges, staying atForm::green(); - If
form.subformis set to referenceother_form, changingother_formwill also changeform.subform, but changingformwon’t;
As a consequence of this, for example, if you were to set the markup form to
something, every form with a name like markup.* that isn’t already set,
would follow the change to the markup form.
Additionally, if the form f0.f1.f2 is set to something, the forms f0 and
f1.f2 would also be set, although they will reference the default form in
that situation, not whatever f0.f1.f2 was set to.
Quiz
Given the following sequence of form::sets, what will each Form be at the
end?
use duat::prelude::*;
fn test() {
form::set("parent", Form::green());
form::set("parent.child.granchild", Form::blue());
form::set("grandparent.parent.child", "parent.child");
form::set("parent", Form::red());
}
See results
- “parent”:
Form::red(). - “parent.child”:
Form::red(). - “parent.child.grandchild”:
Form::blue(). - “grandparent.parent.child”:
Form::red().
Masks
A mask is essentially the opposite of the inheritance concept. Instead of the longer form inheriting from the shorter forms, the shorter forms will be mapped for longer ones.
It works like this: Say I have a File widget, and in it, there are instances
of the function form, used to highlight function identifiers. If there is a
function.error form, and I tell the File to use the error mask, instead
of using the function form, Duat will use function.error.
In duat, by default there are four masks: error, warning, info, and inactive. The first three are used primarily to show color coded notifications. The last one is unused, but you can use it to change how unfocused buffers should be displayed.
You can also add more masks through form::enable_mask. If you want to learn
more about masks and how to use them, you should check out the masks
chapter
List of forms
Currently, in duat, these are the forms in use:
default: Is applied to every text. Can beWidgetdependent, likedefault.LineNumbers.accent: This form is used when formatting error, warning, information, or debug messages. Is used mostly with theerror,warnandinfomasks.caret.main: The form to use when printing the main “caret” (each cursor has a selection, an anchor, and a caret).caret.extra: Same ascaret.main, but for cursors other than the main one.selection.main: Color to be used on the main selection.selection.extra: Color to be used on extra selections.cloak: This form is supposed to be a common “get rid of all forms temporarily” form. You should use this when you want to, for example, remove all visible color from the screen, in order to highlight something. Some plugins make use of this form, likeduat-hopandduat-sneak, which are recreations of some neovim plugins.alias: Is used on aliases, see the map and alias chapter for more information.matched_pair: Isn’t technically part of duat, but it’s part of a default plugin.
Some other forms are used by specific Widgets. Remember, the default form for
every Widget will always be default.{WidgetName}:
-
LineNumbers:linenum.main: The form to be used on the main line’s number.linenum.wrapped: The form to be used on wrapped lines.linenum.wrapped.main: Same, but for the main line, inherits fromlinenum.wrappedby default.
Do note that you can set the form of the remaining lines by setting
default.LineNumbers. And due to form inheritance, settinglinenumwill setlinenum.wrapped,linenum.mainandlinenum.wrapped.main. -
StatusLine:buffer,buffer.new,buffer.unsaved,buffer.new.scratch: Are all used byname_txt, which shows theFile’s name and some other info.mode: Is used bymode_txt.coordandseparator: Are used bymain_txt.selections: Is used byselections_txt.keyandkey.special: Are used bycur_map_txt.
-
Completions:selected.Completionschanges the selected entry’s form.
-
Notifications:notifs.target: The form for the “target” of the notification.notifs.colon: The form used by the':'that follows the target.
Since the
Notificationswidget makes heavy use of masks, you can also setnotifs.target.error, if you want a different target color only when error messages are sent, for example. -
PromptLine:prompt: For the prompt on the prompt line.prompt.colon: For the':'that follows it.caller.infoandcaller.error: For the caller, if it exists or not, respectively.parameter.infoandparameter.error: For parameters, if they fit or not, respectively.regex.literal,regex.operator.(flags|dot|repetition|alternation),regex.class.(unicode|perl|bracketed),regex.bracket.(class|group): A bunch of forms used for highlighting regex searches.
-
LogBook:log_book.(error|warn|info|debug): For the types of messages.log_book.colon: For the':'that follows them.log_book.target: For the “target” of the message.log_book.bracket: For the(s surrounding the target.
-
VertRule:rule.upperandrule.lower: The forms to use above and below the main line.
And finally, there are also all the forms used by duat-treesitter. Since the
queries were taken from nvim-treesitter, the form names follow the same patters
as those from neovim. Remember, setting form will automatically set
form.child and form.child.grandchild, and so forth , unless that form is
already set to something:
| Form name | Purpose |
|---|---|
variable | various variable names |
variable.builtin | built-in variable names (e.g. this, self) |
variable.parameter | parameters of a function |
variable.parameter.builtin | special parameters (e.g. _, it) |
variable.member | object and struct fields |
constant | constant identifiers |
constant.builtin | built-in constant values |
constant.macro | constants defined by the preprocessor |
module | modules or namespaces |
module.builtin | built-in modules or namespaces |
label | GOTO and other labels (e.g. label: in C), including heredoc labels |
string | string literals |
string.documentation | string documenting code (e.g. Python docstrings) |
string.regexp | regular expressions |
string.escape | escape sequences |
string.special | other special strings (e.g. dates) |
string.special.symbol | symbols or atoms |
string.special.path | filenames |
string.special.url | URIs (e.g. hyperlinks) |
character | character literals |
character.special | special characters (e.g. wildcards) |
boolean | boolean literals |
number | numeric literals |
number.float | floating-point number literals |
type | type or class definitions and annotations |
type.builtin | built-in types |
type.definition | identifiers in type definitions (e.g. typedef <type> <identifier> in C) |
attribute | attribute annotations (e.g. Python decorators, Rust lifetimes) |
attribute.builtin | builtin annotations (e.g. @property in Python) |
property | the key in key/value pairs |
function | function definitions |
function.builtin | built-in functions |
function.call | function calls |
function.macro | preprocessor macros |
function.method | method definitions |
function.method.call | method calls |
constructor | constructor calls and definitions |
operator | symbolic operators (e.g. +, *) |
keyword | keywords not fitting into specific categories |
keyword.coroutine | keywords related to coroutines (e.g. go in Go, async/await in Python) |
keyword.function | keywords that define a function (e.g. func in Go, def in Python) |
keyword.operator | operators that are English words (e.g. and, or) |
keyword.import | keywords for including or exporting modules (e.g. import, from in Python) |
keyword.type | keywords describing namespaces and composite types (e.g. struct, enum) |
keyword.modifier | keywords modifying other constructs (e.g. const, static, public) |
keyword.repeat | keywords related to loops (e.g. for, while) |
keyword.return | keywords like return and yield |
keyword.debug | keywords related to debugging |
keyword.exception | keywords related to exceptions (e.g. throw, catch) |
keyword.conditional | keywords related to conditionals (e.g. if, else) |
keyword.conditional.ternary ternary | operator (e.g. ?, :) |
keyword.directive | various preprocessor directives and shebangs |
keyword.directive.define | preprocessor definition directives |
punctuation.delimiter | delimiters (e.g. ;, ., ,) |
punctuation.bracket | brackets (e.g. (), {}, []) |
punctuation.special | special symbols (e.g. {} in string interpolation) |
comment | line and block comments |
comment.documentation | comments documenting code |
comment.error | error-type comments (e.g. ERROR, FIXME, DEPRECATED) |
comment.warning | warning-type comments (e.g. WARNING, FIX, HACK) |
comment.todo | todo-type comments (e.g. TODO, WIP) |
comment.note | note-type comments (e.g. NOTE, INFO, XXX) |
markup.strong | bold text |
markup.italic | italic text |
markup.strikethrough | struck-through text |
markup.underline | underlined text (only for literal underline markup!) |
markup.heading | headings, titles (including markers) |
markup.heading.1 | top-level heading |
markup.heading.2 | section heading |
markup.heading.3 | subsection heading |
markup.heading.4 | and so on |
markup.heading.5 | and so forth |
markup.heading.6 | six levels ought to be enough for anybody |
markup.quote | block quotes |
markup.math | math environments (e.g. $ … $ in LaTeX) |
markup.link | text references, footnotes, citations, etc. |
markup.link.label | link, reference descriptions |
markup.link.url | URL-style links |
markup.raw | literal or verbatim text (e.g. inline code) |
markup.raw.block | literal or verbatim text as a stand-alone block |
markup.list | list markers |
markup.list.checked | checked todo-style list markers |
markup.list.unchecked | unchecked todo-style list markers |
diff.plus | added text (for diff files) |
diff.minus | deleted text (for diff files) |
diff.delta | changed text (for diff files) |
tag | XML-style tag names (e.g. in XML, HTML, etc.) |
tag.builtin | builtin tag names (e.g. HTML5 tags) |
tag.attribute | XML-style tag attributes |
tag.delimiter | XML-style tag delimiters |
map and alias: modifying keys
In Duat, mapping works somewhat like Vim/neovim, but not quite. This is how it works:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
map::<User>("f", "<Esc><A-j>|fold -s -w 80<Enter>");
alias::<Insert>("jk", "<Esc>");
alias::<Prompt>("jk", "<Esc>");
}
In mapping, there are two main functions: map and alias. map will take
the keys as is, and if the sequence matches, outputs the remapping, otherwise,
outputs the keys that were sent. alias does the same thing, but it also
““prints”“ the sequence that was sent, making it look like you are typing
real text. Here’s a showcase of the difference:

Both of these functions also take a required type argument. This type
argument is the Mode where this mapping will take place. So in the first
example, in Insert and Prompt mode, if you type jk, the j will show up
as ““text”“, but when you press k, you will immediately exit to Normal
Mode.
User is a standard Mode in Duat. It is meant to be a “hub” for Plugin
writers to put default mappings on. Sort of like the leader key in Vim/Neovim.
On Normal mode, by default, this mode is entered by pressing the space bar.
While you can change that like this:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
map::<Normal>(" ", "");
// In rust, you have to escap a backslash
map::<Normal>(r"\", " ");
}
You should prefer doing this:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
map::<Normal>(" ", "");
map::<Normal>("\\", User);
}
In this case, instead of putting a sequence of keys to replace the mapped ones, I placed the mode directly.
This is allowed in order to support custom Modes. That way, you can just
place the Mode as the second argument, and the mapping will switch modes
instead of sending keys. This also works with aliases.
Note
In this case, since
Useris a struct with no fields, I could just putUseras the second argument, which acts as a constructor. But in most otherModes, you’re gonna have to write something likeInsert::new()as the argument instead.
List of keys and modifiers
Syntax wise, the keys are very similar to vim style. Regular characters are
placed normally, special keys are enclosed in <,> pairs, and modified keys
are enclosed in these pairs, with a <{mod}-{key}> syntax. Examples:
abc<C-Up><F12>.<A-Enter><AS-Left>.づあっと.
This is the list of recognized special keys:
<Enter>,<Tab>,<Backspace>,<Del>,<Esc>,<Up>,<Down>,<Left>,<Right>,<PageU>,<PageD>,<Home>,<End>,<Ins>,<F{1-12}>,
And these are the allowed modifiers, which, as you can see above, can be composed together:
C => Control,A => Alt,S => Shift,M => Meta,super => Super,hyper => Hyper,
cursor: How to print cursors
The cursor module is like the print module, in that it provides some basic
options on how cursors should be printed. These options primarily concern if
cursors should be printed as “real cursors” (The blinking kind, that can turn
into a bar and stuff), or as just Forms.
cursor::set_mainwill set the “shape” of the main cursor. This takes aCursorShapeargument, and lets you set its shape to a vertical bar, a horizontal bar, and make it blink.cursor::set_extrais the same but for extra cursors. Do note that this may not work on someUis, mainly terminals, which only allow for one cursor at a time.cursor::unset_mainandcursor::unset_extra: Disables cursor shapes for every type of cursor, replacing them with aForm, which will becaret.mainandcaret.extra, respectivelycursor::unset: The same as callingunset_mainandunset_extra.
Frequently used snippets
If you just want some no nonsense snippets to copy paste into your config, this is the chapter for you.
These should also serve as a good entry point for light modification and learning by example.
mapping jk and others to esc
This one is pretty simple. This is normally done on Duat’s native Insert mode,
but you could replace that with any other Insert mode, provided you got the
plugin for one:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
map::<Insert>("jk", "<Esc>");
}
This won’t print anything to the screen while you’re typing, making it seem
like the j key has a bit of delay. If you wish to print 'j' to the screen,
use this:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
map::<Insert>("jk", "<Esc>");
}
Additionally, if you want to write to the file on jk as well, you can do this:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
map::<Insert>("jk", "<Esc>:w<Enter>");
}
If you want to, you can also have this behavioron the PromptLine, i.e., while
writing commands and searches:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
map::<Prompt>("jk", "<Esc>");
}
StatusLine on each Buffer
If you want one StatusLine on every Buffer, you can do that via hooks:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
hook::add::<BufferOpened>(|pa, handle| {
status!("{name_txt}{Spacer}{main_txt}")
.above()
.push_on(pa, handle);
});
}
Prompt and status on same line
In the Kakoune text editor, the status line occupies the same line as the command line and notifications. If you want this behavior in Duat, the following snippet is enough:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
opts::set(|opts| {
opts.one_line_footer = true;
});
}
This will call FooterWidgets::one_line on the window’s FooterWidgets.
If you want one of these on each Buffer, you can do this instead:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
hook::remove("FooterWidgets");
hook::add::<BufferOpened>(|pa, handle| {
FooterWidgets::default().one_line().push_on(pa, handle);
});
}
Common StatusLine parts
The most relevant parts for pretty much every StatusLine are the following.
Formatted status parts:
name_txt: Prints theBuffer’s name and some info about it’s newness.- Uses the forms
buffer,buffer.new,buffer.new.scratchandbuffer.unsaved. mode_txt: The lowercased name of theMode, e.g. “insert”, “normal”.- Uses the form
mode.
- Uses the form
main_txt: Prints the main selection’s column and line, and the number of lines. 1 indexed.- Uses the forms
coordandseparator.
- Uses the forms
sels_txt: Prints the number of selections.- Uses the form
selections;
- Uses the form
current_sequence_txt: Prints the keys being mapped.- Uses the forms
keyandkey.special
- Uses the forms
Unformatted status parts:
main_byte,main_char,main_line,main_col: Parts of the main cursor. 1 indexed.mode_name: The nonTextversion ofmode_txt, just a string.raw_mode: The raw type name of the mode. Could look something likePager<SomeWidget>.selections: The number of selections, no formatting.last_key: The last key that was typed. Useful for asciinema demonstrations.
Other:
Spacer: This isn’t actually aStatusPart, it’s aTagthat can go in anyText, which includes theStatusLine’s.- Forms like
[buffer]. Any form can be placed within those braces, and they are all evaluated at compile time. For more information about them, see the forms chapter
This is the default:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
// Default options for the StatusLine widget
opts::fmt_status(|pa| {
// If on one line footer mode:
let mode = mode_txt();
let param = duat_param_txt();
status!("{Spacer}{name_txt} {mode} {sels_txt} {param} {main_txt}");
// If on regular mode (default):
let mode = mode_txt();
let param = duat_param_txt();
status!("{mode} {name_txt}{Spacer}{sels_txt} {param} {main_txt}")
});
}
Customized main_txt:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
opts::fmt_status(|_| {
status!(
"{name_txt}{Spacer}{} {sels_txt} [coord]c{} l{}[separator]|[coord]{}",
mode_txt(),
main_col,
main_line,
Buffer::len_lines
)
});
}
Customized name_txt:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
opts::fmt_status(|_| {
status!("{name_txt}{Spacer}{} {sels_txt} {main_txt}", mode_txt())
});
}
fn name_txt(buffer: &Buffer) -> Text {
let mut b = Text::builder();
if let Some(name) = buffer.name_set() {
b.push(txt!("[buffer]{name}"));
if !buffer.exists() {
b.push(txt!("[buffer.new][[new buffer]]"));
} else if buffer.text().has_unsaved_changes() {
b.push(txt!("[buffer.unsaved][[has changes]]"));
}
if let Some("rust") = buffer.filetype() {
b.push(txt!("[[🦀]]"));
}
} else {
b.push(txt!("[buffer.new.scratch]?!?!?!"));
}
b.build()
}
Buffer wise tabstops
If you want to change the tabstop size per Buffer, you can just modify the
following snippet:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
hook::add::<BufferOpened>(|pa, handle| {
let buffer = handle.write(pa);
buffer.opts.tabstop = match buffer.filetype() {
Some("markdown" | "bash" | "lua" | "javascript" | "commonlisp") => 2,
_ => 4
};
});
}
If you want, you can also set other options with this, like which characters
should be a part of words. In this case, I’m adding '-' to the list:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
hook::add::<BufferOpened>(|pa, handle| {
let buffer = handle.write(pa);
match buffer.filetype() {
Some("lisp" | "scheme" | "markdown" | "css" | "html") => {
buffer.opts.tabstop = 2;
buffer.opts.extra_word_chars = &['-'];
}
Some("bash" | "lua" | "javascript" | "typescript") => {
buffer.opts.tabstop = 2;
}
_ => buffer.opts.tabstop = 4
}
});
}
Nerdfonts StatusLine
Important
This chapter assumes that you are using some kind of nerd font in your
Ui. This also goes for this page in the book. If you are not using some kind of nerd font in your browser, you will not be able to see the characters being displayed.
If you want to nerd-fontify your StatusLine, you can just redefine some of
the status line parts:
use duat::prelude::*;
fn name_txt(buffer: &Buffer) -> Text {
let mut b = Text::builder();
if let Some(name) = buffer.name_set() {
b.push(txt!("[buffer]{name}"));
if !buffer.exists() {
b.push(txt!(" [buffer.new] "));
} else if buffer.text().has_unsaved_changes() {
b.push(txt!(" [buffer.unsaved] "));
}
} else {
b.push(txt!(" [buffer.new.scratch]{} ", buffer.name()));
}
b.build()
}
Status on Buffers and windows
If you want to have a StatusLine per Buffer, you can add the following:
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
hook::add::<BufferOpened>(|pa, handle| {
status!("{name_txt}{Spacer}{main_txt}").above().push_on(pa, handle);
});
}
The snippet above will place a StatusLine above every single Buffer.
You can go further with this, what if you want different StatusLines,
depending on the Buffer?
use duat::prelude::*;
setup_duat!(setup);
fn setup() {
hook::add::<BufferOpened>(|pa, handle| {
let status = if handle.read(pa).path().contains(".config/duat") {
status!("{name_txt}[config] []{Spacer}{main_txt}")
} else {
status!("{name_txt}{Spacer}{main_txt}")
};
status.above().push_on(pa, handle);
});
}
Scripting Duat
This chapter will teach you how to do “advanced” configuration of duat. It covers more complex topics, and details how the API of duat actually works.
In order to understand duat’s API, you have to understand its memory model. Since the text editor uses a “low level” language for configuration, it has no garbage collection, and you will have to deal with things like references and Rust’s (in)famous borrow checker.
Duat’s API is designed in a way that it should smooth most of these problems out as much as possible. But it is still a work in progress, so you can expect improvements in the future.
The Pass and Duat’s global state
The most important concept when configuring duat is the sharing model of
memory. Every piece of state in Duat is behind some type that requires the
Pass struct.
The Pass struct is how you access duat’s global state. It works through
Rust’s borrow checker, giving you safe access to the variables of the global
state. It works via mutable and immutable references, that is, whenever you
mutably borrow a Pass, you get mutable access to the global state of duat,
and whenever you borrow it normally, you get non mutable access to said state:
use duat::prelude::*;
// As a shorthand, you should use `pa` as the variable's name.
// You don't need to write the types of `pa` and `handle`, they're
// just here for tutorial reasons.
hook::add::<BufferOpened>(|pa: &mut Pass, handle: &Handle<Buffer>| {
// This function will mutably borrow the `Pass`, preventing other
// uses of it, thus following Rust's mutability XOR aliasing rule.
let buf_mut: &mut Buffer = handle.write(pa);
// Given the mutable reference, you can change the `Buffer`.
buf_mut.opts.wrap_lines = false;
buf_mut.opts.tabstop = 2;
// This function will immutably borrow the `Pass`, which means you
// can do other immutable borrows, letting you have as many
// immutable references as you want.
// Note that all function available from an immutable borrow are also
// available with a mutable one.
let buf: &Buffer = handle.read(pa);
// The `Buffer` may not have any filetype if it can't be inferred.
let Some(filetype) = buf.filetype() else {
return;
};
// Calling this now would cause a compile time error, since you would
// have reused a mutable reference (`&mut Buffer`) after getting another
// one (`&Buffer`).
// buf_mut.opts.scrolloff.y = 5;
// The context module will have all the types representing duat's state
// This function retrieves `Handle`s to all `Buffer`s in duat.
let mut other_buffers = context::buffers(pa);
other_buffers.retain(|other| other != handle);
if other_buffers.iter().all(|other| other.filetype(pa) != Some(filetype)) {
context::info!("Opened the first buffer of filetype [a]{filetype}");
context::info!("The buffer is called [a]{}", buf.name());
}
});
The above example showcases the two types of borrowing. The first type is the mutable borrow, which can’t be reused after acquiring other references, and the second one is the immutable borrow, which can coexist with any number of other immutable references.
These two borrows govern all of the configuration of duat, and they can only
bedone on the main thread, given the non Send nature of &Pass and &mut Pass. Additionally, you won’t always have a &mut Pass. In some APIs, you
only get access to a &Pass, which grants only reading access to global state,
making for APIs that are harder to mess up.
Tip
While editing with rust-analyzer, on functions that require a
&Passor&mut Pass, it might suggest a completion like&pa, or&mut pa. You should ignore those suggestions and just typepa, since rust will automatically convert a&mut Passto a&Passgiven the context, sopashould be enough for every single situation.
Global state types
Above, you saw a Handle<Buffer>. This is a handle to a Widget of type
Buffer. You can have a Handle<W> for any widget of type W, which lets you
change them, including the Text that they display.
Internally, a Handle<W> is backed by two datatypes, an RwData<W> and a
RwArea. The RwData type is duat’s global state primitive, every kind of
global access will eventually go through an RwData.
The RwData type
This type serves two primary purposes:
- To hold a value, which can be accessed with a
Pass. - To tell others if the value has been updated.
Lets explore how this happens. Below is a code snippet that shows on the
StatusLine wether a value has changed:
use duat::prelude::*;
fn setup() {
let value = RwData::new(0);
let value_clone = value.clone();
opts::fmt_status(move |_| {
let value_changed = {
// One copy for this function
let value = value_clone.clone();
move || {
if value.has_changed() {
"value changed to "
} else {
""
}
}
};
// And one copy for the `StatusLine`.
// This is done so the `StatusLine` updates automatically.
let value = value_clone.clone();
status!("{name_txt} {value_changed}{value}{Spacer}{main_txt}")
});
cmd::add("change-value", move |pa: &mut Pass, new: usize| {
*value.write(pa) = new;
Ok(None)
});
}
In the snippet above, the value variable will be displayed in the
StatusLine, and every time it changes, the text `“value changed to” will be
shown beside it.
The value inside an RwData is deemed as “changed” if the RwData::write
function was called in any other copy of the same RwData:
use duat::prelude::*;
fn test(pa: &mut Pass) {
let value = RwData::new("hello");
let value_clone = value.clone();
// Since `RwData::read` hasn't been called, both are considered
// to have changed.
assert!(value.has_changed() == true);
assert!(value_clone.has_changed() == true);
// Further calls to `RwData::has_changed` will return `true` until
// you call `RwData::read`
assert!(value.has_changed() == true);
_ = value_clone.read(pa);
*value.write(pa) = "bye";
// Since `value` was written to, copies of `value` will be notified
// that the data within has changed.
assert!(value_clone.has_changed() == true);
// An `RwData::write` also assumes that you have read the value, so
// `value.has_changed()` will return `false`.
assert!(value.has_changed() == false);
// If you don't have a Pass available (rare), you can also say that
// you don't care if the data has changed by calling this function.
value_clone.declare_as_read();
assert!(value_clone.has_changed() == false);
// Without a Pass, you can also tell other copies that a value has
// been changed, even if nothing changed at all
value_clone.declare_written();
assert!(value.has_changed() == true);
}
Widget Handles
A Handle<W: Widget> is a wrapper over two types:
- An
RwData<W>, which holds a widget - An
RwArea
This type is responsible for displaying everything on screen, by taking the
&Text out ofevery widget and printing it on their respective RwAreas.
The most common Handle you will encounter is the Handle<Buffer>,
shorthanded to just Handle. You are able to freely modify it whenever you
have access to an &mut Pass:
use duat::prelude::*;
use std::collections::HashSet;
fn setup() {
cmd::add("dedup-selections", |pa: &mut Pass| {
let buf: Handle<Buffer> = context::current_buffer(pa);
let mut dupes = Vec::new();
// Handle::edit_all will apply an editing function to all cursors
// Because it edits the cursors of the `Buffer`, it needs
// mutable access to it, hence the `&mut Pass`.
buf.edit_all(pa, |c| {
if dupes.iter().any(|dupe| *dupe == c.selection()) {
c.destroy()
} else {
dupes.push(c.selection().to_string());
}
});
Ok(None)
})
.doc(txt!("Removes selections with duplicate text content"), None);
}
The raison d'être of Handles is to keep both the RwArea and RwData<W>
in the same struct, so you can use methods that require reading from both:
use duat::prelude::*;
fn setup() {
opts::fmt_status(|_| {
status!("{name_txt} {}{Spacer}{main_txt}{cursors_on_screen}", mode_txt())
});
}
fn cursors_on_screen(pa: &Pass, handle: &Handle) -> Text {
/// In order to get the range of the `Text` that is on screen,
/// you need both the `Buffer` and the `RwArea` at the same time.
let range = handle.full_printed_range(pa);
let total = handle.selections(pa).iter_within(range).count();
txt!("On screen[separator]:[] [coord]{total}")
}
The type erased RwArea
In Duat, you may create your own Ui by implementing the RawUi trait, which
also requires an area type that implements the RawArea trait.
The RwArea type is a type erased wrapper over this RawArea. When you call
RwArea::write, you will get a &mut Area value, which is also type erased,
but grants direct access to the methods defined in the RawArea trait, without
the need for a Pass.
When you have a Handle, you can access the Area value in two ways:
use duat::{prelude::*, ui::RwArea};
fn test<W: Widget>(pa: &mut Pass, handle: &Handle<W>) {
let rw_area: &RwArea = handle.area();
let area: &mut Area = rw_area.write(pa);
area.set_width(5.0).unwrap();
// Normally, calling `Handle::write` returns only `W`, for
// convenience reasons and because you mostly don't need to
// access the `Area` value.
// You can do this if you need mutable access to both.
let (widget, area): (&mut W, &mut Area) = handle.write_with_area(pa);
area.set_height(10.0).unwrap();
widget.text_mut().replace_range(.., "lmao");
}
If required, you can also access the unerased version of the Area by calling
RwArea::write_as or RwArea::read_as, which takes in a type argument A and
returns Some(area) if the area is actually of type A (i.e., duat was
compiled with that RawArea’s RawUi).
DataMap and MutDataMap
On the earlier example which shows wether a value has changed, you might have
thought that the code looked a little awkward, since we had to put two objects
on the StatusLine just to get it to update properly.
Well, there is actually an existing solution to that, which can be achieved by calling the RwData::map function:
use duat::prelude::*;
fn setup() {
let value = RwData::new(0);
let value_clone = value.clone();
opts::fmt_status(move |_| {
// Clone to check if it has changed from within the function,
// you never really need to do this.
let clone = value_clone.clone();
let show_value = value_clone.map(move |value| {
if clone.has_changed() {
clone.declare_as_read();
format!("value changed to {value}")
} else {
format!("{value}")
}
});
status!("{name_txt} {show_value}{Spacer}{main_txt}")
});
cmd::add("change-value", move |pa: &mut Pass, new: usize| {
*value.write(pa) = new;
Ok(None)
});
}
This returns a DataMap<usize, String>, which works very similarly to an
RwData, except for the fact that it can only be read, and reading is
accompanied by a call to the mapping function, returning its value, as opposed
to usize.
This type also has the property of automatically updating the StatusLine
whenever the value changes, which means that you no longer need to place a
{value} in order to update the StatusLine automatically.
The MutDataMap serves a similar purpose, but it can also write to the inner
value, and is created through RwData::map_mut.
BulkDataWriter for parallelism
One problem with all of the previously mentioned types is that, in order to
write to them, you need to have a &mut Pass with you. Since this type can
only be accessed from the main thread, you cannot update values from other
threads.
This type does not change that fact, however, it lets you “push” updating
functions via the BulkDataWriter::mutate method. This method, which
doesn’ttake a Pass, will send an impl FnOnce function to update the inner
value,and the next time someone tries to call BulkDataWriter::write, which
does takea Pass, that function will be called and will update the value
before returning the `&mut T.
This makes it possible to update a value from another thread:
use duat::{data::BulkDataWriter, prelude::*};
use std::time::Duration;
// You will mostly want this type to be a static variable
static DATA: BulkDataWriter<Duration> = BulkDataWriter::new();
fn setup() {
std::thread::spawn(|| {
let one_sec = Duration::from_secs(1);
while !context::will_reload_or_quit() {
std::thread::sleep(one_sec);
DATA.mutate(move |value| *value += one_sec);
}
});
cmd::add("uptime", |pa: &mut Pass| {
context::info!("The uptime is {:?}", DATA.write(pa));
Ok(None)
});
}
This type is very focused on building APIs, in particular, it is good at
permitting functions that don’t take a Pass to be used. For example, in duat,
the cmd::add function makes use of a BulkDataWriter in order to be called
without a Pass, only actually adding the function when calling cmd::call or
other functions that need access to the list of commands.
Another API that makes use of this is the one backing the map and alias
functions.
Multiple simultaneous writes
One limitation that you might have perceived up to this point is that you can’t
write to two RwData-like structures at the same time, since they would both
need to borrow from the &mut Pass, breaking Rust’s aliasing XOR mutability
rule.
There is, however, one way to do this, which is through the Pass:write_many
method. This method takes in a group of RwData-like structures and gets
mutable references to all of them at the same time.
use duat::prelude::*;
fn test(pa: &mut Pass, lhs: [&RwData<u32>; 2], rhs: &RwData<u32>) {
// You can pass in any tuple (up to 12 elements) or any array
// of `RwData`-like structures and this function will give you a
// mutable reference to all of them.
let ([l0, l1], r): ([&mut u32; 2], &mut u32) = pa.write_many((lhs, rhs));
let trouble = [lhs[0], lhs[0]];
// Note here that I'm writing to the same variable twice.
// Instead of returning two mutable references to the same value, this
// call will just panic, returning control to Duat and cancelling
// whatever you were doing.
let [oops0, oops1] = pa.write_many(trouble);
// If you think this might happen, you can call this instead.
let handled = pa.try_write_many(trouble);
// This function will return an `Err(text)` in the case of failure.
assert!(handled.is_err());
}
The hook module
Hooks in duat are functions that are called automatically whenever some specific event happens. They are very similar to kakoune’s hooks or neovim’s autocmds. However, one thing that distinguishes the versatility of duat’s hooks is the fact that they present you with arguments whose type is inferred at compile time:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// Note the Pass, so you have mutable global state access.
// The type of the second argument is inferred, it is only
// included here for example's sake.
hook::add::<ModeSwitched>(|pa, (old, new): (&str, &str)| {
if old == "Insert" && new == "Normal" {
_ = context::current_buffer(pa).save(pa);
}
});
hook::add::<BufferOpened>(|pa, handle: &Handle| {
let buf = handle.write(pa);
match buf.filetype() {
Some("rust" | "cpp") => {
buf.opts.wrap_lines = true;
buf.opts.wrapping_cap = Some(100);
}
Some("markdown" | "asciidoc") => {
buf.opts.tabstop = 2;
buf.opts.extra_word_chars = &['-'];
}
Some("lua" | "javascript") => buf.opts.tabstop = 2,
_ => {}
};
});
// You can call hooks for many things...
hook::add::<WidgetOpened<LineNumbers>>(|pa, handle| {
// This will put a vertical ruler on the left of the `LineNumbers`
// making for a "stylish" column of numbers.
VertRule::builder().push_on(pa, handle);
});
let key_count = RwData::new(0);
hook::add::<KeyTyped>({
let key_count = key_count.clone();
move |pa, _| {
*key_count.write(pa) += 1;
}
});
opts::fmt_status(move |_| {
let mode_txt = mode_txt();
let key_count = key_count.clone();
status!("{name_txt} {mode_txt}{Spacer}{sels_txt} {main_txt} {key_count}")
})
}
Creating new hooks
Another interesting thing about hooks in duat is that you can create your own.
You do that by implementing the Hookable trait on a type:
use duat::prelude::*;
struct OnIdle(Handle);
impl Hookable for OnIdle {
// The Input type is the value available when calling `hook::add`
type Input<'h> = &'h Handle;
fn get_input<'h>(&'h mut self, _: &mut Pass) -> Self::Input<'h> {
&self.0
}
}
Then, you decide when to trigger said hook:
struct OnIdle(Handle);
impl Hookable for OnIdle {
type Input<'h> = &'h Handle;
fn get_input<'h>(&'h mut self, _: &mut Pass) -> Self::Input<'h> {
&self.0
}
}
use duat::prelude::*;
use std::{
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn setup_hook() {
// Start counting as soon as the user stops typing.
hook::add::<KeyTyped>(|pa, _| COUNTER.store(0, Ordering::Relaxed));
std::thread::spawn(|| {
while !context::will_reload_or_quit() {
std::thread::sleep(Duration::from_secs(1));
let elapsed = COUNTER.fetch_add(1, Ordering::Relaxed);
// Every 60 seconds, trigger an `OnIdle` event
if elapsed + 1 == 60 {
// We have to queue it, since this is being
// called from another thread.
context::queue(|pa| {
let handle = context::current_buffer(pa);
hook::trigger(pa, OnIdle(handle));
});
}
}
});
}
Then, the user can just add their own hooks, which will be called accordingly:
struct OnIdle(Handle);
impl Hookable for OnIdle {
type Input<'h> = &'h Handle;
fn get_input<'h>(&'h mut self, _: &mut Pass) -> Self::Input<'h> {
&self.0
}
}
fn setup_hook() {}
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// This would be called from a `Plugin::plug` function
setup_hook();
hook::add::<OnIdle>(|pa, _| {
let mut saved = 0;
for handle in context::buffers(pa) {
if let Ok(true) = handle.save(pa) {
saved += 1;
}
}
if saved > 0 {
context::info!("Saved [a]{saved}[] buffers from idling");
}
});
}
List of hooks
Here’s the list of currently available hooks, more will be added in the future.
For the list of arguments of each hook, remember that there will always be
&mut Pass argument, and that the other arguments will come in a tuple.
For example, if a hook has two arguments, i32 and bool, they will actually
come in as (i32, bool) in the second argument of the hook. So you should pass
a function like this:
struct FooHook;
impl Hookable for FooHook {
type Input<'h> = (i32, bool);
fn get_input<'h>(&'h mut self, _: &mut Pass) -> Self::Input<'h> {
(0, true)
}
}
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
hook::add::<FooHook>(|pa, (arg1, arg2): (i32, bool)| {
// ...
});
}
Also, when a hook says that it has no arguments, what this actually means is
that the second argument is of type (), so the function argument still needs
to have two arguments in it.
BufferOpened
Triggers after opening a Buffer. You will want to use this hook to set
buffer-wise configuration options:
setup_duat!(setup);
use duat::prelude::*;
fn setup() {
// Options set initially for all `Buffer`s
opts::set(|opts| {
opts.tabstop = 4;
});
// Changing those options on a buffer by buffer basis.
hook::add::<BufferOpened>(|pa, handle| {
let buf = handle.write(pa);
match buf.filetype() {
Some("haskell" | "commonlisp") => buf.opts.tabstop = 2,
Some("txt" | "markdown" | "asciidoc") => {
buf.opts.tabstop = 2;
buf.opts.wrap_lines = true;
buf.opts.wrapping_cap = Some(80);
}
_ => {}
}
});
}
Arguments
- The
Handle<Buffer>of theBufferthat was opened.
BufferSaved
Triggers right after saving a Buffer. This will happen whenever you call any
of the write family of commands, or if Handle::<Buffer>::save is called.
Arguments
- The
Handle<Buffer>that was saved. - A
bool, which istrueif theBufferis being closed, through commands likewq.
BufferClosed
Triggers as a Buffer is being closed, after BufferSaved. This will happen
when you run a command like q or wq, or after calling
Handle::<Buffer>::close. It will also happen after quitting Duat, after
ConfigUnloaded but before ExitedDuat.
Arguments
- The
Handle<Buffer>that was closed.
BufferReloaded
Triggers on every Buffer after the config gets reloaded. This won’t happen
on the first config that was loaded.
Arguments
- The
Handle<Buffer>that was reloaded.
ConfigLoaded
Will trigger right after initially loading the config crate on
~/.config/duat/ or wherever you’re loading the config from.
Arguments
- There are no arguments, just the normal
&mut Pass
ConfigUnloaded
Will trigger right before unloading the config crate on
~/.config/duat/ or wherever you’re loading the config from.
This will also trigger upon exiting Duat.
Arguments
- There are no arguments, just the normal
&mut Pass
ExitedDuat
Triggers after quitting Duat, after ConfigUnloaded and after triggering
BufferClosed on each Buffer.
Arguments
- There are no arguments, just the normal
&mut Pass
FocusedOnDuat
Triggers when Duat gains focus from the operating system.
Arguments
- There are no arguments, just the normal
&mut Pass
UnfocusedFromDuat
Triggers when Duat loses focus from the operating system.
Arguments
- There are no arguments, just the normal
&mut Pass