A library for building declarative text-based user interfaces

Overview

Intuitive

docs.rs Documentation

Intuitive is a component-based library for creating text-based user interfaces (TUIs) easily.

It is heavily inspired by React and SwiftUI, containing features that resemble functional components, hooks, and a (mostly) declarative DSL.

Refer to the Getting Started documentation for a detailed guide on how to get started with Intuitive. Alternatively, head over to the examples directory to see some demo applications.

Design

The main focus of Intuitive is to simplify the implementation of section-based TUIs, such as lazygit's, even at the slight expense of performance. Intuitive attempts to make it easy to write reusable TUI components that

  • encapsulate logic around handling state and key events
  • have complex layouts
  • are easy to read

For example, a complex layout with an input box:

use intuitive::{
  component,
  components::{stack::Flex::*, HStack, Section, Text, VStack},
  error::Result,
  on_key, render,
  state::use_state,
  terminal::Terminal,
};

#[component(Root)]
fn render() {
  let text = use_state(|| String::new());

  let on_key = on_key! { [text]
    KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)),
    KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()),
    KeyEvent { code: Esc, .. } => event::quit(),
  };

  render! {
    VStack(flex: [Block(3), Grow(1)], on_key) {
      Section(title: "Input") {
        Text(text: text.get())
      }

      HStack(flex: [1, 2, 3]) {
        Section(title: "Column 1")
        Section(title: "Column 2")
        Section(title: "Column 3")
      }
    }
  }
}

fn main() -> Result<()> {
  Terminal::new(Root::new())?.run()
}

And the output would look like this:

demo

Disclaimer

Intuitive is closer to a proof-of-concept than to a crate that's ready for prime-time use. There may also be some bugs in the library of components, please raise an issue if you find any. Furthermore, since a large and complex application has yet to be built using Intuitive, it is not a guarantee that it does not have some major flaw making such development difficult.

You might also like...
Simple macros to write colored and formatted text to a terminal. Based on `termcolor`, thus also cross-platform.
Simple macros to write colored and formatted text to a terminal. Based on `termcolor`, thus also cross-platform.

Bunt: simple macro-based terminal colors and styles bunt offers macros to easily print colored and formatted text to a terminal. It is just a convenie

A text renderer for Rust's embedded-graphics crate, based on U8g2
A text renderer for Rust's embedded-graphics crate, based on U8g2

u8g2-fonts This crate is a pure Rust reimplementation of the font subsystem of U8g2. It is intended for the embedded-graphics ecosystem. Licensing Whi

A Rust library for building interactive prompts
A Rust library for building interactive prompts

inquire is a library for building interactive prompts on terminals. Demo Source Usage Put this line in your Cargo.toml, under [dependencies]. inquire

A Rust library for building modular, fast and compact indexes over genomic data

mazu A Rust library for building modular, fast and compact indexes over genomic data Mazu (媽祖)... revered as a tutelary deity of seafarers, including

🎙 A compact library for working with user output
🎙 A compact library for working with user output

🎙 Storyteller A library for working with user output Table of contents 👋 Introduction 🖼 Visualized introduction 📄 Example source code ❓ Origins 💖

Estratto is a powerful and user-friendly Rust library designed for extracting rich audio features from digital audio signals.
Estratto is a powerful and user-friendly Rust library designed for extracting rich audio features from digital audio signals.

estratto 〜 An Audio Feature Extraction Library estratto is a powerful and user-friendly Rust library designed for extracting rich audio features from

A library that creates a terminal-like window with feature-packed drawing of text and easy input handling. MIRROR.

BearLibTerminal provides a pseudoterminal window with a grid of character cells and a simple yet powerful API for flexible textual output and uncompli

colorStyle is a library of styles for command-line text write in Rust.
colorStyle is a library of styles for command-line text write in Rust.

Colorstyle colorStyle is a library of styles for command-line text. Inspired by flylog/colorstyle (golang) Example let text = colorstyle::green("gre

A tiny library and application for animating text in the terminal.
A tiny library and application for animating text in the terminal.

🐌 snailshell A tiny library and application for animating text in the terminal. 🪄 It's so easy to add some flair to your text RPGs or console progra

Comments
  • Add alignment property to Text

    Add alignment property to Text

    This merge request introduces an alignment property to the Text component, and a new Alignment enum with alignment options (left, center, right).

    Breaking Change

    This is a breaking change in that Text::new now takes an additional parameter — since that would be a breaking change no matter what, I added alignment at the beginning of render's parameters to preserve alphabetical order.

    Implementation

    Under the covers, we directly translate the Alignment to a tui-rs Alignment and set it onto the underlying Paragraph. The default is Alignment::Left, so there should be no change in default behavior.

    I also refactored the components::text module a bit, so that it could become a public module (to expose Alignment) without double-exposing the Text component — I followed the pattern from components::stack of creating a pub(super) submodule to hide the component. I couldn't think of a good submodule name, so I used component. I considered using the component name “text,” but components::text::text seemed more confusing than helpful. I'm open to any suggestions, including using “text” if you prefer;

    Fixes #5.

    opened by chrisbouchard 2
  • Components using `use_state` cannot be rendered conditionally

    Components using `use_state` cannot be rendered conditionally

    Components using use_state cannot be safely rendered conditionally. Doing so can lead to a panic, with the following error message: "insufficient calls".

    Here is a minimal reproducible example: https://gist.github.com/ebenpack/3b0c3a1a2b813740003f6a4161ce8ecf

    This appears to be due to the "hook rules", however it's surprising that these rules must be adhered to for the scope of the entire application. I would expect the rule to be scoped to a single component.

    I'm not sure if the way this rule is applied is intended or not, but if it is I think the library would be much more usable with a more locally applied rule. E.g. it would make conditional rendering much easier and safer.

    bug 
    opened by ebenpack 2
  • Implement keyboard focus system

    Implement keyboard focus system

    I'm taking most of my inspiration from this document: https://docs.flutter.dev/development/ui/advanced/focus

    Essentially, they describe a system of focus handling as follows:

    • Focus Tree: Tracks focus nodes and their relationships
    • Focus Node: A single node in the focus tree which can receive focus
    • Focus Chain: the focused nodes, starting from the root and going to the primary focus node
    • Primary Focus Node:
      • The deepest focused node
      • Events go first to this node, and propagate up through the focus chain
    • Focus Traversal:
      • the process of changing focus nodes on TAB
      • Focus Scope: a scope that groups nodes for traversal

    To implement this functionality, two key requirements stand out:

    1. We need to build a focus tree. This can be done in two ways:
    • Use a hook-based system that detects the creation of focus nodes, and adds them to hidden global state. By using callbacks on drop, we can detect different of the tree
    • Add a function to the widget trait which returns Option<FocusTree> - and recursively builds the focus tree
    1. We need widgets to be able to bubble input upwards
    • We can track the current focus node, and drill down to it
    • Or, we can start from the current focus node, and bubble up to the parents - this requires passing a reference to the focus node parent to it's child

    We can either use Rc / Weak style references, or track IDs which correspond to each focus node:

    struct FocusNodeId(i64);
    // Needs to be regenerated on each render call
    static FOCUS_TREE: Mutex<BTreeMap<FocusNodeId, Vec<FocusNodeId>>> = Mutex::new(...);
    static FOCUS_NODES: Mutex<HashMap<FocusNodeId, FocusNode>> = Mutex::new(...);
    

    I think that tracking IDs is the best/simplest solution, though it requires some consideration W.R.T. concurrency.

    Focus hook system:

    mod focus_hook {
        static ROOT: Mutex<Option<Vec<FocusNode>>> = Mutex::new(None);
    
        /// Returns the parent of the current focus node;
        fn parent() -> Weak<FocusNode>;
    
        /// Add new focus node as child
        fn push_child(FocusNode);
    
        /// Move up one level in the tree. Returns all focus nodes that would be children
        /// `push_child` will now add to the parent of the previously pushed focus node
        fn bubble() -> Option<Vec<Rc<FocusNode>>>;
    
         /// Assign focus to the current node
         fn grab_focus(fn: &mut FocusNode);
    }
    
    struct FocusNode {
        inner: Option<Widget>,
        parent: Option<Weak<FocusNode>>,
        focus_children: Option<Vec<Rc<FocusNode>>>
    }
    
    impl FocusNode {
        fn new(component: _) -> _ {
             let mut self_ = Rc::new(FocusNode { parent: None, inner: None, focus_children: None });
             self_.parent = Some(focus_hook::parent());
             focus_hook::push_child(self_.clone());
             /// Render call will populate children
             self_.inner = component.render();
             self_.focus_children = focus_hook::bubble();
             self_
        }
    }
    

    Trait based focus tree:

    pub trait Element {
      fn draw(&self, _rect: Rect, _frame: &mut Frame) {}
      fn on_key(&self, _event: KeyEvent) {}
      fn on_mouse(&self, _rect: Rect, _event: MouseEvent) {}
      fn children(&self) -> Vec<Box<dyn Element>>;
      fn focus_tree(&self) -> Vec<Rc<FocusNode>> {
        // Produce list of focus trees for each child
        // Place self at top of focus tree, with child focus trees as children
        // eg.
        //         f0
        //         / \
        //        o  f1 _
        //       / \     \
        //      o  f2    f3
        //     / \   \   |  \
        //    f4 f5  f6  o  f7
        // Focus tree:
        //       _ f0 __
        //      /  / \  \
        //     /   /  \  \
        //    f4  f5  f2 f1
        //             |  |
        //            f6 f3
        //                |
        //               f7
        let mut focus_trees = vec![];
        for child in self.children() {
            focus_trees.extend(child.focus_tree());
        }
        if let Some(s) = self.as_focus_node() {
            vec![{ s.focus_children = Some(focus_trees); s}])
        } else {
            focus_trees
        }
      }
    
      fn as_focus_node(&self) -> Option<Rc<FocusNode>> { None }
    }
    
    struct FocusNode {
        inner: Option<Widget>,
        parent: Option<Weak<FocusNode>>,
        focus_children: Option<Vec<Rc<FocusNode>>>
    }
    
    impl FocusNode {
        fn new(component: _) -> _ {
             let mut self_ = Rc::new(FocusNode { parent: None, inner: None, focus_children: None });
             self_.parent = Some(focus_hook::parent());
             focus_hook::push_child(self_.clone());
             /// Render call will populate children
             self_.inner = component.render();
             self_.focus_children = self.focus_tree();
             self_
        }
    }
    

    Bubbling approach to handling key events:

    mod focus {
        static ACTIVE_FOCUS_NODE: Mutex<Option<Rc<FocusNode>>>> = Mutex::new(None);
        fn take_focus(focus_node: &FocusNode);
        
        fn handle_key(event: KeyEvent) {
           if let Some(node) = ACTIVE_FOCUS_NODE.lock().unwrap() {
               node.on_key(event);
           }
        }
    }
    
    impl Element for FocusNode {
        fn on_key(&self, event: KeyEvent) {
            if self.inner.on_key(event) == Propagate::Next {
                self.parent.unwrap().on_key(event);
            }
        }
    }
    

    The snippets I've outlined allow describing the focus tree (explicit) / focus chain (implicit), active focus node (explicit).

    The focus scope is relatively easy to implement, as a plain focus node. Focus traversal could be done using the focus tree and visiting parents in order of increasing ID - again, certain implementation details would need to be figured out, but this should be doable.

    Again - I'd be happy to implement this, but I'd like to get an idea for what overall design you think is best before I go ahead and implement this functionality. I should be able to implement an MVP relatively quickly - likely under an experimental_components::focus module.

    opened by philippeitis 2
  • Fix integer underflow bug in section.rs

    Fix integer underflow bug in section.rs

    This would cause watch.rs to crash when build using debug mode. Considering that integer underflow is a problem in release mode, I used saturating_sub / saturating_add to avoid this problem, and included a link to the relevant source code.

    On an another note, I myself am also building a TUI for my own application, and have been flowing moving towards this exact style. I'd like to contribute a few of the widgets I've built - infinitely scrolling lists, text-widgets with selection, copy/paste/cut, backwards / forwards deletion. I've also implemented additional scroll features based on keyboard input.

    I'd also be interested in a focus-grabbing system for widgets (so that all input goes by default to a text entry widget, for example, and commands "bubble" up the stack) - I've implemented a basic version of this, but a more powerful version would allow assigning a deeply nested widget focus by default.

    All of this can be found here: https://github.com/philippeitis/bookworm/tree/master/bookworm-tui/src/ui/widgets

    opened by philippeitis 1
Releases(v0.6.1)
Owner
Enrico Borba
Enrico Borba
Rust API Server: A versatile template for building RESTful interfaces, designed for simplicity in setup and configuration using the Rust programming language.

RUST API SERVER Introduction Welcome to the Rust API Server! This server provides a simple REST interface for your applications. This README will guid

Harry Nguyen 3 Feb 25, 2024
Build terminal user interfaces and dashboards using Rust

tui-rs tui-rs is a Rust library to build rich terminal user interfaces and dashboards. It is heavily inspired by the Javascript library blessed-contri

Florian Dehau 9.3k Jan 4, 2023
`boxy` - declarative box-drawing characters

boxy - declarative box-drawing characters Box-drawing characters are used extensively in text user interfaces software for drawing lines, boxes, and o

Miguel Young 7 Dec 30, 2022
A Text User Interface library for the Rust programming language

Cursive Cursive is a TUI (Text User Interface) library for rust. It uses ncurses by default, but other backends are available. It allows you to build

Alexandre Bury 3.3k Jan 9, 2023
A Text User Interface library for the Rust programming language

Cursive Cursive is a TUI (Text User Interface) library for rust. It uses ncurses by default, but other backends are available. It allows you to build

Alexandre Bury 3.3k Jan 3, 2023
A cross platform minimalistic text user interface

titik Titik is a crossplatform TUI widget library with the goal of being able to interact intuitively on these widgets. It uses crossterm as the under

Jovansonlee Cesar 113 Dec 31, 2022
Tool to create web interfaces to command-line tools

webgate This command line utility allows you to: serve files and directories listed in a config file remotely run shell commands listed in a config fi

Nathan Royer 2 Jan 8, 2022
Batteries included command line interfaces.

CLI Batteries Opinionated batteries-included command line interface runtime utilities. To use it, add it to your Cargo.toml [dependencies] cli-batteri

Remco Bloemen 15 Jan 4, 2023
An implementation of Piet's text interface using cosmic-text

piet-cosmic-text Implements piet's Text interface using the cosmic-text crate. License piet-cosmic-text is free software: you can redistribute it and/

John Nunley 7 Mar 12, 2023
Text-based to-do management CLI & language server

☑️ Todome (日本語版はこちら) Todome is a notation developed for making and editing to-do lists. It is inspired by Todo.txt, and in fact some of the todome not

monaqa 16 Aug 17, 2022