-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cli): add interactive TUI dashboard #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| //! TUI command - Interactive terminal dashboard. | ||
|
|
||
| use anyhow::Result; | ||
| use std::time::Duration; | ||
|
|
||
| /// Run the TUI dashboard. | ||
| pub fn run(poll_interval_ms: u64) -> Result<()> { | ||
| let poll_interval = Duration::from_millis(poll_interval_ms); | ||
| crate::tui::run(poll_interval) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,295 @@ | ||||||||||||||||||||||||||||||||||||||||||
| //! Application state for the TUI dashboard. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| use std::fs; | ||||||||||||||||||||||||||||||||||||||||||
| use std::time::{Duration, Instant}; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Service status information. | ||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Clone)] | ||||||||||||||||||||||||||||||||||||||||||
| pub struct ServiceInfo { | ||||||||||||||||||||||||||||||||||||||||||
| pub name: String, | ||||||||||||||||||||||||||||||||||||||||||
| pub status: ServiceStatus, | ||||||||||||||||||||||||||||||||||||||||||
| pub version: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||
| pub uptime_seconds: Option<u64>, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Service health states. | ||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||||||||||||||||||||||||||||||||||||||||
| #[allow(dead_code)] | ||||||||||||||||||||||||||||||||||||||||||
| pub enum ServiceStatus { | ||||||||||||||||||||||||||||||||||||||||||
| Running, | ||||||||||||||||||||||||||||||||||||||||||
| Stopped, | ||||||||||||||||||||||||||||||||||||||||||
| Unhealthy, | ||||||||||||||||||||||||||||||||||||||||||
| Error, | ||||||||||||||||||||||||||||||||||||||||||
| Starting, | ||||||||||||||||||||||||||||||||||||||||||
| Stopping, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| impl ServiceStatus { | ||||||||||||||||||||||||||||||||||||||||||
| /// Get the status symbol for display. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn symbol(&self) -> &'static str { | ||||||||||||||||||||||||||||||||||||||||||
| match self { | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Running => "●", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Stopped => "○", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Unhealthy => "◐", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Error => "●", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Starting => "◑", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Stopping => "◑", | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Get the status text for display. | ||||||||||||||||||||||||||||||||||||||||||
| #[allow(dead_code)] | ||||||||||||||||||||||||||||||||||||||||||
| pub fn text(&self) -> &'static str { | ||||||||||||||||||||||||||||||||||||||||||
| match self { | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Running => "running", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Stopped => "stopped", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Unhealthy => "unhealthy", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Error => "error", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Starting => "starting", | ||||||||||||||||||||||||||||||||||||||||||
| ServiceStatus::Stopping => "stopping", | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Message type for display. | ||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Clone)] | ||||||||||||||||||||||||||||||||||||||||||
| pub enum MessageType { | ||||||||||||||||||||||||||||||||||||||||||
| Success, | ||||||||||||||||||||||||||||||||||||||||||
| Error, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Main application state. | ||||||||||||||||||||||||||||||||||||||||||
| pub struct App { | ||||||||||||||||||||||||||||||||||||||||||
| /// List of discovered services. | ||||||||||||||||||||||||||||||||||||||||||
| pub services: Vec<ServiceInfo>, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Currently selected service index. | ||||||||||||||||||||||||||||||||||||||||||
| pub selected: usize, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Last refresh timestamp. | ||||||||||||||||||||||||||||||||||||||||||
| pub last_refresh: Instant, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Whether app should quit. | ||||||||||||||||||||||||||||||||||||||||||
| pub should_quit: bool, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Message to display (auto-clears after timeout). | ||||||||||||||||||||||||||||||||||||||||||
| pub message: Option<(String, MessageType, Instant)>, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Message display duration. | ||||||||||||||||||||||||||||||||||||||||||
| pub message_timeout: Duration, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Whether help overlay is visible. | ||||||||||||||||||||||||||||||||||||||||||
| pub show_help: bool, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| impl App { | ||||||||||||||||||||||||||||||||||||||||||
| /// Create a new app instance. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn new() -> Self { | ||||||||||||||||||||||||||||||||||||||||||
| Self { | ||||||||||||||||||||||||||||||||||||||||||
| services: Vec::new(), | ||||||||||||||||||||||||||||||||||||||||||
| selected: 0, | ||||||||||||||||||||||||||||||||||||||||||
| last_refresh: Instant::now(), | ||||||||||||||||||||||||||||||||||||||||||
| should_quit: false, | ||||||||||||||||||||||||||||||||||||||||||
| message: None, | ||||||||||||||||||||||||||||||||||||||||||
| message_timeout: Duration::from_secs(3), | ||||||||||||||||||||||||||||||||||||||||||
| show_help: false, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Tick handler - called on each frame. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn tick(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| // Clear expired messages | ||||||||||||||||||||||||||||||||||||||||||
| if let Some((_, _, created)) = &self.message { | ||||||||||||||||||||||||||||||||||||||||||
| if created.elapsed() >= self.message_timeout { | ||||||||||||||||||||||||||||||||||||||||||
| self.message = None; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Refresh service list from filesystem. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn refresh_services(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| self.services = discover_services(); | ||||||||||||||||||||||||||||||||||||||||||
| self.last_refresh = Instant::now(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Ensure selection is valid | ||||||||||||||||||||||||||||||||||||||||||
| if self.selected >= self.services.len() && !self.services.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||
| self.selected = self.services.len() - 1; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Select the previous service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn select_previous(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| if !self.services.is_empty() && self.selected > 0 { | ||||||||||||||||||||||||||||||||||||||||||
| self.selected -= 1; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Select the next service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn select_next(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| if !self.services.is_empty() && self.selected < self.services.len() - 1 { | ||||||||||||||||||||||||||||||||||||||||||
| self.selected += 1; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Select the first service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn select_first(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| self.selected = 0; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Select the last service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn select_last(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| if !self.services.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||
| self.selected = self.services.len() - 1; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Get the currently selected service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn selected_service(&self) -> Option<&ServiceInfo> { | ||||||||||||||||||||||||||||||||||||||||||
| self.services.get(self.selected) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// Start the selected service. | ||||||||||||||||||||||||||||||||||||||||||
| pub fn start_selected(&mut self) { | ||||||||||||||||||||||||||||||||||||||||||
| if let Some(service) = self.selected_service().cloned() { | ||||||||||||||||||||||||||||||||||||||||||
| if service.status == ServiceStatus::Stopped || service.status == ServiceStatus::Error { | ||||||||||||||||||||||||||||||||||||||||||
| match fgp_daemon::lifecycle::start_service(&service.name) { | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| match fgp_daemon::lifecycle::start_service(&service.name) { | |
| match commands::start::run(&service.name) { |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::stop_service but this module doesn't exist. Other commands use commands::stop::run directly. This should use the existing commands module or implement the stop logic similar to other commands.
| match fgp_daemon::lifecycle::stop_service(&service.name) { | |
| match commands::stop::run(&service.name) { |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::fgp_services_dir() but this function doesn't exist in that module. The existing commands use commands::fgp_services_dir() from src/commands/mod.rs. Use the existing function instead.
| let services_dir = fgp_daemon::lifecycle::fgp_services_dir(); | |
| let services_dir = commands::fgp_services_dir(); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::service_socket_path() but this function doesn't exist. The existing commands use commands::service_socket_path() from src/commands/mod.rs. Use the existing function instead.
| let socket_path = fgp_daemon::lifecycle::service_socket_path(&name); | |
| let socket_path = crate::commands::service_socket_path(&name); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This format_uptime function is duplicated from src/commands/status.rs (lines 137-147). Consider extracting this to a shared utility module to avoid code duplication.
| if secs < 60 { | |
| format!("{}s", secs) | |
| } else if secs < 3600 { | |
| format!("{}m {}s", secs / 60, secs % 60) | |
| } else if secs < 86400 { | |
| format!("{}h {}m", secs / 3600, (secs % 3600) / 60) | |
| } else { | |
| format!("{}d {}h", secs / 86400, (secs % 86400) / 3600) | |
| let days = secs / 86_400; | |
| let rem_after_days = secs % 86_400; | |
| let hours = rem_after_days / 3_600; | |
| let rem_after_hours = rem_after_days % 3_600; | |
| let minutes = rem_after_hours / 60; | |
| let seconds = rem_after_hours % 60; | |
| match (days, hours, minutes) { | |
| (0, 0, 0) => format!("{}s", seconds), | |
| (0, 0, _) => format!("{}m {}s", minutes, seconds), | |
| (0, _, _) => format!("{}h {}m", hours, minutes), | |
| _ => format!("{}d {}h", days, hours), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tokio dependency is added but not used anywhere in the TUI implementation. The event handling uses std::thread and std::sync::mpsc instead of async. This dependency should be removed to avoid unnecessary bloat.