WIP: TUI actor
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful

This commit is contained in:
Paul Campbell 2024-08-10 08:30:23 +01:00
parent 08d2377404
commit 4d19f9e98f
7 changed files with 174 additions and 19 deletions

View file

@ -5,10 +5,13 @@ mod forge;
mod init; mod init;
mod repo; mod repo;
mod server; mod server;
mod webhook;
#[cfg(feature = "tui")]
mod tui;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod webhook;
use git_next_core::git; use git_next_core::git;
@ -33,7 +36,11 @@ enum Command {
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
enum Server { enum Server {
Init, Init,
Start, Start {
/// Display a UI (experimental)
#[arg(long, required = false)]
ui: bool,
},
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -50,9 +57,9 @@ fn main() -> Result<()> {
Server::Init => { Server::Init => {
server::init(&fs)?; server::init(&fs)?;
} }
Server::Start => { Server::Start { ui } => {
let sleep_duration = std::time::Duration::from_secs(10); 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)?;
} }
}, },
} }

View file

@ -1,5 +1,5 @@
// //
mod actor; pub mod actor;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -10,7 +10,7 @@ use crate::{
alerts::{AlertsActor, History}, alerts::{AlertsActor, History},
file_watcher::{watch_file, FileUpdated}, file_watcher::{watch_file, FileUpdated},
}; };
use actor::ServerActor; pub use actor::ServerActor;
use git_next_core::git::RepositoryFactory; use git_next_core::git::RepositoryFactory;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -38,26 +38,23 @@ pub fn init(fs: &FileSystem) -> Result<()> {
} }
pub fn start( pub fn start(
ui: bool,
fs: FileSystem, fs: FileSystem,
net: Network, net: Network,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Result<()> { ) -> Result<()> {
init_logging(); if !ui {
init_logging();
}
let execution = async move { let execution = async move {
info!("Starting Alert Dispatcher..."); info!("Starting Alert Dispatcher...");
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start(); let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
info!("Starting Server..."); info!("Starting Server...");
let server = ServerActor::new( let server =
fs.clone(), ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
net.clone(),
alerts_addr.clone(),
repo,
sleep_duration,
)
.start();
server.do_send(FileUpdated); server.do_send(FileUpdated);
info!("Starting File Watcher..."); info!("Starting File Watcher...");
@ -65,14 +62,28 @@ pub fn start(
let fw_shutdown = watch_file("git-next-server.toml".into(), server.clone().recipient()) let fw_shutdown = watch_file("git-next-server.toml".into(), server.clone().recipient())
.expect("file watcher"); .expect("file watcher");
info!("Server running - Press Ctrl-C to stop..."); if ui {
let _ = actix_rt::signal::ctrl_c().await; let (tx, rx) = std::sync::mpsc::channel::<()>();
info!("Ctrl-C received, shutting down..."); 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); server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(200)).await; actix_rt::time::sleep(std::time::Duration::from_millis(200)).await;
System::current().stop(); System::current().stop();
fw_shutdown.store(true, Ordering::Relaxed);
}; };
let system = System::new(); let system = System::new();
Arbiter::current().spawn(execution); Arbiter::current().spawn(execution);
system.run()?; system.run()?;

View file

@ -0,0 +1,2 @@
//
mod tick;

View file

@ -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<Tick> 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(())
}
}

View file

@ -0,0 +1,4 @@
//
use git_next_core::message;
message!(Tick => std::io::Result<()>, "Start the TUI");

View file

@ -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<Terminal<CrosstermBackend<Stderr>>>,
signal_shutdown: Sender<()>,
}
impl Actor for Tui {
type Context = Context<Self>;
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<Terminal<CrosstermBackend<Stderr>>> {
execute!(stderr(), EnterAlternateScreen)?;
enable_raw_mode()?;
Terminal::new(CrosstermBackend::new(stderr()))
}
fn restore() -> std::io::Result<()> {
execute!(stderr(), LeaveAlternateScreen)?;
disable_raw_mode()
}

View file

@ -0,0 +1,5 @@
//
mod actor;
pub use actor::messages::Tick;
pub use actor::Tui;