Expand description
The core of Duat, this crate is meant to be used only for the creation of plugins for Duat.
The capabilities of duat-core
are largely the same as the those
of Duat, however, the main difference is the multi Ui
APIs of
this crate. In it, the public functions and types are defined in
terms of U: Ui
, which means that they can work on various
different interfaces:
§Quick Start
This crate is composed of a few main modules, which will be used in order to extend Duat:
-
ui
: Has everything to do with the interface of Duat, that includes things like:Widget
s: As the name implies, this is the trait for objects that will show up on the screen. The most noteworthyWidget
isFile
, which displays the contents of a file.WidgetCfg
s: These areWidget
builders. They are used in thesetup
function of Duat’s config, through theOnFileOpen
andOnWindowOpen
hooks.Ui
andRawArea
s: These are used if you want to create your own interface for Duat. Very much a work in progress, so I wouldn’t recommend trying that yet.
-
text
: Defines the struct used to show characters on screenText
: Is everything that Duat shows on screen (exceptUi
specific decorations). This includes a UTF-8 string and tags to modify it.Tag
s: This is how Duat determines howText
will be displayed on screen. There are tags for styling, text alignment, spacing and all sorts of other things.txt!
: This macro, with syntax reminiscent offormat!
from Rust’sstd
, can be used to createText
through thetext::Builder
struct.
-
mode
: Defines how Duat will take input in order to controlWidget
s, includes things like:Mode
s: have the functionsend_key
, which takes a key and the current widget as input, and decides what to do with them. Only oneMode
is active at any given time.map
andalias
: These functions provide vim-style remapping on a givenMode
, also letting you switch modes on key sequences.set
,set_default
,reset
: These functions are used in order to switchMode
on demand. Do note that the switching is done asynchronously.
-
hook
: Provides utilities for hooking functions in Duat -
cmd
: Creation of commands in Duat, which can be called at runtime by the user.add!
: This macro lets you create a command, with one or more callers, and any number ofParameter
sParameter
: A command argument parsed from a string. There are a bunch of predefinedParameter
s, and things likeVec<P>
whereP: Parameter
, can also be asParameter
s, if you want multiple of the same kind.call
,queue
,call_notify
,queue_and
, etc: functions to call or queue commands, which one should be used depends on the context of the function calling them.
-
Form
: Has many options on whatText
should look like, are the same as those found on unix terminals.set
,set_weak
: These functions let you set forms with a name. They can be set to aForm
or reference another name of a form.ColorScheme
s: These are general purposeForm
setters, with a name that can be called from thecolorscheme
command
These are the elements available to you if you want to extend
Duat. Additionally, there are some other things that have been
left out, but they are available in the prelude
, so you can
just import it.
§How to extend Duat
Duat is extended primarily through the use of Plugin
s from
external crates, these will be plugged in the main config through
the plug!
macro, and are modified in place through the builder
pattern.
For this demonstration, I will create a Plugin
that keeps
track of the word count in a File
, without reparsing it every
time said File
changes.
§Creating a Plugin
First of all, you will need cargo
, then, you should create a
crate with cargo init
:
cargo init --lib duat-word-count
cd duat-word-count
Wihin that crate, you’re should add the duat-core
dependency:
cargo add duat-core
Finally, you can remove everything in duat-word-count/src/lib.rs
and start writing your plugin.
// In duat-word-count/src/lib.rs
use duat_core::prelude::*;
/// A [`Plugin`] to count the number of words in [`File`]s
pub struct WordCount;
impl<U: Ui> Plugin<U> for WordCount {
fn plug(self) {
todo!();
}
}
In the example, WordCount
is a plugin that can be included in
Duat’s config
crate. It will give the user the ability to get
how many words are in a File
, without having to reparse the
whole buffer every time, given that it could be a very large file.
In order to configure the Plugin
, you should make use of the
builder pattern, returning the Plugin
on every modification.
use duat_core::prelude::*;
/// A [`Plugin`] to count the number of words in [`File`]s
pub struct WordCount(bool);
impl WordCount {
/// Returns a new instance of the [`WordCount`] plugin
pub fn new() -> Self {
WordCount(false)
}
/// Count everything that isn't whitespace as a word character
pub fn not_whitespace(self) -> Self {
WordCount(true)
}
}
impl<U: Ui> Plugin<U> for WordCount {
fn plug(self) {
todo!();
}
}
Now, there is an option to exclude only whitespace, not just including regular alphanumeric characters. This would count, for example “x(x^3 + 3)” as 3 words, rather than 4.
Next, I need to add something to keep track of the number of words
in a File
. For File
s specifically, there is a built-in way
to keep track of changes through a Reader
:
use std::ops::Range;
use duat_core::{
data::RwData,
file::{BytesDataMap, RangeList},
prelude::*,
text::{Bytes, Moment, MutTags},
};
/// A [`Reader`] to keep track of words in a [`File`]
struct WordCounter {
words: usize,
regex: &'static str,
}
impl<U: Ui> Reader<U> for WordCounter {
fn apply_changes(
pa: &mut Pass,
reader: RwData<Self>,
bytes: BytesDataMap<U>,
moment: Moment,
ranges_to_update: Option<&mut RangeList>,
) {
todo!();
}
fn update_range(&mut self, bytes: &mut Bytes, tags: MutTags, within: Range<usize>) {}
}
Whenever changes take place in a File
, those changes will be
reported in a Moment
, which is essentially just a list of
Change
s that took place. This Moment
will be sent to the
Reader::apply_changes
function, in which you are supposed to
change the internal state of the Reader
to accomodate the
Change
s. Also, ignore update_range
, it wont be used in
this demonstration.
In order to add this Reader
to the File
, we’re going to
need a ReaderCfg
, which is used for configuring Reader
s
before adding them to a File
:
use std::ops::Range;
use duat_core::{
data::RwData,
file::{BytesDataMap, RangeList},
prelude::*,
text::{Bytes, Moment, MutTags},
};
struct WordCounterCfg(bool);
impl<U: Ui> ReaderCfg<U> for WordCounterCfg {
type Reader = WordCounter;
fn init(self, bytes: &mut Bytes) -> Result<Self::Reader, Text> {
let regex = if self.0 { r"\S+" } else { r"\w+" };
let words = bytes.search_fwd(regex, ..).unwrap().count();
Ok(WordCounter { words, regex })
}
}
In this function, I am returning the WordCounter
, with a
precalculated number of words, based on the Bytes
of the
File
’s Text
. Now that there is a count of words, I can
update it based on Change
s:
use duat_core::{
data::RwData,
file::{BytesDataMap, RangeList},
prelude::*,
text::{Bytes, Change, Moment, MutTags},
};
fn word_diff(regex: &str, bytes: &mut Bytes, change: Change<&str>) -> i32 {
let [start, _] = bytes.points_of_line(change.start().line());
let [_, end] = bytes.points_of_line(change.added_end().line());
// Recreate the line as it was before the change
let mut line_before = bytes.strs(start..change.start()).to_string();
line_before.push_str(change.taken_str());
line_before.extend(bytes.strs(change.added_end()..end));
let words_before = line_before.search_fwd(regex, ..).unwrap().count();
let words_after = bytes.search_fwd(regex, start..end).unwrap().count();
words_after as i32 - words_before as i32
}
In this method, I am calculating the difference between the number
of words in the line before and after the Change
took place.
Here Bytes::points_of_line
returns the Point
s where a
given line starts and ends. I know there are better ways to do
this by comparing the text that was taken to what was added,
with the context of the lines of the change, but this is
just a demonstration, and the more efficient method is left as an
exercise to the viewer.
Now, just call this on <WordCounter as Reader>::apply_changes
:
use std::ops::Range;
use duat_core::{
data::RwData,
file::{BytesDataMap, RangeList},
prelude::*,
text::{Bytes, Moment, MutTags},
};
/// A [`Reader`] to keep track of words in a [`File`]
struct WordCounter {
words: usize,
regex: &'static str,
}
impl<U: Ui> Reader<U> for WordCounter {
fn apply_changes(
pa: &mut Pass,
reader: RwData<Self>,
bytes: BytesDataMap<U>,
moment: Moment,
ranges_to_update: Option<&mut RangeList>,
) {
bytes.write_with_reader(pa, &reader, |bytes, reader| {
let diff: i32 = moment
.changes()
.map(|change| word_diff(reader.regex, bytes, change))
.sum();
reader.words = (reader.words as i32 + diff) as usize;
});
}
fn update_range(&mut self, bytes: &mut Bytes, tags: MutTags, within: Range<usize>) {}
}
Note that, in order to modify the WordCounter
or get access to
the Bytes
, you need to use an access function:
BytesDataMap::write_with_reader
, alongside a Pass
and the
RwData<Self>
in question. Duat does this in order to
protect massively shareable state from being modified and read at
the same time, as per the number one rule of Rust. This also
makes code much easier to reason about, and bugs much more
avoidable.
Now, to wrap this all up, the plugin needs to add this Reader
to every opened File
. We do this through the use of a hook:
use duat_core::{hook::OnFileOpen, prelude::*};
/// A [`Plugin`] to count the number of words in [`File`]s
pub struct WordCount(bool);
impl WordCount {
/// Returns a new instance of the [`WordCount`] plugin
pub fn new() -> Self {
WordCount(false)
}
/// Count everything that isn't whitespace as a word character
pub fn not_whitespace(self) -> Self {
WordCount(true)
}
}
impl<U: Ui> Plugin<U> for WordCount {
fn plug(self) {
let not_whitespace = self.0;
hook::add::<OnFileOpen<U>, U>(move |pa, builder| {
builder.add_reader(pa, WordCounterCfg(not_whitespace));
});
}
}
Now, whenever a File
is opened, this Reader
will be added
to it. This is just one out of many types of hook that Duat
provides by default. In Duat, you can even create your own, and
choose when to trigger them.
However, while we have added the Reader
, how is the user
supposed to access this value? Well, one convenient way to do this
is through a simple function:
use duat_core::prelude::*;
/// The number of words in a [`File`]
pub fn file_words<U: Ui>(pa: &Pass, file: &File<U>) -> usize {
if let Some(reader) = file.get_reader::<WordCounter>() {
reader.read(pa, |reader| reader.words)
} else {
0
}
}
Now, we have a finished plugin:
use std::ops::Range;
use duat_core::{
data::RwData,
file::{BytesDataMap, RangeList},
hook::OnFileOpen,
prelude::*,
text::{Bytes, Change, Moment, MutTags},
};
/// A [`Plugin`] to count the number of words in [`File`]s
pub struct WordCount(bool);
impl WordCount {
/// Returns a new instance of [`WordCount`]
pub fn new() -> Self {
WordCount(false)
}
/// Count everything that isn't whitespace as a word character
pub fn not_whitespace(self) -> Self {
WordCount(true)
}
}
impl<U: Ui> Plugin<U> for WordCount {
fn plug(self) {
let not_whitespace = self.0;
hook::add::<OnFileOpen<U>, U>(move |pa, builder| {
builder.add_reader(pa, WordCounterCfg(not_whitespace));
});
}
}
/// The number of words in a [`File`]
pub fn file_words<U: Ui>(pa: &Pass, file: &File<U>) -> usize {
if let Some(reader) = file.get_reader::<WordCounter>() {
reader.read(pa, |reader| reader.words)
} else {
0
}
}
/// A [`Reader`] to keep track of words in a [`File`]
struct WordCounter {
words: usize,
regex: &'static str,
}
impl<U: Ui> Reader<U> for WordCounter {
fn apply_changes(
pa: &mut Pass,
reader: RwData<Self>,
bytes: BytesDataMap<U>,
moment: Moment,
ranges_to_update: Option<&mut RangeList>,
) {
bytes.write_with_reader(pa, &reader, |bytes, reader| {
let diff: i32 = moment
.changes()
.map(|change| word_diff(reader.regex, bytes, change))
.sum();
reader.words = (reader.words as i32 + diff) as usize;
});
}
fn update_range(&mut self, bytes: &mut Bytes, tags: MutTags, within: Range<usize>) {}
}
struct WordCounterCfg(bool);
impl<U: Ui> ReaderCfg<U> for WordCounterCfg {
type Reader = WordCounter;
fn init(self, bytes: &mut Bytes) -> Result<Self::Reader, Text> {
let regex = if self.0 { r"\S+" } else { r"\w+" };
let words = bytes.search_fwd(regex, ..).unwrap().count();
Ok(WordCounter { words, regex })
}
}
fn word_diff(regex: &str, bytes: &mut Bytes, change: Change<&str>) -> i32 {
let [start, _] = bytes.points_of_line(change.start().line());
let [_, end] = bytes.points_of_line(change.added_end().line());
// Recreate the line as it was before the change
let mut line_before = bytes.strs(start..change.start()).to_string();
line_before.push_str(change.taken_str());
line_before.extend(bytes.strs(change.added_end()..end));
let words_before = line_before.search_fwd(regex, ..).unwrap().count();
let words_after = bytes.search_fwd(regex, start..end).unwrap().count();
words_after as i32 - words_before as i32
}
Once you’re done modifying your plugin, you should be ready to
publish it to crates.io. This is the common registry for
packages (crates in Rust), and is also where Duat will pull
plugins from. Before publishing, try to follow these guidelines
in order to improve the usability of the plugin. Now, you should
be able to just do this in the duat-word-count
directory:
cargo publish
Ok, it’s published, but how does one use it?
§Using plugins
Assuming that you’ve already installed duat, you should have a
config crate in ~/.config/duat
(or $XDG_CONFIG_HOME/duat
), in
it, you can call the following command:
cargo add duat-word-count@"*" --rename word-count
Then, in src/lib.rs
, you can add the following:
setup_duat!(setup);
use duat::prelude::*;
use word_count::*;
fn setup() {
plug!(WordCount::new().not_whitespace());
hook::add::<StatusLine<Ui>>(|pa, (sl, _)| {
sl.replace(status!(
"{file_fmt} has [wc]{file_words}[] words{Spacer}{mode_fmt} {sels_fmt} {main_fmt}"
))
});
}
Now, the default StatusLine
should have word count added in,
alongside the other usual things in there. It’s been added in the
{file_words}
part of the string, which just interpolated that
function, imported by use word_count::*;
, into the status line.
There are many other things that plugins can do, like create
custom Widget
s, Mode
s that can change how Duat
behaves, customized commands and hooks, and many such things
Modules§
- cfg
- General printing options for printing
File
s - clipboard
- Clipboard interaction for Duat
- cmd
- Creation and execution of commands.
- context
- Access to widgets and other other parts of the state of Duat
- data
- Duat’s way of sharing and updating state
- file
- The primary widget of Duat, used to display files.
- form
- Utilities for stylizing the text of Duat
- hook
- Utilities for hooks in Duat
- mode
Mode
s that handle user input- prelude
- The prelude of Duat
- text
- The primary data structure in Duat
- ui
Ui
structs and functions
Traits§
- Plugin
- A plugin for Duat
Functions§
- add_
shifts - Adds two shifts together
- crate_
dir - The path for the config crate of Duat
- duat_
name - Takes a type and generates an appropriate name for it
- plugin_
dir - The path for a plugin’s auxiliary files
- src_
crate - Returns the source crate of a given type