2 releases
0.1.1 | Mar 2, 2024 |
---|---|
0.1.0 | Feb 29, 2024 |
#1902 in Command line utilities
60KB
1.5K
SLoC
tui-window
A minimal page and focus manager for Ratatui and Crossterm (though it supports other backends that Ratatui also supports).
tui-window
provides a very minimal setup to allow you quickly build simple,
page/page based TUI (Text-Based-User-Interface) applications.
It is loosely inspired in how HTML works: Declare a tree of widgets, and have tui-window manage application-level concerns like focus management, input redirection to specific widgets, etc.
Features
- Build TUI layouts declaratively. Define a tree of components and only calculate layouts when you need a fine-grain control on the render process.
- An opinionated handle of focus-management: An order of focusable widgets is
calculated (e.g. press
Tab
to focus to the next element). - Create "pages" (collections of trees of widgets) and navigate easily between them.
- Supports native Ratatui widgets.
- Utilities to initialize Ratatui and Crossterm with panic handling out of the box.
Status
This library is actively under development. Feel free to suggest improvements (just keep in mind that the scope of this library is purposely small).
Getting started
Install tui-window
into your project using Cargo:
cargo add tuiwindow
Build a few widgets by implementing Render
-for widgets that don't receive focus- or FocusableRender
for widgets that should receive focus (deriving Default
is not required):
#[derive(Default)]
struct TestWidget {
text_content: String,
}
impl FocusableRender for TestWidget {
fn render(&mut self, render_props: &RenderProps, buff: &mut Buffer, area: Rect) {
if let Some(InputEvent::Key(c)) = render_props.event {
self.text_content.push(c)
}
Paragraph::new(format!(
"Hello world! Focused? {}: {}",
render_props.is_focused, self.text_content
))
.block(
Block::new()
.borders(Borders::all())
.style(if render_props.is_focused {
Style::new().fg(Color::Red)
} else {
Style::new()
}),
)
.wrap(Wrap { trim: false })
.render(area, buff)
}
}
#[derive(Default)]
struct StaticWidget {}
impl Render for StaticWidget {
fn render(&mut self, _render_props: &RenderProps, buff: &mut Buffer, area: Rect) {
Paragraph::new("I'm static")
.block(Block::new().borders(Borders::all()))
.render(area, buff)
}
}
Define the structure of your application (you can style whole pages):
let mut app = PageCollection::new(vec![
Page::new(
"Page 1", // the page's title
'1', // a shortcut for navigating to this page
row_widget!( // macro for evenly-distributing your widgets in rows
SlowWidget::default(),
column_widget!(StaticWidget {}, TestWidget::default())
),
),
Page::new(
"Page 2",
'2',
column_widget!( // macro for evenly-distributing your widgets in
AnotherWidget::default(), //columns
column_widget!(StaticWidget {}, TestWidget::default())
),
)
.with_style(Style::default().bg(Color::White).fg(Color::Black)),
]);
Put it all together in your main
function, setting rendering using the
provided helper TuiCrossterm
:
fn main() -> Result<(), Box<dyn Error>> {
let mut tui = TuiCrossterm::new()?;
let terminal = tui.setup()?;
// define the collection of pages:
let mut app = PageCollection::new(vec![
Page::new(
"Page 1",
'1',
row_widget!(
SlowWidget::default(),
column_widget!(StaticWidget {}, TestWidget::default())
),
),
Page::new(
"Page 2",
'2',
column_widget!(
AnotherWidget::default(),
column_widget!(StaticWidget {}, TestWidget::default())
),
)
.with_style(Style::default().bg(Color::White).fg(Color::Black)),
]);
let mut window = Window::new(&app, |ev| match ev {
// define the termination condition for the app:
tuiwindow::core::InputEvent::Key(c) => *c == 'q',
_ => false,
});
while !window.is_finished() {
terminal.draw(|f| {
let area = f.size();
let buff = f.buffer_mut();
let mut second_buff = buff.clone();
// draw
window.render::<DefaultEventMapper>(&mut app, &mut second_buff, area);
buff.merge(&second_buff);
})?;
}
Ok(())
}
Dependencies
~6–12MB
~127K SLoC