WIP: feat(tui): update state model from server messages
This commit is contained in:
parent
622e144986
commit
a103c3f36e
20 changed files with 504 additions and 98 deletions
|
@ -12,7 +12,8 @@ keywords = { workspace = true }
|
||||||
categories = { workspace = true }
|
categories = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["forgejo", "github"]
|
# default = ["forgejo", "github"]
|
||||||
|
default = ["forgejo", "github", "tui"]
|
||||||
forgejo = ["git-next-forge-forgejo"]
|
forgejo = ["git-next-forge-forgejo"]
|
||||||
github = ["git-next-forge-github"]
|
github = ["git-next-forge-github"]
|
||||||
tui = ["ratatui"]
|
tui = ["ratatui"]
|
||||||
|
|
|
@ -2,10 +2,13 @@
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::repo::{
|
use crate::{
|
||||||
do_send,
|
repo::{
|
||||||
messages::{ReceiveRepoConfig, RegisterWebhook},
|
do_send,
|
||||||
RepoActor,
|
messages::{ReceiveRepoConfig, RegisterWebhook},
|
||||||
|
RepoActor,
|
||||||
|
},
|
||||||
|
server::actor::messages::ServerUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveRepoConfig> for RepoActor {
|
impl Handler<ReceiveRepoConfig> for RepoActor {
|
||||||
|
@ -13,7 +16,14 @@ impl Handler<ReceiveRepoConfig> for RepoActor {
|
||||||
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
||||||
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||||
let repo_config = msg.unwrap();
|
let repo_config = msg.unwrap();
|
||||||
self.repo_details.repo_config.replace(repo_config);
|
self.repo_details.repo_config.replace(repo_config.clone());
|
||||||
|
if let Some(server_addr) = &self.server_addr {
|
||||||
|
server_addr.do_send(ServerUpdate::from((
|
||||||
|
&repo_config,
|
||||||
|
self.repo_details.forge.forge_alias().clone(),
|
||||||
|
self.repo_details.repo_alias.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,9 @@ impl Handler<ValidateRepo> for RepoActor {
|
||||||
};
|
};
|
||||||
logger(self.log.as_ref(), "have repo config");
|
logger(self.log.as_ref(), "have repo config");
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
self.update_tui();
|
||||||
|
|
||||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
||||||
Ok(Positions {
|
Ok(Positions {
|
||||||
main,
|
main,
|
||||||
|
|
|
@ -135,7 +135,7 @@ fn handle_push(
|
||||||
last_commit: &mut Option<Commit>,
|
last_commit: &mut Option<Commit>,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&ActorLog>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
logger(log, "message is for dev branch");
|
logger(log, format!("message is for {branch} branch"));
|
||||||
let commit = Commit::from(push);
|
let commit = Commit::from(push);
|
||||||
if last_commit.as_ref() == Some(&commit) {
|
if last_commit.as_ref() == Some(&commit) {
|
||||||
logger(log, format!("not a new commit on {branch}"));
|
logger(log, format!("not a new commit on {branch}"));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use crate::alerts::messages::NotifyUser;
|
use crate::{alerts::messages::NotifyUser, server::ServerActor};
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
use kxio::network::Network;
|
use kxio::network::Network;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -59,6 +59,7 @@ pub struct RepoActor {
|
||||||
forge: Box<dyn git::ForgeLike>,
|
forge: Box<dyn git::ForgeLike>,
|
||||||
log: Option<ActorLog>,
|
log: Option<ActorLog>,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||||
|
server_addr: Option<Addr<ServerActor>>,
|
||||||
}
|
}
|
||||||
impl RepoActor {
|
impl RepoActor {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -71,6 +72,7 @@ impl RepoActor {
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||||
|
server_addr: Option<Addr<ServerActor>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let message_token = messages::MessageToken::default();
|
let message_token = messages::MessageToken::default();
|
||||||
Self {
|
Self {
|
||||||
|
@ -90,6 +92,18 @@ impl RepoActor {
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
log: None,
|
log: None,
|
||||||
notify_user_recipient,
|
notify_user_recipient,
|
||||||
|
server_addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
fn update_tui(&self) {
|
||||||
|
use crate::server::actor::messages::ServerUpdate;
|
||||||
|
|
||||||
|
if let (Some(server_addr), Some(repo_config)) =
|
||||||
|
(&self.server_addr, &self.repo_details.repo_config)
|
||||||
|
{
|
||||||
|
server_addr.do_send(ServerUpdate::from((&self.repo_details, repo_config)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,7 @@ pub fn a_repo_actor(
|
||||||
repository_factory,
|
repository_factory,
|
||||||
std::time::Duration::from_nanos(1),
|
std::time::Duration::from_nanos(1),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.with_log(actors_log),
|
.with_log(actors_log),
|
||||||
log,
|
log,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod file_updated;
|
mod file_updated;
|
||||||
mod receive_app_config;
|
mod receive_app_config;
|
||||||
mod receive_valid_app_config;
|
mod receive_valid_app_config;
|
||||||
|
mod server_update;
|
||||||
mod shutdown;
|
mod shutdown;
|
||||||
mod subscribe_updates;
|
mod subscribe_updates;
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
alerts::messages::UpdateShout,
|
alerts::messages::UpdateShout,
|
||||||
repo::{messages::CloneRepo, RepoActor},
|
repo::{messages::CloneRepo, RepoActor},
|
||||||
server::actor::{
|
server::actor::{
|
||||||
messages::{ReceiveValidAppConfig, ValidAppConfig},
|
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
||||||
ServerActor,
|
ServerActor,
|
||||||
},
|
},
|
||||||
webhook::{
|
webhook::{
|
||||||
|
@ -21,7 +21,7 @@ use crate::{
|
||||||
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
type Result = ();
|
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 {
|
let ValidAppConfig {
|
||||||
app_config,
|
app_config,
|
||||||
socket_address,
|
socket_address,
|
||||||
|
@ -37,6 +37,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
let webhook_router = WebhookRouterActor::default().start();
|
let webhook_router = WebhookRouterActor::default().start();
|
||||||
let listen_url = app_config.listen().url();
|
let listen_url = app_config.listen().url();
|
||||||
let notify_user_recipient = self.alerts.clone().recipient();
|
let notify_user_recipient = self.alerts.clone().recipient();
|
||||||
|
let server_addr = Some(ctx.address());
|
||||||
// Forge Actors
|
// Forge Actors
|
||||||
for (forge_alias, forge_config) in app_config.forges() {
|
for (forge_alias, forge_config) in app_config.forges() {
|
||||||
let repo_actors = self
|
let repo_actors = self
|
||||||
|
@ -46,6 +47,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
&server_storage,
|
&server_storage,
|
||||||
listen_url,
|
listen_url,
|
||||||
¬ify_user_recipient,
|
¬ify_user_recipient,
|
||||||
|
server_addr.clone(),
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(start_repo_actor)
|
.map(start_repo_actor)
|
||||||
|
@ -69,9 +71,18 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
||||||
self.webhook_actor_addr.replace(webhook_actor_addr);
|
self.webhook_actor_addr.replace(webhook_actor_addr);
|
||||||
let shout = app_config.shout().clone();
|
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.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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ use actix::{Message, Recipient};
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::graph::Log,
|
git::{self, graph::Log, RepoDetails},
|
||||||
message,
|
message,
|
||||||
server::{AppConfig, Storage},
|
server::{AppConfig, Storage},
|
||||||
ForgeAlias, RepoAlias, RepoBranches,
|
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
@ -40,20 +40,61 @@ message!(Shutdown, "Notification to shutdown the server actor");
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
pub enum ServerUpdate {
|
pub enum ServerUpdate {
|
||||||
/// Status of a repo
|
/// List of all configured forges and aliases
|
||||||
UpdateRepoSummary {
|
AppConfigLoaded { app_config: ValidAppConfig },
|
||||||
|
/// Configuration of a repo
|
||||||
|
UpdateRepoConfig {
|
||||||
|
forge_alias: ForgeAlias,
|
||||||
|
repo_alias: RepoAlias,
|
||||||
|
branches: RepoBranches,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Status of the repo
|
||||||
|
UpdateRepoDetails {
|
||||||
forge_alias: ForgeAlias,
|
forge_alias: ForgeAlias,
|
||||||
repo_alias: RepoAlias,
|
repo_alias: RepoAlias,
|
||||||
branches: RepoBranches,
|
branches: RepoBranches,
|
||||||
log: Log,
|
log: Log,
|
||||||
},
|
},
|
||||||
/// remove a repo
|
}
|
||||||
RemoveRepo {
|
|
||||||
forge_alias: ForgeAlias,
|
// impl From<&AppConfig> for ServerUpdate {
|
||||||
repo_alias: RepoAlias,
|
// fn from(app_config: &AppConfig) -> Self {
|
||||||
},
|
// Self::ForgeRepoList {
|
||||||
/// test message
|
// list: app_config
|
||||||
Ping,
|
// .forges()
|
||||||
|
// .flat_map(|(forge_alias, forge_config)| {
|
||||||
|
// forge_config
|
||||||
|
// .repos()
|
||||||
|
// .map(|(repo_alias, _server_repo_config)| (forge_alias.clone(), repo_alias))
|
||||||
|
// .collect::<Vec<_>>()
|
||||||
|
// })
|
||||||
|
// .collect::<Vec<_>>(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl From<(&RepoConfig, ForgeAlias, RepoAlias)> for ServerUpdate {
|
||||||
|
fn from(value: (&RepoConfig, ForgeAlias, RepoAlias)) -> Self {
|
||||||
|
Self::UpdateRepoConfig {
|
||||||
|
forge_alias: value.1,
|
||||||
|
repo_alias: value.2,
|
||||||
|
branches: value.0.branches().clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&RepoDetails, &RepoConfig)> for ServerUpdate {
|
||||||
|
fn from((repo_details, repo_config): (&RepoDetails, &RepoConfig)) -> Self {
|
||||||
|
let branches = repo_config.branches().clone();
|
||||||
|
let git_graph_log = git::graph::log(repo_details);
|
||||||
|
Self::UpdateRepoDetails {
|
||||||
|
forge_alias: repo_details.forge.forge_alias().clone(),
|
||||||
|
repo_alias: repo_details.repo_alias.clone(),
|
||||||
|
branches,
|
||||||
|
log: git_graph_log,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(
|
message!(
|
||||||
|
|
|
@ -118,6 +118,7 @@ impl ServerActor {
|
||||||
server_storage: &Storage,
|
server_storage: &Storage,
|
||||||
listen_url: &ListenUrl,
|
listen_url: &ListenUrl,
|
||||||
notify_user_recipient: &Recipient<NotifyUser>,
|
notify_user_recipient: &Recipient<NotifyUser>,
|
||||||
|
server_addr: Option<Addr<Self>>,
|
||||||
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
||||||
let span =
|
let span =
|
||||||
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
||||||
|
@ -125,8 +126,13 @@ impl ServerActor {
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
tracing::info!("Creating Forge");
|
tracing::info!("Creating Forge");
|
||||||
let mut repos = vec![];
|
let mut repos = vec![];
|
||||||
let creator =
|
let creator = self.create_actor(
|
||||||
self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url);
|
forge_name,
|
||||||
|
forge_config.clone(),
|
||||||
|
server_storage,
|
||||||
|
listen_url,
|
||||||
|
server_addr,
|
||||||
|
);
|
||||||
for (repo_alias, server_repo_config) in forge_config.repos() {
|
for (repo_alias, server_repo_config) in forge_config.repos() {
|
||||||
let forge_repo = creator((
|
let forge_repo = creator((
|
||||||
repo_alias,
|
repo_alias,
|
||||||
|
@ -148,6 +154,7 @@ impl ServerActor {
|
||||||
forge_config: ForgeConfig,
|
forge_config: ForgeConfig,
|
||||||
server_storage: &Storage,
|
server_storage: &Storage,
|
||||||
listen_url: &ListenUrl,
|
listen_url: &ListenUrl,
|
||||||
|
server_addr: Option<Addr<Self>>,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
||||||
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
||||||
|
@ -194,6 +201,7 @@ impl ServerActor {
|
||||||
repository_factory.duplicate(),
|
repository_factory.duplicate(),
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
Some(notify_user_recipient),
|
Some(notify_user_recipient),
|
||||||
|
server_addr.clone(),
|
||||||
);
|
);
|
||||||
(forge_name.clone(), repo_alias, actor)
|
(forge_name.clone(), repo_alias, actor)
|
||||||
}
|
}
|
||||||
|
@ -242,10 +250,4 @@ impl ServerActor {
|
||||||
ctx.address().do_send(msg);
|
ctx.address().do_send(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_server_updates(&self) {
|
|
||||||
self.subscribers.iter().for_each(|subscriber| {
|
|
||||||
subscriber.do_send(ServerUpdate::Ping);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,6 @@ pub fn start(
|
||||||
info!("Starting Server...");
|
info!("Starting Server...");
|
||||||
let server =
|
let server =
|
||||||
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
||||||
server.do_send(FileUpdated);
|
|
||||||
|
|
||||||
info!("Starting File Watcher...");
|
info!("Starting File Watcher...");
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
@ -75,18 +74,18 @@ pub fn start(
|
||||||
|
|
||||||
let (tx_shutdown, rx_shutdown) = channel::<()>();
|
let (tx_shutdown, rx_shutdown) = channel::<()>();
|
||||||
let tui_addr = tui::Tui::new(tx_shutdown).start();
|
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(SubscribeToUpdates::new(tui_addr.clone().recipient()));
|
||||||
|
server.do_send(FileUpdated); // update file after ui subscription in place
|
||||||
loop {
|
loop {
|
||||||
let _ = tui_addr.send(tui::Tick).await;
|
let _ = tui_addr.send(tui::Tick).await;
|
||||||
if rx_shutdown.try_recv().is_ok() {
|
if rx_shutdown.try_recv().is_ok() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// actix_rt::task::yield_now().await;
|
actix_rt::time::sleep(Duration::from_millis(16)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
server.do_send(FileUpdated);
|
||||||
info!("Server running - Press Ctrl-C to stop...");
|
info!("Server running - Press Ctrl-C to stop...");
|
||||||
let _ = signal::ctrl_c().await;
|
let _ = signal::ctrl_c().await;
|
||||||
info!("Ctrl-C received, shutting down...");
|
info!("Ctrl-C received, shutting down...");
|
||||||
|
|
105
crates/cli/src/tui/README.md
Normal file
105
crates/cli/src/tui/README.md
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
# 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) | |
|
||||||
|
| +--------------------------------------------------------------------+ |
|
||||||
|
+------------------------------------------------------------------------+
|
||||||
|
```
|
|
@ -1,27 +1,87 @@
|
||||||
use std::time::Instant;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use actix::Handler;
|
use actix::Handler;
|
||||||
|
|
||||||
use crate::{server::actor::messages::ServerUpdate, tui::Tui};
|
use crate::{
|
||||||
|
server::actor::messages::ServerUpdate,
|
||||||
|
tui::{
|
||||||
|
actor::{ForgeState, RepoState, StateMode, ViewState},
|
||||||
|
Tui,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
impl Handler<ServerUpdate> for Tui {
|
impl Handler<ServerUpdate> for Tui {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
self.state.tap();
|
||||||
match msg {
|
match msg {
|
||||||
ServerUpdate::UpdateRepoSummary {
|
ServerUpdate::AppConfigLoaded { app_config } => {
|
||||||
|
let 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,
|
||||||
|
branches: rc.branches().clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
repo_alias.clone(),
|
||||||
|
RepoState::Identified { repo_alias },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.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<_, _>>();
|
||||||
|
self.state.mode = StateMode::Configured { app_config, forges }
|
||||||
|
}
|
||||||
|
ServerUpdate::UpdateRepoConfig {
|
||||||
|
forge_alias,
|
||||||
|
repo_alias,
|
||||||
|
branches,
|
||||||
|
} => {
|
||||||
|
// self.state.repos_mut().insert(
|
||||||
|
// ForgeRepoKey::new(forge_alias, repo_alias),
|
||||||
|
// RepoState::Loaded {
|
||||||
|
// branches,
|
||||||
|
// log: Log::default(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
ServerUpdate::UpdateRepoDetails {
|
||||||
forge_alias,
|
forge_alias,
|
||||||
repo_alias,
|
repo_alias,
|
||||||
branches,
|
branches,
|
||||||
log,
|
log,
|
||||||
} => todo!(),
|
} => {
|
||||||
ServerUpdate::RemoveRepo {
|
// self.state.repos_mut().insert(
|
||||||
forge_alias,
|
// ForgeRepoKey::new(forge_alias, repo_alias),
|
||||||
repo_alias,
|
// RepoState::Loaded { branches, log },
|
||||||
} => todo!(),
|
// );
|
||||||
ServerUpdate::Ping => {
|
|
||||||
self.last_ping = Instant::now();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
//
|
//
|
||||||
use std::{borrow::BorrowMut, time::Instant};
|
use actix::Handler;
|
||||||
|
|
||||||
use actix::{ActorContext, Handler};
|
|
||||||
use ratatui::{
|
|
||||||
crossterm::event::{self, KeyCode, KeyEventKind},
|
|
||||||
style::Stylize as _,
|
|
||||||
widgets::Paragraph,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::tui::actor::{messages::Tick, Tui};
|
use crate::tui::actor::{messages::Tick, Tui};
|
||||||
|
|
||||||
|
@ -14,44 +7,9 @@ impl Handler<Tick> for Tui {
|
||||||
type Result = std::io::Result<()>;
|
type Result = std::io::Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
|
||||||
if let Some(terminal) = self.terminal.borrow_mut() {
|
self.state.tap();
|
||||||
terminal.draw(|frame| {
|
self.draw()?;
|
||||||
let area = frame.area();
|
self.handle_input(ctx)?;
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(format!(
|
|
||||||
"(press 'q' to quit) Ping:[{:?}] UI:[{:?}]",
|
|
||||||
self.last_ping,
|
|
||||||
Instant::now()
|
|
||||||
))
|
|
||||||
.white()
|
|
||||||
.on_blue(),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
})?;
|
|
||||||
} else {
|
|
||||||
eprintln!("No terminal setup");
|
|
||||||
}
|
|
||||||
if event::poll(std::time::Duration::from_millis(16))? {
|
|
||||||
if let event::Event::Key(key) = event::read()? {
|
|
||||||
if key.kind == KeyEventKind::Press {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => {
|
|
||||||
// execute!(stderr(), LeaveAlternateScreen)?;
|
|
||||||
// disable_raw_mode()?;
|
|
||||||
ctx.stop();
|
|
||||||
if let Err(err) = self.signal_shutdown.send(()) {
|
|
||||||
tracing::error!(?err, "Failed to signal shutdown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//
|
//
|
||||||
use git_next_core::message;
|
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;
|
mod handlers;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
mod model;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{stderr, Stderr},
|
io::{stderr, Stderr},
|
||||||
sync::mpsc::Sender,
|
sync::mpsc::Sender,
|
||||||
time::Instant,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix::{Actor, Context};
|
use actix::{Actor, ActorContext as _, Context};
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::{
|
crossterm::{
|
||||||
|
event::{self, KeyCode, KeyEventKind},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
},
|
},
|
||||||
|
@ -23,7 +26,7 @@ use ratatui::{
|
||||||
pub struct Tui {
|
pub struct Tui {
|
||||||
terminal: Option<Terminal<CrosstermBackend<Stderr>>>,
|
terminal: Option<Terminal<CrosstermBackend<Stderr>>>,
|
||||||
signal_shutdown: Sender<()>,
|
signal_shutdown: Sender<()>,
|
||||||
last_ping: Instant,
|
pub state: State,
|
||||||
}
|
}
|
||||||
impl Actor for Tui {
|
impl Actor for Tui {
|
||||||
type Context = Context<Self>;
|
type Context = Context<Self>;
|
||||||
|
@ -58,9 +61,48 @@ impl Tui {
|
||||||
Self {
|
Self {
|
||||||
terminal: None,
|
terminal: None,
|
||||||
signal_shutdown,
|
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>>> {
|
fn init() -> std::io::Result<Terminal<CrosstermBackend<Stderr>>> {
|
||||||
|
|
144
crates/cli/src/tui/actor/model.rs
Normal file
144
crates/cli/src/tui/actor/model.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
//
|
||||||
|
use ratatui::{
|
||||||
|
prelude::{Buffer, Rect},
|
||||||
|
style::Stylize as _,
|
||||||
|
symbols::border,
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{block::Title, Block, Paragraph, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use git_next_core::{git::graph::Log, BranchName, ForgeAlias, RepoAlias, RepoBranches};
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, time::Instant};
|
||||||
|
|
||||||
|
use crate::server::actor::messages::ValidAppConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct State {
|
||||||
|
last_update: Instant,
|
||||||
|
pub mode: StateMode,
|
||||||
|
}
|
||||||
|
impl State {
|
||||||
|
pub fn initial() -> Self {
|
||||||
|
Self {
|
||||||
|
last_update: Instant::now(),
|
||||||
|
mode: StateMode::Initial { tick: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tap(&mut self) {
|
||||||
|
self.last_update = Instant::now();
|
||||||
|
if let StateMode::Initial { tick } = &mut self.mode {
|
||||||
|
*tick += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum StateMode {
|
||||||
|
/// UI has started but has no information on the state of the server
|
||||||
|
Initial { tick: usize },
|
||||||
|
/// The application configuration has been loaded, individual forges and repos have their own
|
||||||
|
/// states
|
||||||
|
Configured {
|
||||||
|
app_config: ValidAppConfig,
|
||||||
|
forges: BTreeMap<ForgeAlias, ForgeState>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum ViewState {
|
||||||
|
#[default]
|
||||||
|
Collapsed,
|
||||||
|
Expanded,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
},
|
||||||
|
Configured {
|
||||||
|
repo_alias: RepoAlias,
|
||||||
|
branches: RepoBranches,
|
||||||
|
},
|
||||||
|
Ready {
|
||||||
|
repo_alias: RepoAlias,
|
||||||
|
branches: RepoBranches,
|
||||||
|
view_state: ViewState,
|
||||||
|
main: BranchName,
|
||||||
|
next: BranchName,
|
||||||
|
dev: BranchName,
|
||||||
|
log: Log,
|
||||||
|
},
|
||||||
|
Alert {
|
||||||
|
repo_alias: RepoAlias,
|
||||||
|
branches: RepoBranches,
|
||||||
|
view_state: ViewState,
|
||||||
|
main: BranchName,
|
||||||
|
next: BranchName,
|
||||||
|
dev: BranchName,
|
||||||
|
log: Log,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ratatui::layout::Alignment::Center))
|
||||||
|
.title(
|
||||||
|
Title::from(Line::from(vec![
|
||||||
|
" [q]uit ".into(),
|
||||||
|
format!("{:?}", self.last_update).into(),
|
||||||
|
]))
|
||||||
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
|
.position(ratatui::widgets::block::Position::Bottom),
|
||||||
|
)
|
||||||
|
.border_set(border::THICK);
|
||||||
|
|
||||||
|
match &self.mode {
|
||||||
|
StateMode::Initial { tick } => Paragraph::new(format!("Loading...{tick}"))
|
||||||
|
.centered()
|
||||||
|
.block(block)
|
||||||
|
.render(area, buf),
|
||||||
|
StateMode::Configured { app_config, forges } => ConfiguredAppWidget {
|
||||||
|
app_config,
|
||||||
|
forges,
|
||||||
|
block,
|
||||||
|
}
|
||||||
|
.render(area, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfiguredAppWidget<'a> {
|
||||||
|
app_config: &'a ValidAppConfig,
|
||||||
|
forges: &'a BTreeMap<ForgeAlias, ForgeState>,
|
||||||
|
block: Block<'a>,
|
||||||
|
}
|
||||||
|
impl<'a> Widget for ConfiguredAppWidget<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let mut lines: Vec<Line> = vec![format!("Forges: ({})", self.forges.keys().len()).into()];
|
||||||
|
let mut forge_aliases = self
|
||||||
|
.forges
|
||||||
|
.keys()
|
||||||
|
.map(|key| key.to_string().into())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
lines.append(&mut forge_aliases);
|
||||||
|
Paragraph::new(lines).block(self.block).render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,8 @@ impl ServerRepoConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided
|
/// 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) {
|
match (&self.main, &self.next, &self.dev) {
|
||||||
(Some(main), Some(next), Some(dev)) => Some(RepoConfig::new(
|
(Some(main), Some(next), Some(dev)) => Some(RepoConfig::new(
|
||||||
RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()),
|
RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
|
|
||||||
use take_until::TakeUntilExt;
|
use take_until::TakeUntilExt;
|
||||||
|
|
Loading…
Reference in a new issue