Skip to content

feat(cli): add interactive TUI dashboard#2

Merged
wolfiesch merged 2 commits intomasterfrom
feature/tui-dashboard
Jan 15, 2026
Merged

feat(cli): add interactive TUI dashboard#2
wolfiesch merged 2 commits intomasterfrom
feature/tui-dashboard

Conversation

@wolfiesch
Copy link
Collaborator

Summary

  • Add terminal-based dashboard for FGP daemon monitoring using Ratatui
  • Real-time service status display with color-coded indicators
  • Keyboard navigation with vim-style bindings
  • Start/stop service control
  • Auto-refresh with configurable polling interval

Features

Key Action
↑/k Select previous service
↓/j Select next service
s/Enter Start selected service
x Stop selected service
r Force refresh
? Toggle help overlay
q/Esc Quit

Usage

fgp tui              # Launch with default 2s polling
fgp tui --poll 500   # Launch with 500ms polling

Test plan

  • Launch fgp tui and verify service list appears
  • Test navigation with arrow keys and vim keys
  • Start/stop a service and verify status updates
  • Verify help overlay toggles with ?
  • Verify quit works with q and Esc

🤖 Generated with Claude Code

Add terminal-based dashboard for FGP daemon monitoring using Ratatui:

- Service list with real-time status (running/stopped/unhealthy/error)
- Keyboard navigation (vim keys + arrows)
- Start/stop service actions
- Auto-refresh with configurable polling interval
- Help overlay toggle
- Success/error message feedback

Usage: fgp tui [--poll <ms>]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 15, 2026 05:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an interactive terminal-based UI (TUI) dashboard for monitoring FGP daemon services using the Ratatui framework. The dashboard provides real-time service status monitoring with color-coded indicators, vim-style keyboard navigation, and service control capabilities (start/stop).

Changes:

  • Added TUI dashboard with real-time service monitoring and control
  • Implemented keyboard navigation with vim-style bindings and service management actions
  • Added auto-refresh with configurable polling interval (default 2000ms)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/tui/ui.rs UI rendering logic including header, service table, footer with keybindings, and help overlay
src/tui/mod.rs Main TUI entry point and event loop handling keyboard input and refresh events
src/tui/event.rs Event handling system using threads for input, UI ticks, and service polling
src/tui/app.rs Application state management, service discovery, and status checking logic
src/main.rs Added TUI command to CLI enum
src/commands/tui.rs TUI command implementation that delegates to tui module
src/commands/mod.rs Added tui module declaration
Cargo.toml Added ratatui, crossterm dependencies and tokio (unused)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to +53
# Async runtime for TUI events
tokio = { version = "1", features = ["sync", "time", "rt-multi-thread"] }

Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
# Async runtime for TUI events
tokio = { version = "1", features = ["sync", "time", "rt-multi-thread"] }

Copilot uses AI. Check for mistakes.
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) {
Copy link

Copilot AI Jan 15, 2026

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::start_service but this module doesn't exist. Other commands in the codebase use commands::start::run directly. This should likely use the existing commands module or duplicate the start logic here, similar to how other commands work.

Suggested change
match fgp_daemon::lifecycle::start_service(&service.name) {
match commands::start::run(&service.name) {

Copilot uses AI. Check for mistakes.
if let Some(service) = self.selected_service().cloned() {
if service.status == ServiceStatus::Running || service.status == ServiceStatus::Unhealthy
{
match fgp_daemon::lifecycle::stop_service(&service.name) {
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
match fgp_daemon::lifecycle::stop_service(&service.name) {
match commands::stop::run(&service.name) {

Copilot uses AI. Check for mistakes.

/// Discover all installed services.
fn discover_services() -> Vec<ServiceInfo> {
let services_dir = fgp_daemon::lifecycle::fgp_services_dir();
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
let services_dir = fgp_daemon::lifecycle::fgp_services_dir();
let services_dir = commands::fgp_services_dir();

Copilot uses AI. Check for mistakes.
None => continue,
};

let socket_path = fgp_daemon::lifecycle::service_socket_path(&name);
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
let socket_path = fgp_daemon::lifecycle::service_socket_path(&name);
let socket_path = crate::commands::service_socket_path(&name);

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +52
let title = Line::from(vec![
Span::styled(
" FGP Dashboard ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
format!("Updated: {} ", time_str),
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded whitespace for spacing is fragile and won't adapt to different terminal widths or content lengths. Consider using Layout constraints or calculating spacing dynamically based on available width.

Suggested change
let title = Line::from(vec![
Span::styled(
" FGP Dashboard ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
format!("Updated: {} ", time_str),
let left_label = " FGP Dashboard ";
let right_label = format!("Updated: {} ", time_str);
let left_width = left_label.len() as u16;
let right_width = right_label.len() as u16;
let available_width = area.width;
let padding_width = available_width.saturating_sub(left_width + right_width);
let mut padding = String::new();
padding.reserve(padding_width as usize);
for _ in 0..padding_width {
padding.push(' ');
}
let title = Line::from(vec![
Span::styled(
left_label,
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(padding),
Span::styled(
right_label,

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +298
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)
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
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),

Copilot uses AI. Check for mistakes.
@wolfiesch wolfiesch merged commit bbb53a2 into master Jan 15, 2026
3 checks passed
@wolfiesch wolfiesch deleted the feature/tui-dashboard branch January 15, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant