Crate duat_core

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

    • Widgets: As the name implies, this is the trait for objects that will show up on the screen. The most noteworthy Widget is File, which displays the contents of a file.
    • WidgetCfgs: These are Widget builders. They are used in the setup function of Duat’s config, through the OnFileOpen and OnWindowOpen hooks.
    • Ui and RawAreas: 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 screen

    • Text: Is everything that Duat shows on screen (except Ui specific decorations). This includes a UTF-8 string and tags to modify it.
    • Tags: This is how Duat determines how Text 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 of format! from Rust’s std, can be used to create Text through the text::Builder struct.
  • mode: Defines how Duat will take input in order to control Widgets, includes things like:

    • Modes: have the function send_key, which takes a key and the current widget as input, and decides what to do with them. Only one Mode is active at any given time.
    • map and alias: These functions provide vim-style remapping on a given Mode, also letting you switch modes on key sequences.
    • set, set_default, reset: These functions are used in order to switch Mode on demand. Do note that the switching is done asynchronously.
  • hook: Provides utilities for hooking functions in Duat

    • Hookable: An event that you want to provide hooks for, in order to trigger functions whenever it takes place.
    • add, add_grouped, remove: These functions let you add or remove functions from Hookable events. Their arguments are statically determine by said Hookables.
  • 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 of Parameters
    • Parameter: A command argument parsed from a string. There are a bunch of predefined Parameters, and things like Vec<P> where P: Parameter, can also be as Parameters, 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: How to stylize Text

    • Form: Has many options on what Text 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 a Form or reference another name of a form.
    • ColorSchemes: These are general purpose Form setters, with a name that can be called from the colorscheme 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 Plugins 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 Files 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 Changes 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 Changes. 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 Readers 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 Changes:

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 Points 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 Widgets, Modes that can change how Duat behaves, customized commands and hooks, and many such things

Modules§

cfg
General printing options for printing Files
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
Modes 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