From f22e3ee95e2e676c188519b513381f7707f4786f Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 28 Oct 2025 16:31:35 +0100 Subject: [PATCH] Simplify navigation and add vi-style keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major UI simplification and navigation improvements: Changes: - Removed panel selection concept entirely (no more Shift+Tab) - Service selection always visible with blue highlighting - Up/Down arrows now directly control service selection - Added j/k vi-style navigation keys as alternatives to arrow keys - Removed panel focus borders - all panels look uniform - Service commands (s/S) work without panel focus requirements - Updated keyboard shortcuts to reflect simplified navigation Navigation: - Tab: Switch hosts - ↑↓/jk: Select service (always works) - R: Rebuild host - s: Start service - S: Stop service - q: Quit The interface is now much simpler and more intuitive with direct service control. --- Cargo.lock | 6 +- agent/Cargo.toml | 2 +- dashboard/Cargo.toml | 2 +- dashboard/src/main.rs | 2 +- dashboard/src/ui/mod.rs | 186 ++++++--------------------- dashboard/src/ui/theme.rs | 12 -- dashboard/src/ui/widgets/services.rs | 6 +- shared/Cargo.toml | 2 +- 8 files changed, 45 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c74d13..870fd0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.31" +version = "0.1.32" dependencies = [ "anyhow", "chrono", @@ -291,7 +291,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.31" +version = "0.1.32" dependencies = [ "anyhow", "async-trait", @@ -314,7 +314,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.31" +version = "0.1.32" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 3cda7d8..57004a1 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.32" +version = "0.1.33" edition = "2021" [dependencies] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 9633806..d32a964 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.32" +version = "0.1.33" edition = "2021" [dependencies] diff --git a/dashboard/src/main.rs b/dashboard/src/main.rs index c1639a9..865711a 100644 --- a/dashboard/src/main.rs +++ b/dashboard/src/main.rs @@ -14,7 +14,7 @@ use app::Dashboard; /// Get hardcoded version fn get_version() -> &'static str { - "v0.1.32" + "v0.1.33" } /// Check if running inside tmux session diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index c6f5503..c1e6585 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use crossterm::event::{Event, KeyCode, KeyModifiers}; +use crossterm::event::{Event, KeyCode}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::Style, @@ -37,15 +37,6 @@ pub enum CommandType { } /// Panel types for focus management -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PanelType { - System, - Services, - Backup, -} - -impl PanelType { -} /// Widget states for a specific host #[derive(Clone)] @@ -92,8 +83,6 @@ pub struct TuiApp { available_hosts: Vec, /// Host index for navigation host_index: usize, - /// Currently focused panel - focused_panel: PanelType, /// Should quit application should_quit: bool, /// Track if user manually navigated away from localhost @@ -109,7 +98,6 @@ impl TuiApp { current_host: None, available_hosts: Vec::new(), host_index: 0, - focused_panel: PanelType::System, // Start with System panel focused should_quit: false, user_navigated_away: false, config, @@ -271,54 +259,49 @@ impl TuiApp { } } KeyCode::Char('s') => { - if self.focused_panel == PanelType::Services { - // Service start command - if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { - if self.start_command(&hostname, CommandType::ServiceStart, service_name.clone()) { - return Ok(Some(UiCommand::ServiceStart { hostname, service_name })); - } + // Service start command + if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { + if self.start_command(&hostname, CommandType::ServiceStart, service_name.clone()) { + return Ok(Some(UiCommand::ServiceStart { hostname, service_name })); } } } KeyCode::Char('S') => { - if self.focused_panel == PanelType::Services { - // Service stop command - if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { - if self.start_command(&hostname, CommandType::ServiceStop, service_name.clone()) { - return Ok(Some(UiCommand::ServiceStop { hostname, service_name })); - } + // Service stop command + if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { + if self.start_command(&hostname, CommandType::ServiceStop, service_name.clone()) { + return Ok(Some(UiCommand::ServiceStop { hostname, service_name })); } } } KeyCode::Char('b') => { - if self.focused_panel == PanelType::Backup { - // Trigger backup - if let Some(hostname) = self.current_host.clone() { - self.start_command(&hostname, CommandType::BackupTrigger, hostname.clone()); - return Ok(Some(UiCommand::TriggerBackup { hostname })); - } + // Trigger backup + if let Some(hostname) = self.current_host.clone() { + self.start_command(&hostname, CommandType::BackupTrigger, hostname.clone()); + return Ok(Some(UiCommand::TriggerBackup { hostname })); } } KeyCode::Tab => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - // Shift+Tab cycles through panels - self.next_panel(); - } else { - // Tab cycles to next host - self.navigate_host(1); + // Tab cycles to next host + self.navigate_host(1); + } + KeyCode::Up | KeyCode::Char('k') => { + // Move service selection up + if let Some(hostname) = self.current_host.clone() { + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.services_widget.select_previous(); } } - KeyCode::BackTab => { - // BackTab (Shift+Tab on some terminals) also cycles panels - self.next_panel(); - } - KeyCode::Up => { - // Scroll up in focused panel - self.scroll_focused_panel(-1); - } - KeyCode::Down => { - // Scroll down in focused panel - self.scroll_focused_panel(1); + KeyCode::Down | KeyCode::Char('j') => { + // Move service selection down + if let Some(hostname) = self.current_host.clone() { + let total_services = { + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.services_widget.get_total_services_count() + }; + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.services_widget.select_next(total_services); + } } _ => {} } @@ -359,25 +342,6 @@ impl TuiApp { } - /// Switch to next panel (Shift+Tab) - only cycles through visible panels - pub fn next_panel(&mut self) { - let visible_panels = self.get_visible_panels(); - if visible_panels.len() <= 1 { - return; // Can't switch if only one or no panels visible - } - - // Find current panel index in visible panels - if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) { - // Move to next visible panel - let next_index = (current_index + 1) % visible_panels.len(); - self.focused_panel = visible_panels[next_index]; - } else { - // Current panel not visible, switch to first visible panel - self.focused_panel = visible_panels[0]; - } - - info!("Switched to panel: {:?}", self.focused_panel); - } @@ -478,61 +442,8 @@ impl TuiApp { } } - /// Scroll the focused panel up or down - pub fn scroll_focused_panel(&mut self, direction: i32) { - if let Some(hostname) = self.current_host.clone() { - let focused_panel = self.focused_panel; // Get the value before borrowing - let host_widgets = self.get_or_create_host_widgets(&hostname); - - match focused_panel { - PanelType::System => { - if direction > 0 { - host_widgets.system_scroll_offset = host_widgets.system_scroll_offset.saturating_add(1); - } else { - host_widgets.system_scroll_offset = host_widgets.system_scroll_offset.saturating_sub(1); - } - info!("System panel scroll offset: {}", host_widgets.system_scroll_offset); - } - PanelType::Services => { - // For services panel, Up/Down moves selection cursor, not scroll - let total_services = host_widgets.services_widget.get_total_services_count(); - - if direction > 0 { - host_widgets.services_widget.select_next(total_services); - info!("Services selection moved down"); - } else { - host_widgets.services_widget.select_previous(); - info!("Services selection moved up"); - } - } - PanelType::Backup => { - if direction > 0 { - host_widgets.backup_scroll_offset = host_widgets.backup_scroll_offset.saturating_add(1); - } else { - host_widgets.backup_scroll_offset = host_widgets.backup_scroll_offset.saturating_sub(1); - } - info!("Backup panel scroll offset: {}", host_widgets.backup_scroll_offset); - } - } - } - } - /// Get list of currently visible panels - fn get_visible_panels(&self) -> Vec { - let mut visible_panels = vec![PanelType::System, PanelType::Services]; - - // Check if backup panel should be shown - if let Some(hostname) = &self.current_host { - if let Some(host_widgets) = self.host_widgets.get(hostname) { - if host_widgets.backup_widget.has_data() { - visible_panels.push(PanelType::Backup); - } - } - } - - visible_panels - } /// Render the dashboard (real btop-style multi-panel layout) pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) { @@ -601,7 +512,7 @@ impl TuiApp { // Render services widget for current host if let Some(hostname) = self.current_host.clone() { - let is_focused = self.focused_panel == PanelType::Services; + let is_focused = true; // Always show service selection let (scroll_offset, pending_transitions) = { let host_widgets = self.get_or_create_host_widgets(&hostname); (host_widgets.services_scroll_offset, host_widgets.pending_service_transitions.clone()) @@ -730,25 +641,10 @@ impl TuiApp { // Global shortcuts shortcuts.push("Tab: Switch Host".to_string()); - shortcuts.push("Shift+Tab: Switch Panel".to_string()); - - // Scroll shortcuts (always available) - shortcuts.push("↑↓: Scroll".to_string()); - - // Global rebuild shortcut (works on any panel) + shortcuts.push("↑↓/jk: Select Service".to_string()); shortcuts.push("R: Rebuild Host".to_string()); - - // Panel-specific shortcuts - match self.focused_panel { - PanelType::Services => { - shortcuts.push("S: Start".to_string()); - shortcuts.push("Shift+S: Stop".to_string()); - } - PanelType::Backup => { - shortcuts.push("B: Trigger Backup".to_string()); - } - _ => {} - } + shortcuts.push("S: Start Service".to_string()); + shortcuts.push("Shift+S: Stop Service".to_string()); // Always show quit shortcuts.push("Q: Quit".to_string()); @@ -757,11 +653,7 @@ impl TuiApp { } fn render_system_panel(&mut self, frame: &mut Frame, area: Rect, _metric_store: &MetricStore) { - let system_block = if self.focused_panel == PanelType::System { - Components::focused_widget_block("system") - } else { - Components::widget_block("system") - }; + let system_block = Components::widget_block("system"); let inner_area = system_block.inner(area); frame.render_widget(system_block, area); // Get current host widgets, create if none exist @@ -776,11 +668,7 @@ impl TuiApp { } fn render_backup_panel(&mut self, frame: &mut Frame, area: Rect) { - let backup_block = if self.focused_panel == PanelType::Backup { - Components::focused_widget_block("backup") - } else { - Components::widget_block("backup") - }; + let backup_block = Components::widget_block("backup"); let inner_area = backup_block.inner(area); frame.render_widget(backup_block, area); diff --git a/dashboard/src/ui/theme.rs b/dashboard/src/ui/theme.rs index d03f043..510ae38 100644 --- a/dashboard/src/ui/theme.rs +++ b/dashboard/src/ui/theme.rs @@ -289,18 +289,6 @@ impl Components { ) } - /// Widget block with focus indicator (blue border) - pub fn focused_widget_block(title: &str) -> Block<'_> { - Block::default() - .title(title) - .borders(Borders::ALL) - .style(Style::default().fg(Theme::highlight()).bg(Theme::background())) // Blue border for focus - .title_style( - Style::default() - .fg(Theme::highlight()) // Blue title for focus - .bg(Theme::background()), - ) - } } impl Typography { diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index 12316b2..1acc3c5 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -443,11 +443,7 @@ impl ServicesWidget { /// Render with focus, scroll, and pending transitions for visual feedback pub fn render_with_transitions(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, pending_transitions: &HashMap) { - let services_block = if is_focused { - Components::focused_widget_block("services") - } else { - Components::widget_block("services") - }; + let services_block = Components::widget_block("services"); let inner_area = services_block.inner(area); frame.render_widget(services_block, area); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d60e934..55e27f8 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.32" +version = "0.1.33" edition = "2021" [dependencies]