From d848c944834ee66af2a9d745158a876d6332347f Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Mon, 12 Aug 2024 21:25:24 +0100 Subject: [PATCH] WIP: feat(tui): update state model from server messages --- crates/cli/Cargo.toml | 3 +- crates/cli/src/main.rs | 2 + .../src/repo/handlers/receive_ci_status.rs | 5 +- .../src/repo/handlers/receive_repo_config.rs | 2 +- crates/cli/src/repo/handlers/validate_repo.rs | 32 +- .../src/repo/handlers/webhook_notification.rs | 2 +- crates/cli/src/repo/mod.rs | 55 ++- crates/cli/src/repo/tests/given.rs | 1 + crates/cli/src/server/actor/handlers/mod.rs | 1 + .../handlers/receive_valid_app_config.rs | 19 +- .../server/actor/handlers/server_update.rs | 14 + crates/cli/src/server/actor/messages.rs | 27 +- crates/cli/src/server/actor/mod.rs | 18 +- crates/cli/src/server/mod.rs | 7 +- crates/cli/src/tui/README.md | 121 +++++++ .../src/tui/actor/handlers/server_update.rs | 51 ++- crates/cli/src/tui/actor/handlers/tick.rs | 50 +-- crates/cli/src/tui/actor/messages.rs | 2 +- crates/cli/src/tui/actor/mod.rs | 50 ++- crates/cli/src/tui/actor/model.rs | 337 ++++++++++++++++++ .../cli/src/tui/components/configured_app.rs | 52 +++ .../cli/src/tui/components/forge/collapsed.rs | 15 + .../cli/src/tui/components/forge/expanded.rs | 48 +++ crates/cli/src/tui/components/forge/mod.rs | 34 ++ crates/cli/src/tui/components/mod.rs | 5 + crates/cli/src/tui/components/repo/alert.rs | 68 ++++ .../cli/src/tui/components/repo/configured.rs | 23 ++ .../cli/src/tui/components/repo/identified.rs | 21 ++ crates/cli/src/tui/components/repo/mod.rs | 92 +++++ crates/cli/src/tui/components/repo/ready.rs | 65 ++++ crates/cli/src/tui/mod.rs | 1 + crates/core/src/config/server_repo_config.rs | 3 +- crates/core/src/git/graph.rs | 1 - crates/core/src/git/user_notification.rs | 17 + 34 files changed, 1134 insertions(+), 110 deletions(-) create mode 100644 crates/cli/src/server/actor/handlers/server_update.rs create mode 100644 crates/cli/src/tui/README.md create mode 100644 crates/cli/src/tui/actor/model.rs create mode 100644 crates/cli/src/tui/components/configured_app.rs create mode 100644 crates/cli/src/tui/components/forge/collapsed.rs create mode 100644 crates/cli/src/tui/components/forge/expanded.rs create mode 100644 crates/cli/src/tui/components/forge/mod.rs create mode 100644 crates/cli/src/tui/components/mod.rs create mode 100644 crates/cli/src/tui/components/repo/alert.rs create mode 100644 crates/cli/src/tui/components/repo/configured.rs create mode 100644 crates/cli/src/tui/components/repo/identified.rs create mode 100644 crates/cli/src/tui/components/repo/mod.rs create mode 100644 crates/cli/src/tui/components/repo/ready.rs 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/main.rs b/crates/cli/src/main.rs index f054c2c..713ca9b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,4 +1,6 @@ // +#![allow(clippy::module_name_repetitions)] + mod alerts; mod file_watcher; mod forge; diff --git a/crates/cli/src/repo/handlers/receive_ci_status.rs b/crates/cli/src/repo/handlers/receive_ci_status.rs index 6d8e272..8f25b5b 100644 --- a/crates/cli/src/repo/handlers/receive_ci_status.rs +++ b/crates/cli/src/repo/handlers/receive_ci_status.rs @@ -22,6 +22,8 @@ impl Handler for RepoActor { let repo_alias = self.repo_details.repo_alias.clone(); let message_token = self.message_token; let sleep_duration = self.sleep_duration; + let graph_log = graph::log(&self.repo_details); + self.update_tui_log(graph_log.clone()); debug!(?status, ""); match status { @@ -34,13 +36,14 @@ impl Handler for RepoActor { } Status::Fail => { tracing::warn!("Checks have failed"); + notify_user( self.notify_user_recipient.as_ref(), UserNotification::CICheckFailed { forge_alias, repo_alias, commit: next, - log: graph::log(&self.repo_details), + log: graph_log, }, log.as_ref(), ); diff --git a/crates/cli/src/repo/handlers/receive_repo_config.rs b/crates/cli/src/repo/handlers/receive_repo_config.rs index 12934d8..12111a2 100644 --- a/crates/cli/src/repo/handlers/receive_repo_config.rs +++ b/crates/cli/src/repo/handlers/receive_repo_config.rs @@ -14,7 +14,7 @@ impl Handler for RepoActor { 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.update_tui_branches(); 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..6ccc366 100644 --- a/crates/cli/src/repo/handlers/validate_repo.rs +++ b/crates/cli/src/repo/handlers/validate_repo.rs @@ -3,10 +3,13 @@ use actix::prelude::*; use tracing::{debug, instrument, Instrument as _}; -use crate::repo::{ - do_send, logger, - messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo}, - notify_user, RepoActor, +use crate::{ + repo::{ + do_send, logger, + messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo}, + notify_user, RepoActor, + }, + server::actor::messages::RepoUpdate, }; use git_next_core::git::validation::positions::{validate, Error, Positions}; @@ -41,14 +44,18 @@ impl Handler for RepoActor { format!("accepted token: {}", self.message_token), ); + self.update_tui(RepoUpdate::ValidateRepo); + // Repository positions let Some(ref open_repository) = self.open_repository else { logger(self.log.as_ref(), "no open repository"); + self.alert_tui("repo not open"); return; }; logger(self.log.as_ref(), "have open repository"); let Some(repo_config) = self.repo_details.repo_config.clone() else { logger(self.log.as_ref(), "no repo config"); + self.alert_tui("no repo config"); return; }; logger(self.log.as_ref(), "have repo config"); @@ -75,10 +82,11 @@ impl Handler for RepoActor { self.log.as_ref(), ); } else { - // do nothing + self.update_tui(RepoUpdate::Okay); } } Err(Error::Retryable(message)) => { + self.alert_tui(format!("[retryable: {message}]")); logger(self.log.as_ref(), message); let addr = ctx.address(); let message_token = self.message_token; @@ -95,13 +103,17 @@ impl Handler for RepoActor { .into_actor(self) .wait(ctx); } - Err(Error::UserIntervention(user_notification)) => notify_user( - self.notify_user_recipient.as_ref(), - user_notification, - self.log.as_ref(), - ), + Err(Error::UserIntervention(user_notification)) => { + self.alert_tui(format!("[USER INTERVENTION: {user_notification}]")); + notify_user( + self.notify_user_recipient.as_ref(), + user_notification, + self.log.as_ref(), + ); + } Err(Error::NonRetryable(message)) => { logger(self.log.as_ref(), message); + // TODO: alert tui } } } 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..c782093 100644 --- a/crates/cli/src/repo/mod.rs +++ b/crates/cli/src/repo/mod.rs @@ -1,7 +1,13 @@ // use actix::prelude::*; -use crate::alerts::messages::NotifyUser; +use crate::{ + alerts::messages::NotifyUser, + server::{ + actor::messages::{RepoUpdate, ServerUpdate}, + ServerActor, + }, +}; use derive_more::Deref; use kxio::network::Network; use std::time::Duration; @@ -59,6 +65,7 @@ pub struct RepoActor { forge: Box, log: Option, notify_user_recipient: Option>, + server_addr: Option>, } impl RepoActor { #[allow(clippy::too_many_arguments)] @@ -71,6 +78,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 +98,51 @@ impl RepoActor { sleep_duration, log: None, notify_user_recipient, + server_addr, + } + } + + fn update_tui_branches(&self) { + #[cfg(feature = "tui")] + { + use crate::server::actor::messages::RepoUpdate; + let Some(repo_config) = &self.repo_details.repo_config else { + return; + }; + let branches = repo_config.branches().clone(); + self.update_tui(RepoUpdate::Branches { branches }); + } + } + + fn update_tui_log(&self, log: git::graph::Log) { + #[cfg(feature = "tui")] + { + self.update_tui(RepoUpdate::Log { log }); + } + } + + fn alert_tui(&self, alert: impl Into) { + #[cfg(feature = "tui")] + { + self.update_tui(RepoUpdate::Alert { + alert: alert.into(), + }); + } + } + + fn update_tui(&self, repo_update: RepoUpdate) { + #[cfg(feature = "tui")] + { + let Some(server_addr) = &self.server_addr else { + return; + }; + + let update = ServerUpdate::RepoUpdate { + forge_alias: self.repo_details.forge.forge_alias().clone(), + repo_alias: self.repo_details.repo_alias.clone(), + repo_update, + }; + server_addr.do_send(update); } } } 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..1028cab 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,18 @@ 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: ValidAppConfig { + app_config, + socket_address, + storage: server_storage, + }, + }, + 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..ecf22d5 100644 --- a/crates/cli/src/server/actor/messages.rs +++ b/crates/cli/src/server/actor/messages.rs @@ -1,5 +1,5 @@ +// use actix::{Message, Recipient}; -//- use derive_more::Constructor; use git_next_core::{ @@ -40,20 +40,23 @@ 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: ValidAppConfig }, + + RepoUpdate { forge_alias: ForgeAlias, repo_alias: RepoAlias, - branches: RepoBranches, - log: Log, + repo_update: RepoUpdate, }, - /// remove a repo - RemoveRepo { - forge_alias: ForgeAlias, - repo_alias: RepoAlias, - }, - /// test message - Ping, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RepoUpdate { + Branches { branches: RepoBranches }, + Log { log: Log }, + ValidateRepo, + Okay, + Alert { alert: String }, } 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/README.md b/crates/cli/src/tui/README.md new file mode 100644 index 0000000..bafa379 --- /dev/null +++ b/crates/cli/src/tui/README.md @@ -0,0 +1,121 @@ +# TUI Actor + +- Maintains it's own copy of data for holding state +- Is notified of update via actor messages from the Server and Repo Actors +- State is somewhat heirarchical + +## State + +```rust +enum TuiState { + Initial, + Configured { + app_config: ValidAppConfig, + forges: BTreeMap, + }, +} +enum RepoState { + Identified { repo_alias: RepoAlias }, + Configured { repo_alias: RepoAlias, branches: RepoBranches }, + Ready { repo_alias: RepoAlias, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log }, + Alert { repo_alias: RepoAlias, message: String, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log }, +} +``` + +## State Transitions: + +### `TuiState` + +```mermaid +stateDiagram-v2 + * --> Initial + Initial --> Configured +``` + +- `message!(Configure, ValidAppConfig, "Initialise UI with valid config");` + +### `RepoState` + +```mermaid +stateDiagram-v2 + * --> Identified + Identified --> Configured + Identified --> Ready + Configured --> Ready + Ready --> Alert + Configured --> Alert + Identified --> Alert + Alert --> Ready +``` + +- `Identified` - from AppConfig where a repo alias is listed, but repo config needs to be loaded from `.git-next.toml` +- `Configured` - as `Identified` but either branches are identified in server config, OR the `.git-next.toml` file has been loaded +- `Ready` - as `Configured` but the branch positions have been validated and do not require user intervention +- `Alert` - as `Ready` but user intervention is required + +## Widget + +Initial mock up of UI. Possibly add some borders and padding as it looks a little squached together. + +``` ++ gh +- jo + + test (main/next/dev) + - tasyn (main/next/dev) + * 12ab32f (dev) added feature X + * bce43b1 (next) added feature Y + * f43e379 (main) added feature Z + - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main + * 239cefd (main/next) fix bug A + * b4c290a (dev) +``` + +Adding a border around open forges: + +``` ++ gh +- jo --------------------------------------------------------------------+ +| + test (main/next/dev) | +| - tasyn (main/next/dev) | +| * 12ab32f (dev) added feature X | +| * bce43b1 (next) added feature Y | +| * f43e379 (main) added feature Z | +| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main | +| * 239cefd (main/next) fix bug A | +| * b4c290a (dev) | ++------------------------------------------------------------------------+ +``` + +Adding a border around open forges and repos (I like this one the best): + +``` ++ gh +- jo --------------------------------------------------------------------+ +| + test (main/next/dev) | +| - tasyn (main/next/dev) ---------------------------------------------+ | +| | * 12ab32f (dev) added feature X | | +| | * bce43b1 (next) added feature Y | | +| | * f43e379 (main) added feature Z | | +| +--------------------------------------------------------------------+ | +| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main -+ | +| | * 239cefd (main/next) fix bug A | | +| | * b4c290a (dev) | | +| +--------------------------------------------------------------------+ | ++------------------------------------------------------------------------+ +``` + +## Logging + +- tui-logger to create an optional panel to show the normal server logs + +## Branch Graph + +- tui-nodes ? + +## Scrolling + +- tui-scrollview + +## Tree View + +- tui-tree-widget diff --git a/crates/cli/src/tui/actor/handlers/server_update.rs b/crates/cli/src/tui/actor/handlers/server_update.rs index d5c6bad..af557ff 100644 --- a/crates/cli/src/tui/actor/handlers/server_update.rs +++ b/crates/cli/src/tui/actor/handlers/server_update.rs @@ -1,27 +1,50 @@ -use std::time::Instant; - +// use actix::Handler; -use crate::{server::actor::messages::ServerUpdate, tui::Tui}; +use crate::{ + server::actor::messages::{RepoUpdate, ServerUpdate}, + tui::{actor::ServerState, 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.mode = ServerState::from(app_config); + } + + ServerUpdate::RepoUpdate { forge_alias, repo_alias, - branches, - log, - } => todo!(), - ServerUpdate::RemoveRepo { - forge_alias, - repo_alias, - } => todo!(), - ServerUpdate::Ping => { - self.last_ping = Instant::now(); + repo_update, + } => { + if let ServerState::Configured { forges } = &mut self.state.mode { + let Some(forge_state) = forges.get_mut(&forge_alias) else { + return; + }; + let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else { + return; + }; + match repo_update { + RepoUpdate::Branches { branches } => { + repo_state.update_branches(branches); + } + RepoUpdate::Log { log } => { + repo_state.update_log(log); + } + RepoUpdate::ValidateRepo => repo_state.update_message("polling..."), + RepoUpdate::Okay => { + repo_state.update_message("okay"); + repo_state.clear_alert(); + } + RepoUpdate::Alert { alert } => { + repo_state.alert(alert); + } + } + } } } } 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..e1fd518 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, + pub 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..51dfcaf --- /dev/null +++ b/crates/cli/src/tui/actor/model.rs @@ -0,0 +1,337 @@ +// +use ratatui::{ + layout::Alignment, + prelude::{Buffer, Rect}, + style::Stylize as _, + symbols::border, + text::Line, + widgets::{block::Title, Block, Paragraph, Widget}, +}; + +use git_next_core::{ + git::{graph::Log, Commit}, + ForgeAlias, RepoAlias, RepoBranches, +}; + +use std::{collections::BTreeMap, fmt::Display, time::Instant}; + +use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct State { + last_update: Instant, + started: Instant, + pub mode: ServerState, +} +impl State { + pub fn initial() -> Self { + Self { + last_update: Instant::now(), + started: Instant::now(), + mode: ServerState::Initial { tick: 0 }, + } + } + + pub fn tap(&mut self) { + self.last_update = Instant::now(); + if let ServerState::Initial { tick } = &mut self.mode { + *tick += 1; + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ServerState { + /// UI has started but has no information on the state of the server + Initial { tick: usize }, // NOTE: for use with throbber-widgets-tui ? + + /// The application configuration has been loaded, individual forges and repos have their own + /// states + Configured { + forges: BTreeMap, + }, +} +impl ServerState { + pub fn update_branches( + &mut self, + forge_alias: &ForgeAlias, + repo_alias: &RepoAlias, + branches: RepoBranches, + ) { + if let Self::Configured { forges } = self { + let Some(forge_state) = forges.get_mut(forge_alias) else { + return; + }; + let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else { + return; + }; + match repo_state { + RepoState::Configured { + branches: state_branches, + .. + } + | RepoState::Ready { + branches: state_branches, + .. + } + | RepoState::Alert { + branches: state_branches, + .. + } => *state_branches = branches, + + RepoState::Identified { .. } => (), + } + } + } + + pub fn update_log(&mut self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias, log: Log) { + if let Self::Configured { forges } = self { + let Some(forge_state) = forges.get_mut(forge_alias) else { + return; + }; + let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else { + return; + }; + match repo_state { + RepoState::Ready { log: state_log, .. } + | RepoState::Alert { log: state_log, .. } => *state_log = log, + + RepoState::Identified { .. } | RepoState::Configured { .. } => (), + } + } + } +} +impl From for ServerState { + fn from(app_config: ValidAppConfig) -> Self { + Self::Configured { + forges: app_config + .app_config + .forges() + .map(|(forge_alias, config)| { + ( + forge_alias, + config + .repos() + .map(|(repo_alias, server_repo_config)| { + (repo_alias, server_repo_config.repo_config()) + }) + .map( + |(repo_alias, option_repo_config)| match option_repo_config { + Some(rc) => ( + repo_alias.clone(), + RepoState::Configured { + repo_alias, + message: "configured".into(), + branches: rc.branches().clone(), + }, + ), + None => ( + repo_alias.clone(), + RepoState::Identified { + repo_alias, + message: "identified".into(), + }, + ), + }, + ) + .collect::>(), + ) + }) + .map(|(forge_alias, vec_repo_alias_state)| { + let forge_state: ForgeState = ForgeState { + alias: forge_alias.clone(), + view_state: ViewState::default(), + repos: vec_repo_alias_state.into_iter().collect::>(), + }; + (forge_alias, forge_state) + }) + .collect::>(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum ViewState { + Collapsed, + #[default] + Expanded, +} +impl Display for ViewState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let view_state = match self { + Self::Collapsed => "+", + Self::Expanded => "-", + }; + write!(f, "{view_state}") + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ForgeState { + pub alias: ForgeAlias, + pub view_state: ViewState, + pub repos: BTreeMap, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RepoState { + Identified { + repo_alias: RepoAlias, + message: String, + }, + Configured { + repo_alias: RepoAlias, + message: String, + branches: RepoBranches, + }, + Ready { + repo_alias: RepoAlias, + message: String, + branches: RepoBranches, + view_state: ViewState, + main: Commit, + next: Commit, + dev: Commit, + log: Log, + }, + Alert { + repo_alias: RepoAlias, + message: String, + branches: RepoBranches, + view_state: ViewState, + main: Commit, + next: Commit, + dev: Commit, + log: Log, + alert: String, + }, +} +impl RepoState { + pub fn update_branches(&mut self, branches: RepoBranches) { + match self { + Self::Configured { + branches: state_branches, + .. + } + | Self::Ready { + branches: state_branches, + .. + } + | Self::Alert { + branches: state_branches, + .. + } => *state_branches = branches, + + Self::Identified { .. } => (), + } + } + + pub fn update_log(&mut self, log: Log) { + match self { + Self::Ready { log: state_log, .. } | Self::Alert { log: state_log, .. } => { + *state_log = log; + } + + Self::Identified { .. } | Self::Configured { .. } => (), + } + } + + pub fn update_message(&mut self, msg: impl Into) { + match self { + Self::Identified { message, .. } + | Self::Configured { message, .. } + | Self::Ready { message, .. } + | Self::Alert { message, .. } => *message = msg.into(), + } + } + + pub fn clear_alert(&mut self) { + match self { + Self::Identified { .. } | Self::Configured { .. } | Self::Ready { .. } => (), + Self::Alert { + repo_alias, + message, + branches, + view_state, + main, + next, + dev, + log, + .. + } => { + *self = Self::Ready { + repo_alias: repo_alias.clone(), + message: message.clone(), + branches: branches.clone(), + view_state: *view_state, + main: main.clone(), + next: next.clone(), + dev: dev.clone(), + log: log.clone(), + } + } + } + } + + pub(crate) fn alert(&mut self, msg: String) { + match self { + Self::Identified { .. } | Self::Configured { .. } => (), + Self::Ready { + repo_alias, + message, + branches, + view_state, + main, + next, + dev, + log, + } => { + *self = Self::Alert { + repo_alias: repo_alias.clone(), + message: message.clone(), + branches: branches.clone(), + view_state: *view_state, + main: main.clone(), + next: next.clone(), + dev: dev.clone(), + log: log.clone(), + alert: msg, + } + } + Self::Alert { alert, .. } => *alert = msg, + } + } +} + +impl Widget for &State { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let block = Block::bordered() + .title(Title::from(" Git-Next ".bold()).alignment(Alignment::Center)) + .title( + Title::from(Line::from(vec![ + " [q]uit ".into(), + format!( + "{}s ", + self.last_update.duration_since(self.started).as_secs() + ) + .into(), + ])) + .alignment(Alignment::Center) + .position(ratatui::widgets::block::Position::Bottom), + ) + .border_set(border::THICK); + let interior = block.inner(area); + block.render(area, buf); + match &self.mode { + ServerState::Initial { tick } => Paragraph::new(format!("Loading...{tick}")) + .centered() + .render(interior, buf), + ServerState::Configured { forges } => { + ConfiguredAppWidget { forges }.render(interior, buf); + } + } + } +} diff --git a/crates/cli/src/tui/components/configured_app.rs b/crates/cli/src/tui/components/configured_app.rs new file mode 100644 index 0000000..cc9ad04 --- /dev/null +++ b/crates/cli/src/tui/components/configured_app.rs @@ -0,0 +1,52 @@ +use std::collections::BTreeMap; + +use git_next_core::ForgeAlias; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + text::Line, + widgets::Widget, +}; + +use crate::tui::actor::{ForgeState, ViewState}; + +use super::forge::ForgeWidget; + +pub struct ConfiguredAppWidget<'a> { + pub forges: &'a BTreeMap, +} +impl<'a> Widget for ConfiguredAppWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let layout_app = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) + .split(area); + + Line::from(format!("Forges: ({})", self.forges.keys().len())).render(layout_app[0], buf); + + let layout_forge_list = Layout::default() + .direction(Direction::Vertical) + .constraints( + self.forges + .iter() + .map(|(_alias, state)| match state.view_state { + ViewState::Collapsed => Constraint::Length(1), + ViewState::Expanded => Constraint::Fill(1), + }), + ) + .split(layout_app[1]); + + self.forges + .iter() + .map(|(forge_alias, state)| ForgeWidget { + forge_alias, + repos: &state.repos, + view_state: state.view_state, + }) + .enumerate() + .for_each(|(i, w)| w.render(layout_forge_list[i], buf)); + } +} diff --git a/crates/cli/src/tui/components/forge/collapsed.rs b/crates/cli/src/tui/components/forge/collapsed.rs new file mode 100644 index 0000000..72761e5 --- /dev/null +++ b/crates/cli/src/tui/components/forge/collapsed.rs @@ -0,0 +1,15 @@ +// +use git_next_core::ForgeAlias; +use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget}; + +pub struct CollapsedForgeWidget<'a> { + pub forge_alias: &'a ForgeAlias, +} +impl<'a> Widget for CollapsedForgeWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + Text::from(format!("- {}", self.forge_alias)).render(area, buf); + } +} diff --git a/crates/cli/src/tui/components/forge/expanded.rs b/crates/cli/src/tui/components/forge/expanded.rs new file mode 100644 index 0000000..6bb1ec2 --- /dev/null +++ b/crates/cli/src/tui/components/forge/expanded.rs @@ -0,0 +1,48 @@ +// +use std::collections::BTreeMap; + +use git_next_core::{ForgeAlias, RepoAlias}; +use ratatui::{ + buffer::Buffer, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + widgets::{block::Title, Block, Borders, Widget}, +}; + +use crate::tui::{actor::RepoState, components::repo::RepoWidget}; + +pub struct ExpandedForgeWidget<'a> { + pub forge_alias: &'a ForgeAlias, + pub repos: &'a BTreeMap, +} +impl<'a> Widget for ExpandedForgeWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let block = Block::default() + .title(Title::from(self.forge_alias.to_string()).alignment(Alignment::Left)) + .borders(Borders::ALL); + let layout_forge = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) + .split(block.inner(area)); + block.render(area, buf); + + let layout_repo_list = Layout::default() + .direction(Direction::Vertical) + .constraints(self.repos.iter().map(|(_alias, state)| match state { + RepoState::Identified { .. } | RepoState::Configured { .. } => { + Constraint::Length(1) + } + + RepoState::Ready { .. } | RepoState::Alert { .. } => Constraint::Fill(1), + })) + .split(layout_forge[1]); + + self.repos + .values() + .map(|repo_state| RepoWidget { repo_state }) + .enumerate() + .for_each(|(i, w)| w.render(layout_repo_list[i], buf)); + } +} diff --git a/crates/cli/src/tui/components/forge/mod.rs b/crates/cli/src/tui/components/forge/mod.rs new file mode 100644 index 0000000..3851d78 --- /dev/null +++ b/crates/cli/src/tui/components/forge/mod.rs @@ -0,0 +1,34 @@ +// +mod collapsed; +mod expanded; + +use std::collections::BTreeMap; + +use git_next_core::{ForgeAlias, RepoAlias}; +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; + +use crate::tui::actor::{RepoState, ViewState}; + +pub struct ForgeWidget<'a> { + pub forge_alias: &'a ForgeAlias, + pub repos: &'a BTreeMap, + pub view_state: ViewState, +} +impl<'a> Widget for ForgeWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + match self.view_state { + ViewState::Collapsed => collapsed::CollapsedForgeWidget { + forge_alias: self.forge_alias, + } + .render(area, buf), + ViewState::Expanded => expanded::ExpandedForgeWidget { + forge_alias: self.forge_alias, + repos: self.repos, + } + .render(area, buf), + } + } +} diff --git a/crates/cli/src/tui/components/mod.rs b/crates/cli/src/tui/components/mod.rs new file mode 100644 index 0000000..161ca94 --- /dev/null +++ b/crates/cli/src/tui/components/mod.rs @@ -0,0 +1,5 @@ +mod configured_app; +mod forge; +mod repo; + +pub use configured_app::ConfiguredAppWidget; diff --git a/crates/cli/src/tui/components/repo/alert.rs b/crates/cli/src/tui/components/repo/alert.rs new file mode 100644 index 0000000..76508c5 --- /dev/null +++ b/crates/cli/src/tui/components/repo/alert.rs @@ -0,0 +1,68 @@ +// +use git_next_core::{ + git::{graph::Log, Commit}, + RepoAlias, RepoBranches, +}; + +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + text::{Line, Text}, + widgets::{Paragraph, Widget}, +}; + +use crate::tui::actor::ViewState; + +pub struct AlertRepoWidget<'a> { + pub repo_alias: &'a RepoAlias, + pub message: &'a str, + pub branches: &'a RepoBranches, + pub view_state: &'a ViewState, + pub main: &'a Commit, + pub next: &'a Commit, + pub dev: &'a Commit, + pub log: &'a Log, + pub alert: &'a str, +} +impl<'a> Widget for AlertRepoWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) + .split(area); + Paragraph::new(Text::from(vec![ + Line::from(vec![ + self.view_state.to_string().into(), + " ".into(), + self.repo_alias.to_string().into(), + " (".into(), + self.branches.main().to_string().into(), + "/".into(), + self.branches.next().to_string().into(), + "/".into(), + self.branches.dev().to_string().into(), + ") [alert:".into(), + self.alert.into(), + "] [".into(), + self.message.into(), + "]".into(), + ]), + Line::from(vec![ + self.main.to_string().into(), + " , ".into(), + self.next.to_string().into(), + " , ".into(), + self.dev.to_string().into(), + "}".into(), + ]), + ])) + .render(layout[0], buf); + Paragraph::new(Text::from( + self.log.iter().cloned().map(Line::from).collect::>(), + )) + .render(layout[1], buf); + } +} diff --git a/crates/cli/src/tui/components/repo/configured.rs b/crates/cli/src/tui/components/repo/configured.rs new file mode 100644 index 0000000..baf0696 --- /dev/null +++ b/crates/cli/src/tui/components/repo/configured.rs @@ -0,0 +1,23 @@ +// +use git_next_core::{RepoAlias, RepoBranches}; + +use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget}; + +pub struct ConfiguredRepoWidget<'a> { + pub repo_alias: &'a RepoAlias, + pub message: &'a str, + pub branches: &'a RepoBranches, +} +impl<'a> Widget for ConfiguredRepoWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let repo_alias = &self.repo_alias; + let main = self.branches.main(); + let next = self.branches.next(); + let dev = self.branches.dev(); + let message = self.message; + Text::from(format!("- {repo_alias} ({main}/{next}/{dev}) [{message}]")).render(area, buf); + } +} diff --git a/crates/cli/src/tui/components/repo/identified.rs b/crates/cli/src/tui/components/repo/identified.rs new file mode 100644 index 0000000..183c236 --- /dev/null +++ b/crates/cli/src/tui/components/repo/identified.rs @@ -0,0 +1,21 @@ +// +use git_next_core::RepoAlias; + +use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget}; + +pub struct IdentifiedRepoWidget<'a> { + pub repo_alias: &'a RepoAlias, + pub message: &'a str, +} +impl<'a> Widget for IdentifiedRepoWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + Text::from(format!( + "- {} (loading...) [{}]", + self.repo_alias, self.message + )) + .render(area, buf); + } +} diff --git a/crates/cli/src/tui/components/repo/mod.rs b/crates/cli/src/tui/components/repo/mod.rs new file mode 100644 index 0000000..9e3c083 --- /dev/null +++ b/crates/cli/src/tui/components/repo/mod.rs @@ -0,0 +1,92 @@ +// +mod alert; +mod configured; +mod identified; +mod ready; + +use alert::AlertRepoWidget; +use configured::ConfiguredRepoWidget; +use identified::IdentifiedRepoWidget; +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; +use ready::ReadyRepoWidget; + +use crate::tui::actor::RepoState; + +pub struct RepoWidget<'a> { + pub repo_state: &'a RepoState, +} +impl<'a> Widget for RepoWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + match self.repo_state { + RepoState::Identified { + repo_alias, + message, + } => { + IdentifiedRepoWidget { + repo_alias, + message, + } + .render(area, buf); + } + + RepoState::Configured { + repo_alias, + message, + branches, + .. + } => ConfiguredRepoWidget { + repo_alias, + message, + branches, + } + .render(area, buf), + + RepoState::Ready { + repo_alias, + message, + branches, + view_state, + main, + next, + dev, + log, + } => ReadyRepoWidget { + repo_alias, + message, + branches, + view_state, + main, + next, + dev, + log, + } + .render(area, buf), + + RepoState::Alert { + repo_alias, + branches, + view_state, + main, + next, + dev, + log, + message, + alert, + } => AlertRepoWidget { + repo_alias, + message, + branches, + view_state, + main, + next, + dev, + log, + alert, + } + .render(area, buf), + }; + } +} diff --git a/crates/cli/src/tui/components/repo/ready.rs b/crates/cli/src/tui/components/repo/ready.rs new file mode 100644 index 0000000..3f9cda5 --- /dev/null +++ b/crates/cli/src/tui/components/repo/ready.rs @@ -0,0 +1,65 @@ +// +use git_next_core::{ + git::{graph::Log, Commit}, + RepoAlias, RepoBranches, +}; + +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + text::{Line, Text}, + widgets::{Paragraph, Widget}, +}; + +use crate::tui::actor::ViewState; + +pub struct ReadyRepoWidget<'a> { + pub repo_alias: &'a RepoAlias, + pub message: &'a str, + pub branches: &'a RepoBranches, + pub view_state: &'a ViewState, + pub main: &'a Commit, + pub next: &'a Commit, + pub dev: &'a Commit, + pub log: &'a Log, +} +impl<'a> Widget for ReadyRepoWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) + .split(area); + Paragraph::new(Text::from(vec![ + Line::from(vec![ + self.view_state.to_string().into(), + " ".into(), + self.repo_alias.to_string().into(), + " (".into(), + self.branches.main().to_string().into(), + "/".into(), + self.branches.next().to_string().into(), + "/".into(), + self.branches.dev().to_string().into(), + ") [".into(), + self.message.into(), + "]".into(), + ]), + Line::from(vec![ + self.main.to_string().into(), + " , ".into(), + self.next.to_string().into(), + " , ".into(), + self.dev.to_string().into(), + "}".into(), + ]), + ])) + .render(layout[0], buf); + Paragraph::new(Text::from( + self.log.iter().cloned().map(Line::from).collect::>(), + )) + .render(layout[1], buf); + } +} diff --git a/crates/cli/src/tui/mod.rs b/crates/cli/src/tui/mod.rs index 508e8de..c60a4bb 100644 --- a/crates/cli/src/tui/mod.rs +++ b/crates/cli/src/tui/mod.rs @@ -1,5 +1,6 @@ // mod actor; +pub mod components; pub use actor::messages::Tick; pub use actor::Tui; diff --git a/crates/core/src/config/server_repo_config.rs b/crates/core/src/config/server_repo_config.rs index 44f4456..83013eb 100644 --- a/crates/core/src/config/server_repo_config.rs +++ b/crates/core/src/config/server_repo_config.rs @@ -47,7 +47,8 @@ impl ServerRepoConfig { } /// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided - pub(crate) fn repo_config(&self) -> Option { + #[must_use] + pub fn repo_config(&self) -> Option { match (&self.main, &self.next, &self.dev) { (Some(main), Some(next), Some(dev)) => Some(RepoConfig::new( RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()), 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; diff --git a/crates/core/src/git/user_notification.rs b/crates/core/src/git/user_notification.rs index 00692f3..828aea3 100644 --- a/crates/core/src/git/user_notification.rs +++ b/crates/core/src/git/user_notification.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + // use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias}; use serde_json::json; @@ -115,3 +117,18 @@ impl UserNotification { } } } +impl Display for UserNotification { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::CICheckFailed { commit, .. } => format!("CI Check Failed [{commit}]"), + Self::RepoConfigLoadFailure { reason, .. } => { + format!("Failed to load repo config: {reason}") + } + Self::WebhookRegistration { reason, .. } => { + format!("Failed to register webhook: {reason}") + } + Self::DevNotBasedOnMain { .. } => "Dev not based on main".to_string(), + }; + write!(f, "{message}") + } +}