diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index afa3cac..68b41be 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,7 +12,8 @@ keywords = { workspace = true } categories = { workspace = true } [features] -default = ["forgejo", "github"] +# default = ["forgejo", "github"] +default = ["forgejo", "github", "tui"] forgejo = ["git-next-forge-forgejo"] github = ["git-next-forge-github"] tui = ["ratatui"] diff --git a/crates/cli/src/repo/handlers/receive_repo_config.rs b/crates/cli/src/repo/handlers/receive_repo_config.rs index 12934d8..7f0eb1d 100644 --- a/crates/cli/src/repo/handlers/receive_repo_config.rs +++ b/crates/cli/src/repo/handlers/receive_repo_config.rs @@ -2,10 +2,13 @@ use actix::prelude::*; use tracing::instrument; -use crate::repo::{ - do_send, - messages::{ReceiveRepoConfig, RegisterWebhook}, - RepoActor, +use crate::{ + repo::{ + do_send, + messages::{ReceiveRepoConfig, RegisterWebhook}, + RepoActor, + }, + server::actor::messages::ServerUpdate, }; impl Handler for RepoActor { @@ -13,7 +16,14 @@ impl Handler for RepoActor { #[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))] fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result { let repo_config = msg.unwrap(); - self.repo_details.repo_config.replace(repo_config); + self.repo_details.repo_config.replace(repo_config.clone()); + if let Some(server_addr) = &self.server_addr { + server_addr.do_send(ServerUpdate::from(( + &repo_config, + self.repo_details.forge.forge_alias().clone(), + self.repo_details.repo_alias.clone(), + ))); + } do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); } diff --git a/crates/cli/src/repo/handlers/validate_repo.rs b/crates/cli/src/repo/handlers/validate_repo.rs index a693140..f117caa 100644 --- a/crates/cli/src/repo/handlers/validate_repo.rs +++ b/crates/cli/src/repo/handlers/validate_repo.rs @@ -53,6 +53,9 @@ impl Handler for RepoActor { }; logger(self.log.as_ref(), "have repo config"); + #[cfg(feature = "tui")] + self.update_tui(); + match validate(&**open_repository, &self.repo_details, &repo_config) { Ok(Positions { main, diff --git a/crates/cli/src/repo/handlers/webhook_notification.rs b/crates/cli/src/repo/handlers/webhook_notification.rs index 930d61e..0774875 100644 --- a/crates/cli/src/repo/handlers/webhook_notification.rs +++ b/crates/cli/src/repo/handlers/webhook_notification.rs @@ -135,7 +135,7 @@ fn handle_push( last_commit: &mut Option, log: Option<&ActorLog>, ) -> Result<(), ()> { - logger(log, "message is for dev branch"); + logger(log, format!("message is for {branch} branch")); let commit = Commit::from(push); if last_commit.as_ref() == Some(&commit) { logger(log, format!("not a new commit on {branch}")); diff --git a/crates/cli/src/repo/mod.rs b/crates/cli/src/repo/mod.rs index 7a0c3bc..684c97f 100644 --- a/crates/cli/src/repo/mod.rs +++ b/crates/cli/src/repo/mod.rs @@ -1,7 +1,7 @@ // use actix::prelude::*; -use crate::alerts::messages::NotifyUser; +use crate::{alerts::messages::NotifyUser, server::ServerActor}; use derive_more::Deref; use kxio::network::Network; use std::time::Duration; @@ -59,6 +59,7 @@ pub struct RepoActor { forge: Box, log: Option, notify_user_recipient: Option>, + server_addr: Option>, } impl RepoActor { #[allow(clippy::too_many_arguments)] @@ -71,6 +72,7 @@ impl RepoActor { repository_factory: Box, sleep_duration: std::time::Duration, notify_user_recipient: Option>, + server_addr: Option>, ) -> Self { let message_token = messages::MessageToken::default(); Self { @@ -90,6 +92,18 @@ impl RepoActor { sleep_duration, log: None, notify_user_recipient, + server_addr, + } + } + + #[cfg(feature = "tui")] + fn update_tui(&self) { + use crate::server::actor::messages::ServerUpdate; + + if let (Some(server_addr), Some(repo_config)) = + (&self.server_addr, &self.repo_details.repo_config) + { + server_addr.do_send(ServerUpdate::from((&self.repo_details, repo_config))); } } } diff --git a/crates/cli/src/repo/tests/given.rs b/crates/cli/src/repo/tests/given.rs index e8da64e..c0a6b91 100644 --- a/crates/cli/src/repo/tests/given.rs +++ b/crates/cli/src/repo/tests/given.rs @@ -195,6 +195,7 @@ pub fn a_repo_actor( repository_factory, std::time::Duration::from_nanos(1), None, + None, ) .with_log(actors_log), log, diff --git a/crates/cli/src/server/actor/handlers/mod.rs b/crates/cli/src/server/actor/handlers/mod.rs index 3633000..a5e6358 100644 --- a/crates/cli/src/server/actor/handlers/mod.rs +++ b/crates/cli/src/server/actor/handlers/mod.rs @@ -1,5 +1,6 @@ mod file_updated; mod receive_app_config; mod receive_valid_app_config; +mod server_update; 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 6380c67..1f23216 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 @@ -8,7 +8,7 @@ use crate::{ alerts::messages::UpdateShout, repo::{messages::CloneRepo, RepoActor}, server::actor::{ - messages::{ReceiveValidAppConfig, ValidAppConfig}, + messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig}, ServerActor, }, webhook::{ @@ -21,7 +21,7 @@ use crate::{ impl Handler for ServerActor { type Result = (); - fn handle(&mut self, msg: ReceiveValidAppConfig, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result { let ValidAppConfig { app_config, socket_address, @@ -37,6 +37,7 @@ impl Handler for ServerActor { let webhook_router = WebhookRouterActor::default().start(); let listen_url = app_config.listen().url(); let notify_user_recipient = self.alerts.clone().recipient(); + let server_addr = Some(ctx.address()); // Forge Actors for (forge_alias, forge_config) in app_config.forges() { let repo_actors = self @@ -46,6 +47,7 @@ impl Handler for ServerActor { &server_storage, listen_url, ¬ify_user_recipient, + server_addr.clone(), ) .into_iter() .map(start_repo_actor) @@ -69,9 +71,9 @@ impl Handler for ServerActor { WebhookActor::new(socket_address, webhook_router.recipient()).start(); self.webhook_actor_addr.replace(webhook_actor_addr); let shout = app_config.shout().clone(); - self.app_config.replace(app_config); + self.app_config.replace(app_config.clone()); + self.do_send(ServerUpdate::AppConfigLoaded { app_config }, ctx); self.alerts.do_send(UpdateShout::new(shout)); - self.send_server_updates(); } } diff --git a/crates/cli/src/server/actor/handlers/server_update.rs b/crates/cli/src/server/actor/handlers/server_update.rs new file mode 100644 index 0000000..0d5624d --- /dev/null +++ b/crates/cli/src/server/actor/handlers/server_update.rs @@ -0,0 +1,14 @@ +use actix::Handler; + +// +use crate::server::{actor::messages::ServerUpdate, ServerActor}; + +impl Handler for ServerActor { + type Result = (); + + fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { + self.subscribers.iter().for_each(move |subscriber| { + subscriber.do_send(msg.clone()); + }); + } +} diff --git a/crates/cli/src/server/actor/messages.rs b/crates/cli/src/server/actor/messages.rs index d361890..a229052 100644 --- a/crates/cli/src/server/actor/messages.rs +++ b/crates/cli/src/server/actor/messages.rs @@ -3,10 +3,10 @@ use actix::{Message, Recipient}; use derive_more::Constructor; use git_next_core::{ - git::graph::Log, + git::{self, graph::Log, RepoDetails}, message, server::{AppConfig, Storage}, - ForgeAlias, RepoAlias, RepoBranches, + ForgeAlias, RepoAlias, RepoBranches, RepoConfig, }; use std::net::SocketAddr; @@ -40,20 +40,61 @@ message!(Shutdown, "Notification to shutdown the server actor"); #[derive(Clone, Debug, PartialEq, Eq, Message)] #[rtype(result = "()")] pub enum ServerUpdate { - /// Status of a repo - UpdateRepoSummary { + /// List of all configured forges and aliases + AppConfigLoaded { app_config: AppConfig }, + /// Configuration of a repo + UpdateRepoConfig { + forge_alias: ForgeAlias, + repo_alias: RepoAlias, + branches: RepoBranches, + }, + + /// Status of the repo + UpdateRepoDetails { forge_alias: ForgeAlias, repo_alias: RepoAlias, branches: RepoBranches, log: Log, }, - /// remove a repo - RemoveRepo { - forge_alias: ForgeAlias, - repo_alias: RepoAlias, - }, - /// test message - Ping, +} + +// impl From<&AppConfig> for ServerUpdate { +// fn from(app_config: &AppConfig) -> Self { +// Self::ForgeRepoList { +// list: app_config +// .forges() +// .flat_map(|(forge_alias, forge_config)| { +// forge_config +// .repos() +// .map(|(repo_alias, _server_repo_config)| (forge_alias.clone(), repo_alias)) +// .collect::>() +// }) +// .collect::>(), +// } +// } +// } + +impl From<(&RepoConfig, ForgeAlias, RepoAlias)> for ServerUpdate { + fn from(value: (&RepoConfig, ForgeAlias, RepoAlias)) -> Self { + Self::UpdateRepoConfig { + forge_alias: value.1, + repo_alias: value.2, + branches: value.0.branches().clone(), + } + } +} + +impl From<(&RepoDetails, &RepoConfig)> for ServerUpdate { + fn from((repo_details, repo_config): (&RepoDetails, &RepoConfig)) -> Self { + let branches = repo_config.branches().clone(); + let git_graph_log = git::graph::log(repo_details); + Self::UpdateRepoDetails { + forge_alias: repo_details.forge.forge_alias().clone(), + repo_alias: repo_details.repo_alias.clone(), + branches, + log: git_graph_log, + } + } } message!( diff --git a/crates/cli/src/server/actor/mod.rs b/crates/cli/src/server/actor/mod.rs index d87ebe8..74eefab 100644 --- a/crates/cli/src/server/actor/mod.rs +++ b/crates/cli/src/server/actor/mod.rs @@ -118,6 +118,7 @@ impl ServerActor { server_storage: &Storage, listen_url: &ListenUrl, notify_user_recipient: &Recipient, + server_addr: Option>, ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { let span = tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); @@ -125,8 +126,13 @@ impl ServerActor { let _guard = span.enter(); tracing::info!("Creating Forge"); let mut repos = vec![]; - let creator = - self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url); + let creator = self.create_actor( + forge_name, + forge_config.clone(), + server_storage, + listen_url, + server_addr, + ); for (repo_alias, server_repo_config) in forge_config.repos() { let forge_repo = creator(( repo_alias, @@ -148,6 +154,7 @@ impl ServerActor { forge_config: ForgeConfig, server_storage: &Storage, listen_url: &ListenUrl, + server_addr: Option>, ) -> impl Fn( (RepoAlias, &ServerRepoConfig, Recipient), ) -> (ForgeAlias, RepoAlias, RepoActor) { @@ -194,6 +201,7 @@ impl ServerActor { repository_factory.duplicate(), sleep_duration, Some(notify_user_recipient), + server_addr.clone(), ); (forge_name.clone(), repo_alias, actor) } @@ -242,10 +250,4 @@ 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 211304c..988f034 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -59,7 +59,6 @@ pub fn start( info!("Starting Server..."); let server = ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); - server.do_send(FileUpdated); info!("Starting File Watcher..."); #[allow(clippy::expect_used)] @@ -75,18 +74,18 @@ pub fn start( 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())); + server.do_send(FileUpdated); // update file after ui subscription in place loop { let _ = tui_addr.send(tui::Tick).await; if rx_shutdown.try_recv().is_ok() { break; } - // actix_rt::task::yield_now().await; + actix_rt::time::sleep(Duration::from_millis(16)).await; } } } else { + server.do_send(FileUpdated); info!("Server running - Press Ctrl-C to stop..."); let _ = signal::ctrl_c().await; info!("Ctrl-C received, shutting down..."); diff --git a/crates/cli/src/tui/actor/handlers/server_update.rs b/crates/cli/src/tui/actor/handlers/server_update.rs index d5c6bad..0a34e1a 100644 --- a/crates/cli/src/tui/actor/handlers/server_update.rs +++ b/crates/cli/src/tui/actor/handlers/server_update.rs @@ -1,27 +1,47 @@ -use std::time::Instant; - use actix::Handler; +use git_next_core::git::graph::Log; -use crate::{server::actor::messages::ServerUpdate, tui::Tui}; +use crate::{ + server::actor::messages::ServerUpdate, + tui::{ + actor::model::{ForgeRepoKey, RepoState, State}, + Tui, + }, +}; // impl Handler for Tui { type Result = (); fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { + self.state.tap(); match msg { - ServerUpdate::UpdateRepoSummary { + ServerUpdate::AppConfigLoaded { app_config } => { + self.state = State::loaded(app_config); + } + ServerUpdate::UpdateRepoConfig { + forge_alias, + repo_alias, + branches, + } => { + // self.state.repos_mut().insert( + // ForgeRepoKey::new(forge_alias, repo_alias), + // RepoState::Loaded { + // branches, + // log: Log::default(), + // }, + // ); + } + ServerUpdate::UpdateRepoDetails { forge_alias, repo_alias, branches, log, - } => todo!(), - ServerUpdate::RemoveRepo { - forge_alias, - repo_alias, - } => todo!(), - ServerUpdate::Ping => { - self.last_ping = Instant::now(); + } => { + // self.state.repos_mut().insert( + // ForgeRepoKey::new(forge_alias, repo_alias), + // RepoState::Loaded { branches, log }, + // ); } } } diff --git a/crates/cli/src/tui/actor/handlers/tick.rs b/crates/cli/src/tui/actor/handlers/tick.rs index 215bf1f..44ecb83 100644 --- a/crates/cli/src/tui/actor/handlers/tick.rs +++ b/crates/cli/src/tui/actor/handlers/tick.rs @@ -1,12 +1,5 @@ // -use std::{borrow::BorrowMut, time::Instant}; - -use actix::{ActorContext, Handler}; -use ratatui::{ - crossterm::event::{self, KeyCode, KeyEventKind}, - style::Stylize as _, - widgets::Paragraph, -}; +use actix::Handler; use crate::tui::actor::{messages::Tick, Tui}; @@ -14,44 +7,9 @@ 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 => { - // - } - _ => (), - } - } - } - } - + self.state.tap(); + self.draw()?; + self.handle_input(ctx)?; Ok(()) } } diff --git a/crates/cli/src/tui/actor/messages.rs b/crates/cli/src/tui/actor/messages.rs index 3af3fc1..b47cbc8 100644 --- a/crates/cli/src/tui/actor/messages.rs +++ b/crates/cli/src/tui/actor/messages.rs @@ -1,4 +1,4 @@ // use git_next_core::message; -message!(Tick => std::io::Result<()>, "Start the TUI"); +message!(Tick => std::io::Result<()>, "Update the TUI"); diff --git a/crates/cli/src/tui/actor/mod.rs b/crates/cli/src/tui/actor/mod.rs index b00b4a3..6519960 100644 --- a/crates/cli/src/tui/actor/mod.rs +++ b/crates/cli/src/tui/actor/mod.rs @@ -1,17 +1,20 @@ // mod handlers; pub mod messages; +mod model; use std::{ io::{stderr, Stderr}, sync::mpsc::Sender, - time::Instant, }; -use actix::{Actor, Context}; +use actix::{Actor, ActorContext as _, Context}; + +pub use model::*; use ratatui::{ crossterm::{ + event::{self, KeyCode, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, @@ -23,7 +26,7 @@ use ratatui::{ pub struct Tui { terminal: Option>>, signal_shutdown: Sender<()>, - last_ping: Instant, + state: State, } impl Actor for Tui { type Context = Context; @@ -58,9 +61,48 @@ impl Tui { Self { terminal: None, signal_shutdown, - last_ping: Instant::now(), + state: State::initial(), } } + + pub const fn state(&self) -> &State { + &self.state + } + + fn draw(&mut self) -> std::io::Result<()> { + let t = self.terminal.take(); + if let Some(mut terminal) = t { + terminal.draw(|frame| { + frame.render_widget(self.state(), frame.area()); + })?; + self.terminal = Some(terminal); + } else { + eprintln!("No terminal setup"); + } + Ok(()) + } + + fn handle_input(&self, ctx: &mut ::Context) -> std::io::Result<()> { + 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') => { + ctx.stop(); + if let Err(err) = self.signal_shutdown.send(()) { + tracing::error!(?err, "Failed to signal shutdown"); + } + } + KeyCode::Esc => { + // + } + _ => (), + } + } + } + } + Ok(()) + } } fn init() -> std::io::Result>> { diff --git a/crates/cli/src/tui/actor/model.rs b/crates/cli/src/tui/actor/model.rs new file mode 100644 index 0000000..686f0f2 --- /dev/null +++ b/crates/cli/src/tui/actor/model.rs @@ -0,0 +1,127 @@ +// +use ratatui::{ + prelude::{Buffer, Rect}, + style::Stylize as _, + symbols::border, + text::Line, + widgets::{block::Title, Block, Paragraph, Widget}, +}; + +use git_next_core::{git::graph::Log, server::AppConfig, ForgeAlias, RepoAlias, RepoBranches}; + +use std::{collections::BTreeMap, time::Instant}; + +use derive_more::derive::Constructor; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct State { + last_update: Instant, + mode: StateMode, + app_config: Option, +} +impl State { + pub fn initial() -> Self { + Self { + last_update: Instant::now(), + mode: StateMode::default(), + app_config: None, + } + } + + pub fn loaded(app_config: AppConfig) -> Self { + Self { + last_update: Instant::now(), + mode: StateMode::Loaded, + app_config: Some(app_config), + } + } + + pub fn tap(&mut self) { + self.last_update = Instant::now(); + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum StateMode { + #[default] + Initial, + Loaded, +} + +pub type ReposState = BTreeMap; + +#[derive(Clone, Debug, Constructor)] +pub struct RepoEntry { + pub forge_alias: ForgeAlias, + pub repo_alias: RepoAlias, + pub repo_state: RepoState, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)] +pub struct ForgeRepoKey { + forge_alias: ForgeAlias, + repo_alias: RepoAlias, +} +impl ForgeRepoKey { + pub const fn forge_alias(&self) -> &ForgeAlias { + &self.forge_alias + } + pub const fn repo_alias(&self) -> &RepoAlias { + &self.repo_alias + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RepoState { + Loading, + Loaded { branches: RepoBranches, log: Log }, +} + +impl Widget for &State { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + match self.mode { + StateMode::Initial => Paragraph::new("Loading...").centered(), + StateMode::Loaded => Paragraph::new( + vec!["TODO".into()], + // + // self.repos() + // .iter() + // .map(RepoEntry::from) + // .enumerate() + // .map(|(i, repo)| { + // let state = match repo.repo_state { + // crate::tui::actor::RepoState::Loading => "Loading...".to_string(), + // crate::tui::actor::RepoState::Loaded { branches, log } => { + // format!("{branches}: {log:?}") + // } + // }; + // Line::from(format!( + // "{i}: {}/{}: {state}", + // repo.forge_alias, repo.repo_alias + // )) + // }) + // .collect::>(), + ) + .centered(), + } + .block( + Block::bordered() + .title( + Title::from(" Git-Next ".bold()).alignment(ratatui::layout::Alignment::Center), + ) + .title( + Title::from(Line::from(vec![ + " [q]uit ".into(), + format!("{:?}", self.last_update).into(), + ])) + .alignment(ratatui::layout::Alignment::Center) + .position(ratatui::widgets::block::Position::Bottom), + ) + .border_set(border::THICK), + ) + .render(area, buf); + } +} diff --git a/crates/core/src/git/graph.rs b/crates/core/src/git/graph.rs index 718c0da..5c6c5f7 100644 --- a/crates/core/src/git/graph.rs +++ b/crates/core/src/git/graph.rs @@ -1,5 +1,4 @@ // - use std::borrow::ToOwned; use take_until::TakeUntilExt;