diff --git a/crates/ui/src/app/mod.rs b/crates/ui/src/app/mod.rs index 43bb6fd..2aa52d2 100644 --- a/crates/ui/src/app/mod.rs +++ b/crates/ui/src/app/mod.rs @@ -189,6 +189,25 @@ fn run_tui_event_loop( continue; } + // Handle help overlay scrolling + if app.show_help { + match key.code { + KeyCode::Up | KeyCode::Char('k') => { + app.scroll_help_up(); + continue; + } + KeyCode::Down | KeyCode::Char('j') => { + app.scroll_help_down(); + continue; + } + KeyCode::Esc | KeyCode::Char('?') => { + app.show_help = false; + continue; + } + _ => {} + } + } + match key.code { KeyCode::Char('q') => { // Exit and clean up @@ -223,6 +242,8 @@ fn run_tui_event_loop( } else { app.scroll_logs_up(); } + } else if app.selected_tab == 3 { + app.scroll_help_up(); } else if app.selected_tab == 0 { app.previous_workflow(); } else if app.selected_tab == 1 { @@ -240,6 +261,8 @@ fn run_tui_event_loop( } else { app.scroll_logs_down(); } + } else if app.selected_tab == 3 { + app.scroll_help_down(); } else if app.selected_tab == 0 { app.next_workflow(); } else if app.selected_tab == 1 { diff --git a/crates/ui/src/app/state.rs b/crates/ui/src/app/state.rs index 60c1ac7..842b200 100644 --- a/crates/ui/src/app/state.rs +++ b/crates/ui/src/app/state.rs @@ -42,6 +42,9 @@ pub struct App { pub log_search_matches: Vec, // Indices of logs that match the search pub log_search_match_idx: usize, // Current match index for navigation + // Help tab scrolling + pub help_scroll: usize, // Scrolling position for help content + // Background log processing pub log_processor: LogProcessor, pub processed_logs: Vec, @@ -207,6 +210,7 @@ impl App { log_filter_level: Some(LogFilterLevel::All), log_search_matches: Vec::new(), log_search_match_idx: 0, + help_scroll: 0, // Background log processing log_processor: LogProcessor::new(), @@ -807,6 +811,18 @@ impl App { } } + // Scroll help content up + pub fn scroll_help_up(&mut self) { + self.help_scroll = self.help_scroll.saturating_sub(1); + } + + // Scroll help content down + pub fn scroll_help_down(&mut self) { + // The help content has a fixed number of lines, so we set a reasonable max + const MAX_HELP_SCROLL: usize = 30; // Adjust based on help content length + self.help_scroll = (self.help_scroll + 1).min(MAX_HELP_SCROLL); + } + // Update progress for running workflows pub fn update_running_workflow_progress(&mut self) { if let Some(idx) = self.current_execution { diff --git a/crates/ui/src/views/help_overlay.rs b/crates/ui/src/views/help_overlay.rs index 0a5a732..52ad94e 100644 --- a/crates/ui/src/views/help_overlay.rs +++ b/crates/ui/src/views/help_overlay.rs @@ -1,7 +1,7 @@ // Help overlay rendering use ratatui::{ backend::CrosstermBackend, - layout::Rect, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, BorderType, Borders, Paragraph, Wrap}, @@ -9,11 +9,22 @@ use ratatui::{ }; use std::io; -// Render the help tab -pub fn render_help_tab(f: &mut Frame>, area: Rect) { - let help_text = vec![ +// Render the help tab with scroll support +pub fn render_help_content( + f: &mut Frame>, + area: Rect, + scroll_offset: usize, +) { + // Split the area into columns for better organization + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(area); + + // Left column content + let left_help_text = vec![ Line::from(Span::styled( - "Keyboard Controls", + "🗂 NAVIGATION", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), @@ -21,35 +32,391 @@ pub fn render_help_tab(f: &mut Frame>, area: Rect) Line::from(""), Line::from(vec![ Span::styled( - "Tab", + "Tab / Shift+Tab", Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD), ), Span::raw(" - Switch between tabs"), ]), - // More help text would follow... + Line::from(vec![ + Span::styled( + "1-4 / w,x,l,h", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Jump to specific tab"), + ]), + Line::from(vec![ + Span::styled( + "↑/↓ or k/j", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Navigate lists"), + ]), + Line::from(vec![ + Span::styled( + "Enter", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Select/View details"), + ]), + Line::from(vec![ + Span::styled( + "Esc", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Back/Exit help"), + ]), + Line::from(""), + Line::from(Span::styled( + "🚀 WORKFLOW MANAGEMENT", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled( + "Space", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle workflow selection"), + ]), + Line::from(vec![ + Span::styled( + "r", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Run selected workflows"), + ]), + Line::from(vec![ + Span::styled( + "a", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Select all workflows"), + ]), + Line::from(vec![ + Span::styled( + "n", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Deselect all workflows"), + ]), + Line::from(vec![ + Span::styled( + "Shift+R", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Reset workflow status"), + ]), + Line::from(vec![ + Span::styled( + "t", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Trigger remote workflow"), + ]), + Line::from(""), + Line::from(Span::styled( + "🔧 EXECUTION MODES", + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled( + "e", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle emulation mode"), + ]), + Line::from(vec![ + Span::styled( + "v", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle validation mode"), + ]), + Line::from(""), + Line::from(vec![Span::styled( + "Runtime Modes:", + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![ + Span::raw(" • "), + Span::styled("Docker", Style::default().fg(Color::Blue)), + Span::raw(" - Container isolation (default)"), + ]), + Line::from(vec![ + Span::raw(" • "), + Span::styled("Podman", Style::default().fg(Color::Blue)), + Span::raw(" - Rootless containers"), + ]), + Line::from(vec![ + Span::raw(" • "), + Span::styled("Emulation", Style::default().fg(Color::Red)), + Span::raw(" - Process mode (UNSAFE)"), + ]), + Line::from(vec![ + Span::raw(" • "), + Span::styled("Secure Emulation", Style::default().fg(Color::Yellow)), + Span::raw(" - Sandboxed processes"), + ]), ]; - let help_widget = Paragraph::new(help_text) + // Right column content + let right_help_text = vec![ + Line::from(Span::styled( + "📄 LOGS & SEARCH", + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled( + "s", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle log search"), + ]), + Line::from(vec![ + Span::styled( + "f", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle log filter"), + ]), + Line::from(vec![ + Span::styled( + "c", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Clear search & filter"), + ]), + Line::from(vec![ + Span::styled( + "n", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Next search match"), + ]), + Line::from(vec![ + Span::styled( + "↑/↓", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Scroll logs/Navigate"), + ]), + Line::from(""), + Line::from(Span::styled( + "ℹ️ TAB OVERVIEW", + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled( + "1. Workflows", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Browse & select workflows"), + ]), + Line::from(vec![Span::raw(" • View workflow files")]), + Line::from(vec![Span::raw(" • Select multiple for batch execution")]), + Line::from(vec![Span::raw(" • Trigger remote workflows")]), + Line::from(""), + Line::from(vec![ + Span::styled( + "2. Execution", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Monitor job progress"), + ]), + Line::from(vec![Span::raw(" • View job status and details")]), + Line::from(vec![Span::raw(" • Enter job details with Enter")]), + Line::from(vec![Span::raw(" • Navigate step execution")]), + Line::from(""), + Line::from(vec![ + Span::styled( + "3. Logs", + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - View execution logs"), + ]), + Line::from(vec![Span::raw(" • Search and filter logs")]), + Line::from(vec![Span::raw(" • Real-time log streaming")]), + Line::from(vec![Span::raw(" • Navigate search results")]), + Line::from(""), + Line::from(vec![ + Span::styled( + "4. Help", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - This comprehensive guide"), + ]), + Line::from(""), + Line::from(Span::styled( + "🎯 QUICK ACTIONS", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled( + "?", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Toggle help overlay"), + ]), + Line::from(vec![ + Span::styled( + "q", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - Quit application"), + ]), + Line::from(""), + Line::from(Span::styled( + "💡 TIPS", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::raw("• Use "), + Span::styled("emulation mode", Style::default().fg(Color::Red)), + Span::raw(" when containers"), + ]), + Line::from(vec![Span::raw(" are unavailable or for quick testing")]), + Line::from(""), + Line::from(vec![ + Span::raw("• "), + Span::styled("Secure emulation", Style::default().fg(Color::Yellow)), + Span::raw(" provides sandboxing"), + ]), + Line::from(vec![Span::raw(" for untrusted workflows")]), + Line::from(""), + Line::from(vec![ + Span::raw("• Use "), + Span::styled("validation mode", Style::default().fg(Color::Green)), + Span::raw(" to check"), + ]), + Line::from(vec![Span::raw(" workflows without execution")]), + Line::from(""), + Line::from(vec![ + Span::raw("• "), + Span::styled("Preserve containers", Style::default().fg(Color::Blue)), + Span::raw(" on failure"), + ]), + Line::from(vec![Span::raw(" for debugging (Docker/Podman only)")]), + ]; + + // Apply scroll offset to the content + let left_help_text = if scroll_offset < left_help_text.len() { + left_help_text.into_iter().skip(scroll_offset).collect() + } else { + vec![Line::from("")] + }; + + let right_help_text = if scroll_offset < right_help_text.len() { + right_help_text.into_iter().skip(scroll_offset).collect() + } else { + vec![Line::from("")] + }; + + // Render left column + let left_widget = Paragraph::new(left_help_text) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title(Span::styled(" Help ", Style::default().fg(Color::Yellow))), + .title(Span::styled( + " WRKFLW Help - Controls & Features ", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )), ) .wrap(Wrap { trim: true }); - f.render_widget(help_widget, area); + // Render right column + let right_widget = Paragraph::new(right_help_text) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(Span::styled( + " Interface Guide & Tips ", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + ) + .wrap(Wrap { trim: true }); + + f.render_widget(left_widget, chunks[0]); + f.render_widget(right_widget, chunks[1]); } // Render a help overlay -pub fn render_help_overlay(f: &mut Frame>) { +pub fn render_help_overlay(f: &mut Frame>, scroll_offset: usize) { let size = f.size(); - // Create a slightly smaller centered modal - let width = size.width.min(60); - let height = size.height.min(20); + // Create a larger centered modal to accommodate comprehensive help content + let width = (size.width * 9 / 10).min(120); // Use 90% of width, max 120 chars + let height = (size.height * 9 / 10).min(40); // Use 90% of height, max 40 lines let x = (size.width - width) / 2; let y = (size.height - height) / 2; @@ -60,10 +427,32 @@ pub fn render_help_overlay(f: &mut Frame>) { height, }; - // Create a clear background + // Create a semi-transparent dark background for better visibility let clear = Block::default().style(Style::default().bg(Color::Black)); f.render_widget(clear, size); - // Render the help content - render_help_tab(f, help_area); + // Add a border around the entire overlay for better visual separation + let overlay_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Double) + .style(Style::default().bg(Color::Black).fg(Color::White)) + .title(Span::styled( + " Press ? or Esc to close help ", + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::ITALIC), + )); + + f.render_widget(overlay_block, help_area); + + // Create inner area for content + let inner_area = Rect { + x: help_area.x + 1, + y: help_area.y + 1, + width: help_area.width.saturating_sub(2), + height: help_area.height.saturating_sub(2), + }; + + // Render the help content with scroll support + render_help_content(f, inner_area, scroll_offset); } diff --git a/crates/ui/src/views/mod.rs b/crates/ui/src/views/mod.rs index 6e8021e..07b852e 100644 --- a/crates/ui/src/views/mod.rs +++ b/crates/ui/src/views/mod.rs @@ -15,7 +15,7 @@ use std::io; pub fn render_ui(f: &mut Frame>, app: &mut App) { // Check if help should be shown as an overlay if app.show_help { - help_overlay::render_help_overlay(f); + help_overlay::render_help_overlay(f, app.help_scroll); return; } @@ -48,7 +48,7 @@ pub fn render_ui(f: &mut Frame>, app: &mut App) { } } 2 => logs_tab::render_logs_tab(f, app, main_chunks[1]), - 3 => help_overlay::render_help_tab(f, main_chunks[1]), + 3 => help_overlay::render_help_content(f, main_chunks[1], app.help_scroll), _ => {} } diff --git a/crates/ui/src/views/status_bar.rs b/crates/ui/src/views/status_bar.rs index eb39052..506d5ee 100644 --- a/crates/ui/src/views/status_bar.rs +++ b/crates/ui/src/views/status_bar.rs @@ -181,7 +181,7 @@ pub fn render_status_bar(f: &mut Frame>, app: &App, "[No logs to display]" } } - 3 => "[?] Toggle help overlay", + 3 => "[↑/↓] Scroll help [?] Toggle help overlay", _ => "", }; status_items.push(Span::styled(