Crate tuirealm[][src]

Expand description

tui-realm

tui-realm is a tui framework to build applications with a React/Elm inspired-by approach

Get Started

Adding tui-realm as dependency

tuirealm = "0.4.3"

or if you want the std components library

tuirealm = { version = "0.3.2", features = [ "with-components" ] }

Since the library requires crossterm as backend, you will be required also to put crossterm as dependency:

crossterm = { version = "0.19" }

Let’s implement a View with update and view functions

This is the most important part, since we’re going to implmenent the main function with all the logic. Let’s underline our purpose:

  1. we want to declare a view, which will be a container and a façade for our components and will handle focus for us
  2. we want to mount some components inside the view
  3. we want to periodically refresh the view, through the view() function, which will use the render() method of each component
  4. we want to handle events through messages Msg using an update() function.
extern crate crossterm;
extern crate tuirealm;
extern crate tui;

use crossterm::event::{poll, read, Event};
use crossterm::event::DisableMouseCapture;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};

use std::io::{stdout, Stdout};
use std::thread::sleep;
use std::time::{Duration, Instant};

use tuirealm::components::input;
use tuirealm::props::borders::{BorderType, Borders};
use tuirealm::{InputType, Msg, Payload, PropsBuilder, Update, Value, View};

// tui
use tui::backend::CrosstermBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::Color;
use tui::Terminal;


pub const MSG_KEY_ESC: Msg = Msg::OnKey(KeyEvent {
    code: KeyCode::Esc,
    modifiers: KeyModifiers::NONE,
});

const MY_COMPONENT: &str = "MY_COMPONENT";

// Let's setup an input handler

pub(crate) struct InputHandler;

impl InputHandler {

    pub fn new() -> InputHandler {
        InputHandler {}
    }

    pub fn read_event(&self) -> Result<Option<Event>, ()> {
        if let Ok(available) = poll(Duration::from_millis(10)) {
            match available {
                true => {
                    // Read event
                    if let Ok(ev) = read() {
                        Ok(Some(ev))
                    } else {
                        Err(())
                    }
                }
                false => Ok(None),
            }
        } else {
            Err(())
        }
    }
}

// Let's setup a Context


pub struct Context {
    pub(crate) input_hnd: InputHandler,
    pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
}

impl Context {
    pub fn new() -> Context {
        let _ = enable_raw_mode();
        // Create terminal
        let mut stdout = stdout();
        assert!(execute!(stdout, EnterAlternateScreen).is_ok());
        Context {
            input_hnd: InputHandler::new(),
            terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
        }
    }

    pub fn enter_alternate_screen(&mut self) {
        let _ = execute!(
            self.terminal.backend_mut(),
            EnterAlternateScreen,
            DisableMouseCapture
        );
    }

    pub fn leave_alternate_screen(&mut self) {
        let _ = execute!(
            self.terminal.backend_mut(),
            LeaveAlternateScreen,
            DisableMouseCapture
        );
    }

    pub fn clear_screen(&mut self) {
        let _ = self.terminal.clear();
    }
}

impl Drop for Context {
    fn drop(&mut self) {
        // Re-enable terminal stuff
        self.leave_alternate_screen();
        let _ = disable_raw_mode();
    }
}

// Let's create the model

struct Model {
    quit: bool,
    redraw: bool,
    view: View,
}

fn main() {
    let mut ctx: Context = Context::new();
    // We need to setup the terminal, entering alternate screen
    ctx.enter_alternate_screen();
    ctx.clear_screen();
    // Let's create a View
    let mut myview: View = View::init();
    // Let's mount all the components we need
    myview.mount(
        MY_COMPONENT,
        Box::new(input::Input::new(
            input::InputPropsBuilder::default()
                .with_input(InputType::Text)
                .with_foreground(Color::Yellow)
                .with_label(String::from("Input component"))
                .build(),
        )),
    );
    // ...
    // Give focus to our component
    myview.active(MY_COMPONENT);
    // Prepare states
    let mut states: Model = Model { quit: false, redraw: true, view: myview, };
    // Loop until states.quit is false
    /*
    while !states.quit {
         // Listen for input events
         if let Ok(Some(ev)) = ctx.input_hnd.read_event() {
            // Pass event to view
            let msg = myview.on(ev);
            states.redraw = true;
            // Call the elm-like update
            update(&mut states, &mut myview, msg);
        }
        // If redraw, draw interface
        if states.redraw {
            // Call the elm elm-like vie1 function
            view(&mut ctx, &myview);
            states.redraw = false;
        }
        sleep(Duration::from_millis(10));
    }
    */
    // Finalize context
    drop(ctx);
}

// -- view

fn view(ctx: &mut Context, view: &View) {
    let _ = ctx.terminal.draw(|f| {
        // Prepare chunks
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .margin(1)
            .constraints([Constraint::Length(3)].as_ref())
            .split(f.size());
        view.render(MY_COMPONENT, f, chunks[0]);
    });
}

// -- update

impl Update for Model {
    fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
        let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
        match ref_msg {
            None => None, // Exit after None
            Some(msg) => match msg {
                (COMPONENT_INPUT, Msg::OnChange(Payload::One(Value::Str(input)))) => {
                    eprintln!("User submitted: {}", input);
                    None
                }
                (_, &MSG_KEY_ESC) => {
                    // Quit on esc
                    self.quit = true;
                    None
                }
                _ => None,
            },
        }
    }
}
Run

Examples

See the examples directory to check out how to setup tui-realm. If you want a real in-production implementation, check out my project termscp

Update

The update function is used to handle the messages returned from the view. The name update is just a convention, you can call this function as you prefer. I suggest you to use update, though, since it’s like in Elm. The usual signature of this function is fn update(model: &mut MyModel, msg: Option<(String, Msg)>) -> Option<(String, Msg)> We don’t have to mind what model is right now, it may be everything, probably it contains the View and some states, who cares; what is important here is the msg. The message is a tuple made up by a String, which is the identifier of the component which raised the event and a Msg enum, which contains the message itself. What can we do with this then? Well, we can make a super-cool match case where we match the tuple (ID, Msg) and for each branch, we can make everything we want. And as you’ve probably already noticed this function returns a Message tuple too. This should already have triggered you! Yes, you can call this function recursively and chain different events. Just a friendly reminder: you can obviously match only the event or only the ID of the component in the match case, just mind of the top-bottom priority ;)

Match Keys in Update

If you’re in trouble handling key events, you’re not a bad dev, it happened to me too the first time. To handle them, my suggestion is to create a keymap module where you store as const all the key you need. Just follow this example:

extern crate crossterm;
extern crate tuirealm;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tuirealm::Msg;

// -- keys

pub const MSG_KEY_ESC: Msg = Msg::OnKey(KeyEvent {
    code: KeyCode::Esc,
    modifiers: KeyModifiers::NONE,
});

pub const MSG_KEY_TAB: Msg = Msg::OnKey(KeyEvent {
    code: KeyCode::Tab,
    modifiers: KeyModifiers::NONE,
});
Run

Then you’ll be able to match them as in the examples: (MY_COMPONENT, &MSG_KEY_TAB) => {...}

Components

Components represents a logical layer above a tui widget. Components allows you to handle properties and states for widgets and this will be rendered. The component is implemented through a trait which is called Component indeed. For each component you must implement these methods:

  • render: this method renders the component inside the area passed as argument. Mind that the component should be rendered only if props.visible is true.
  • update: update the component properties. You can return a Msg if you want and change the states if needed.
  • get_props: returns a copy of the component properties
  • on: handle an input event; returns a Msg
  • get_state: returns the current state to be exposed to the user.
  • blur: change the focus state for the component. This won’t affect the view.
  • active: enable the focus state for the component. This won’t affect the view.

Each component should have properties, provided by the Props struct and, if required some states, which are defined for each struct. States mustn’t be public. I strongly suggest to implement a PropsBuilder, via the namesake trait, for each component properties, in order to make easier to define properties for each component, while if you don’t want to do so, you can use the GenericPropsBuilder.

Re-exports

pub use self::props::borders;
pub use self::props::texts;
pub use self::props::GenericPropsBuilder;
pub use self::props::InputType;
pub use self::props::PropPayload;
pub use self::props::PropValue;
pub use self::props::Props;
pub use self::props::PropsBuilder;
pub use self::view::View;

Modules

event

events

props

Props

tui

tui

view

View

Enums

Msg

Msg

Payload

Payload

Value

Value

Traits

Component

Component

Update

Update

Type Definitions

Canvas

Canvas