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.
This commit is contained in:
parent
c965586537
commit
76b6eb3903
15 changed files with 283 additions and 22 deletions
|
@ -12,7 +12,7 @@ keywords = { workspace = true }
|
||||||
categories = { workspace = true }
|
categories = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["forgejo", "github", "tui"]
|
default = ["forgejo", "github"]
|
||||||
forgejo = ["git-next-forge-forgejo"]
|
forgejo = ["git-next-forge-forgejo"]
|
||||||
github = ["git-next-forge-github"]
|
github = ["git-next-forge-github"]
|
||||||
tui = ["ratatui"]
|
tui = ["ratatui"]
|
||||||
|
|
|
@ -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,12 @@ enum Command {
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
enum Server {
|
enum Server {
|
||||||
Init,
|
Init,
|
||||||
Start,
|
Start {
|
||||||
|
/// Display a UI (experimental)
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
#[arg(long, required = false)]
|
||||||
|
ui: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
@ -50,9 +58,15 @@ fn main() -> Result<()> {
|
||||||
Server::Init => {
|
Server::Init => {
|
||||||
server::init(&fs)?;
|
server::init(&fs)?;
|
||||||
}
|
}
|
||||||
Server::Start => {
|
#[cfg(not(feature = "tui"))]
|
||||||
|
Server::Start {} => {
|
||||||
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(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)?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,4 @@ mod file_updated;
|
||||||
mod receive_app_config;
|
mod receive_app_config;
|
||||||
mod receive_valid_app_config;
|
mod receive_valid_app_config;
|
||||||
mod shutdown;
|
mod shutdown;
|
||||||
|
mod subscribe_updates;
|
||||||
|
|
|
@ -71,6 +71,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
let shout = app_config.shout().clone();
|
let shout = app_config.shout().clone();
|
||||||
self.app_config.replace(app_config);
|
self.app_config.replace(app_config);
|
||||||
self.alerts.do_send(UpdateShout::new(shout));
|
self.alerts.do_send(UpdateShout::new(shout));
|
||||||
|
self.send_server_updates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
crates/cli/src/server/actor/handlers/subscribe_updates.rs
Normal file
10
crates/cli/src/server/actor/handlers/subscribe_updates.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use crate::server::actor::{messages::SubscribeToUpdates, ServerActor};
|
||||||
|
|
||||||
|
//
|
||||||
|
impl actix::Handler<SubscribeToUpdates> for ServerActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
self.subscribers.push(msg.unwrap());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
|
use actix::{Message, Recipient};
|
||||||
//-
|
//-
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
|
git::graph::Log,
|
||||||
message,
|
message,
|
||||||
server::{AppConfig, Storage},
|
server::{AppConfig, Storage},
|
||||||
|
ForgeAlias, RepoAlias, RepoBranches,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
@ -33,3 +36,28 @@ message!(
|
||||||
);
|
);
|
||||||
|
|
||||||
message!(Shutdown, "Notification to shutdown the server actor");
|
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<ServerUpdate>,
|
||||||
|
"Subscribe to receive updates from the server"
|
||||||
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use messages::ReceiveAppConfig;
|
use messages::{ReceiveAppConfig, ServerUpdate};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -58,6 +58,8 @@ pub struct ServerActor {
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
||||||
|
|
||||||
|
subscribers: Vec<Recipient<ServerUpdate>>,
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
||||||
}
|
}
|
||||||
|
@ -82,6 +84,7 @@ impl ServerActor {
|
||||||
net,
|
net,
|
||||||
alerts,
|
alerts,
|
||||||
repository_factory: repo,
|
repository_factory: repo,
|
||||||
|
subscribers: Vec::default(),
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
repo_actors: BTreeMap::new(),
|
repo_actors: BTreeMap::new(),
|
||||||
message_log: None,
|
message_log: None,
|
||||||
|
@ -239,4 +242,10 @@ impl ServerActor {
|
||||||
ctx.address().do_send(msg);
|
ctx.address().do_send(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_server_updates(&self) {
|
||||||
|
self.subscribers.iter().for_each(|subscriber| {
|
||||||
|
subscriber.do_send(ServerUpdate::Ping);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
//
|
//
|
||||||
mod actor;
|
pub mod actor;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
use actix_rt::signal;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
alerts::{AlertsActor, History},
|
alerts::{AlertsActor, History},
|
||||||
file_watcher::{watch_file, FileUpdated},
|
file_watcher::{watch_file, FileUpdated},
|
||||||
};
|
};
|
||||||
use actor::ServerActor;
|
|
||||||
|
#[allow(clippy::module_name_repetitions)]
|
||||||
|
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 +42,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 +66,39 @@ 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;
|
#[cfg(feature = "tui")]
|
||||||
info!("Ctrl-C received, shutting down...");
|
{
|
||||||
|
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);
|
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()?;
|
||||||
|
|
4
crates/cli/src/tui/actor/handlers/mod.rs
Normal file
4
crates/cli/src/tui/actor/handlers/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
mod server_update;
|
||||||
|
mod tick;
|
||||||
|
|
28
crates/cli/src/tui/actor/handlers/server_update.rs
Normal file
28
crates/cli/src/tui/actor/handlers/server_update.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use actix::Handler;
|
||||||
|
|
||||||
|
use crate::{server::actor::messages::ServerUpdate, tui::Tui};
|
||||||
|
|
||||||
|
//
|
||||||
|
impl Handler<ServerUpdate> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
crates/cli/src/tui/actor/handlers/tick.rs
Normal file
57
crates/cli/src/tui/actor/handlers/tick.rs
Normal file
|
@ -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<Tick> 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(())
|
||||||
|
}
|
||||||
|
}
|
4
crates/cli/src/tui/actor/messages.rs
Normal file
4
crates/cli/src/tui/actor/messages.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
use git_next_core::message;
|
||||||
|
|
||||||
|
message!(Tick => std::io::Result<()>, "Start the TUI");
|
75
crates/cli/src/tui/actor/mod.rs
Normal file
75
crates/cli/src/tui/actor/mod.rs
Normal file
|
@ -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<Terminal<CrosstermBackend<Stderr>>>,
|
||||||
|
signal_shutdown: Sender<()>,
|
||||||
|
last_ping: Instant,
|
||||||
|
}
|
||||||
|
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 fn new(signal_shutdown: Sender<()>) -> Self {
|
||||||
|
Self {
|
||||||
|
terminal: None,
|
||||||
|
signal_shutdown,
|
||||||
|
last_ping: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
5
crates/cli/src/tui/mod.rs
Normal file
5
crates/cli/src/tui/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//
|
||||||
|
mod actor;
|
||||||
|
|
||||||
|
pub use actor::messages::Tick;
|
||||||
|
pub use actor::Tui;
|
|
@ -99,7 +99,6 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
.fetch_counter
|
.fetch_counter
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| git::fetch::Error::Lock)?;
|
.map_err(|_| git::fetch::Error::Lock)?;
|
||||||
println!("Fetch: {i}");
|
|
||||||
self.fetch_counter
|
self.fetch_counter
|
||||||
.write()
|
.write()
|
||||||
.map_err(|_| git::fetch::Error::Lock)
|
.map_err(|_| git::fetch::Error::Lock)
|
||||||
|
|
Loading…
Reference in a new issue