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.
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 features:
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 install duat
Or, if you want the master
version of duat, from the latest commit, you can
do this:
cargo install --git https://github.com/AhoyISki/duat --features git-deps
When calling either of these commands, if it didn't already exist, a default
version of Duat's configuration will be installed in $XDG_CONFIG_HOME/duat
(~/.config/duat
for Linux). If you want to get the latest version of the
default configuration, you can just remove that, alongside other directories
used by Duat:
rm -rf ~/.config/duat/ ~/.cache/duat/ ~/.local/share/duat/
cargo install --git https://github.com/AhoyISki/duat --features gid-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
. The first time you run duat
, the compilation of the config
crate
will take a while, but after running the reload
command from duat, it should
recompile in ~0.5 to ~2.5 seconds, depending on what you changed in the config.
To see how you can configure duat, look at the next chapter in this book.
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_kak { use duat::prelude::*; #[derive(Default)] pub struct Kak; impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) { todo!() } } } mod duat_catppuccin { use duat::prelude::*; #[derive(Default)] pub struct Catppuccin; impl duat_core::Plugin<Ui> for Catppuccin { fn plug(self, _: &duat_core::Plugins<Ui>) { todo!() } } } setup_duat!(setup); use duat::prelude::*; fn setup() { add_plugins(); } fn add_plugins() { plug(duat_kak::Kak::default()); plug(duat_catppuccin::Catppuccin::default()); } }
Using Plugin
s
You can add Plugin
s to duat by calling the plug
function. By default, the
Treesitter
and MatchPairs
plugins are added, providing syntax highlighting,
automatic indentation, and matching parenthesis highlighting.
#![allow(unused)] fn main() { mod duat_kak { use duat::prelude::*; #[derive(Default)] pub struct Kak; impl Kak { pub fn new() -> Self { Self } pub fn f_and_t_set_search(self) -> Self { Self } pub fn with_no_indent_on_capital_i(self) -> Self { Self } } impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) { todo!() } } } setup_duat!(setup); use duat::prelude::*; fn setup() { plug( duat_kak::Kak::new() .f_and_t_set_search() .with_no_indent_on_capital_i(), ); } }
In this case, the Kak
plugin (from duat-kak
) is being modified via the
builder pattern in order to have the following settings:
- The
f
andt
keys will set the search pattern, son
will match them. - When pressing
I
, don't reindent.
Then, it is being added to Duat. The f_and_t_set_search
and
with_no_indent_on_capital_i
modify the Kak
plugin and then return it back
to be plugged. This is the builder pattern, and is how all plugins are added.
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::print; }
This is importing the print
module, as opposed to importing its items
directly, like this:
#![allow(unused)] fn main() { use duat::prelude::*; use duat::print::*; }
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() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Insert; impl Mode<Ui> for Insert { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } #[derive(Default)] pub struct Kak; impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) {} } } setup_duat!(setup); use duat::prelude::*; fn setup() { plug(duat_kak::Kak::default()); print::wrap_at(150); print::trailing_new_line(''); form::set("caret.main", Form::yellow()); cmd::add!("set-rel-lines", |pa, ln: cmd::Handles<LineNumbers<Ui>>| { ln.on_flags(pa, |pa, handle| { handle.write(pa).rel_abs(); }); Ok(Some(txt!("Lines were set to [a]relative absolute").build())) }); map::<duat_kak::Insert>("jk", "<Esc>:w<Enter>"); } }
The exceptions to this are the map
, alias
and plug
functions, 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.
print
: How duat prints files
The print module has a bunch of simple functions to change how duat should
print File
widgets:
#![allow(unused)] fn main() { setup_duat!(setup); use duat::prelude::*; fn setup() { print::dont_wrap(); print::indent_wraps(true); print::tabstop(4); print::scrolloff(3, 3); print::word_chars!("A-Za-z0-9_-_"); print::new_line(' '); } }
These are the default options (for File
s). One thing to note about this
module, is that it is for File
s and File
s only. That is, nothing in
here will affect other widgets, like LineNumbers
or StatusLine
. If you want
to modify those, you can head over to the hook
chapter.
The functions in this module will affect all files unconditionally, if you want
to do that on a File
by File
basis, again, see the hook
chapter.
This is the current list of options in this module:
print::dont_wrap
: The default, don't wrap text at all;print::wrap_on_edge
: Wraps the text on the edge of its area;print::wrap_at
: Wraps at a distance from the left edge, can go over the right edge;print::wrap_on_word
: Wraps on word termination, instead of any character;print::indent_wraps
: Copies the indentation on wrapped lines, can be used with the other options;print::tabstop
: The width of tabs, is also used to determine how many spaces a\t
should add. Is 4 by default;print::new_line
: What character to print when printing'\n'
s. Is' '
by default;print::trailing_new_line
: What character to print when printing'\n'
s that are preceeded by spaces. Is not set by default;
The print::word_chars!
macro
The print::word_chars!
macro determines which characters should be part of a
word. This is used by wrap_on_words
and tends to also be used to modify text
(in the <Ctrl-Backspace>
key in most editors, for example).
This macro is evaluated at compile time, so you don't have to worry about it being correct or not, and the syntax is similar to regex, but only with ranges, like this:
#![allow(unused)] fn main() { setup_duat!(setup); use duat::prelude::*; fn setup() { print::word_chars!("A-Za-z0-9---_-_"); } }
In this case, every sequence of lowercase and capital letters, numbers, dashes and underscores would be considered part of a word.
form
: How text is colored
In duat, the way text is styled is through Form
s. 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 defaut
form. The
default
form, unlike other forms, can have Widget
specific implementations,
like default.StatusLine
, which will change the default
form only on
StatusLine
s, 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 duat_core::Plugin<duat::Ui> for Catppuccin { fn plug(self, _: &duat_core::Plugins<Ui>) { 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 referenceform
; - If
form.subform
is set toForm::green()
, it won't be changed whenform
changes, staying atForm::green()
; - If
form.subform
is set to referenceother_form
, changingother_form
will also changeform.subform
, but changingform
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::set
s, 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() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Insert; impl Mode<Ui> for Insert { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } #[derive(Default)] pub struct Kak; impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) {} } } setup_duat!(setup); use duat::prelude::*; fn setup() { // Adds kakoune-like editing modes, like Insert, Normal and OneKey plug(duat_kak::Kak::default()); map::<User>("f", "<Esc>|fold -s<Enter>"); alias::<duat_kak::Insert>("jk", "<Esc>"); alias::<Prompt<Ui>>("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.
In the functions, you can see a type argument. This type argument is the Mode
where this mapping will take place. So here, in duat-kak
's Insert
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 duat-kak
, by default, this mode is entered by pressing the space bar.
While you can change that like this:
#![allow(unused)] fn main() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Normal; impl Mode<Ui> for Normal { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } #[derive(Default)] pub struct Kak; impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) {} } } setup_duat!(setup); use duat::prelude::*; fn setup() { plug(duat_kak::Kak::default()); map::<duat_kak::Normal>(" ", ""); // In rust, you have to escap a backslash map::<duat_kak::Normal>("\\", " "); } }
You should prefer doing this:
#![allow(unused)] fn main() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Normal; impl Mode<Ui> for Normal { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } #[derive(Default)] pub struct Kak; impl duat_core::Plugin<Ui> for Kak { fn plug(self, _: &duat_core::Plugins<Ui>) {} } } setup_duat!(setup); use duat::prelude::*; fn setup() { plug(duat_kak::Kak::default()); map::<duat_kak::Normal>(" ", ""); map::<duat_kak::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 Mode
s. 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 alias
es.
[!NOTE]
In this case, since
User
is a struct with no fields, I could just putUser
as the second argument, which acts as a constructor. But in most otherMode
s, 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 Form
s.
cursor::set_main
will set the "shape" of the main cursor. This takes aCursorShape
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 someUi
s, mainly terminals, which only allow for one cursor at a time.cursor::unset_main
andcursor::unset_extra
: Disables cursor shapes for every type of cursor, replacing them with aForm
, which will becaret.main
andcaret.extra
, respectivelycursor::unset
: The same as callingunset_main
andunset_extra
.
List of forms
Currently, in duat
, these are the forms in use:
default
: Is applied to every text. Can beWidget
dependent, likedefault.LineNumbers
.accent
: This form is used when formatting error, warning, information, or debug messages. Is used mostly with theerror
,warn
andinfo
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 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-hop
andduat-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
-
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.wrapped
by default.
Do note that you can set the form of the remaining lines by setting
default.LineNumbers
. And due to form inheritance, settinglinenum
will set all three forms. -
StatusLine
:file
,file.new
,file.unsaved
,file.new.scratch
: Are all used byname_txt
, which shows theFile
's name and some other info.mode
: Is used bymode_txt
.coord
andseparator
: Are used bymain_txt
.selections
: Is used byselections_txt
.key
andkey.special
: Are used bycur_map_txt
.
-
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 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.info
andcaller.error
: For the caller, if it exists or not, respectively.parameter.info
andparameter.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
andrule.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:
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 |
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 |
Frequently used snippets
If you just want some no nonsense snippets to copy paste into your config, this is the chapter for you.
For every snippet, assume that the snippet is placed inside of the setup
function, given the following setup:
#![allow(unused)] fn main() { setup_duat!(setup); use duat::prelude::*; fn setup() { // Here be snippets. } }
mapping jk to esc
This one is pretty simple. Assuming you are using some sort of Insert
Mode
:
#![allow(unused)] fn main() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Insert; impl Mode<Ui> for Insert { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } } use duat::prelude::*; setup_duat!(setup); fn setup() { // Or vim::Insert, or helix::Insert, when those come out. map::<duat_kak::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() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Insert; impl Mode<Ui> for Insert { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } } use duat::prelude::*; setup_duat!(setup); fn setup() { alias::<duat_kak::Insert>("jk", "<Esc>"); } }
Additionally, if you want to write to the file on jk
as well, you can do this:
#![allow(unused)] fn main() { mod duat_kak { use duat::{prelude::{*, mode::KeyEvent}}; #[derive(Clone)] pub struct Insert; impl Mode<Ui> for Insert { type Widget = File; fn send_key(&mut self, _: &mut Pass, _: KeyEvent, _: Handle<File>) { todo!(); } } } use duat::prelude::*; setup_duat!(setup); fn setup() { alias::<duat_kak::Insert>("jk", "<Esc>:w<Enter>"); } }
If you want to, you can also make this happen on the PromptLine
, i.e., while writing commands and searches:
#![allow(unused)] fn main() { use duat::prelude::*; setup_duat!(setup); fn setup() { alias::<Prompt<Ui>>("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::*; hook::remove("FooterWidgets"); hook::add::<WindowCreated>(|pa, builder| { builder.push(FooterWidgets::default().one_line()); }); }
If you want one of these on each file, you can do this instead:
#![allow(unused)] fn main() { use duat::prelude::*; hook::remove("FooterWidgets"); hook::add::<File>(|pa, (cfg, builder)| { builder.push(FooterWidgets::default().one_line()); cfg }); }
With both of these, you can still modify the StatusLine
with the usual
hook
s:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|pa, (cfg, _)| { let mode_upper = mode_txt(pa).map(pa, |mode| txt!("[mode]{}", mode.to_string().to_uppercase()).build() ); cfg.fmt(status!("{Spacer}{name_txt} {mode_upper} {sels_txt} {main_txt}")) }); }
Common StatusLine parts
The most relevant parts for pretty much every StatusLine
are the following.
Formatted status parts:
name_txt
: Prints theFile
's name and some info about it's newness.- Uses the forms
file
,file.new
,file.new.scratch
andfile.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
coord
andseparator
.
- Uses the forms
sels_txt
: Prints the number of selections.- Uses the form
selections
;
- Uses the form
cur_map_txt
: Prints the keys being mapped.- Uses the forms
key
andkey.special
- Uses the forms
Unformatted status parts:
main_byte
,main_char
,main_line
,main_col
: Parts of the main cursor.mode_name
: The raw type name of the mode. Could look something likePrompt<IncSearcher<SearchFwd>, Ui>, Ui>
.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 aTag
that can go in anyText
, which includes theStatusLine
's.AlignLeft
,AlignCenter
,AlignRight
: TheseTag
s (like all others) can also be used in theStatusLine
. However, do note that they are applied line wise. Using any of them will shift the whole line's alignment. For that reason, aSpacer
should generally be preferred.- Forms like
[file]
. Any form can be placed within those braces, and they are all evaluated at compile time.
Some examples
This is the default:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|pa, (cfg, _)| { cfg.fmt(status!("{name_txt}{Spacer}{} {sels_txt} {main_txt}", mode_txt(pa))) }); }
If you want a one sided StatusLine
, you can do this:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|pa, (cfg, _)| { cfg.fmt(status!("{Spacer}{name_txt} {} {sels_txt} {main_txt}", mode_txt(pa))) }); }
Customized main_txt
:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|pa, (cfg, _)| { cfg.fmt(status!( "{name_txt}{Spacer}{} {sels_txt} [coord]c{main_col} l{main_line}[separator]|[coord]{}", mode_txt(pa), |file: &File| file.text().len().line() )) }); }
Customized name_txt
:
#![allow(unused)] fn main() { use duat::prelude::*; fn name_txt(file: &File) -> Text { let mut b = Text::builder(); if let Some(name) = file.name_set() { b.push(txt!("[file]{name}")); if !file.exists() { b.push(txt!("[file.new][[new file]]")); } else if file.text().has_unsaved_changes() { b.push(txt!("[file.unsaved][[has changes]]")); } if let Some("rust") = file.filetype() { b.push(txt!("[[🦀]]")); } } else { b.push(txt!("[file.new.scratch]?!?!?!")); } b.build() } hook::add::<StatusLine<Ui>>(|pa, (cfg, _)| { cfg.fmt(status!("{name_txt}{Spacer}{} {sels_txt} {main_txt}", mode_txt(pa))) }); }
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::*; hook::add::<File>(|_, (cfg, _)| { match cfg.filetype() { Some("markdown" | "bash" | "lua" | "javascript" | "lisp") => cfg.tabstop(2), _ => cfg.tabstop(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:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<File>(|_, (cfg, _)| { match cfg.filetype() { Some("lisp" | "scheme" | "markdown" | "css" | "html") => { let wc = print::w_chars!("A-Za-z0-9_-_---"); cfg.tabstop(2).word_chars(wc) } Some("bash" | "lua" | "javascript" | "typescript") => cfg.tabstop(2), _ => cfg.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:
#![allow(unused)] fn main() { use duat::prelude::*; fn name_txt(file: &File) -> Text { let mut b = Text::builder(); if let Some(name) = file.name_set() { b.push(txt!("[file]{name}")); if !file.exists() { b.push(txt!(" [file.new] ")); } else if file.text().has_unsaved_changes() { b.push(txt!(" [file.unsaved] ")); } } else { b.push(txt!(" [file.new.scratch]{} ", file.name())); } b.build() } }
Status on files and on window
In these snippets, you might have noticed that I'm mapping all StatusLine
s,
not just some of them. So how would one map only the StatusLine
s belonging to
files then? Or depending on other factors?
You could just replace all hooks for File
s and WindowCreated
, but that is
rather tedious and verbose. Instead, you can just do the following:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|_, (cfg, builder)| { if builder.file().is_some() { cfg.fmt(status!("{Spacer}{name_txt}")) } else { cfg } }); }
The snippet above will only remap the StatusLine
when it is pushe onto a
File
.
You can go even further, by, e.g. checking on the filetype:
#![allow(unused)] fn main() { use duat::prelude::*; hook::add::<StatusLine<Ui>>(|pa, (cfg, builder)| { if let Some(file) = builder.file() { if let Some("rust") = file.filetype(pa) && file.read(pa).path().contains(".config/duat") { cfg.fmt(status!("[config] {Spacer}{name_txt}")) } else { cfg.fmt(status!("{Spacer}{name_txt}")) } } else { cfg } }); }