Fix keyboard navigation and panel scrolling issues

- Remove Network panel from navigation cycle
- Fix system panel scrolling to work in both directions
- Add complete scroll support to Services and Backup panels
- Update panel cycling to System → Services → Backup only
- Enhance scroll indicators with proper bounds checking
- Clean up unused Network panel code and references

Resolves issues with non-functional up/down scrolling and
mystery network panel appearing during navigation.
This commit is contained in:
Christoffer Martinsson 2025-10-23 21:01:11 +02:00
parent 1b46aa2f13
commit 6b18cdf562
4 changed files with 106 additions and 48 deletions

View File

@ -24,32 +24,29 @@ pub enum PanelType {
System, System,
Services, Services,
Backup, Backup,
Network,
} }
impl PanelType { impl PanelType {
/// Get all panel types in order /// Get all panel types in order
pub fn all() -> [PanelType; 4] { pub fn all() -> [PanelType; 3] {
[PanelType::System, PanelType::Services, PanelType::Backup, PanelType::Network] [PanelType::System, PanelType::Services, PanelType::Backup]
} }
/// Get the next panel in cycle /// Get the next panel in cycle (System → Services → Backup → System)
pub fn next(self) -> PanelType { pub fn next(self) -> PanelType {
match self { match self {
PanelType::System => PanelType::Services, PanelType::System => PanelType::Services,
PanelType::Services => PanelType::Backup, PanelType::Services => PanelType::Backup,
PanelType::Backup => PanelType::Network, PanelType::Backup => PanelType::System,
PanelType::Network => PanelType::System,
} }
} }
/// Get the previous panel in cycle /// Get the previous panel in cycle (System ← Services ← Backup ← System)
pub fn previous(self) -> PanelType { pub fn previous(self) -> PanelType {
match self { match self {
PanelType::System => PanelType::Network, PanelType::System => PanelType::Backup,
PanelType::Services => PanelType::System, PanelType::Services => PanelType::System,
PanelType::Backup => PanelType::Services, PanelType::Backup => PanelType::Services,
PanelType::Network => PanelType::Backup,
} }
} }
} }
@ -67,7 +64,6 @@ pub struct HostWidgets {
pub system_scroll_offset: usize, pub system_scroll_offset: usize,
pub services_scroll_offset: usize, pub services_scroll_offset: usize,
pub backup_scroll_offset: usize, pub backup_scroll_offset: usize,
pub network_scroll_offset: usize,
/// Last update time for this host /// Last update time for this host
pub last_update: Option<Instant>, pub last_update: Option<Instant>,
} }
@ -81,7 +77,6 @@ impl HostWidgets {
system_scroll_offset: 0, system_scroll_offset: 0,
services_scroll_offset: 0, services_scroll_offset: 0,
backup_scroll_offset: 0, backup_scroll_offset: 0,
network_scroll_offset: 0,
last_update: None, last_update: None,
} }
} }
@ -350,14 +345,6 @@ impl TuiApp {
} }
info!("Backup panel scroll offset: {}", host_widgets.backup_scroll_offset); info!("Backup panel scroll offset: {}", host_widgets.backup_scroll_offset);
} }
PanelType::Network => {
if direction > 0 {
host_widgets.network_scroll_offset = host_widgets.network_scroll_offset.saturating_add(1);
} else {
host_widgets.network_scroll_offset = host_widgets.network_scroll_offset.saturating_sub(1);
}
info!("Network panel scroll offset: {}", host_widgets.network_scroll_offset);
}
} }
} }
} }
@ -430,10 +417,14 @@ impl TuiApp {
// Render services widget for current host // Render services widget for current host
if let Some(hostname) = self.current_host.clone() { if let Some(hostname) = self.current_host.clone() {
let is_focused = self.focused_panel == PanelType::Services; let is_focused = self.focused_panel == PanelType::Services;
let scroll_offset = {
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.services_scroll_offset
};
let host_widgets = self.get_or_create_host_widgets(&hostname); let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets host_widgets
.services_widget .services_widget
.render_with_focus(frame, content_chunks[1], is_focused); // Services takes full right side .render_with_focus_and_scroll(frame, content_chunks[1], is_focused, scroll_offset); // Services takes full right side
} }
// Render statusbar at the bottom // Render statusbar at the bottom
@ -571,9 +562,6 @@ impl TuiApp {
PanelType::Backup => { PanelType::Backup => {
shortcuts.push("B: Trigger Backup".to_string()); shortcuts.push("B: Trigger Backup".to_string());
} }
PanelType::Network => {
shortcuts.push("N: Network Info".to_string());
}
} }
// Always show quit // Always show quit
@ -612,8 +600,12 @@ impl TuiApp {
// Get current host widgets for backup widget // Get current host widgets for backup widget
if let Some(hostname) = self.current_host.clone() { if let Some(hostname) = self.current_host.clone() {
let scroll_offset = {
let host_widgets = self.get_or_create_host_widgets(&hostname); let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.backup_widget.render(frame, inner_area); host_widgets.backup_scroll_offset
};
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.backup_widget.render_with_scroll(frame, inner_area, scroll_offset);
} }
} }

View File

@ -325,6 +325,13 @@ impl Widget for BackupWidget {
} }
fn render(&mut self, frame: &mut Frame, area: Rect) { fn render(&mut self, frame: &mut Frame, area: Rect) {
self.render_with_scroll(frame, area, 0);
}
}
impl BackupWidget {
/// Render with scroll offset support
pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize) {
let mut lines = Vec::new(); let mut lines = Vec::new();
// Latest backup section // Latest backup section
@ -422,10 +429,33 @@ impl Widget for BackupWidget {
])); ]));
} }
// Apply scroll offset
let total_lines = lines.len();
let available_height = area.height as usize;
// Calculate scroll boundaries
let max_scroll = if total_lines > available_height {
total_lines - available_height
} else {
total_lines.saturating_sub(1)
};
let effective_scroll = scroll_offset.min(max_scroll);
// Apply scrolling if needed
if scroll_offset > 0 || total_lines > available_height {
let visible_lines: Vec<_> = lines
.into_iter()
.skip(effective_scroll)
.take(available_height)
.collect();
let paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines));
frame.render_widget(paragraph, area);
} else {
let paragraph = Paragraph::new(ratatui::text::Text::from(lines)); let paragraph = Paragraph::new(ratatui::text::Text::from(lines));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} }
} }
}
impl BackupWidget { impl BackupWidget {
/// Format timestamp for display /// Format timestamp for display

View File

@ -313,8 +313,13 @@ impl Widget for ServicesWidget {
} }
impl ServicesWidget { impl ServicesWidget {
/// Render with optional focus indicator /// Render with optional focus indicator and scroll support
pub fn render_with_focus(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) { pub fn render_with_focus(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) {
self.render_with_focus_and_scroll(frame, area, is_focused, 0);
}
/// Render with focus indicator and scroll offset
pub fn render_with_focus_and_scroll(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) {
let services_block = if is_focused { let services_block = if is_focused {
Components::focused_widget_block("services") Components::focused_widget_block("services")
} else { } else {
@ -374,9 +379,26 @@ impl ServicesWidget {
} }
} }
// Render all lines within available space // Apply scroll offset and render visible lines
let available_lines = content_chunks[1].height as usize; let available_lines = content_chunks[1].height as usize;
let lines_to_show = available_lines.min(display_lines.len()); let total_lines = display_lines.len();
// Calculate scroll boundaries
let max_scroll = if total_lines > available_lines {
total_lines - available_lines
} else {
total_lines.saturating_sub(1)
};
let effective_scroll = scroll_offset.min(max_scroll);
// Get visible lines after scrolling
let visible_lines: Vec<_> = display_lines
.iter()
.skip(effective_scroll)
.take(available_lines)
.collect();
let lines_to_show = visible_lines.len();
if lines_to_show > 0 { if lines_to_show > 0 {
let service_chunks = Layout::default() let service_chunks = Layout::default()
@ -384,8 +406,7 @@ impl ServicesWidget {
.constraints(vec![Constraint::Length(1); lines_to_show]) .constraints(vec![Constraint::Length(1); lines_to_show])
.split(content_chunks[1]); .split(content_chunks[1]);
for (i, (line_text, line_status, is_sub, sub_info)) in for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate()
display_lines.iter().take(lines_to_show).enumerate()
{ {
let spans = if *is_sub && sub_info.is_some() { let spans = if *is_sub && sub_info.is_some() {
// Use custom sub-service span creation // Use custom sub-service span creation
@ -400,20 +421,31 @@ impl ServicesWidget {
} }
} }
// Show indicator if there are more services than we can display // Show scroll indicator if there are more services than we can display
if display_lines.len() > available_lines { if total_lines > available_lines {
let more_count = display_lines.len() - available_lines; let hidden_above = effective_scroll;
if available_lines > 0 { let hidden_below = total_lines.saturating_sub(effective_scroll + available_lines);
if hidden_above > 0 || hidden_below > 0 {
let scroll_text = if hidden_above > 0 && hidden_below > 0 {
format!("... {} above, {} below", hidden_above, hidden_below)
} else if hidden_above > 0 {
format!("... {} more above", hidden_above)
} else {
format!("... {} more below", hidden_below)
};
if available_lines > 0 && lines_to_show > 0 {
let last_line_area = Rect { let last_line_area = Rect {
x: content_chunks[1].x, x: content_chunks[1].x,
y: content_chunks[1].y + (available_lines - 1) as u16, y: content_chunks[1].y + (lines_to_show - 1) as u16,
width: content_chunks[1].width, width: content_chunks[1].width,
height: 1, height: 1,
}; };
let more_text = format!("... and {} more services", more_count); let scroll_para = Paragraph::new(scroll_text).style(Typography::muted());
let more_para = Paragraph::new(more_text).style(Typography::muted()); frame.render_widget(scroll_para, last_line_area);
frame.render_widget(more_para, last_line_area); }
} }
} }
} }

View File

@ -520,9 +520,13 @@ impl SystemWidget {
let total_lines = lines.len(); let total_lines = lines.len();
let available_height = area.height as usize; let available_height = area.height as usize;
if total_lines > available_height { // Always apply scrolling if scroll_offset > 0, even if content fits
// Content is larger than area, apply scrolling if scroll_offset > 0 || total_lines > available_height {
let max_scroll = total_lines.saturating_sub(available_height); let max_scroll = if total_lines > available_height {
total_lines - available_height
} else {
total_lines.saturating_sub(1)
};
let effective_scroll = scroll_offset.min(max_scroll); let effective_scroll = scroll_offset.min(max_scroll);
// Take only the visible portion after scrolling // Take only the visible portion after scrolling
@ -535,7 +539,7 @@ impl SystemWidget {
let paragraph = Paragraph::new(Text::from(visible_lines)); let paragraph = Paragraph::new(Text::from(visible_lines));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} else { } else {
// All content fits, render normally // All content fits and no scroll offset, render normally
let paragraph = Paragraph::new(Text::from(lines)); let paragraph = Paragraph::new(Text::from(lines));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} }