From 76b6eb3903d5960394fd21d6afb671e9fbffb07a Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 10 Aug 2024 08:30:23 +0100 Subject: [PATCH] feat(tui): (experimental) tui option When the 'tui' feature is enabled, then server start accepts an optional --ui parameter. When specified a ratatui ui will display, showing liveness and a ping update when a valid config is loaded. --- crates/cli/Cargo.toml | 2 +- crates/cli/src/main.rs | 22 +++++- crates/cli/src/server/actor/handlers/mod.rs | 1 + .../handlers/receive_valid_app_config.rs | 1 + .../actor/handlers/subscribe_updates.rs | 10 +++ crates/cli/src/server/actor/messages.rs | 28 +++++++ crates/cli/src/server/actor/mod.rs | 11 ++- crates/cli/src/server/mod.rs | 56 ++++++++++---- crates/cli/src/tui/actor/handlers/mod.rs | 4 + .../src/tui/actor/handlers/server_update.rs | 28 +++++++ crates/cli/src/tui/actor/handlers/tick.rs | 57 ++++++++++++++ crates/cli/src/tui/actor/messages.rs | 4 + crates/cli/src/tui/actor/mod.rs | 75 +++++++++++++++++++ crates/cli/src/tui/mod.rs | 5 ++ crates/core/src/git/repository/open/otest.rs | 1 - 15 files changed, 283 insertions(+), 22 deletions(-) create mode 100644 crates/cli/src/server/actor/handlers/subscribe_updates.rs create mode 100644 crates/cli/src/tui/actor/handlers/mod.rs create mode 100644 crates/cli/src/tui/actor/handlers/server_update.rs create mode 100644 crates/cli/src/tui/actor/handlers/tick.rs create mode 100644 crates/cli/src/tui/actor/messages.rs create mode 100644 crates/cli/src/tui/actor/mod.rs create mode 100644 crates/cli/src/tui/mod.rs diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e1ff60d..afa3cac 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,7 +12,7 @@ keywords = { workspace = true } categories = { workspace = true } [features] -default = ["forgejo", "github", "tui"] +default = ["forgejo", "github"] forgejo = ["git-next-forge-forgejo"] github = ["git-next-forge-github"] tui = ["ratatui"] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 8b8f021..f054c2c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -5,10 +5,13 @@ mod forge; mod init; mod repo; mod server; -mod webhook; + +#[cfg(feature = "tui")] +mod tui; #[cfg(test)] mod tests; +mod webhook; use git_next_core::git; @@ -33,7 +36,12 @@ enum Command { #[derive(Parser, Debug)] enum Server { Init, - Start, + Start { + /// Display a UI (experimental) + #[cfg(feature = "tui")] + #[arg(long, required = false)] + ui: bool, + }, } fn main() -> Result<()> { @@ -50,9 +58,15 @@ fn main() -> Result<()> { Server::Init => { server::init(&fs)?; } - Server::Start => { + #[cfg(not(feature = "tui"))] + Server::Start {} => { let sleep_duration = std::time::Duration::from_secs(10); - server::start(fs, net, repository_factory, sleep_duration)?; + server::start(false, fs, net, repository_factory, sleep_duration)?; + } + #[cfg(feature = "tui")] + Server::Start { ui } => { + let sleep_duration = std::time::Duration::from_secs(10); + server::start(ui, fs, net, repository_factory, sleep_duration)?; } }, } diff --git a/crates/cli/src/server/actor/handlers/mod.rs b/crates/cli/src/server/actor/handlers/mod.rs index f488dce..3633000 100644 --- a/crates/cli/src/server/actor/handlers/mod.rs +++ b/crates/cli/src/server/actor/handlers/mod.rs @@ -2,3 +2,4 @@ mod file_updated; mod receive_app_config; mod receive_valid_app_config; mod shutdown; +mod subscribe_updates; diff --git a/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs b/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs index 2e3b3cd..6380c67 100644 --- a/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs @@ -71,6 +71,7 @@ impl Handler for ServerActor { let shout = app_config.shout().clone(); self.app_config.replace(app_config); self.alerts.do_send(UpdateShout::new(shout)); + self.send_server_updates(); } } diff --git a/crates/cli/src/server/actor/handlers/subscribe_updates.rs b/crates/cli/src/server/actor/handlers/subscribe_updates.rs new file mode 100644 index 0000000..a11a310 --- /dev/null +++ b/crates/cli/src/server/actor/handlers/subscribe_updates.rs @@ -0,0 +1,10 @@ +use crate::server::actor::{messages::SubscribeToUpdates, ServerActor}; + +// +impl actix::Handler for ServerActor { + type Result = (); + + fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result { + self.subscribers.push(msg.unwrap()); + } +} diff --git a/crates/cli/src/server/actor/messages.rs b/crates/cli/src/server/actor/messages.rs index 1a7e1dd..d361890 100644 --- a/crates/cli/src/server/actor/messages.rs +++ b/crates/cli/src/server/actor/messages.rs @@ -1,9 +1,12 @@ +use actix::{Message, Recipient}; //- use derive_more::Constructor; use git_next_core::{ + git::graph::Log, message, server::{AppConfig, Storage}, + ForgeAlias, RepoAlias, RepoBranches, }; use std::net::SocketAddr; @@ -33,3 +36,28 @@ message!( ); message!(Shutdown, "Notification to shutdown the server actor"); + +#[derive(Clone, Debug, PartialEq, Eq, Message)] +#[rtype(result = "()")] +pub enum ServerUpdate { + /// Status of a repo + UpdateRepoSummary { + forge_alias: ForgeAlias, + repo_alias: RepoAlias, + branches: RepoBranches, + log: Log, + }, + /// remove a repo + RemoveRepo { + forge_alias: ForgeAlias, + repo_alias: RepoAlias, + }, + /// test message + Ping, +} + +message!( + SubscribeToUpdates, + Recipient, + "Subscribe to receive updates from the server" +); diff --git a/crates/cli/src/server/actor/mod.rs b/crates/cli/src/server/actor/mod.rs index fab2b90..d87ebe8 100644 --- a/crates/cli/src/server/actor/mod.rs +++ b/crates/cli/src/server/actor/mod.rs @@ -1,6 +1,6 @@ // use actix::prelude::*; -use messages::ReceiveAppConfig; +use messages::{ReceiveAppConfig, ServerUpdate}; use tracing::error; #[cfg(test)] @@ -58,6 +58,8 @@ pub struct ServerActor { sleep_duration: std::time::Duration, repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr>, + subscribers: Vec>, + // testing message_log: Option>>>, } @@ -82,6 +84,7 @@ impl ServerActor { net, alerts, repository_factory: repo, + subscribers: Vec::default(), sleep_duration, repo_actors: BTreeMap::new(), message_log: None, @@ -239,4 +242,10 @@ impl ServerActor { ctx.address().do_send(msg); } } + + fn send_server_updates(&self) { + self.subscribers.iter().for_each(|subscriber| { + subscriber.do_send(ServerUpdate::Ping); + }); + } } diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index 8acabe4..211304c 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -1,16 +1,20 @@ // -mod actor; +pub mod actor; #[cfg(test)] mod tests; use actix::prelude::*; +use actix_rt::signal; use crate::{ alerts::{AlertsActor, History}, file_watcher::{watch_file, FileUpdated}, }; -use actor::ServerActor; + +#[allow(clippy::module_name_repetitions)] +pub use actor::ServerActor; + use git_next_core::git::RepositoryFactory; use anyhow::{Context, Result}; @@ -38,26 +42,23 @@ pub fn init(fs: &FileSystem) -> Result<()> { } pub fn start( + ui: bool, fs: FileSystem, net: Network, repo: Box, sleep_duration: std::time::Duration, ) -> Result<()> { - init_logging(); + if !ui { + init_logging(); + } let execution = async move { info!("Starting Alert Dispatcher..."); let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start(); info!("Starting Server..."); - let server = ServerActor::new( - fs.clone(), - net.clone(), - alerts_addr.clone(), - repo, - sleep_duration, - ) - .start(); + let server = + ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); server.do_send(FileUpdated); info!("Starting File Watcher..."); @@ -65,14 +66,39 @@ pub fn start( let fw_shutdown = watch_file("git-next-server.toml".into(), server.clone().recipient()) .expect("file watcher"); - info!("Server running - Press Ctrl-C to stop..."); - let _ = actix_rt::signal::ctrl_c().await; - info!("Ctrl-C received, shutting down..."); + if ui { + #[cfg(feature = "tui")] + { + use crate::server::actor::messages::SubscribeToUpdates; + use crate::tui; + use std::sync::mpsc::channel; + + let (tx_shutdown, rx_shutdown) = channel::<()>(); + let tui_addr = tui::Tui::new(tx_shutdown).start(); + // tui_addr.do_send(tui::Tick); + let _ = tui_addr.send(tui::Tick).await; + server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient())); + loop { + let _ = tui_addr.send(tui::Tick).await; + if rx_shutdown.try_recv().is_ok() { + break; + } + // actix_rt::task::yield_now().await; + } + } + } else { + info!("Server running - Press Ctrl-C to stop..."); + let _ = signal::ctrl_c().await; + info!("Ctrl-C received, shutting down..."); + } + + // shutdown + fw_shutdown.store(true, Ordering::Relaxed); server.do_send(crate::server::actor::messages::Shutdown); actix_rt::time::sleep(std::time::Duration::from_millis(200)).await; System::current().stop(); - fw_shutdown.store(true, Ordering::Relaxed); }; + let system = System::new(); Arbiter::current().spawn(execution); system.run()?; diff --git a/crates/cli/src/tui/actor/handlers/mod.rs b/crates/cli/src/tui/actor/handlers/mod.rs new file mode 100644 index 0000000..38b5dea --- /dev/null +++ b/crates/cli/src/tui/actor/handlers/mod.rs @@ -0,0 +1,4 @@ +// +mod server_update; +mod tick; + diff --git a/crates/cli/src/tui/actor/handlers/server_update.rs b/crates/cli/src/tui/actor/handlers/server_update.rs new file mode 100644 index 0000000..d5c6bad --- /dev/null +++ b/crates/cli/src/tui/actor/handlers/server_update.rs @@ -0,0 +1,28 @@ +use std::time::Instant; + +use actix::Handler; + +use crate::{server::actor::messages::ServerUpdate, tui::Tui}; + +// +impl Handler for Tui { + type Result = (); + + fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { + match msg { + ServerUpdate::UpdateRepoSummary { + forge_alias, + repo_alias, + branches, + log, + } => todo!(), + ServerUpdate::RemoveRepo { + forge_alias, + repo_alias, + } => todo!(), + ServerUpdate::Ping => { + self.last_ping = Instant::now(); + } + } + } +} diff --git a/crates/cli/src/tui/actor/handlers/tick.rs b/crates/cli/src/tui/actor/handlers/tick.rs new file mode 100644 index 0000000..215bf1f --- /dev/null +++ b/crates/cli/src/tui/actor/handlers/tick.rs @@ -0,0 +1,57 @@ +// +use std::{borrow::BorrowMut, time::Instant}; + +use actix::{ActorContext, Handler}; +use ratatui::{ + crossterm::event::{self, KeyCode, KeyEventKind}, + style::Stylize as _, + widgets::Paragraph, +}; + +use crate::tui::actor::{messages::Tick, Tui}; + +impl Handler for Tui { + type Result = std::io::Result<()>; + + fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result { + if let Some(terminal) = self.terminal.borrow_mut() { + terminal.draw(|frame| { + let area = frame.area(); + frame.render_widget( + Paragraph::new(format!( + "(press 'q' to quit) Ping:[{:?}] UI:[{:?}]", + self.last_ping, + Instant::now() + )) + .white() + .on_blue(), + area, + ); + })?; + } else { + eprintln!("No terminal setup"); + } + if event::poll(std::time::Duration::from_millis(16))? { + if let event::Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => { + // execute!(stderr(), LeaveAlternateScreen)?; + // disable_raw_mode()?; + ctx.stop(); + if let Err(err) = self.signal_shutdown.send(()) { + tracing::error!(?err, "Failed to signal shutdown"); + } + } + KeyCode::Esc => { + // + } + _ => (), + } + } + } + } + + Ok(()) + } +} diff --git a/crates/cli/src/tui/actor/messages.rs b/crates/cli/src/tui/actor/messages.rs new file mode 100644 index 0000000..3af3fc1 --- /dev/null +++ b/crates/cli/src/tui/actor/messages.rs @@ -0,0 +1,4 @@ +// +use git_next_core::message; + +message!(Tick => std::io::Result<()>, "Start the TUI"); diff --git a/crates/cli/src/tui/actor/mod.rs b/crates/cli/src/tui/actor/mod.rs new file mode 100644 index 0000000..b00b4a3 --- /dev/null +++ b/crates/cli/src/tui/actor/mod.rs @@ -0,0 +1,75 @@ +// +mod handlers; +pub mod messages; + +use std::{ + io::{stderr, Stderr}, + sync::mpsc::Sender, + time::Instant, +}; + +use actix::{Actor, Context}; + +use ratatui::{ + crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + }, + prelude::CrosstermBackend, + Terminal, +}; + +#[derive(Debug)] +pub struct Tui { + terminal: Option>>, + signal_shutdown: Sender<()>, + last_ping: Instant, +} +impl Actor for Tui { + type Context = Context; + fn started(&mut self, _ctx: &mut Self::Context) { + match init() { + Err(err) => { + eprintln!("Failed to enable raw mode: {err:?}"); + } + Ok(terminal) => { + self.terminal.replace(terminal); + } + } + } + fn stopped(&mut self, _ctx: &mut Self::Context) { + if let Err(err) = restore() { + match std::env::consts::OS { + "linux" | "macos" => { + eprintln!( + "Failed to restore terminal: Type `reset` to restore terminal: {err:?}" + ); + } + "windows" => { + println!("Failed to restore terminal: Reopen a new terminal: {err:?}"); + } + _ => println!("Failed to restore terminal: {err:?}"), + } + } + } +} +impl Tui { + pub fn new(signal_shutdown: Sender<()>) -> Self { + Self { + terminal: None, + signal_shutdown, + last_ping: Instant::now(), + } + } +} + +fn init() -> std::io::Result>> { + execute!(stderr(), EnterAlternateScreen)?; + enable_raw_mode()?; + Terminal::new(CrosstermBackend::new(stderr())) +} + +fn restore() -> std::io::Result<()> { + execute!(stderr(), LeaveAlternateScreen)?; + disable_raw_mode() +} diff --git a/crates/cli/src/tui/mod.rs b/crates/cli/src/tui/mod.rs new file mode 100644 index 0000000..508e8de --- /dev/null +++ b/crates/cli/src/tui/mod.rs @@ -0,0 +1,5 @@ +// +mod actor; + +pub use actor::messages::Tick; +pub use actor::Tui; diff --git a/crates/core/src/git/repository/open/otest.rs b/crates/core/src/git/repository/open/otest.rs index 04d7fa8..f36c3f9 100644 --- a/crates/core/src/git/repository/open/otest.rs +++ b/crates/core/src/git/repository/open/otest.rs @@ -99,7 +99,6 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository { .fetch_counter .read() .map_err(|_| git::fetch::Error::Lock)?; - println!("Fetch: {i}"); self.fetch_counter .write() .map_err(|_| git::fetch::Error::Lock)