Compare commits

...

4 Commits

Author SHA1 Message Date
Benedikt Peetz 296ebdb0cd
Fix(handlers::main): Close ci after a command input 2023-07-24 23:54:45 +02:00
Benedikt Peetz fe3849a7b7
Feat(ui): Add status panel, which shows command statuses and errors 2023-07-24 23:53:21 +02:00
Benedikt Peetz d46a0c3a24
Fix(lua_command::handle): Move lua_command handler to separate thread
This makes it possible to have lua code execute commands and receive
their output value, without risking a deadlock.
2023-07-24 23:45:44 +02:00
Benedikt Peetz b67dbf8e31
Refactor(treewide): Remove the repl, reuse of e. handling is hard
The event handling is deeply ingrained in the ui code, the commands are
focused around the ui code, in short splitting of the event handling and
command system from the ui is intentionally hard and in my opinion not
really worth it right now.
2023-07-24 23:38:16 +02:00
16 changed files with 371 additions and 286 deletions

View File

@ -7,10 +7,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["cli"] default = ["tui"]
full = ["cli", "tui"]
cli = ["tokio/io-std"]
tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"] tui = ["dep:tui", "dep:tui-textarea", "dep:crossterm", "dep:tokio-util", "dep:serde", "dep:indexmap"]
[dependencies] [dependencies]
@ -18,7 +15,7 @@ clap = { version = "4.3.19", features = ["derive"] }
cli-log = "2.0" cli-log = "2.0"
anyhow = "1.0" anyhow = "1.0"
matrix-sdk = "0.6" matrix-sdk = "0.6"
tokio = { version = "1.29", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "io-std"] }
# lua stuff # lua stuff
lua_macros = { path = "./lua_macros" } lua_macros = { path = "./lua_macros" }

View File

@ -11,13 +11,7 @@ pub struct Args {
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum Command { pub enum Command {
/// Starts a repl, for the lua interface
#[cfg(feature = "cli")]
#[clap(value_parser)]
Repl {},
/// Starts the main tui client /// Starts the main tui client
#[cfg(feature = "tui")]
#[clap(value_parser)] #[clap(value_parser)]
Start {}, Start {},
} }

View File

@ -1,17 +1,12 @@
//mod app;
//mod ui;
//mod accounts;
mod cli; mod cli;
pub mod event_handler;
#[cfg(feature = "tui")]
mod tui_app; mod tui_app;
#[cfg(feature = "cli")]
mod repl;
use clap::Parser; use clap::Parser;
use crate::cli::{Args, Command}; use crate::cli::{Args, Command};
#[cfg(feature = "tui")]
pub use tui_app::*; pub use tui_app::*;
#[tokio::main] #[tokio::main]
@ -19,20 +14,8 @@ async fn main() -> anyhow::Result<()> {
cli_log::init_cli_log!(); cli_log::init_cli_log!();
let args = Args::parse(); let args = Args::parse();
let command = args.subcommand.unwrap_or( let command = args.subcommand.unwrap_or(Command::Start {});
#[cfg(all(feature = "tui", not(feature = "cli")))]
Command::Start {},
#[cfg(all(feature = "cli", not(feature = "tui")))]
Command::Repl {},
#[cfg(all(feature = "cli", feature = "tui"))]
Command::Start {},
);
match command { match command {
#[cfg(feature = "cli")]
Command::Repl {} => {
repl::run().await?;
}
#[cfg(feature = "tui")]
Command::Start {} => { Command::Start {} => {
let mut app = app::App::new()?; let mut app = app::App::new()?;
app.run().await?; app.run().await?;

View File

@ -1,41 +0,0 @@
use std::io::ErrorKind;
use anyhow::{Context, Result};
use cli_log::{info, warn};
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
pub async fn run() -> Result<()> {
let mut stdin = stdin();
let mut stdout = stdout();
let mut buffer = [0; 1];
let mut new_command = vec![];
loop {
stdout
.write("trinitrix:> ".as_bytes())
.await
.context("Failed to write prompt")?;
stdout.flush().await.context("Failed to flush prompt")?;
new_command.clear();
loop {
if let Err(err) = stdin.read_exact(&mut buffer).await {
if err.kind() == ErrorKind::UnexpectedEof {
warn!("Unexpected EOF, we assume the user quit.");
return Ok(());
} else {
Err(err).context("Failed to read next character")?;
}
}
if buffer == "\n".as_bytes() {
break;
} else {
new_command.append(&mut buffer.to_vec());
}
}
info!(
"Got user repl input: {}",
String::from_utf8(new_command.clone())
.context("Failed to convert user input to utf8 string")?
)
}
}

View File

@ -2,6 +2,7 @@ use cli_log::debug;
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
RaiseError(String),
Greet(String), Greet(String),
Exit, Exit,
CommandLineShow, CommandLineShow,

View File

@ -1,66 +1,105 @@
use crate::app::{command_interface::Command, events::event_types::EventStatus, App}; use crate::app::{command_interface::Command, events::event_types::EventStatus, App};
use anyhow::Result; use anyhow::{Context, Result};
use cli_log::info; use cli_log::{info, warn, trace};
use tokio::sync::mpsc;
pub async fn handle( pub async fn handle(
app: &mut App<'_>, app: &mut App<'_>,
command: &Command, command: &Command,
send_output: bool, output_callback: &Option<mpsc::Sender<String>>,
) -> Result<(EventStatus, String)> { ) -> Result<EventStatus> {
macro_rules! set_status_output { // 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) => { ($str:expr) => {
if send_output { app.status.add_status_message($str.to_owned());
app.ui.set_command_output($str); };
($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),+) => { ($str:expr, $($args:ident),+) => {
if send_output { if let Some(sender) = output_callback {
app.ui.set_command_output(&format!($str, $($args),+)); sender
.send(format!($str, $($args),+))
.await
.context("Failed to send command main output")?;
} }
}; };
} }
info!("Handling command: {:#?}", command);
trace!("Handling command: {:#?}", command);
Ok(match command { Ok(match command {
Command::Exit => ( Command::Exit => {
EventStatus::Terminate, send_status_output!("Terminating the application..");
"Terminated the application".to_owned(), EventStatus::Terminate
), }
Command::CommandLineShow => { Command::CommandLineShow => {
app.ui.cli_enable(); app.ui.cli_enable();
set_status_output!("CLI online"); send_status_output!("CLI online");
(EventStatus::Ok, "".to_owned()) EventStatus::Ok
} }
Command::CommandLineHide => { Command::CommandLineHide => {
app.ui.cli_disable(); app.ui.cli_disable();
set_status_output!("CLI offline"); send_status_output!("CLI offline");
(EventStatus::Ok, "".to_owned()) EventStatus::Ok
} }
Command::CyclePlanes => { Command::CyclePlanes => {
app.ui.cycle_main_input_position(); app.ui.cycle_main_input_position();
set_status_output!("Switched main input position"); send_status_output!("Switched main input position");
(EventStatus::Ok, "".to_owned()) EventStatus::Ok
} }
Command::CyclePlanesRev => { Command::CyclePlanesRev => {
app.ui.cycle_main_input_position_rev(); app.ui.cycle_main_input_position_rev();
set_status_output!("Switched main input position; reversed"); send_status_output!("Switched main input position; reversed");
(EventStatus::Ok, "".to_owned()) EventStatus::Ok
} }
Command::RoomMessageSend(msg) => { Command::RoomMessageSend(msg) => {
if let Some(room) = app.status.room_mut() { if let Some(room) = app.status.room_mut() {
room.send(msg.clone()).await?; 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); send_status_output!("Send message: `{}`", msg);
(EventStatus::Ok, "".to_owned()) EventStatus::Ok
} }
Command::Greet(name) => { Command::Greet(name) => {
info!("Greated {}", name); send_main_output!("Hi, {}!", name);
set_status_output!("Hi, {}!", name); EventStatus::Ok
(EventStatus::Ok, "".to_owned())
} }
Command::Help(_) => todo!(), Command::Help(_) => todo!(),
Command::RaiseError(err) => {
send_error_output!(err);
EventStatus::Ok
},
}) })
} }

View File

@ -1,38 +1,14 @@
use std::{sync::Arc, time::Duration}; use anyhow::Result;
use cli_log::trace;
use anyhow::{Context, Result};
use cli_log::{debug, info};
use tokio::{task, time::timeout};
use crate::app::{events::event_types::EventStatus, App}; use crate::app::{events::event_types::EventStatus, App};
// This function is here mainly to reserve this spot for further processing of the lua command.
// TODO(@Soispha): Move the lua executor thread code from app to this module
pub async fn handle(app: &mut App<'_>, command: String) -> Result<EventStatus> { pub async fn handle(app: &mut App<'_>, command: String) -> Result<EventStatus> {
info!("Recieved ci command: `{command}`; executing.."); trace!("Recieved ci command: `{command}`; executing..");
let local = task::LocalSet::new(); app.lua_command_tx.send(command).await?;
// Run the local task set.
let output = local
.run_until(async move {
let lua = Arc::clone(&app.lua);
debug!("before_handle");
let c_handle = task::spawn_local(async move {
lua.load(&command)
// FIXME this assumes string output only
.eval_async::<String>()
.await
.with_context(|| format!("Failed to execute: `{command}`"))
});
debug!("after_handle");
c_handle
})
.await;
debug!("after_thread");
let output = timeout(Duration::from_secs(10), output)
.await
.context("Failed to join lua command executor")???;
info!("Command returned: `{}`", output);
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
} }

View File

@ -150,26 +150,29 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
}; };
} }
central::InputPosition::CLI => { central::InputPosition::CLI => {
if let Some(_) = app.ui.cli { if let Some(cli) = &app.ui.cli {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
}) => { }) => {
let ci_event = app.ui let ci_event = cli
.cli .lines()
.as_mut() .get(0)
.expect("This is already checked") .expect(
.lines() "One line always exists,
.get(0) and others can't exists
.expect( because we collect on
"There can only be one line in the buffer, as we collect it on enter being inputted" enter",
) )
.to_owned(); .to_owned();
app.tx app.tx
.send(Event::LuaCommand(ci_event)) .send(Event::LuaCommand(ci_event))
.await .await
.context("Failed to send lua command to internal event stream")?; .context("Failed to send lua command to internal event stream")?;
app.tx
.send(Event::CommandEvent(Command::CommandLineHide, None))
.await?;
} }
_ => { _ => {
app.ui app.ui

View File

@ -1,7 +1,7 @@
mod handlers; mod handlers;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cli_log::{info, trace}; use cli_log::trace;
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@ -27,22 +27,12 @@ impl Event {
.await .await
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)), .with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)),
Event::CommandEvent(event, callback_tx) => { Event::CommandEvent(event, callback_tx) => command::handle(app, event, callback_tx)
let (result, output) = command::handle(app, event, callback_tx.is_some()) .await
.await .with_context(|| format!("Failed to handle command event: `{:#?}`", event)),
.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::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned()) Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned())
.await .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() { Event::InputEvent(event) => match app.status.state() {
State::None => unreachable!( State::None => unreachable!(

View File

@ -2,22 +2,26 @@ pub mod command_interface;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::path::Path; use std::{path::Path, thread};
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use cli_log::info; use cli_log::{error, info};
use matrix_sdk::Client; use matrix_sdk::Client;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use status::{State, Status}; use status::{State, Status};
use tokio::{ use tokio::{
runtime::Builder,
sync::{mpsc, Mutex}, sync::{mpsc, Mutex},
task::LocalSet, task::{self, LocalSet},
}; };
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
accounts::{Account, AccountsManager}, accounts::{Account, AccountsManager},
app::{command_interface::generate_ci_functions, events::event_types::Event}, app::{
command_interface::{generate_ci_functions, Command},
events::event_types::Event,
},
ui::{central, setup}, ui::{central, setup},
}; };
@ -39,34 +43,66 @@ pub struct App<'ui> {
impl App<'_> { impl App<'_> {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
fn set_up_lua(tx: mpsc::Sender<Event>) -> mpsc::Sender<String> { fn set_up_lua(event_call_tx: mpsc::Sender<Event>) -> mpsc::Sender<String> {
async fn exec_lua_command(
command: &str,
event_call_tx: mpsc::Sender<Event>,
) -> Result<()> {
let second_event_call_tx = event_call_tx.clone();
let lua = LUA
.get_or_init(|| {
Mutex::new(generate_ci_functions(
mlua::Lua::new(),
second_event_call_tx,
))
})
.lock()
.await;
info!("Recieved command to execute: `{}`", &command);
let output = lua
.load(command)
// FIXME this assumes string output only
.eval_async::<String>()
.await;
match output {
Ok(out) => {
info!("Function `{}` returned: `{}`", command, out);
}
Err(err) => {
error!("Function `{}` returned error: `{}`", command, err);
event_call_tx
.send(Event::CommandEvent(
Command::RaiseError(err.to_string()),
None,
))
.await?;
}
};
Ok(())
}
info!("Setting up Lua context.."); info!("Setting up Lua context..");
static LUA: OnceCell<Mutex<mlua::Lua>> = OnceCell::new(); static LUA: OnceCell<Mutex<mlua::Lua>> = OnceCell::new();
let (lua_command_tx, mut rx) = mpsc::channel(256); let (lua_command_tx, mut rx) = mpsc::channel::<String>(256);
let local_set = LocalSet::new(); thread::spawn(move || {
local_set.spawn_local(async move { let rt = Builder::new_current_thread().enable_all().build().expect(
let lua = LUA "Should always be able to build tokio runtime for lua command handling",
.get_or_init(|| Mutex::new(generate_ci_functions(mlua::Lua::new(), tx))) );
.lock() let local = LocalSet::new();
.await; local.spawn_local(async move {
info!("Initialized Lua context"); info!("Lua command handling initialized, waiting for commands..");
while let Some(command) = rx.recv().await {
info!("Recieved lua command: {}", &command);
let local_event_call_tx = event_call_tx.clone();
while let Some(command) = rx.recv().await { task::spawn_local(async move {
info!("Recieved command to execute: `{}`", &command); exec_lua_command(&command, local_event_call_tx).await.expect("This should return all relevent errors by other messages, this should never error");
let output = lua });
.load(&command) }
// FIXME this assumes string output only });
.eval_async::<String>() rt.block_on(local);
.await
.with_context(|| format!("Failed to execute: `{command}`"));
info!(
"Function `{}` returned: `{}`",
command,
output.unwrap_or("<returned error>".to_owned())
);
}
}); });
lua_command_tx lua_command_tx

View File

@ -25,6 +25,19 @@ pub struct Room {
view_scroll: Option<usize>, view_scroll: Option<usize>,
} }
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 { pub struct Status {
state: State, state: State,
account_name: String, account_name: String,
@ -33,6 +46,7 @@ pub struct Status {
client: Option<Client>, client: Option<Client>,
rooms: IndexMap<String, Room>, rooms: IndexMap<String, Room>,
current_room_id: String, current_room_id: String,
status_messages: Vec<StatusMessage>,
} }
impl Room { impl Room {
@ -125,14 +139,40 @@ impl Status {
Self { Self {
state: State::None, state: State::None,
account_name: "".to_string(), account_name: "".to_owned(),
account_user_id: "".to_string(), account_user_id: "".to_owned(),
client, client,
rooms, 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<StatusMessage> {
&self.status_messages
}
pub fn account_name(&self) -> &String { pub fn account_name(&self) -> &String {
&self.account_name &self.account_name
} }

View File

@ -2,6 +2,6 @@ pub mod app;
pub mod ui; pub mod ui;
pub mod accounts; pub mod accounts;
pub use app::*; //pub use app::*;
pub use ui::*; //pub use ui::*;
pub use accounts::*; //pub use accounts::*;

View File

@ -11,12 +11,13 @@ use crossterm::{
}; };
use tui::{ use tui::{
backend::CrosstermBackend, backend::CrosstermBackend,
style::Color,
widgets::{Block, Borders, ListState}, widgets::{Block, Borders, ListState},
Terminal, Terminal,
}; };
use tui_textarea::TextArea; use tui_textarea::TextArea;
use crate::ui::terminal_prepare; use crate::ui::{terminal_prepare, textarea_inactivate, textarea_activate};
use super::setup; use super::setup;
@ -28,10 +29,121 @@ pub enum InputPosition {
Rooms, Rooms,
Messages, Messages,
MessageCompose, MessageCompose,
CommandMonitor,
RoomInfo, RoomInfo,
CLI, CLI,
} }
impl InputPosition {
// calculate to widgets colors, based of which widget is currently selected
pub fn colors(
&self,
mut cli: &mut Option<TextArea<'_>>,
mut message_compose: &mut TextArea<'_>,
) -> Vec<Color> {
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> { pub struct UI<'a> {
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
input_position: InputPosition, input_position: InputPosition,
@ -89,7 +201,8 @@ impl UI<'_> {
InputPosition::Status => InputPosition::Rooms, InputPosition::Status => InputPosition::Rooms,
InputPosition::Rooms => InputPosition::Messages, InputPosition::Rooms => InputPosition::Messages,
InputPosition::Messages => InputPosition::MessageCompose, InputPosition::Messages => InputPosition::MessageCompose,
InputPosition::MessageCompose => InputPosition::RoomInfo, InputPosition::MessageCompose => InputPosition::CommandMonitor,
InputPosition::CommandMonitor => InputPosition::RoomInfo,
InputPosition::RoomInfo => match self.cli { InputPosition::RoomInfo => match self.cli {
Some(_) => InputPosition::CLI, Some(_) => InputPosition::CLI,
None => InputPosition::Status, None => InputPosition::Status,
@ -107,7 +220,8 @@ impl UI<'_> {
InputPosition::Rooms => InputPosition::Status, InputPosition::Rooms => InputPosition::Status,
InputPosition::Messages => InputPosition::Rooms, InputPosition::Messages => InputPosition::Rooms,
InputPosition::MessageCompose => InputPosition::Messages, InputPosition::MessageCompose => InputPosition::Messages,
InputPosition::RoomInfo => InputPosition::MessageCompose, InputPosition::RoomInfo => InputPosition::CommandMonitor,
InputPosition::CommandMonitor => InputPosition::MessageCompose,
InputPosition::CLI => InputPosition::RoomInfo, InputPosition::CLI => InputPosition::RoomInfo,
}; };
} }
@ -153,12 +267,12 @@ impl UI<'_> {
} }
pub async fn update_setup(&mut self) -> Result<()> { 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, Some(c) => c,
None => bail!("SetupUI instance not found"), None => bail!("SetupUI instance not found"),
}; };
ui.update(&mut self.terminal).await?; setup_ui.update(&mut self.terminal).await?;
Ok(()) Ok(())
} }

View File

@ -1,19 +1,13 @@
use std::cmp; use std::cmp;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tui::{ use tui::layout::{Constraint, Direction, Layout};
layout::{Constraint, Direction, Layout},
style::Color,
};
use crate::{ use crate::app::status::Status;
app::status::Status,
ui::{textarea_activate, textarea_inactivate},
};
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; pub mod widgets;
@ -57,90 +51,12 @@ impl UI<'_> {
let right_chunks = Layout::default() let right_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(4)].as_ref()) .constraints([Constraint::Length(5), Constraint::Min(4)].as_ref())
.split(main_chunks[2]); .split(main_chunks[2]);
// calculate to widgets colors, based of which widget is currently selected let colors = self
let colors = match self.input_position { .input_position
InputPosition::Status => { .colors(&mut self.cli, &mut self.message_compose);
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,
]
}
};
// initiate the widgets // initiate the widgets
let status_panel = status::init(status, &colors); let status_panel = status::init(status, &colors);
@ -148,6 +64,7 @@ impl UI<'_> {
let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) let (messages_panel, mut messages_state) = messages::init(status.room(), &colors)
.context("Failed to initiate the messages widget")?; .context("Failed to initiate the messages widget")?;
let room_info_panel = room_info::init(status.room(), &colors); let room_info_panel = room_info::init(status.room(), &colors);
let command_monitor = command_monitor::init(status.status_messages(), &colors);
// render the widgets // render the widgets
self.terminal.draw(|frame| { self.terminal.draw(|frame| {
@ -160,6 +77,7 @@ impl UI<'_> {
None => (), None => (),
}; };
frame.render_widget(room_info_panel, right_chunks[0]); frame.render_widget(room_info_panel, right_chunks[0]);
frame.render_widget(command_monitor, right_chunks[1]);
})?; })?;
Ok(()) Ok(())

View File

@ -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<StatusMessage>, colors: &Vec<Color>) -> 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)
}

View File

@ -2,3 +2,4 @@ pub mod messages;
pub mod room_info; pub mod room_info;
pub mod rooms; pub mod rooms;
pub mod status; pub mod status;
pub mod command_monitor;