WIP: feat(tui): update state model from server messages
This commit is contained in:
parent
622e144986
commit
d848c94483
34 changed files with 1134 additions and 110 deletions
|
@ -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"]
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
mod alerts;
|
||||
mod file_watcher;
|
||||
mod forge;
|
||||
|
|
|
@ -22,6 +22,8 @@ impl Handler<ReceiveCIStatus> 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<ReceiveCIStatus> 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(),
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ impl Handler<ReceiveRepoConfig> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ValidateRepo> 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<ValidateRepo> 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<ValidateRepo> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ fn handle_push(
|
|||
last_commit: &mut Option<Commit>,
|
||||
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}"));
|
||||
|
|
|
@ -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<dyn git::ForgeLike>,
|
||||
log: Option<ActorLog>,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
}
|
||||
impl RepoActor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -71,6 +78,7 @@ impl RepoActor {
|
|||
repository_factory: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
) -> 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<String>) {
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,6 +195,7 @@ pub fn a_repo_actor(
|
|||
repository_factory,
|
||||
std::time::Duration::from_nanos(1),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.with_log(actors_log),
|
||||
log,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod file_updated;
|
||||
mod receive_app_config;
|
||||
mod receive_valid_app_config;
|
||||
mod server_update;
|
||||
mod shutdown;
|
||||
mod subscribe_updates;
|
||||
|
|
|
@ -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<ReceiveValidAppConfig> 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<ReceiveValidAppConfig> 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<ReceiveValidAppConfig> for ServerActor {
|
|||
&server_storage,
|
||||
listen_url,
|
||||
¬ify_user_recipient,
|
||||
server_addr.clone(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(start_repo_actor)
|
||||
|
@ -69,9 +71,18 @@ impl Handler<ReceiveValidAppConfig> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal file
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use actix::Handler;
|
||||
|
||||
//
|
||||
use crate::server::{actor::messages::ServerUpdate, ServerActor};
|
||||
|
||||
impl Handler<ServerUpdate> 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());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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!(
|
||||
|
|
|
@ -118,6 +118,7 @@ impl ServerActor {
|
|||
server_storage: &Storage,
|
||||
listen_url: &ListenUrl,
|
||||
notify_user_recipient: &Recipient<NotifyUser>,
|
||||
server_addr: Option<Addr<Self>>,
|
||||
) -> 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<Addr<Self>>,
|
||||
) -> impl Fn(
|
||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
||||
) -> (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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...");
|
||||
|
|
121
crates/cli/src/tui/README.md
Normal file
121
crates/cli/src/tui/README.md
Normal file
|
@ -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<ForgeAlias, RepoState>,
|
||||
},
|
||||
}
|
||||
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
|
|
@ -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<ServerUpdate> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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 => {
|
||||
//
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state.tap();
|
||||
self.draw()?;
|
||||
self.handle_input(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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<Terminal<CrosstermBackend<Stderr>>>,
|
||||
signal_shutdown: Sender<()>,
|
||||
last_ping: Instant,
|
||||
pub state: State,
|
||||
}
|
||||
impl Actor for Tui {
|
||||
type Context = Context<Self>;
|
||||
|
@ -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 <Self as actix::Actor>::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<Terminal<CrosstermBackend<Stderr>>> {
|
||||
|
|
337
crates/cli/src/tui/actor/model.rs
Normal file
337
crates/cli/src/tui/actor/model.rs
Normal file
|
@ -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<ForgeAlias, ForgeState>,
|
||||
},
|
||||
}
|
||||
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<ValidAppConfig> 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::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.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::<BTreeMap<_, _>>(),
|
||||
};
|
||||
(forge_alias, forge_state)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<RepoAlias, RepoState>,
|
||||
}
|
||||
|
||||
#[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<String>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
crates/cli/src/tui/components/configured_app.rs
Normal file
52
crates/cli/src/tui/components/configured_app.rs
Normal file
|
@ -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<ForgeAlias, ForgeState>,
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
15
crates/cli/src/tui/components/forge/collapsed.rs
Normal file
15
crates/cli/src/tui/components/forge/collapsed.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
48
crates/cli/src/tui/components/forge/expanded.rs
Normal file
48
crates/cli/src/tui/components/forge/expanded.rs
Normal file
|
@ -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<RepoAlias, RepoState>,
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
34
crates/cli/src/tui/components/forge/mod.rs
Normal file
34
crates/cli/src/tui/components/forge/mod.rs
Normal file
|
@ -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<RepoAlias, RepoState>,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
5
crates/cli/src/tui/components/mod.rs
Normal file
5
crates/cli/src/tui/components/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod configured_app;
|
||||
mod forge;
|
||||
mod repo;
|
||||
|
||||
pub use configured_app::ConfiguredAppWidget;
|
68
crates/cli/src/tui/components/repo/alert.rs
Normal file
68
crates/cli/src/tui/components/repo/alert.rs
Normal file
|
@ -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::<Vec<_>>(),
|
||||
))
|
||||
.render(layout[1], buf);
|
||||
}
|
||||
}
|
23
crates/cli/src/tui/components/repo/configured.rs
Normal file
23
crates/cli/src/tui/components/repo/configured.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
21
crates/cli/src/tui/components/repo/identified.rs
Normal file
21
crates/cli/src/tui/components/repo/identified.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
92
crates/cli/src/tui/components/repo/mod.rs
Normal file
92
crates/cli/src/tui/components/repo/mod.rs
Normal file
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
65
crates/cli/src/tui/components/repo/ready.rs
Normal file
65
crates/cli/src/tui/components/repo/ready.rs
Normal file
|
@ -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::<Vec<_>>(),
|
||||
))
|
||||
.render(layout[1], buf);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//
|
||||
mod actor;
|
||||
pub mod components;
|
||||
|
||||
pub use actor::messages::Tick;
|
||||
pub use actor::Tui;
|
||||
|
|
|
@ -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<RepoConfig> {
|
||||
#[must_use]
|
||||
pub fn repo_config(&self) -> Option<RepoConfig> {
|
||||
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()),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use take_until::TakeUntilExt;
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue