mirror of
https://github.com/orhun/systeroid
synced 2024-10-02 21:53:31 +00:00
feat(tui): add stateful list widget
This commit is contained in:
parent
4df05f94fe
commit
c950826eb4
|
@ -1,4 +1,5 @@
|
|||
use crate::command::Command;
|
||||
use crate::widgets::StatefulList;
|
||||
|
||||
/// Application controller.
|
||||
#[derive(Debug)]
|
||||
|
@ -7,6 +8,8 @@ pub struct App {
|
|||
pub running: bool,
|
||||
/// Input buffer.
|
||||
pub input: Option<String>,
|
||||
/// List of sysctl variables.
|
||||
pub variable_list: StatefulList<String>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
|
@ -14,6 +17,10 @@ impl Default for App {
|
|||
Self {
|
||||
running: true,
|
||||
input: None,
|
||||
variable_list: StatefulList::with_items(vec![
|
||||
String::from("data1"),
|
||||
String::from("data2"),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +29,12 @@ impl App {
|
|||
/// Runs the given command and updates the application.
|
||||
pub fn run_command(&mut self, command: Command) {
|
||||
match command {
|
||||
Command::ScrollUp => {
|
||||
self.variable_list.previous();
|
||||
}
|
||||
Command::ScrollDown => {
|
||||
self.variable_list.next();
|
||||
}
|
||||
Command::UpdateInput(v) => match self.input.as_mut() {
|
||||
Some(input) => {
|
||||
input.push(v);
|
||||
|
|
|
@ -3,6 +3,10 @@ use termion::event::Key;
|
|||
/// Possible application commands.
|
||||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
/// Scroll up on the widget.
|
||||
ScrollUp,
|
||||
/// Scroll down on the widget.
|
||||
ScrollDown,
|
||||
/// Update the input buffer.
|
||||
UpdateInput(char),
|
||||
/// Clear the input buffer.
|
||||
|
@ -25,6 +29,8 @@ impl Command {
|
|||
}
|
||||
} else {
|
||||
match key {
|
||||
Key::Up => Command::ScrollUp,
|
||||
Key::Down => Command::ScrollDown,
|
||||
Key::Char(':') => Command::UpdateInput(' '),
|
||||
Key::Esc => Command::Exit,
|
||||
_ => Command::None,
|
||||
|
|
|
@ -12,6 +12,8 @@ pub mod error;
|
|||
pub mod event;
|
||||
/// User interface renderer.
|
||||
pub mod ui;
|
||||
/// Custom widgets.
|
||||
pub mod widgets;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::command::Command;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::app::App;
|
||||
use tui::backend::Backend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||
use tui::text::Span;
|
||||
use tui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph};
|
||||
use tui::Frame;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
|
@ -13,9 +14,42 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
|
|||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(rect.height - 3), Constraint::Min(3)].as_ref())
|
||||
.split(rect);
|
||||
render_variable_list(frame, chunks[0], app);
|
||||
render_input_prompt(frame, chunks[1], rect.height - 2, app);
|
||||
}
|
||||
|
||||
/// Renders the list that contains the sysctl variables.
|
||||
fn render_variable_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app: &mut App) {
|
||||
frame.render_stateful_widget(
|
||||
List::new(
|
||||
app.variable_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|text| ListItem::new(Span::raw(text)))
|
||||
.collect::<Vec<ListItem<'_>>>(),
|
||||
)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::all())
|
||||
.border_style(Style::default().fg(Color::White))
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().bg(Color::Black)),
|
||||
)
|
||||
.highlight_symbol("> "),
|
||||
rect,
|
||||
&mut app.variable_list.state,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders the input prompt for running commands.
|
||||
fn render_input_prompt<B: Backend>(
|
||||
frame: &mut Frame<'_, B>,
|
||||
rect: Rect,
|
||||
cursor_y: u16,
|
||||
app: &mut App,
|
||||
) {
|
||||
if let Some(input) = &app.input {
|
||||
frame.set_cursor(input.width() as u16 + 2, rect.height - 2);
|
||||
frame.set_cursor(input.width() as u16 + 2, cursor_y);
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(match &app.input {
|
||||
|
@ -29,6 +63,6 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
|
|||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().bg(Color::Black)),
|
||||
),
|
||||
chunks[1],
|
||||
rect,
|
||||
);
|
||||
}
|
||||
|
|
74
systeroid-tui/src/widgets.rs
Normal file
74
systeroid-tui/src/widgets.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use tui::widgets::ListState;
|
||||
|
||||
/// List widget with TUI controlled states.
|
||||
#[derive(Debug)]
|
||||
pub struct StatefulList<T> {
|
||||
/// List items (states).
|
||||
pub items: Vec<T>,
|
||||
/// State that can be modified by TUI.
|
||||
pub state: ListState,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
/// Constructs a new instance of `StatefulList`.
|
||||
pub fn new(items: Vec<T>, mut state: ListState) -> StatefulList<T> {
|
||||
state.select(Some(0));
|
||||
Self { items, state }
|
||||
}
|
||||
|
||||
/// Construct a new `StatefulList` with given items.
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
Self::new(items, ListState::default())
|
||||
}
|
||||
|
||||
/// Returns the selected item.
|
||||
pub fn selected(&self) -> Option<&T> {
|
||||
self.items.get(self.state.selected()?)
|
||||
}
|
||||
|
||||
/// Selects the next item.
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
/// Selects the previous item.
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_stateful_list() {
|
||||
let mut list = StatefulList::with_items(vec!["data1", "data2", "data3"]);
|
||||
list.state.select(Some(1));
|
||||
assert_eq!(Some(&"data2"), list.selected());
|
||||
list.next();
|
||||
assert_eq!(Some(2), list.state.selected());
|
||||
list.previous();
|
||||
assert_eq!(Some(1), list.state.selected());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue