Merge pull request #38 from bahdotsh/improve-tui-help-tab

feat(ui): enhance TUI help tab with comprehensive documentation and s…
This commit is contained in:
Gokul
2025-08-13 15:29:22 +05:30
committed by GitHub
5 changed files with 448 additions and 20 deletions

View File

@@ -189,6 +189,25 @@ fn run_tui_event_loop(
continue; 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 { match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
// Exit and clean up // Exit and clean up
@@ -223,6 +242,8 @@ fn run_tui_event_loop(
} else { } else {
app.scroll_logs_up(); app.scroll_logs_up();
} }
} else if app.selected_tab == 3 {
app.scroll_help_up();
} else if app.selected_tab == 0 { } else if app.selected_tab == 0 {
app.previous_workflow(); app.previous_workflow();
} else if app.selected_tab == 1 { } else if app.selected_tab == 1 {
@@ -240,6 +261,8 @@ fn run_tui_event_loop(
} else { } else {
app.scroll_logs_down(); app.scroll_logs_down();
} }
} else if app.selected_tab == 3 {
app.scroll_help_down();
} else if app.selected_tab == 0 { } else if app.selected_tab == 0 {
app.next_workflow(); app.next_workflow();
} else if app.selected_tab == 1 { } else if app.selected_tab == 1 {

View File

@@ -42,6 +42,9 @@ pub struct App {
pub log_search_matches: Vec<usize>, // Indices of logs that match the search pub log_search_matches: Vec<usize>, // Indices of logs that match the search
pub log_search_match_idx: usize, // Current match index for navigation 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 // Background log processing
pub log_processor: LogProcessor, pub log_processor: LogProcessor,
pub processed_logs: Vec<ProcessedLogEntry>, pub processed_logs: Vec<ProcessedLogEntry>,
@@ -207,6 +210,7 @@ impl App {
log_filter_level: Some(LogFilterLevel::All), log_filter_level: Some(LogFilterLevel::All),
log_search_matches: Vec::new(), log_search_matches: Vec::new(),
log_search_match_idx: 0, log_search_match_idx: 0,
help_scroll: 0,
// Background log processing // Background log processing
log_processor: LogProcessor::new(), 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 // Update progress for running workflows
pub fn update_running_workflow_progress(&mut self) { pub fn update_running_workflow_progress(&mut self) {
if let Some(idx) = self.current_execution { if let Some(idx) = self.current_execution {

View File

@@ -1,7 +1,7 @@
// Help overlay rendering // Help overlay rendering
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::CrosstermBackend,
layout::Rect, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Wrap}, widgets::{Block, BorderType, Borders, Paragraph, Wrap},
@@ -9,11 +9,22 @@ use ratatui::{
}; };
use std::io; use std::io;
// Render the help tab // Render the help tab with scroll support
pub fn render_help_tab(f: &mut Frame<CrosstermBackend<io::Stdout>>, area: Rect) { pub fn render_help_content(
let help_text = vec![ f: &mut Frame<CrosstermBackend<io::Stdout>>,
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( Line::from(Span::styled(
"Keyboard Controls", "🗂 NAVIGATION",
Style::default() Style::default()
.fg(Color::Cyan) .fg(Color::Cyan)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
@@ -21,35 +32,391 @@ pub fn render_help_tab(f: &mut Frame<CrosstermBackend<io::Stdout>>, area: Rect)
Line::from(""), Line::from(""),
Line::from(vec![ Line::from(vec![
Span::styled( Span::styled(
"Tab", "Tab / Shift+Tab",
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Yellow)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
), ),
Span::raw(" - Switch between tabs"), 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(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .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 }); .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 // Render a help overlay
pub fn render_help_overlay(f: &mut Frame<CrosstermBackend<io::Stdout>>) { pub fn render_help_overlay(f: &mut Frame<CrosstermBackend<io::Stdout>>, scroll_offset: usize) {
let size = f.size(); let size = f.size();
// Create a slightly smaller centered modal // Create a larger centered modal to accommodate comprehensive help content
let width = size.width.min(60); let width = (size.width * 9 / 10).min(120); // Use 90% of width, max 120 chars
let height = size.height.min(20); let height = (size.height * 9 / 10).min(40); // Use 90% of height, max 40 lines
let x = (size.width - width) / 2; let x = (size.width - width) / 2;
let y = (size.height - height) / 2; let y = (size.height - height) / 2;
@@ -60,10 +427,32 @@ pub fn render_help_overlay(f: &mut Frame<CrosstermBackend<io::Stdout>>) {
height, height,
}; };
// Create a clear background // Create a semi-transparent dark background for better visibility
let clear = Block::default().style(Style::default().bg(Color::Black)); let clear = Block::default().style(Style::default().bg(Color::Black));
f.render_widget(clear, size); f.render_widget(clear, size);
// Render the help content // Add a border around the entire overlay for better visual separation
render_help_tab(f, help_area); 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);
} }

View File

@@ -15,7 +15,7 @@ use std::io;
pub fn render_ui(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) { pub fn render_ui(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) {
// Check if help should be shown as an overlay // Check if help should be shown as an overlay
if app.show_help { if app.show_help {
help_overlay::render_help_overlay(f); help_overlay::render_help_overlay(f, app.help_scroll);
return; return;
} }
@@ -48,7 +48,7 @@ pub fn render_ui(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) {
} }
} }
2 => logs_tab::render_logs_tab(f, app, main_chunks[1]), 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),
_ => {} _ => {}
} }

View File

@@ -181,7 +181,7 @@ pub fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App,
"[No logs to display]" "[No logs to display]"
} }
} }
3 => "[?] Toggle help overlay", 3 => "[↑/↓] Scroll help [?] Toggle help overlay",
_ => "", _ => "",
}; };
status_items.push(Span::styled( status_items.push(Span::styled(