From 0288bdb0ad824e4c62ccd6e3011637c18636dde6 Mon Sep 17 00:00:00 2001 From: Soispha Date: Mon, 24 Jul 2023 23:53:21 +0200 Subject: [PATCH] Feat(ui): Add status panel, which shows command statuses and errors --- src/tui_app/app/command_interface.rs | 1 + .../event_types/event/handlers/command.rs | 98 +++++++++----- .../app/events/event_types/event/mod.rs | 20 +-- src/tui_app/app/status.rs | 50 ++++++- src/tui_app/ui/central/mod.rs | 124 +++++++++++++++++- src/tui_app/ui/central/update/mod.rs | 103 ++------------- .../central/update/widgets/command_monitor.rs | 34 +++++ src/tui_app/ui/central/update/widgets/mod.rs | 1 + 8 files changed, 282 insertions(+), 149 deletions(-) create mode 100644 src/tui_app/ui/central/update/widgets/command_monitor.rs diff --git a/src/tui_app/app/command_interface.rs b/src/tui_app/app/command_interface.rs index 59a9740..610c28c 100644 --- a/src/tui_app/app/command_interface.rs +++ b/src/tui_app/app/command_interface.rs @@ -2,6 +2,7 @@ use cli_log::debug; #[derive(Debug)] pub enum Command { + RaiseError(String), Greet(String), Exit, CommandLineShow, diff --git a/src/tui_app/app/events/event_types/event/handlers/command.rs b/src/tui_app/app/events/event_types/event/handlers/command.rs index 4a3f9dc..241a533 100644 --- a/src/tui_app/app/events/event_types/event/handlers/command.rs +++ b/src/tui_app/app/events/event_types/event/handlers/command.rs @@ -1,55 +1,84 @@ -use crate::{ - app::{command_interface::Command, events::event_types::EventStatus, status::State, App}, - ui::central::InputPosition, -}; -use anyhow::Result; -use cli_log::info; +use crate::app::{command_interface::Command, events::event_types::EventStatus, App}; +use anyhow::{Context, Result}; +use cli_log::{info, warn, trace}; +use tokio::sync::mpsc; pub async fn handle( app: &mut App<'_>, command: &Command, - send_output: bool, -) -> Result<(EventStatus, String)> { - macro_rules! set_status_output { + output_callback: &Option>, +) -> Result { + // A command returns both _status output_ (what you would normally print to stderr) + // and _main output_ (the output which is normally printed to stdout). + // We simulate these by returning the main output to the lua function, and printing the + // status output to a status ui field. + // + // Every function should return some status output to show the user, that something is + // happening, while only some functions return some value to the main output, as this + // is reserved for functions called only for their output (for example `greet()`). + macro_rules! send_status_output { ($str:expr) => { - if send_output { - app.ui.set_command_output($str); + app.status.add_status_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_status_message(format!($str, $($args),+)); + }; + } + macro_rules! send_error_output { + ($str:expr) => { + app.status.add_error_message($str.to_owned()); + }; + ($str:expr, $($args:ident),+) => { + app.status.add_error_message(format!($str, $($args),+)); + }; + } + macro_rules! send_main_output { + ($str:expr) => { + if let Some(sender) = output_callback { + sender + .send($str.to_owned()) + .await + .context("Failed to send command main output")?; } }; ($str:expr, $($args:ident),+) => { - if send_output { - app.ui.set_command_output(&format!($str, $($args),+)); + if let Some(sender) = output_callback { + sender + .send(format!($str, $($args),+)) + .await + .context("Failed to send command main output")?; } }; } - info!("Handling command: {:#?}", command); + + trace!("Handling command: {:#?}", command); Ok(match command { - Command::Exit => ( - EventStatus::Terminate, - "Terminated the application".to_owned(), - ), + Command::Exit => { + send_status_output!("Terminating the application.."); + EventStatus::Terminate + } Command::CommandLineShow => { app.ui.cli_enable(); - set_status_output!("CLI online"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI online"); + EventStatus::Ok } Command::CommandLineHide => { app.ui.cli_disable(); - set_status_output!("CLI offline"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("CLI offline"); + EventStatus::Ok } Command::CyclePlanes => { app.ui.cycle_main_input_position(); - set_status_output!("Switched main input position"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position"); + EventStatus::Ok } Command::CyclePlanesRev => { app.ui.cycle_main_input_position_rev(); - set_status_output!("Switched main input position; reversed"); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Switched main input position; reversed"); + EventStatus::Ok } Command::SetModeNormal => { @@ -67,15 +96,22 @@ pub async fn handle( Command::RoomMessageSend(msg) => { if let Some(room) = app.status.room_mut() { room.send(msg.clone()).await?; + } else { + // TODO(@Soispha): Should this raise a lua error? It could be very confusing, + // when a user doesn't read the log. + warn!("Can't send message: `{}`, as there is no open room!", &msg); } - set_status_output!("Send message: `{}`", msg); - (EventStatus::Ok, "".to_owned()) + send_status_output!("Send message: `{}`", msg); + EventStatus::Ok } Command::Greet(name) => { - info!("Greated {}", name); - set_status_output!("Hi, {}!", name); - (EventStatus::Ok, "".to_owned()) + send_main_output!("Hi, {}!", name); + EventStatus::Ok } Command::Help(_) => todo!(), + Command::RaiseError(err) => { + send_error_output!(err); + EventStatus::Ok + }, }) } diff --git a/src/tui_app/app/events/event_types/event/mod.rs b/src/tui_app/app/events/event_types/event/mod.rs index c279439..309e506 100644 --- a/src/tui_app/app/events/event_types/event/mod.rs +++ b/src/tui_app/app/events/event_types/event/mod.rs @@ -1,7 +1,7 @@ mod handlers; use anyhow::{Context, Result}; -use cli_log::{info, trace}; +use cli_log::trace; use crossterm::event::Event as CrosstermEvent; use tokio::sync::mpsc::Sender; @@ -27,22 +27,12 @@ impl Event { .await .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), - Event::CommandEvent(event, callback_tx) => { - let (result, output) = command::handle(app, event, callback_tx.is_some()) - .await - .with_context(|| format!("Failed to handle command event: `{:#?}`", event))?; - - if let Some(callback_tx) = callback_tx { - callback_tx - .send(output.clone()) - .await - .with_context(|| format!("Failed to send command output: {}", output))?; - } - Ok(result) - } + Event::CommandEvent(event, callback_tx) => command::handle(app, event, callback_tx) + .await + .with_context(|| format!("Failed to handle command event: `{:#?}`", event)), Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned()) .await - .with_context(|| format!("Failed to handle lua code: `{:#?}`", lua_code)), + .with_context(|| format!("Failed to handle lua code: `{}`", lua_code)), Event::InputEvent(event) => match app.status.state() { State::Normal => main::handle_normal(app, event) diff --git a/src/tui_app/app/status.rs b/src/tui_app/app/status.rs index 0334301..770249a 100644 --- a/src/tui_app/app/status.rs +++ b/src/tui_app/app/status.rs @@ -28,6 +28,19 @@ pub struct Room { view_scroll: Option, } +pub struct StatusMessage { + content: String, + is_error: bool, +} +impl StatusMessage { + pub fn content(&self) -> String { + self.content.clone() + } + pub fn is_error(&self) -> bool { + self.is_error + } +} + pub struct Status { state: State, account_name: String, @@ -36,6 +49,7 @@ pub struct Status { client: Option, rooms: IndexMap, current_room_id: String, + status_messages: Vec, } impl fmt::Display for State { @@ -137,15 +151,41 @@ impl Status { }; Self { - state: State::Normal, - account_name: "".to_string(), - account_user_id: "".to_string(), - client, + state: State::None, + account_name: "".to_owned(), + account_user_id: "".to_owned(), + client, rooms, - current_room_id: "".to_string(), + current_room_id: "".to_owned(), + status_messages: vec![StatusMessage { + content: "Initialized!".to_owned(), + is_error: false, + }], } } + pub fn add_status_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: false, + }) + } + pub fn add_error_message(&mut self, msg: String) { + // TODO(@Soispha): This could allocate a lot of ram, when we don't + // add a limit to the messages. + // This needs to be proven. + self.status_messages.push(StatusMessage { + content: msg, + is_error: true, + }) + } + pub fn status_messages(&self) -> &Vec { + &self.status_messages + } + pub fn account_name(&self) -> &String { &self.account_name } diff --git a/src/tui_app/ui/central/mod.rs b/src/tui_app/ui/central/mod.rs index b2bf4e1..c889406 100644 --- a/src/tui_app/ui/central/mod.rs +++ b/src/tui_app/ui/central/mod.rs @@ -11,12 +11,13 @@ use crossterm::{ }; use tui::{ backend::CrosstermBackend, + style::Color, widgets::{Block, Borders, ListState}, Terminal, }; use tui_textarea::TextArea; -use crate::ui::terminal_prepare; +use crate::ui::{terminal_prepare, textarea_inactivate, textarea_activate}; use super::setup; @@ -28,10 +29,121 @@ pub enum InputPosition { Rooms, Messages, MessageCompose, + CommandMonitor, RoomInfo, CLI, } +impl InputPosition { + // calculate to widgets colors, based of which widget is currently selected + pub fn colors( + &self, + mut cli: &mut Option>, + mut message_compose: &mut TextArea<'_>, + ) -> Vec { + match self { + InputPosition::Status => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Rooms => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::Messages => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::MessageCompose => { + textarea_activate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::RoomInfo => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + Color::DarkGray, + ] + } + InputPosition::CLI => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_activate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + ] + } + InputPosition::CommandMonitor => { + textarea_inactivate(&mut message_compose); + if let Some(cli) = &mut cli { + textarea_inactivate(cli); + } + vec![ + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::DarkGray, + Color::White, + ] + } + } + } +} + pub struct UI<'a> { terminal: Terminal>, input_position: InputPosition, @@ -89,7 +201,8 @@ impl UI<'_> { InputPosition::Status => InputPosition::Rooms, InputPosition::Rooms => InputPosition::Messages, InputPosition::Messages => InputPosition::MessageCompose, - InputPosition::MessageCompose => InputPosition::RoomInfo, + InputPosition::MessageCompose => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::RoomInfo, InputPosition::RoomInfo => match self.cli { Some(_) => InputPosition::CLI, None => InputPosition::Status, @@ -107,7 +220,8 @@ impl UI<'_> { InputPosition::Rooms => InputPosition::Status, InputPosition::Messages => InputPosition::Rooms, InputPosition::MessageCompose => InputPosition::Messages, - InputPosition::RoomInfo => InputPosition::MessageCompose, + InputPosition::RoomInfo => InputPosition::CommandMonitor, + InputPosition::CommandMonitor => InputPosition::MessageCompose, InputPosition::CLI => InputPosition::RoomInfo, }; } @@ -157,12 +271,12 @@ impl UI<'_> { } pub async fn update_setup(&mut self) -> Result<()> { - let ui = match &mut self.setup_ui { + let setup_ui = match &mut self.setup_ui { Some(c) => c, None => bail!("SetupUI instance not found"), }; - ui.update(&mut self.terminal).await?; + setup_ui.update(&mut self.terminal).await?; Ok(()) } diff --git a/src/tui_app/ui/central/update/mod.rs b/src/tui_app/ui/central/update/mod.rs index 2b1ed2d..67cfb31 100644 --- a/src/tui_app/ui/central/update/mod.rs +++ b/src/tui_app/ui/central/update/mod.rs @@ -1,20 +1,13 @@ use std::cmp; use anyhow::{Context, Result}; -use tui::{ - layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, -}; +use tui::layout::{Constraint, Direction, Layout}; -use crate::{ - app::status::Status, - ui::{textarea_activate, textarea_inactivate}, -}; +use crate::app::status::Status; -use self::widgets::{messages, room_info, rooms, status}; +use self::widgets::{command_monitor, messages, room_info, rooms, status}; -use super::{InputPosition, UI}; +use super::UI; pub mod widgets; @@ -66,90 +59,12 @@ impl UI<'_> { let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(4)].as_ref()) + .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref()) .split(main_chunks[2]); - // calculate to widgets colors, based of which widget is currently selected - let colors = match self.input_position { - InputPosition::Status => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Rooms => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::Messages => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::White, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::MessageCompose => { - textarea_activate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - InputPosition::RoomInfo => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_inactivate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::White, - ] - } - InputPosition::CLI => { - textarea_inactivate(&mut self.message_compose); - if let Some(cli) = &mut self.cli { - textarea_activate(cli); - } - vec![ - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - Color::DarkGray, - ] - } - }; + let colors = self + .input_position + .colors(&mut self.cli, &mut self.message_compose); // initiate the widgets let mode_indicator = Paragraph::new(status.state().to_string()) @@ -164,6 +79,7 @@ impl UI<'_> { let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) .context("Failed to initiate the messages widget")?; let room_info_panel = room_info::init(status.room(), &colors); + let command_monitor = command_monitor::init(status.status_messages(), &colors); // render the widgets self.terminal.draw(|frame| { @@ -177,6 +93,7 @@ impl UI<'_> { None => (), }; frame.render_widget(room_info_panel, right_chunks[0]); + frame.render_widget(command_monitor, right_chunks[1]); })?; Ok(()) diff --git a/src/tui_app/ui/central/update/widgets/command_monitor.rs b/src/tui_app/ui/central/update/widgets/command_monitor.rs new file mode 100644 index 0000000..9e548ed --- /dev/null +++ b/src/tui_app/ui/central/update/widgets/command_monitor.rs @@ -0,0 +1,34 @@ +use tui::{ + layout::Alignment, + style::{Color, Style}, + text::Text, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::{app::status::StatusMessage, ui::central::InputPosition}; + +pub fn init<'a>(status_events: &Vec, colors: &Vec) -> Paragraph<'a> { + let mut command_monitor = Text::default(); + + status_events.iter().for_each(|event| { + // TODO(@Soispha): The added text (`event.content()`) doesn't wrap nicely, + // it would be nice if it did. + command_monitor.extend(Text::styled( + event.content(), + Style::default().fg(if event.is_error() { + Color::Red + } else { + Color::Cyan + }), + )); + }); + + Paragraph::new(command_monitor) + .block( + Block::default() + .title("Command Montior") + .borders(Borders::ALL) + .style(Style::default().fg(colors[InputPosition::CommandMonitor as usize])), + ) + .alignment(Alignment::Center) +} diff --git a/src/tui_app/ui/central/update/widgets/mod.rs b/src/tui_app/ui/central/update/widgets/mod.rs index 850c27f..d145b95 100644 --- a/src/tui_app/ui/central/update/widgets/mod.rs +++ b/src/tui_app/ui/central/update/widgets/mod.rs @@ -2,3 +2,4 @@ pub mod messages; pub mod room_info; pub mod rooms; pub mod status; +pub mod command_monitor;