From 4d19f9e98f55bb8efd103b0bec4831a2681159de Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 10 Aug 2024 08:30:23 +0100 Subject: [PATCH] WIP: TUI actor --- crates/cli/src/main.rs | 15 +++-- crates/cli/src/server/mod.rs | 41 ++++++++----- crates/cli/src/tui/actor/handlers/mod.rs | 2 + crates/cli/src/tui/actor/handlers/tick.rs | 54 +++++++++++++++++ crates/cli/src/tui/actor/messages.rs | 4 ++ crates/cli/src/tui/actor/mod.rs | 72 +++++++++++++++++++++++ crates/cli/src/tui/mod.rs | 5 ++ 7 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 crates/cli/src/tui/actor/handlers/mod.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/src/main.rs b/crates/cli/src/main.rs index 8b8f021..be6cbdc 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,11 @@ enum Command { #[derive(Parser, Debug)] enum Server { Init, - Start, + Start { + /// Display a UI (experimental) + #[arg(long, required = false)] + ui: bool, + }, } fn main() -> Result<()> { @@ -50,9 +57,9 @@ fn main() -> Result<()> { Server::Init => { server::init(&fs)?; } - Server::Start => { + Server::Start { ui } => { let sleep_duration = std::time::Duration::from_secs(10); - server::start(fs, net, repository_factory, sleep_duration)?; + server::start(ui, fs, net, repository_factory, sleep_duration)?; } }, } diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index 8acabe4..0e763fd 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -1,5 +1,5 @@ // -mod actor; +pub mod actor; #[cfg(test)] mod tests; @@ -10,7 +10,7 @@ use crate::{ alerts::{AlertsActor, History}, file_watcher::{watch_file, FileUpdated}, }; -use actor::ServerActor; +pub use actor::ServerActor; use git_next_core::git::RepositoryFactory; use anyhow::{Context, Result}; @@ -38,26 +38,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 +62,28 @@ 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 { + let (tx, rx) = std::sync::mpsc::channel::<()>(); + actix_rt::task::spawn_blocking(|| { + println!("Start Terminal..."); + // TODO: how does server send messages to Tui? + crate::tui::Tui::new(tx).start().do_send(crate::tui::Tick); + }); + println!("Waiting for shutdown..."); + let _ = rx.recv(); // block until shutdown is signaled + } else { + info!("Server running - Press Ctrl-C to stop..."); + let _ = actix_rt::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..5aee20b --- /dev/null +++ b/crates/cli/src/tui/actor/handlers/mod.rs @@ -0,0 +1,2 @@ +// +mod tick; 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..b01a59a --- /dev/null +++ b/crates/cli/src/tui/actor/handlers/tick.rs @@ -0,0 +1,54 @@ +// +use std::borrow::BorrowMut; + +use actix::{ActorContext, AsyncContext, 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 { + ctx.notify_later(Tick, std::time::Duration::from_millis(16)); + if let Some(terminal) = self.terminal.borrow_mut() { + terminal.draw(|frame| { + let area = frame.area(); + frame.render_widget( + Paragraph::new("Hello Ratatui! (press 'q' to quit)") + .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..38844b8 --- /dev/null +++ b/crates/cli/src/tui/actor/mod.rs @@ -0,0 +1,72 @@ +// +mod handlers; +pub mod messages; + +use std::{ + io::{stderr, Stderr}, + sync::mpsc::Sender, +}; + +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<()>, +} +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 const fn new(signal_shutdown: Sender<()>) -> Self { + Self { + terminal: None, + signal_shutdown, + } + } +} + +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;