Crate sanguine

source ·
Expand description

sanguine

Create dynamic, performant TUI applications in Rust.

Sanguine was created from the need for a library tailored for complex TUI applications such as text editors and terminal multiplexers. The Rust ecosystem contains many great crates for building TUI apps, but many are geared towards small dashboard-like apps and implement immediate-mode rendering or struggle with mouse events.

Sanguine implements a tree-based layout API that can be updated at runtime, with a custom constraint algorithm geared towards rendering to the terminal. Layout results are cached between renders for performance, and are only recomputed when the layout is changed. Widgets can be nested and mouse events are handled properly for widgets at any depth - widgets only need to handle mouse events based on local position. Widgets can optionally specify a cursor location to allow for implementations of text editor windows and more.

It is built on top of TermwizBufferedTerminal, which optimizes terminal writes to maximize performance.

Features:

  • Dynamic, Tree-based layout API
  • Extensible widget trait
  • First-class mouse events support
    • Automatic propagation
    • Hover and click support
  • Global and widget-local event handlers
  • Generic API
    • Custom user event type for message passing
    • Custom state type for core app state
  • Focus
    • Switch focus by direction or directly

Demo

demo

Demo Usage

$ git clone git@github.com:willothy/sanguine.git

$ cd sanguine

$ cargo run --example demo

Keymaps:

  • Control + q: Quit
  • Shift + Tab: Cycle focus
  • Shift + Up/Down/Left/Right: Switch focus by direction
  • Up/Down/Left/Right: Switch menu item
  • Enter: Select menu item
use sanguine::{
    error::*,
    event::{Event, UserEvent},
    layout::{Axis, Constraint, Direction, Rect},
    widgets::{Border, Menu, TextBox},
    App, Config, Layout,
};
use termwiz::input::{KeyCode, KeyEvent, Modifiers};

pub fn main() -> Result<()> {
    // Create the layout struct
    let mut layout = Layout::new();

    // Create a TextBox widget, wrapped by a Border widget
    let textbox = TextBox::new();
    // Get a copy of the textbox buffer
    let textbox_buffer = textbox.buffer();
    let editor_1 = Border::new("Shared TextBox".to_owned(), textbox);

    // create a menu widget, and add some items to it
    let mut menu = Menu::new("Demo menu");
    menu.add_item("Quit", "", |_, _, event_tx| {
        // exit button using the event sender
        event_tx.send(UserEvent::Exit).ok();
    });
    menu.add_item("Delete", "", {
        // use a shared copy of the textbox buffer, and delete the last character of the buffer
        let textbox_buffer = textbox_buffer.clone();
        move |_, _, _| {
            let mut w = textbox_buffer.write().unwrap();
            let len = w.len();
            let last = w.last_mut().unwrap();
            if last.is_empty() && len > 1 {
                w.pop();
            } else if !last.is_empty() {
                last.pop();
            }
        }
    });
    menu.add_item("Get line count: ", "<unknown>", move |this, menu, _| {
        // count buffer lines, and update the menu item
        menu.update_tag(this, |_| textbox_buffer.read().unwrap().len().to_string())
    });

    // Add the first editor to the layout
    let left = layout.add_leaf(editor_1);

    // Add the menu widget
    let top_right = layout.add_leaf(Border::new("Menu".to_owned(), menu));

    // Add a floating window
    layout.add_floating(
        // The window will contain a text box
        Border::new("Example Float", TextBox::new()),
        Rect {
            x: 10.,
            y: 10.,
            width: 25.,
            height: 5.,
        },
    );

    // Clone the first editor to add it to the layout again
    // This widget will be *shared* between the two windows, meaning that changes to the underlying
    // buffer will be shown in both windows and focusing on either window will allow you to edit
    // the same buffer.
    let bot_right = layout.clone_leaf(left);

    // Add the second editor to the layout
    // let bot_right = layout.add_leaf(editor_2);

    // Create a container to hold the two right hand side editors
    let right = layout.add_with_children(
        // The container will be a vertical layout
        Axis::Vertical,
        // The container will take up all available space
        Some(Constraint::fill()),
        // The container will contain the cloned first editor, and the second editor
        [top_right, bot_right],
    );

    // Get the root node of the layout
    let root = layout.root();
    // Ensure that the root container is laid out horizontally
    layout.set_direction(root, Axis::Horizontal);

    // Add the left window (leaf) and the right container to the root
    layout.add_child(root, left);
    layout.add_child(root, right);

    // Create the sanguine app, providing a handler for *global* input events.
    // In this case, we only handle occurrences of Shift+Tab, which we use to cycle focus.
    // If Shift+Tab is pressed, we return true to signal that the event should not be
    // propagated.
    let mut app = App::new_with_handler(
        layout,
        // The default config is fine for this example
        Config::default(),
        |state: &mut App, event: &Event<_>, _| {
            match event {
                Event::Key(KeyEvent {
                    key: KeyCode::Tab,
                    modifiers: Modifiers::SHIFT,
                }) => {
                    state.cycle_focus()?;
                }
                Event::Key(KeyEvent {
                    key:
                        k @ (KeyCode::UpArrow
                        | KeyCode::DownArrow
                        | KeyCode::LeftArrow
                        | KeyCode::RightArrow),
                    modifiers: Modifiers::SHIFT,
                }) => {
                    let dir = match k {
                        KeyCode::UpArrow => Direction::Up,
                        KeyCode::DownArrow => Direction::Down,
                        KeyCode::LeftArrow => Direction::Left,
                        KeyCode::RightArrow => Direction::Right,
                        _ => unreachable!(),
                    };
                    state.focus_direction(dir)?;
                }
                _ => return Ok(false),
            }
            Ok(true)
        },
    )?;
    // Set the initial focus to the left node.
    // Only windows can be focused, attempting to focus a container will throw an error.
    app.set_focus(left)?;

    // The main render loop, which will run until the user closes the application (defaults to
    // Ctrl-q).
    while app.handle_events()? {
        app.render()?;
    }

    Ok(())
}

Re-exports

Modules

Structs

  • The main application struct, responsible for managing the layout tree, keeping track of focus, and rendering the widgets.
  • Contains configuration options for the Sanguine application.

Traits

  • The core widget trait that all widgets must implement. This trait provides the methods that the layout engine uses to interact with widgets.

Type Definitions