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:
parent
1b46aa2f13
commit
6b18cdf562
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user