Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 shows matches to the word under the cursor:

#![allow(unused)]
fn main() {
use duat::prelude::*;

pub struct HighlightMatch;

impl Plugin for HighlightMatch {
    fn plug(self, plugins: &Plugins) {
        hook::add::<Buffer>(|pa, handle| {
            form::set_weak("same_word", Form::underlined());
            handle.write(pa).add_parser(|mut tracker| {
                tracker.track_area();
                HighlightMatchParser {
                    tagger: Tagger::new(),
                }
            })
        });
    }
}

struct HighlightMatchParser {
    tagger: Tagger,
}

impl Parser for HighlightMatchParser {
    fn update(&mut self, pa: &mut Pass, handle: &Handle, on: Vec<Range<Point>>) {
        handle.text_mut(pa).remove_tags(self.tagger, ..);
        let Some(range) = handle.edit_main(pa, |c| c.search_fwd(r"\A\w+").next()) else {
            return;
        };
        let start = handle
            .edit_main(pa, |c| c.search_rev(r"\w*\z").next())
            .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");

        let mut first_range: Option<Range<usize>> = None;
        for range in on {
            for (i, range) in parts
                .bytes
                .search_fwd(r"\w+", range.clone())
                .unwrap()
                .filter(|r| parts.bytes.strs(r.clone()) == pat)
                .enumerate()
            {
                if let Some(first_range) = first_range.clone() {
                    if i == 1 {
                        parts
                            .tags
                            .insert(self.tagger, first_range, form_id.to_tag(50));
                    }
                    parts.tags.insert(self.tagger, range, form_id.to_tag(50));
                } else {
                    first_range = Some(range);
                }
            }
        }
    }
}
}

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:

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:

  • --cfg will open the ~/.config/duat/src/lib.rs.
  • --cfg-manifest will open ~/.config/duat/Cargo.toml. You can also pass --cfg to open both.
  • --no-load will not load any config, running the default one.
  • --load will pick a path to look for a config on. This lets you have.
  • --profile will pick a profile to load.
  • --open will open N windows to place the files passed as arguments. multiple configurations, if you wish.
  • --reload will recompile the config crate.
  • --clean calls cargo clean on the config crate.
  • --update will update dependencies to their latest compatible version.
  • --init-config will clear the replace the config crate with the default version. If --load was passed, clears that path instead.
  • --init-plugin will initialize a crate for plugin development at the given path.
  • --repository will pick a repository for the plugin.
  • --author will pick an author for the plugin.
  • --help displays these flags.
  • --version displays 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:

  • word characters follow Duat's [word chars], which are normally used to define where lines wrap.
  • WORD characters 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, w writes the current Buffer. If there's an path argument, saves to that path.
  • quit, q quits the current Buffer, fails if it isn't written. If there's an argument, quits that Buffer instead.
  • quit!, q! quits the current Buffer, even if it isn't written. If there's an argument, quits that Buffer instead.
  • write-quit, wq writes and quits the current Buffer. If there's an argument, quits that Buffer instead.
  • write-all, wa writes all Buffers.
  • quit-all, qa tries to quit all Buffers, failing if some aren't written.
  • quit-all!, qa! quits all Buffers, even if they aren't written.
  • write-all-quit, waq writes to all Buffers and quits.
  • write-all-quit!, waq! writes to all Buffers and quits, even if writing fails for some reason.

Switching Buffers

  • edit, e opens a new Buffer on the current window.
  • open, o opens a new Buffer on another window.
  • buffer, b switches to another Buffer.
  • next-buffer switches to the next Buffer opened.
  • prev-buffer switches to the previous Buffer opened.
  • last-buffer switches to the previously focused Buffer.
  • swap Swaps the positions of the current Buffer and another. If there are two arguments, swaps those two buffers instead.

Other

  • set-form takes in a name and 0 to 3 colors (##rrggbb, rgb r g b or hsl h s l) and sets that name's fg, bg and ul colors. More settings coming in the future.
  • colorscheme sets the colorscheme.
  • alias Aliases 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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
use duat::prelude::*;
use duat::opts;
}

This is importing the opts module, as opposed to importing its items directly, like this:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
setup_duat!(setup);
use duat::prelude::*;

fn setup() {
    // Default options for the Buffer widget
    opts::set(|opts| {
        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;
    });

    // Default options for the LineNumbers widget
    opts::set_lines(|opts| {
        // Relative as opposed to absolute numbering.
        opts.relative = false;
        // On which side to align numbers other than the main line's.
        opts.align = std::fmt::Alignment::Left;
        // On which side to align the main line's number.
        opts.main_align = std::fmt::Alignment::Right;
        // Place the LineNumbers on the right of the Buffer, instead of on the left.
        opts.on_the_right = false;
    });

    // Sets a Kakoune style "one line" footer
    opts::one_line_footer(false);

    // Place the footer on top, instead of on the bottom of the screen.
    opts::footer_on_top(false);

    // Default options for the StatusLine widget
    opts::set_status(|pa| {
        // If on one line footer mode:
        let mode = mode_txt();
        let param = duat_param_txt();
        status!("{AlignRight}{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}")
    });

    // Default options for the LogBook widget
    opts::set_logs(|opts| {});

    // Default options for the Notifications widget
    opts::set_notifs(|opts| {});
}
}

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:

#![allow(unused)]
fn main() {
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 Form argument will be used to color the form directly.
  • A &str argument 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:

#![allow(unused)]
fn main() {
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.subform is unset, it will reference form;
  • If form.subform is set to Form::green(), it won't be changed when form changes, staying at Form::green();
  • If form.subform is set to reference other_form, changing other_form will also change form.subform, but changing form won'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?

#![allow(unused)]
fn main() {
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 files 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

map and alias: modifying keys

In Duat, mapping works somewhat like Vim/neovim, but not quite. This is how it works:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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 User is a struct with no fields, I could just put User as the second argument, which acts as a constructor. But in most other Modes, you're gonna have to write something like Insert::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_main will set the "shape" of the main cursor. This takes a CursorShape argument, and lets you set its shape to a vertical bar, a horizontal bar, and make it blink.
  • cursor::set_extra is the same but for extra cursors. Do note that this may not work on some Uis, mainly terminals, which only allow for one cursor at a time.
  • cursor::unset_main and cursor::unset_extra: Disables cursor shapes for every type of cursor, replacing them with a Form, which will be caret.main and caret.extra, respectively
  • cursor::unset: The same as calling unset_main and unset_extra.

List of forms

Currently, in duat, these are the forms in use:

  • default: Is applied to every text. Can be Widget dependent, like default.LineNumbers.
  • accent: This form is used when formatting error, warning, information, or debug messages. Is used mostly with the error, warn and info masks.
  • caret.main: The form to use when printing the main "caret" (each cursor has a selection, an anchor, and a caret).
  • caret.extra: Same as caret.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, like duat-hop and duat-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 from linenum.wrapped by default.

    Do note that you can set the form of the remaining lines by setting default.LineNumbers. And due to form inheritance, setting linenum will set linenum.wrapped, linenum.main and linenum.wrapped.main.

  • StatusLine:

    • file, file.new, file.unsaved, file.new.scratch: Are all used by name_txt, which shows the File's name and some other info.
    • mode: Is used by mode_txt.
    • coord and separator: Are used by main_txt.
    • selections: Is used by selections_txt.
    • key and key.special: Are used by cur_map_txt.
  • Completions:

    • selected.Completions changes 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 Notifications widget makes heavy use of masks, you can also set notifs.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.info and caller.error: For the caller, if it exists or not, respectively.
    • parameter.info and parameter.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.upper and rule.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 namePurpose
variablevarious variable names
variable.builtinbuilt-in variable names (e.g. this, self)
variable.parameterparameters of a function
variable.parameter.builtinspecial parameters (e.g. _, it)
variable.memberobject and struct fields
constantconstant identifiers
constant.builtinbuilt-in constant values
constant.macroconstants defined by the preprocessor
modulemodules or namespaces
module.builtinbuilt-in modules or namespaces
labelGOTO and other labels (e.g. label: in C), including heredoc labels
stringstring literals
string.documentationstring documenting code (e.g. Python docstrings)
string.regexpregular expressions
string.escapeescape sequences
string.specialother special strings (e.g. dates)
string.special.symbolsymbols or atoms
string.special.pathfilenames
string.special.urlURIs (e.g. hyperlinks)
charactercharacter literals
character.specialspecial characters (e.g. wildcards)
booleanboolean literals
numbernumeric literals
number.floatfloating-point number literals
typetype or class definitions and annotations
type.builtinbuilt-in types
type.definitionidentifiers in type definitions (e.g. typedef in C)
attributeattribute annotations (e.g. Python decorators, Rust lifetimes)
attribute.builtinbuiltin annotations (e.g. @property in Python)
propertythe key in key/value pairs
functionfunction definitions
function.builtinbuilt-in functions
function.callfunction calls
function.macropreprocessor macros
function.methodmethod definitions
function.method.callmethod calls
constructorconstructor calls and definitions
operatorsymbolic operators (e.g. +, *)
keywordkeywords not fitting into specific categories
keyword.coroutinekeywords related to coroutines (e.g. go in Go, async/await in Python)
keyword.functionkeywords that define a function (e.g. func in Go, def in Python)
keyword.operatoroperators that are English words (e.g. and, or)
keyword.importkeywords for including or exporting modules (e.g. import, from in Python)
keyword.typekeywords describing namespaces and composite types (e.g. struct, enum)
keyword.modifierkeywords modifying other constructs (e.g. const, static, public)
keyword.repeatkeywords related to loops (e.g. for, while)
keyword.returnkeywords like return and yield
keyword.debugkeywords related to debugging
keyword.exceptionkeywords related to exceptions (e.g. throw, catch)
keyword.conditionalkeywords related to conditionals (e.g. if, else)
keyword.conditional.ternary ternaryoperator (e.g. ?, :)
keyword.directivevarious preprocessor directives and shebangs
keyword.directive.definepreprocessor definition directives
punctuation.delimiterdelimiters (e.g. ;, ., ,)
punctuation.bracketbrackets (e.g. (), {}, [])
punctuation.specialspecial symbols (e.g. {} in string interpolation)
commentline and block comments
comment.documentationcomments documenting code
comment.errorerror-type comments (e.g. ERROR, FIXME, DEPRECATED)
comment.warningwarning-type comments (e.g. WARNING, FIX, HACK)
comment.todotodo-type comments (e.g. TODO, WIP)
comment.notenote-type comments (e.g. NOTE, INFO, XXX)
markup.strongbold text
markup.italicitalic text
markup.strikethroughstruck-through text
markup.underlineunderlined text (only for literal underline markup!)
markup.headingheadings, titles (including markers)
markup.heading.1top-level heading
markup.heading.2section heading
markup.heading.3subsection heading
markup.heading.4and so on
markup.heading.5and so forth
markup.heading.6six levels ought to be enough for anybody
markup.quoteblock quotes
markup.mathmath environments (e.g. $ ... $ in LaTeX)
markup.linktext references, footnotes, citations, etc.
markup.link.labellink, reference descriptions
markup.link.urlURL-style links
markup.rawliteral or verbatim text (e.g. inline code)
markup.raw.blockliteral or verbatim text as a stand-alone block
markup.listlist markers
markup.list.checkedchecked todo-style list markers
markup.list.uncheckedunchecked todo-style list markers
diff.plusadded text (for diff files)
diff.minusdeleted text (for diff files)
diff.deltachanged text (for diff files)
tagXML-style tag names (e.g. in XML, HTML, etc.)
tag.builtinbuiltin tag names (e.g. HTML5 tags)
tag.attributeXML-style tag attributes
tag.delimiterXML-style tag delimiters

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 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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    map::<Prompt>("jk", "<Esc>");
}
}

StatusLine on each File

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:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    opts::one_line_footer(true);
}
}

This will call [FooterWidgets::one_line] on the window's FooterWidgets.

If you want one of these on each file, you can do this instead:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    hook::remove("FooterWidgets");

    hook::add::<Buffer>(|pa, handle| {
        FooterWidgets::default().one_line().push_on(pa, handle);
        Ok(())
    });
}
}

Common StatusLine parts

The most relevant parts for pretty much every StatusLine are the following.

Formatted status parts:

  • name_txt: Prints the Buffer's name and some info about it's newness.
  • Uses the forms buffer, buffer.new, buffer.new.scratch and buffer.unsaved.
  • mode_txt: The lowercased name of the Mode, e.g. "insert", "normal".
    • Uses the form mode.
  • main_txt: Prints the main selection's column and line, and the number of lines. 1 indexed.
    • Uses the forms coord and separator.
  • sels_txt: Prints the number of selections.
    • Uses the form selections;
  • cur_map_txt: Prints the keys being mapped.
    • Uses the forms key and key.special

Unformatted status parts:

  • main_byte, main_char, main_line, main_col: Parts of the main cursor.
  • mode_name: The non Text version of mode_txt, just a string.
  • raw_mode: The raw type name of the mode. Could look something like Prompt<IncSearcher<SearchFwd>>>.
  • 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 a StatusPart, it's a Tag that can go in any Text, which includes the StatusLine's.
  • AlignLeft, AlignCenter, AlignRight: These Tags (like all others) can also be used in the StatusLine. However, do note that they are applied line wise. Using any of them will shift the whole line's alignment. For that reason, a Spacer should generally be preferred.
  • 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

Some examples

This is the default:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    // Default options for the StatusLine widget
    opts::set_status(|pa| {
        // If on one line footer mode:
        let mode = mode_txt();
        let param = duat_param_txt();
        status!("{AlignRight}{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:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    opts::set_status(|_| {
        status!(
            "{name_txt}{Spacer}{} {sels_txt} [coord]c{} l{}[separator]|[coord]{}",
            mode_txt(),
            main_col,
            main_line,
            Buffer::len_lines 
        )
    });
}
}

Customized name_txt:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    opts::set_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()
}
}

Relative and aligned LineNumbers

File wise tabstops

If you want to change the tabstop size per file, you can just modify the following snippet:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    hook::add::<Buffer>(|pa, handle| {
        let buffer = handle.write(pa);
        buffer.opts.tabstop = match buffer.filetype() {
            Some("markdown" | "bash" | "lua" | "javascript" | "lisp") => 2, 
            _ => 4
        };
        Ok(())
    });
}
}

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:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    hook::add::<Buffer>(|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
        }
        Ok(())
    });
}
}

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:

#![allow(unused)]
fn main() {
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 files and on window

If you want to have a StatusLine per Buffer, you can add the following:

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    hook::add::<Buffer>(|pa, handle| {
        status!("{name_txt}{Spacer}{main_txt}").above().push_on(pa, handle);
        Ok(())
    });
}
}

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?

#![allow(unused)]
fn main() {
use duat::prelude::*;
setup_duat!(setup);

fn setup() {
    hook::add::<Buffer>(|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);
        Ok(())
    });
}
}

Scripting Duat

The Pass and Duat's global state

The hook module

The Text struct

Builder and txt!

Tags: manipulating Text

Modding the StatusLine

The context module

cmd: Runtime commands

Modifying the layout

Extending Duat

duat-core

Plugins

Widgets

Modes

Parsers