From 3ffd587b05d255e7571a70f0828fe7dc18f0aab7 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Jan 2023 10:54:02 -0800 Subject: [PATCH] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20initia?= =?UTF-8?q?l=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created using spr 1.3.4 --- wicket/src/lib.rs | 381 +-------------------------- wicket/src/screens/component.rs | 13 +- wicket/src/screens/mod.rs | 7 +- wicket/src/screens/rack.rs | 11 +- wicket/src/screens/splash.rs | 18 +- wicket/src/wicketd.rs | 3 +- wicket/src/widgets/screen_button.rs | 3 +- wicket/src/wizard.rs | 383 ++++++++++++++++++++++++++++ 8 files changed, 400 insertions(+), 419 deletions(-) create mode 100644 wicket/src/wizard.rs diff --git a/wicket/src/lib.rs b/wicket/src/lib.rs index 71b669dcc6b..2da59d13367 100644 --- a/wicket/src/lib.rs +++ b/wicket/src/lib.rs @@ -9,389 +9,12 @@ //! that will guide the user through the steps the need to take //! in an intuitive manner. -use crossterm::event::Event as TermEvent; -use crossterm::event::EventStream; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use crossterm::event::{MouseEvent, MouseEventKind}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, - LeaveAlternateScreen, -}; -use futures::StreamExt; -use slog::{error, info, Drain}; -use std::io::{stdout, Stdout}; -use std::net::SocketAddrV6; -use std::sync::mpsc::{channel, Receiver, Sender}; -use tokio::time::{interval, Duration}; -use tui::backend::CrosstermBackend; -use tui::Terminal; - pub(crate) mod defaults; pub(crate) mod inventory; mod screens; pub mod update; mod wicketd; mod widgets; +mod wizard; -use inventory::Inventory; -use screens::{Height, ScreenId, Screens}; -use wicketd::{WicketdHandle, WicketdManager}; -use wicketd_client::types::RackV1Inventory; -use widgets::RackState; - -pub const MARGIN: Height = Height(5); - -// We can avoid a bunch of unnecessary type parameters by picking them ahead of time. -pub type Term = Terminal>; -pub type Frame<'a> = tui::Frame<'a, CrosstermBackend>; - -/// The core type of this library is the `Wizard`. -/// -/// A `Wizard` manages a set of screens, where each screen represents a -/// specific step in the user process. Each screen is drawable, and the -/// active screen is rendered on every tick. The [`Wizard`] manages which -/// screen is active, issues the rendering operation to the terminal, and -/// communicates with other threads and async tasks to receive user input -/// and drive backend services. -pub struct Wizard { - // The currently active screen - active_screen: ScreenId, - - // All the screens managed by the [`Wizard`] - screens: Screens, - - // The [`Wizard`] is purely single threaded. Every interaction with the - // outside world is via channels. All receiving from the outside world - // comes in via an `Event` over a single channel. - // - // Doing this allows us to record and replay all received events, which - // will deterministically draw the output of the UI, as long as we disable - // any output to downstream services. - // - // This effectively acts as a way to mock real responses from servers - // without ever having to run those servers or even send the requests that - // triggered the incoming events! - // - // Note that for resize events or other terminal specific events we'll - // likely have to "output" them to fake the same interaction. - events_rx: Receiver, - - // We save a copy here so we can hand it out to event producers - events_tx: Sender, - - // The internal state of the Wizard - // This contains all updatable data - state: State, - - // A mechanism for interacting with `wicketd` - #[allow(unused)] - wicketd: WicketdHandle, - - // When the Wizard is run, this will be extracted and moved - // into a tokio task. - wicketd_manager: Option, - - // The terminal we are rendering to - terminal: Term, - - // Our friendly neighborhood logger - log: slog::Logger, - - // The tokio runtime for everything outside the main thread - tokio_rt: tokio::runtime::Runtime, -} - -#[allow(clippy::new_without_default)] -impl Wizard { - pub fn new() -> Wizard { - // TODO: make this configurable? - let wicketd_addr: SocketAddrV6 = "[::1]:8000".parse().unwrap(); - let log = Self::setup_log("/tmp/wicket.log").unwrap(); - let screens = Screens::new(&log); - let (events_tx, events_rx) = channel(); - let state = State::new(); - let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend).unwrap(); - let tokio_rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - let (wicketd, wicketd_manager) = - WicketdManager::new(&log, events_tx.clone(), wicketd_addr); - Wizard { - screens, - active_screen: ScreenId::Splash, - events_rx, - events_tx, - state, - wicketd, - wicketd_manager: Some(wicketd_manager), - terminal, - log, - tokio_rt, - } - } - - pub fn setup_log(path: &str) -> anyhow::Result { - let file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - - let decorator = slog_term::PlainDecorator::new(file); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - Ok(slog::Logger::root(drain, slog::o!())) - } - - pub fn run(&mut self) -> anyhow::Result<()> { - self.start_tokio_runtime(); - enable_raw_mode()?; - execute!( - self.terminal.backend_mut(), - EnterAlternateScreen, - EnableMouseCapture - )?; - self.mainloop()?; - disable_raw_mode()?; - execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - Ok(()) - } - - fn mainloop(&mut self) -> anyhow::Result<()> { - info!(self.log, "Starting main loop"); - let rect = self.terminal.get_frame().size(); - // Size the rack for the initial draw - self.state.rack_state.resize(rect.width, rect.height, &MARGIN); - - // Draw the initial screen - let screen = self.screens.get_mut(self.active_screen); - screen.resize(&mut self.state, rect.width, rect.height); - screen.draw(&self.state, &mut self.terminal)?; - - loop { - let screen = self.screens.get_mut(self.active_screen); - // unwrap is safe because we always hold onto a Sender - let event = self.events_rx.recv().unwrap(); - match event { - Event::Tick => { - let actions = screen.on(&mut self.state, ScreenEvent::Tick); - self.handle_actions(actions)?; - } - Event::Term(TermEvent::Key(key_event)) => { - if is_control_c(&key_event) { - info!(self.log, "CTRL-C Pressed. Exiting."); - break; - } - let actions = screen.on( - &mut self.state, - ScreenEvent::Term(TermEvent::Key(key_event)), - ); - self.handle_actions(actions)?; - } - Event::Term(TermEvent::Resize(width, height)) => { - self.state.rack_state.resize(width, height, &MARGIN); - screen.resize(&mut self.state, width, height); - screen.draw(&self.state, &mut self.terminal)?; - } - Event::Term(TermEvent::Mouse(mouse_event)) => { - self.state.mouse = - Point { x: mouse_event.column, y: mouse_event.row }; - let actions = screen.on( - &mut self.state, - ScreenEvent::Term(TermEvent::Mouse(mouse_event)), - ); - self.handle_actions(actions)?; - } - Event::Inventory(inventory) => { - if let Err(e) = - self.state.inventory.update_inventory(inventory) - { - error!(self.log, "Failed to update inventory: {e}",); - } else { - // Inventory changed. Redraw the screen. - screen.draw(&self.state, &mut self.terminal)?; - } - } - _ => info!(self.log, "{:?}", event), - } - } - Ok(()) - } - - fn handle_actions(&mut self, actions: Vec) -> anyhow::Result<()> { - for action in actions { - match action { - Action::Redraw => { - let screen = self.screens.get_mut(self.active_screen); - screen.draw(&self.state, &mut self.terminal)?; - } - Action::SwitchScreen(id) => { - self.active_screen = id; - let screen = self.screens.get_mut(id); - let rect = self.terminal.get_frame().size(); - - screen.resize(&mut self.state, rect.width, rect.height); - - // Simulate a mouse movement for the current position - // because the mouse may be in a different position when transitioning - // between screens. - let mouse_event = MouseEvent { - kind: MouseEventKind::Moved, - column: self.state.mouse.x, - row: self.state.mouse.y, - modifiers: KeyModifiers::NONE, - }; - let event = - ScreenEvent::Term(TermEvent::Mouse(mouse_event)); - // We ignore actions, as they can only be draw actions, and - // we are about to draw. - let _ = screen.on(&mut self.state, event); - screen.draw(&self.state, &mut self.terminal)?; - } - } - } - Ok(()) - } - - fn start_tokio_runtime(&mut self) { - let events_tx = self.events_tx.clone(); - let log = self.log.clone(); - let wicketd_manager = self.wicketd_manager.take().unwrap(); - self.tokio_rt.block_on(async { - run_event_listener(log.clone(), events_tx).await; - tokio::spawn(async move { - wicketd_manager.run().await; - }); - }); - } -} - -fn is_control_c(key_event: &KeyEvent) -> bool { - key_event.code == KeyCode::Char('c') - && key_event.modifiers == KeyModifiers::CONTROL -} - -/// Listen for terminal related events -async fn run_event_listener(log: slog::Logger, events_tx: Sender) { - info!(log, "Starting event listener"); - tokio::spawn(async move { - let mut events = EventStream::new(); - let mut ticker = interval(Duration::from_millis(30)); - loop { - tokio::select! { - _ = ticker.tick() => { - if events_tx.send(Event::Tick).is_err() { - info!(log, "Event listener completed"); - // The receiver was dropped. Program is ending. - return; - } - } - event = events.next() => { - let event = match event { - None => { - error!(log, "Event stream completed. Shutting down."); - return; - } - Some(Ok(event)) => event, - Some(Err(e)) => { - // TODO: Issue a shutdown - error!(log, "Failed to receive event: {:?}", e); - return; - } - }; - if events_tx.send(Event::Term(event)).is_err() { - info!(log, "Event listener completed"); - // The receiver was dropped. Program is ending. - return; - } - - } - } - } - }); -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct Point { - pub x: u16, - pub y: u16, -} - -/// The data state of the Wizard -/// -/// Data is not tied to any specific screen and is updated upon event receipt. -#[derive(Debug)] -pub struct State { - pub inventory: Inventory, - pub rack_state: RackState, - pub mouse: Point, -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -impl State { - pub fn new() -> State { - State { - inventory: Inventory::default(), - rack_state: RackState::new(), - mouse: Point::default(), - } - } -} - -/// Send requests to RSS -/// -/// Replies come in as [`Event`]s -pub struct RssManager {} - -/// An event that will update state in the wizard -/// -/// This can be a keypress, mouse event, or response from a downstream service. -#[derive(Debug)] -pub enum Event { - /// An input event from the terminal - Term(TermEvent), - - /// An Inventory Update Event - Inventory(RackV1Inventory), - - /// The tick of a Timer - /// This can be used to draw a frame to the terminal - Tick, - //... TODO: Replies from MGS & RSS -} - -/// An action for the system to take. -/// -/// This can be something like a screen transition or calling a downstream -/// service. Screens never take actions directly, but they are the only ones -/// that know what visual content an input such as a key press or mouse event -/// is meant for and what action should be taken in that case. -pub enum Action { - Redraw, - SwitchScreen(ScreenId), -} - -/// Events sent to a screen -/// -/// These are a subset of [`Event`] -pub enum ScreenEvent { - /// An input event from the terminal - Term(crossterm::event::Event), - - /// The tick of a timer - Tick, -} +pub use crate::wizard::*; diff --git a/wicket/src/screens/component.rs b/wicket/src/screens/component.rs index 197b4d1a222..c57604e5ce8 100644 --- a/wicket/src/screens/component.rs +++ b/wicket/src/screens/component.rs @@ -9,16 +9,13 @@ use crate::defaults::colors::*; use crate::defaults::dimensions::RectExt; use crate::defaults::dimensions::MENUBAR_HEIGHT; use crate::defaults::style; +use crate::screens::ScreenId; use crate::widgets::Control; use crate::widgets::ControlId; use crate::widgets::HelpMenuState; use crate::widgets::{HelpButton, HelpButtonState, HelpMenu}; use crate::widgets::{ScreenButton, ScreenButtonState}; -use crate::Action; -use crate::Frame; -use crate::ScreenEvent; -use crate::ScreenId; -use crate::State; +use crate::wizard::{Action, Frame, ScreenEvent, State, Term}; use crossterm::event::Event as TermEvent; use crossterm::event::{ KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, @@ -303,11 +300,7 @@ impl ComponentScreen { } impl Screen for ComponentScreen { - fn draw( - &self, - state: &State, - terminal: &mut crate::Term, - ) -> anyhow::Result<()> { + fn draw(&self, state: &State, terminal: &mut Term) -> anyhow::Result<()> { terminal.draw(|f| { self.draw_background(f); self.draw_menubar(f, state); diff --git a/wicket/src/screens/mod.rs b/wicket/src/screens/mod.rs index 5e202046e4b..2e5a5a5f1dd 100644 --- a/wicket/src/screens/mod.rs +++ b/wicket/src/screens/mod.rs @@ -6,11 +6,8 @@ mod component; mod rack; mod splash; -use crate::Action; -use crate::ScreenEvent; -use crate::State; -use crate::Term; -use crate::TermEvent; +use crate::wizard::{Action, ScreenEvent, State, Term}; +use crossterm::event::Event as TermEvent; use slog::Logger; use component::ComponentScreen; diff --git a/wicket/src/screens/rack.rs b/wicket/src/screens/rack.rs index eee384b7d16..11cee49a7be 100644 --- a/wicket/src/screens/rack.rs +++ b/wicket/src/screens/rack.rs @@ -12,10 +12,7 @@ use crate::widgets::Control; use crate::widgets::ControlId; use crate::widgets::HelpMenuState; use crate::widgets::{Banner, HelpButton, HelpButtonState, HelpMenu, Rack}; -use crate::Action; -use crate::Frame; -use crate::ScreenEvent; -use crate::State; +use crate::wizard::{Action, Frame, ScreenEvent, State, Term}; use crossterm::event::Event as TermEvent; use crossterm::event::{ KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, @@ -276,11 +273,7 @@ impl RackScreen { } impl Screen for RackScreen { - fn draw( - &self, - state: &State, - terminal: &mut crate::Term, - ) -> anyhow::Result<()> { + fn draw(&self, state: &State, terminal: &mut Term) -> anyhow::Result<()> { terminal.draw(|f| { self.draw_background(f); self.draw_rack(state, f); diff --git a/wicket/src/screens/splash.rs b/wicket/src/screens/splash.rs index 4e489567f77..dd7b170f931 100644 --- a/wicket/src/screens/splash.rs +++ b/wicket/src/screens/splash.rs @@ -10,10 +10,8 @@ use super::{Screen, ScreenId}; use crate::defaults::colors::*; use crate::defaults::dimensions::RectExt; use crate::widgets::{Logo, LogoState, LOGO_HEIGHT, LOGO_WIDTH}; -use crate::Action; -use crate::Frame; -use crate::ScreenEvent; -use crate::TermEvent; +use crate::wizard::{Action, Frame, ScreenEvent, State, Term}; +use crossterm::event::Event as TermEvent; use tui::style::{Color, Style}; use tui::widgets::Block; @@ -61,11 +59,7 @@ impl SplashScreen { } impl Screen for SplashScreen { - fn draw( - &self, - _state: &crate::State, - terminal: &mut crate::Term, - ) -> anyhow::Result<()> { + fn draw(&self, _state: &State, terminal: &mut Term) -> anyhow::Result<()> { terminal.draw(|f| { self.draw_background(f); self.animate_logo(f); @@ -73,11 +67,7 @@ impl Screen for SplashScreen { Ok(()) } - fn on( - &mut self, - _state: &mut crate::State, - event: ScreenEvent, - ) -> Vec { + fn on(&mut self, _state: &mut State, event: ScreenEvent) -> Vec { match event { ScreenEvent::Tick => { self.state.frame += 1; diff --git a/wicket/src/wicketd.rs b/wicket/src/wicketd.rs index 9529b525870..51db0ab741f 100644 --- a/wicket/src/wicketd.rs +++ b/wicket/src/wicketd.rs @@ -4,7 +4,6 @@ //! Code for talking to wicketd -use crate::Event; use slog::{debug, o, warn, Logger}; use std::net::SocketAddrV6; use std::sync::mpsc::Sender; @@ -12,6 +11,8 @@ use tokio::sync::mpsc; use tokio::time::{interval, Duration, MissedTickBehavior}; use wicketd_client::types::RackV1Inventory; +use crate::wizard::Event; + const WICKETD_POLL_INTERVAL: Duration = Duration::from_secs(5); const WICKETD_TIMEOUT_MS: u32 = 1000; diff --git a/wicket/src/widgets/screen_button.rs b/wicket/src/widgets/screen_button.rs index cc2fd297cc9..41178a4de45 100644 --- a/wicket/src/widgets/screen_button.rs +++ b/wicket/src/widgets/screen_button.rs @@ -4,10 +4,11 @@ //! A help button that brings up a help menu when selected +use crate::screens::ScreenId; + use super::get_control_id; use super::Control; use super::ControlId; -use crate::ScreenId; use tui::buffer::Buffer; use tui::layout::Rect; use tui::style::Style; diff --git a/wicket/src/wizard.rs b/wicket/src/wizard.rs new file mode 100644 index 00000000000..91025c08ac7 --- /dev/null +++ b/wicket/src/wizard.rs @@ -0,0 +1,383 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crossterm::event::Event as TermEvent; +use crossterm::event::EventStream; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{MouseEvent, MouseEventKind}; +use crossterm::execute; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, + LeaveAlternateScreen, +}; +use futures::StreamExt; +use slog::{error, info, Drain}; +use std::io::{stdout, Stdout}; +use std::net::SocketAddrV6; +use std::sync::mpsc::{channel, Receiver, Sender}; +use tokio::time::{interval, Duration}; +use tui::backend::CrosstermBackend; +use tui::Terminal; + +use crate::inventory::Inventory; +use crate::screens::{Height, ScreenId, Screens}; +use crate::wicketd::{WicketdHandle, WicketdManager}; +use crate::widgets::RackState; +use wicketd_client::types::RackV1Inventory; + +pub const MARGIN: Height = Height(5); + +// We can avoid a bunch of unnecessary type parameters by picking them ahead of time. +pub type Term = Terminal>; +pub type Frame<'a> = tui::Frame<'a, CrosstermBackend>; + +/// The core type of this library is the `Wizard`. +/// +/// A `Wizard` manages a set of screens, where each screen represents a +/// specific step in the user process. Each screen is drawable, and the +/// active screen is rendered on every tick. The [`Wizard`] manages which +/// screen is active, issues the rendering operation to the terminal, and +/// communicates with other threads and async tasks to receive user input +/// and drive backend services. +pub struct Wizard { + // The currently active screen + active_screen: ScreenId, + + // All the screens managed by the [`Wizard`] + screens: Screens, + + // The [`Wizard`] is purely single threaded. Every interaction with the + // outside world is via channels. All receiving from the outside world + // comes in via an `Event` over a single channel. + // + // Doing this allows us to record and replay all received events, which + // will deterministically draw the output of the UI, as long as we disable + // any output to downstream services. + // + // This effectively acts as a way to mock real responses from servers + // without ever having to run those servers or even send the requests that + // triggered the incoming events! + // + // Note that for resize events or other terminal specific events we'll + // likely have to "output" them to fake the same interaction. + events_rx: Receiver, + + // We save a copy here so we can hand it out to event producers + events_tx: Sender, + + // The internal state of the Wizard + // This contains all updatable data + state: State, + + // A mechanism for interacting with `wicketd` + #[allow(unused)] + wicketd: WicketdHandle, + + // When the Wizard is run, this will be extracted and moved + // into a tokio task. + wicketd_manager: Option, + + // The terminal we are rendering to + terminal: Term, + + // Our friendly neighborhood logger + log: slog::Logger, + + // The tokio runtime for everything outside the main thread + tokio_rt: tokio::runtime::Runtime, +} + +#[allow(clippy::new_without_default)] +impl Wizard { + pub fn new() -> Wizard { + // TODO: make this configurable? + let wicketd_addr: SocketAddrV6 = "[::1]:8000".parse().unwrap(); + let log = Self::setup_log("/tmp/wicket.log").unwrap(); + let screens = Screens::new(&log); + let (events_tx, events_rx) = channel(); + let state = State::new(); + let backend = CrosstermBackend::new(stdout()); + let terminal = Terminal::new(backend).unwrap(); + let tokio_rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + let (wicketd, wicketd_manager) = + WicketdManager::new(&log, events_tx.clone(), wicketd_addr); + Wizard { + screens, + active_screen: ScreenId::Splash, + events_rx, + events_tx, + state, + wicketd, + wicketd_manager: Some(wicketd_manager), + terminal, + log, + tokio_rt, + } + } + + pub fn setup_log(path: &str) -> anyhow::Result { + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + + let decorator = slog_term::PlainDecorator::new(file); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + + Ok(slog::Logger::root(drain, slog::o!())) + } + + pub fn run(&mut self) -> anyhow::Result<()> { + self.start_tokio_runtime(); + enable_raw_mode()?; + execute!( + self.terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + self.mainloop()?; + disable_raw_mode()?; + execute!( + self.terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + Ok(()) + } + + fn mainloop(&mut self) -> anyhow::Result<()> { + info!(self.log, "Starting main loop"); + let rect = self.terminal.get_frame().size(); + // Size the rack for the initial draw + self.state.rack_state.resize(rect.width, rect.height, &MARGIN); + + // Draw the initial screen + let screen = self.screens.get_mut(self.active_screen); + screen.resize(&mut self.state, rect.width, rect.height); + screen.draw(&self.state, &mut self.terminal)?; + + loop { + let screen = self.screens.get_mut(self.active_screen); + // unwrap is safe because we always hold onto a Sender + let event = self.events_rx.recv().unwrap(); + match event { + Event::Tick => { + let actions = screen.on(&mut self.state, ScreenEvent::Tick); + self.handle_actions(actions)?; + } + Event::Term(TermEvent::Key(key_event)) => { + if is_control_c(&key_event) { + info!(self.log, "CTRL-C Pressed. Exiting."); + break; + } + let actions = screen.on( + &mut self.state, + ScreenEvent::Term(TermEvent::Key(key_event)), + ); + self.handle_actions(actions)?; + } + Event::Term(TermEvent::Resize(width, height)) => { + self.state.rack_state.resize(width, height, &MARGIN); + screen.resize(&mut self.state, width, height); + screen.draw(&self.state, &mut self.terminal)?; + } + Event::Term(TermEvent::Mouse(mouse_event)) => { + self.state.mouse = + Point { x: mouse_event.column, y: mouse_event.row }; + let actions = screen.on( + &mut self.state, + ScreenEvent::Term(TermEvent::Mouse(mouse_event)), + ); + self.handle_actions(actions)?; + } + Event::Inventory(inventory) => { + if let Err(e) = + self.state.inventory.update_inventory(inventory) + { + error!(self.log, "Failed to update inventory: {e}",); + } else { + // Inventory changed. Redraw the screen. + screen.draw(&self.state, &mut self.terminal)?; + } + } + _ => info!(self.log, "{:?}", event), + } + } + Ok(()) + } + + fn handle_actions(&mut self, actions: Vec) -> anyhow::Result<()> { + for action in actions { + match action { + Action::Redraw => { + let screen = self.screens.get_mut(self.active_screen); + screen.draw(&self.state, &mut self.terminal)?; + } + Action::SwitchScreen(id) => { + self.active_screen = id; + let screen = self.screens.get_mut(id); + let rect = self.terminal.get_frame().size(); + + screen.resize(&mut self.state, rect.width, rect.height); + + // Simulate a mouse movement for the current position + // because the mouse may be in a different position when transitioning + // between screens. + let mouse_event = MouseEvent { + kind: MouseEventKind::Moved, + column: self.state.mouse.x, + row: self.state.mouse.y, + modifiers: KeyModifiers::NONE, + }; + let event = + ScreenEvent::Term(TermEvent::Mouse(mouse_event)); + // We ignore actions, as they can only be draw actions, and + // we are about to draw. + let _ = screen.on(&mut self.state, event); + screen.draw(&self.state, &mut self.terminal)?; + } + } + } + Ok(()) + } + + fn start_tokio_runtime(&mut self) { + let events_tx = self.events_tx.clone(); + let log = self.log.clone(); + let wicketd_manager = self.wicketd_manager.take().unwrap(); + self.tokio_rt.block_on(async { + run_event_listener(log.clone(), events_tx).await; + tokio::spawn(async move { + wicketd_manager.run().await; + }); + }); + } +} + +fn is_control_c(key_event: &KeyEvent) -> bool { + key_event.code == KeyCode::Char('c') + && key_event.modifiers == KeyModifiers::CONTROL +} + +/// Listen for terminal related events +async fn run_event_listener(log: slog::Logger, events_tx: Sender) { + info!(log, "Starting event listener"); + tokio::spawn(async move { + let mut events = EventStream::new(); + let mut ticker = interval(Duration::from_millis(30)); + loop { + tokio::select! { + _ = ticker.tick() => { + if events_tx.send(Event::Tick).is_err() { + info!(log, "Event listener completed"); + // The receiver was dropped. Program is ending. + return; + } + } + event = events.next() => { + let event = match event { + None => { + error!(log, "Event stream completed. Shutting down."); + return; + } + Some(Ok(event)) => event, + Some(Err(e)) => { + // TODO: Issue a shutdown + error!(log, "Failed to receive event: {:?}", e); + return; + } + }; + if events_tx.send(Event::Term(event)).is_err() { + info!(log, "Event listener completed"); + // The receiver was dropped. Program is ending. + return; + } + + } + } + } + }); +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Point { + pub x: u16, + pub y: u16, +} + +/// The data state of the Wizard +/// +/// Data is not tied to any specific screen and is updated upon event receipt. +#[derive(Debug)] +pub struct State { + pub inventory: Inventory, + pub rack_state: RackState, + pub mouse: Point, +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +impl State { + pub fn new() -> State { + State { + inventory: Inventory::default(), + rack_state: RackState::new(), + mouse: Point::default(), + } + } +} + +/// Send requests to RSS +/// +/// Replies come in as [`Event`]s +pub struct RssManager {} + +/// An event that will update state in the wizard +/// +/// This can be a keypress, mouse event, or response from a downstream service. +#[derive(Debug)] +pub enum Event { + /// An input event from the terminal + Term(TermEvent), + + /// An Inventory Update Event + Inventory(RackV1Inventory), + + /// The tick of a Timer + /// This can be used to draw a frame to the terminal + Tick, + //... TODO: Replies from MGS & RSS +} + +/// An action for the system to take. +/// +/// This can be something like a screen transition or calling a downstream +/// service. Screens never take actions directly, but they are the only ones +/// that know what visual content an input such as a key press or mouse event +/// is meant for and what action should be taken in that case. +pub enum Action { + Redraw, + SwitchScreen(ScreenId), +} + +/// Events sent to a screen +/// +/// These are a subset of [`Event`] +pub enum ScreenEvent { + /// An input event from the terminal + Term(crossterm::event::Event), + + /// The tick of a timer + Tick, +}