diff --git a/candid_tui/src/app.rs b/candid_tui/src/app.rs index 8f5304d..9c12ade 100644 --- a/candid_tui/src/app.rs +++ b/candid_tui/src/app.rs @@ -1,21 +1,46 @@ +//! Module for maintaining app state + use crate::util::ListState; use candid_client::*; +/// Container for everything related to application state pub struct AppState<'a> { + /// The title to be displayed in the top left pub title: &'a str, + + /// Whether or not the app should terminate pub should_quit: bool, + + /// The connection to the CANdid server, from which frames are read pub server: CandidConnection, + + /// The frames that have been received from the server to this point pub frame_history: ListState<&'a str>, } impl<'a> AppState<'a> { + /// Initialize a new app pub fn new(title: &'a str, server: CandidConnection) -> AppState<'a> { AppState { title, should_quit: false, server, - frame_history: ListState::new(Vec::new()), + frame_history: ListState::new(), + } + } + + pub fn on_key(&mut self, c: char) { + match c { + 'q' => self.should_quit = true, + _ => {} } } + + pub fn on_up(&mut self) { + self.frame_history.previous(); + } + pub fn on_down(&mut self) { + self.frame_history.next(); + } } diff --git a/candid_tui/src/event.rs b/candid_tui/src/event.rs new file mode 100644 index 0000000..c50b35e --- /dev/null +++ b/candid_tui/src/event.rs @@ -0,0 +1,73 @@ +//! A module for configuring and executing event loops. An `Events` instance will listen to `stdin +//! for user input and read frames from the server, aggregating these events to be sent back to the +//! app itself. +//! +//! All code in this module is *heavily* inspired by [`tui-rs`'s demo +//! code](https://github.com/fdehau/tui-rs/blob/master/examples/util/event.rs) + +use std::sync::mpsc; +use std::{io, thread}; + +use termion::event::Key; +use termion::input::TermRead; + +pub enum Event { + Input(I), +} + +/// An event handler that wraps termion input and tick events. +/// Each event thype is handled in its own thread and returned to a common `Receiver` +pub struct Events { + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, + // TODO: Add thread for reading from server +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + + let input_handle = { + let tx = tx.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + match evt { + Ok(key) => { + if let Err(_) = tx.send(Event::Input(key)) { + return; + } + if key == config.exit_key { + return; + } + } + Err(_) => {} + } + } + }) + }; + + Events { rx, input_handle } + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + } + } +} diff --git a/candid_tui/src/main.rs b/candid_tui/src/main.rs index 99ea1dc..ab9fabd 100644 --- a/candid_tui/src/main.rs +++ b/candid_tui/src/main.rs @@ -1,15 +1,18 @@ mod app; +mod event; mod ui; mod util; use candid_client::*; use crate::app::AppState; +use crate::event::{Event, Events}; use std::io; use clap::{App, Arg}; +use termion::event::Key; use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; @@ -43,11 +46,33 @@ fn main() { // Initialize the app state let mut app = AppState::new("CANdid TUI", server); - app.frame_history.push("frame 1"); + app.frame_history.push("frame 1"); // Placeholder frames for now app.frame_history.push("frame 2"); app.frame_history.push("frame 3"); app.frame_history.push("frame 4"); app.frame_history.push("frame 5"); - ui::draw(&mut terminal, &app).unwrap(); + + // Initialize the event aggregator + let events = Events::new(); + + loop { + // Refresh the ui + ui::draw(&mut terminal, &app).unwrap(); + + // Handle an incoming event + match events.next().unwrap() { + Event::Input(key) => match key { + Key::Char(c) => app.on_key(c), + Key::Up => app.on_up(), + Key::Down => app.on_down(), + _ => {} + }, + } + + // Exit if necessary, set in app.on_key(q) + if app.should_quit { + break; + } + } std::thread::sleep(std::time::Duration::from_millis(1000)); } diff --git a/candid_tui/src/ui.rs b/candid_tui/src/ui.rs index b5e1e17..8b2be02 100644 --- a/candid_tui/src/ui.rs +++ b/candid_tui/src/ui.rs @@ -10,12 +10,12 @@ use tui::Terminal; pub fn draw(terminal: &mut Terminal, app: &AppState) -> Result<(), io::Error> { terminal.draw(|mut f| { let size = f.size(); - // Box, size of the terminal, all borders, title from app state + // List all the frames received to this point SelectableList::default() .block(Block::default().borders(Borders::ALL).title(app.title)) .items(&app.frame_history.items) .select(Some(app.frame_history.selected)) - //.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)) + .highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)) .highlight_symbol(">") .render(&mut f, size) }) diff --git a/candid_tui/src/util.rs b/candid_tui/src/util.rs index dfb1db4..481e934 100644 --- a/candid_tui/src/util.rs +++ b/candid_tui/src/util.rs @@ -1,23 +1,31 @@ +/// Keeps track of the position in a list, ued for SelectableLists pub struct ListState { pub items: Vec, pub selected: usize, } impl ListState { - pub fn new(items: Vec) -> ListState { - ListState { items, selected: 0 } + /// Initialize a new ListState with no items + pub fn new() -> ListState { + ListState { + items: Vec::new(), + selected: 0, + } } + /// Add an item to the end of the list pub fn push(&mut self, item: I) { self.items.push(item); } + /// Select the previous item in the list pub fn previous(&mut self) { if self.selected > 0 { self.selected -= 1; } } + /// Select the next item in the list pub fn next(&mut self) { if self.selected < self.items.len() - 1 { self.selected += 1