Add tabbed help UI with enhanced navigation
- Refactor `render_help` to display tabbed UI for help topics. - Introduce `help_tab_index` to manage selected tab state. - Allow navigation between help tabs using Tab, h/l, and number keys (1-5). - Enhance visual design of help sections using styled tabs and categorized content. - Update input handling to reset tab state upon exit from help mode.
This commit is contained in:
@@ -60,6 +60,7 @@ pub struct ChatApp {
|
|||||||
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
|
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
|
||||||
selected_session_index: usize, // Index of selected session in browser
|
selected_session_index: usize, // Index of selected session in browser
|
||||||
save_name_buffer: String, // Buffer for entering save name
|
save_name_buffer: String, // Buffer for entering save name
|
||||||
|
help_tab_index: usize, // Currently selected help tab (0-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatApp {
|
impl ChatApp {
|
||||||
@@ -108,6 +109,7 @@ impl ChatApp {
|
|||||||
saved_sessions: Vec::new(),
|
saved_sessions: Vec::new(),
|
||||||
selected_session_index: 0,
|
selected_session_index: 0,
|
||||||
save_name_buffer: String::new(),
|
save_name_buffer: String::new(),
|
||||||
|
help_tab_index: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
(app, session_rx)
|
(app, session_rx)
|
||||||
@@ -232,6 +234,10 @@ impl ChatApp {
|
|||||||
self.selected_session_index
|
self.selected_session_index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn help_tab_index(&self) -> usize {
|
||||||
|
self.help_tab_index
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cycle_focus_forward(&mut self) {
|
pub fn cycle_focus_forward(&mut self) {
|
||||||
self.focused_panel = match self.focused_panel {
|
self.focused_panel = match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
@@ -1041,7 +1047,7 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"load" | "open" => {
|
"load" | "open" | "o" => {
|
||||||
// Load saved sessions and enter browser mode
|
// Load saved sessions and enter browser mode
|
||||||
match self.storage.list_sessions() {
|
match self.storage.list_sessions() {
|
||||||
Ok(sessions) => {
|
Ok(sessions) => {
|
||||||
@@ -1175,9 +1181,27 @@ impl ChatApp {
|
|||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::Help => match key.code {
|
InputMode::Help => match key.code {
|
||||||
KeyCode::Esc | KeyCode::Enter => {
|
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
|
||||||
self.mode = InputMode::Normal;
|
self.mode = InputMode::Normal;
|
||||||
|
self.help_tab_index = 0; // Reset to first tab
|
||||||
}
|
}
|
||||||
|
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
|
||||||
|
// Next tab
|
||||||
|
if self.help_tab_index < 4 {
|
||||||
|
self.help_tab_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
|
||||||
|
// Previous tab
|
||||||
|
if self.help_tab_index > 0 {
|
||||||
|
self.help_tab_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('1') => self.help_tab_index = 0,
|
||||||
|
KeyCode::Char('2') => self.help_tab_index = 1,
|
||||||
|
KeyCode::Char('3') => self.help_tab_index = 2,
|
||||||
|
KeyCode::Char('4') => self.help_tab_index = 3,
|
||||||
|
KeyCode::Char('5') => self.help_tab_index = 4,
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::SessionBrowser => match key.code {
|
InputMode::SessionBrowser => match key.code {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
|||||||
match app.mode() {
|
match app.mode() {
|
||||||
InputMode::ProviderSelection => render_provider_selector(frame, app),
|
InputMode::ProviderSelection => render_provider_selector(frame, app),
|
||||||
InputMode::ModelSelection => render_model_selector(frame, app),
|
InputMode::ModelSelection => render_model_selector(frame, app),
|
||||||
InputMode::Help => render_help(frame),
|
InputMode::Help => render_help(frame, app),
|
||||||
InputMode::SessionBrowser => render_session_browser(frame, app),
|
InputMode::SessionBrowser => render_session_browser(frame, app),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1083,23 +1083,48 @@ fn render_model_selector(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
frame.render_stateful_widget(list, area, &mut state);
|
frame.render_stateful_widget(list, area, &mut state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_help(frame: &mut Frame<'_>) {
|
fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
let area = centered_rect(70, 60, frame.area());
|
let area = centered_rect(75, 70, frame.area());
|
||||||
frame.render_widget(Clear, area);
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
let help_text = vec![
|
let tab_index = app.help_tab_index();
|
||||||
Line::from("MODES:"),
|
let tabs = vec!["Navigation", "Editing", "Visual", "Commands", "Sessions"];
|
||||||
Line::from(" Normal → default mode for navigation"),
|
|
||||||
Line::from(" Insert → editing input text"),
|
// Build tab line
|
||||||
Line::from(" Visual → selecting text"),
|
let mut tab_spans = Vec::new();
|
||||||
Line::from(" Command → executing commands (: prefix)"),
|
for (i, tab_name) in tabs.iter().enumerate() {
|
||||||
|
if i == tab_index {
|
||||||
|
tab_spans.push(Span::styled(
|
||||||
|
format!(" {} ", tab_name),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Black)
|
||||||
|
.bg(Color::LightMagenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
tab_spans.push(Span::styled(
|
||||||
|
format!(" {} ", tab_name),
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if i < tabs.len() - 1 {
|
||||||
|
tab_spans.push(Span::raw(" │ "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let help_text = match tab_index {
|
||||||
|
0 => vec![ // Navigation
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("PANEL NAVIGATION:"),
|
Line::from(vec![
|
||||||
|
Span::styled("PANEL NAVIGATION", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" Tab → cycle panels forward"),
|
Line::from(" Tab → cycle panels forward"),
|
||||||
Line::from(" Shift+Tab → cycle panels backward"),
|
Line::from(" Shift+Tab → cycle panels backward"),
|
||||||
Line::from(" (Panels: Chat, Thinking, Input)"),
|
Line::from(" (Panels: Chat, Thinking, Input)"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("CURSOR MOVEMENT (Normal mode, Chat/Thinking panels):"),
|
Line::from(vec![
|
||||||
|
Span::styled("CURSOR MOVEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" h/← l/→ → move left/right by character"),
|
Line::from(" h/← l/→ → move left/right by character"),
|
||||||
Line::from(" j/↓ k/↑ → move down/up by line"),
|
Line::from(" j/↓ k/↑ → move down/up by line"),
|
||||||
Line::from(" w → forward to next word start"),
|
Line::from(" w → forward to next word start"),
|
||||||
@@ -1110,69 +1135,199 @@ fn render_help(frame: &mut Frame<'_>) {
|
|||||||
Line::from(" $ / End → end of line"),
|
Line::from(" $ / End → end of line"),
|
||||||
Line::from(" gg → jump to top"),
|
Line::from(" gg → jump to top"),
|
||||||
Line::from(" G → jump to bottom"),
|
Line::from(" G → jump to bottom"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SCROLLING", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" Ctrl+d/u → scroll half page down/up"),
|
Line::from(" Ctrl+d/u → scroll half page down/up"),
|
||||||
Line::from(" Ctrl+f/b → scroll full page down/up"),
|
Line::from(" Ctrl+f/b → scroll full page down/up"),
|
||||||
Line::from(" PageUp/Down → scroll full page"),
|
Line::from(" PageUp/Down → scroll full page"),
|
||||||
|
],
|
||||||
|
1 => vec![ // Editing
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("EDITING (Normal mode):"),
|
Line::from(vec![
|
||||||
|
Span::styled("ENTERING INSERT MODE", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" i / Enter → enter insert mode at cursor"),
|
Line::from(" i / Enter → enter insert mode at cursor"),
|
||||||
Line::from(" a → append after cursor"),
|
Line::from(" a → append after cursor"),
|
||||||
Line::from(" A → append at end of line"),
|
Line::from(" A → append at end of line"),
|
||||||
Line::from(" I → insert at start of line"),
|
Line::from(" I → insert at start of line"),
|
||||||
Line::from(" o → insert line below and enter insert mode"),
|
Line::from(" o → insert line below and enter insert mode"),
|
||||||
Line::from(" O → insert line above and enter insert mode"),
|
Line::from(" O → insert line above and enter insert mode"),
|
||||||
Line::from(" dd → clear input buffer"),
|
|
||||||
Line::from(" p → paste from clipboard to input"),
|
|
||||||
Line::from(" Esc → return to normal mode"),
|
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("INSERT MODE:"),
|
Line::from(vec![
|
||||||
|
Span::styled("INSERT MODE KEYS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" Enter → send message"),
|
Line::from(" Enter → send message"),
|
||||||
Line::from(" Ctrl+J → insert newline"),
|
Line::from(" Ctrl+J → insert newline (multiline message)"),
|
||||||
Line::from(" Ctrl+↑/↓ → navigate input history"),
|
Line::from(" Ctrl+↑/↓ → navigate input history"),
|
||||||
Line::from(" Ctrl+A → start of line"),
|
Line::from(" Ctrl+A → jump to start of line"),
|
||||||
Line::from(" Ctrl+E → end of line"),
|
Line::from(" Ctrl+E → jump to end of line"),
|
||||||
Line::from(" Ctrl+W → word forward"),
|
Line::from(" Ctrl+W → word forward"),
|
||||||
Line::from(" Ctrl+B → word backward"),
|
Line::from(" Ctrl+B → word backward"),
|
||||||
Line::from(" Esc → return to normal mode"),
|
Line::from(" Esc → return to normal mode"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("VISUAL MODE (all panels):"),
|
Line::from(vec![
|
||||||
|
Span::styled("NORMAL MODE OPERATIONS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
|
Line::from(" dd → clear input buffer"),
|
||||||
|
Line::from(" p → paste from clipboard to input"),
|
||||||
|
],
|
||||||
|
2 => vec![ // Visual
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("VISUAL MODE", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" v → enter visual mode at cursor"),
|
Line::from(" v → enter visual mode at cursor"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SELECTION MOVEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
Line::from(" h/j/k/l → extend selection left/down/up/right"),
|
Line::from(" h/j/k/l → extend selection left/down/up/right"),
|
||||||
Line::from(" w / e / b → extend by word (start/end/back)"),
|
Line::from(" w → extend to next word start"),
|
||||||
Line::from(" 0 / ^ / $ → extend to line start/first char/end"),
|
Line::from(" e → extend to word end"),
|
||||||
Line::from(" y → yank (copy) selection"),
|
Line::from(" b → extend backward to previous word"),
|
||||||
Line::from(" d → yank selection (delete in Input)"),
|
Line::from(" 0 → extend to line start"),
|
||||||
|
Line::from(" ^ → extend to first non-blank"),
|
||||||
|
Line::from(" $ → extend to line end"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("VISUAL MODE OPERATIONS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
|
Line::from(" y → yank (copy) selection to clipboard"),
|
||||||
|
Line::from(" d → cut selection (Input panel only)"),
|
||||||
Line::from(" v / Esc → exit visual mode"),
|
Line::from(" v / Esc → exit visual mode"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("COMMANDS (press : then type):"),
|
Line::from(vec![
|
||||||
|
Span::styled("NOTES", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" • Visual mode works across all panels (Chat, Thinking, Input)"),
|
||||||
|
Line::from(" • Yanked text is available for paste with 'p' in normal mode"),
|
||||||
|
],
|
||||||
|
3 => vec![ // Commands
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("COMMAND MODE", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
|
Line::from(" Press ':' to enter command mode, then type one of:"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("HELP & NAVIGATION", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
Line::from(" :h, :help → show this help"),
|
Line::from(" :h, :help → show this help"),
|
||||||
Line::from(" :m, :model → select model"),
|
|
||||||
Line::from(" :n, :new → start new conversation"),
|
|
||||||
Line::from(" :c, :clear → clear current conversation"),
|
|
||||||
Line::from(" :save [name] → save current session"),
|
|
||||||
Line::from(" :load, :sessions → browse/load saved sessions"),
|
|
||||||
Line::from(" :q, :quit → quit application"),
|
Line::from(" :q, :quit → quit application"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("QUICK KEYS:"),
|
Line::from(vec![
|
||||||
Line::from(" q → quit (from normal mode)"),
|
Span::styled("MODEL MANAGEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
Line::from(" Ctrl+C → quit"),
|
]),
|
||||||
|
Line::from(" :m, :model → open model selector"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("Press Esc or Enter to close this help."),
|
Line::from(vec![
|
||||||
];
|
Span::styled("CONVERSATION MANAGEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" :n, :new → start new conversation"),
|
||||||
|
Line::from(" :c, :clear → clear current conversation"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SESSION MANAGEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" :save [name] → save current session (with optional name)"),
|
||||||
|
Line::from(" :w [name] → alias for :save"),
|
||||||
|
Line::from(" :load, :o, :open → browse and load saved sessions"),
|
||||||
|
Line::from(" :sessions, :ls → browse saved sessions"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("QUICK SHORTCUTS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" q (normal mode) → quit without :"),
|
||||||
|
Line::from(" Ctrl+C → quit immediately"),
|
||||||
|
],
|
||||||
|
4 => vec![ // Sessions
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SESSION MANAGEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
|
||||||
|
]),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SAVING SESSIONS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" :save → save with auto-generated name"),
|
||||||
|
Line::from(" :save my-session → save with custom name"),
|
||||||
|
Line::from(" • AI generates description automatically (configurable)"),
|
||||||
|
Line::from(" • Sessions stored in platform-specific directories"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("LOADING SESSIONS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" :load, :o, :open → browse and select session"),
|
||||||
|
Line::from(" :sessions, :ls → browse saved sessions"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("SESSION BROWSER KEYS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" j/k or ↑/↓ → navigate sessions"),
|
||||||
|
Line::from(" Enter → load selected session"),
|
||||||
|
Line::from(" d → delete selected session"),
|
||||||
|
Line::from(" Esc → close browser"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("STORAGE LOCATIONS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan))
|
||||||
|
]),
|
||||||
|
Line::from(" Linux → ~/.local/share/owlen/sessions"),
|
||||||
|
Line::from(" Windows → %APPDATA%\\owlen\\sessions"),
|
||||||
|
Line::from(" macOS → ~/Library/Application Support/owlen/sessions"),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("CONTEXT PRESERVATION", Style::default().add_modifier(Modifier::BOLD).fg(Color::Green))
|
||||||
|
]),
|
||||||
|
Line::from(" • Full conversation history is preserved when saving"),
|
||||||
|
Line::from(" • All context is restored when loading a session"),
|
||||||
|
Line::from(" • Continue conversations seamlessly across restarts"),
|
||||||
|
],
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
let paragraph = Paragraph::new(help_text).block(
|
// Create layout for tabs and content
|
||||||
Block::default()
|
let layout = Layout::default()
|
||||||
.title(Span::styled(
|
.direction(Direction::Vertical)
|
||||||
"Help",
|
.constraints([
|
||||||
Style::default()
|
Constraint::Length(3), // Tab bar
|
||||||
.fg(Color::LightMagenta)
|
Constraint::Min(0), // Content
|
||||||
.add_modifier(Modifier::BOLD),
|
Constraint::Length(2), // Navigation hint
|
||||||
))
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Render tabs
|
||||||
|
let tabs_block = Block::default()
|
||||||
|
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
|
||||||
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135)));
|
||||||
|
let tabs_para = Paragraph::new(Line::from(tab_spans)).block(tabs_block);
|
||||||
|
frame.render_widget(tabs_para, layout[0]);
|
||||||
|
|
||||||
|
// Render content
|
||||||
|
let content_block = Block::default()
|
||||||
|
.borders(Borders::LEFT | Borders::RIGHT)
|
||||||
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135)));
|
||||||
|
let content_para = Paragraph::new(help_text).block(content_block);
|
||||||
|
frame.render_widget(content_para, layout[1]);
|
||||||
|
|
||||||
|
// Render navigation hint
|
||||||
|
let nav_hint = Line::from(vec![
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled("Tab/h/l", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(":Switch "),
|
||||||
|
Span::styled("1-5", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(":Jump "),
|
||||||
|
Span::styled("Esc/q", Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(":Close "),
|
||||||
|
]);
|
||||||
|
let nav_block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135)));
|
||||||
);
|
let nav_para = Paragraph::new(nav_hint)
|
||||||
|
.block(nav_block)
|
||||||
frame.render_widget(paragraph, area);
|
.alignment(Alignment::Center);
|
||||||
|
frame.render_widget(nav_para, layout[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
|
|||||||
Reference in New Issue
Block a user