From c725bb1ce640e0e12932447f6a8b0e5d606d9416 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Thu, 2 Oct 2025 02:07:23 +0200 Subject: [PATCH 1/2] 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. --- crates/owlen-tui/src/chat_app.rs | 28 ++- crates/owlen-tui/src/ui.rs | 325 +++++++++++++++++++++++-------- 2 files changed, 266 insertions(+), 87 deletions(-) diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 5a27040..0de4d5d 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -60,6 +60,7 @@ pub struct ChatApp { saved_sessions: Vec, // Cached list of saved sessions selected_session_index: usize, // Index of selected session in browser save_name_buffer: String, // Buffer for entering save name + help_tab_index: usize, // Currently selected help tab (0-4) } impl ChatApp { @@ -108,6 +109,7 @@ impl ChatApp { saved_sessions: Vec::new(), selected_session_index: 0, save_name_buffer: String::new(), + help_tab_index: 0, }; (app, session_rx) @@ -232,6 +234,10 @@ impl ChatApp { self.selected_session_index } + pub fn help_tab_index(&self) -> usize { + self.help_tab_index + } + pub fn cycle_focus_forward(&mut self) { self.focused_panel = match self.focused_panel { FocusedPanel::Chat => { @@ -1041,7 +1047,7 @@ impl ChatApp { } } } - "load" | "open" => { + "load" | "open" | "o" => { // Load saved sessions and enter browser mode match self.storage.list_sessions() { Ok(sessions) => { @@ -1175,9 +1181,27 @@ impl ChatApp { _ => {} }, InputMode::Help => match key.code { - KeyCode::Esc | KeyCode::Enter => { + KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { 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 { diff --git a/crates/owlen-tui/src/ui.rs b/crates/owlen-tui/src/ui.rs index 7ce8b3e..517f97f 100644 --- a/crates/owlen-tui/src/ui.rs +++ b/crates/owlen-tui/src/ui.rs @@ -80,7 +80,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) { match app.mode() { InputMode::ProviderSelection => render_provider_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), _ => {} } @@ -1083,96 +1083,251 @@ fn render_model_selector(frame: &mut Frame<'_>, app: &ChatApp) { frame.render_stateful_widget(list, area, &mut state); } -fn render_help(frame: &mut Frame<'_>) { - let area = centered_rect(70, 60, frame.area()); +fn render_help(frame: &mut Frame<'_>, app: &ChatApp) { + let area = centered_rect(75, 70, frame.area()); frame.render_widget(Clear, area); - let help_text = vec![ - Line::from("MODES:"), - Line::from(" Normal → default mode for navigation"), - Line::from(" Insert → editing input text"), - Line::from(" Visual → selecting text"), - Line::from(" Command → executing commands (: prefix)"), - Line::from(""), - Line::from("PANEL NAVIGATION:"), - Line::from(" Tab → cycle panels forward"), - Line::from(" Shift+Tab → cycle panels backward"), - Line::from(" (Panels: Chat, Thinking, Input)"), - Line::from(""), - Line::from("CURSOR MOVEMENT (Normal mode, Chat/Thinking panels):"), - Line::from(" h/← l/→ → move left/right by character"), - Line::from(" j/↓ k/↑ → move down/up by line"), - Line::from(" w → forward to next word start"), - Line::from(" e → forward to word end"), - Line::from(" b → backward to previous word"), - Line::from(" 0 / Home → start of line"), - Line::from(" ^ → first non-blank character"), - Line::from(" $ / End → end of line"), - Line::from(" gg → jump to top"), - Line::from(" G → jump to bottom"), - Line::from(" Ctrl+d/u → scroll half page down/up"), - Line::from(" Ctrl+f/b → scroll full page down/up"), - Line::from(" PageUp/Down → scroll full page"), - Line::from(""), - Line::from("EDITING (Normal mode):"), - Line::from(" i / Enter → enter insert mode at cursor"), - Line::from(" a → append after cursor"), - Line::from(" A → append at end 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 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("INSERT MODE:"), - Line::from(" Enter → send message"), - Line::from(" Ctrl+J → insert newline"), - Line::from(" Ctrl+↑/↓ → navigate input history"), - Line::from(" Ctrl+A → start of line"), - Line::from(" Ctrl+E → end of line"), - Line::from(" Ctrl+W → word forward"), - Line::from(" Ctrl+B → word backward"), - Line::from(" Esc → return to normal mode"), - Line::from(""), - Line::from("VISUAL MODE (all panels):"), - Line::from(" v → enter visual mode at cursor"), - 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(" 0 / ^ / $ → extend to line start/first char/end"), - Line::from(" y → yank (copy) selection"), - Line::from(" d → yank selection (delete in Input)"), - Line::from(" v / Esc → exit visual mode"), - Line::from(""), - Line::from("COMMANDS (press : then type):"), - 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(""), - Line::from("QUICK KEYS:"), - Line::from(" q → quit (from normal mode)"), - Line::from(" Ctrl+C → quit"), - Line::from(""), - Line::from("Press Esc or Enter to close this help."), - ]; + let tab_index = app.help_tab_index(); + let tabs = vec!["Navigation", "Editing", "Visual", "Commands", "Sessions"]; - let paragraph = Paragraph::new(help_text).block( - Block::default() - .title(Span::styled( - "Help", + // Build tab line + let mut tab_spans = Vec::new(); + for (i, tab_name) in tabs.iter().enumerate() { + if i == tab_index { + tab_spans.push(Span::styled( + format!(" {} ", tab_name), Style::default() - .fg(Color::LightMagenta) + .fg(Color::Black) + .bg(Color::LightMagenta) .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Rgb(95, 20, 135))), - ); + )); + } else { + tab_spans.push(Span::styled( + format!(" {} ", tab_name), + Style::default().fg(Color::Gray), + )); + } + if i < tabs.len() - 1 { + tab_spans.push(Span::raw(" │ ")); + } + } - frame.render_widget(paragraph, area); + let help_text = match tab_index { + 0 => vec![ // Navigation + Line::from(""), + Line::from(vec![ + Span::styled("PANEL NAVIGATION", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)) + ]), + Line::from(" Tab → cycle panels forward"), + Line::from(" Shift+Tab → cycle panels backward"), + Line::from(" (Panels: Chat, Thinking, Input)"), + Line::from(""), + 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(" j/↓ k/↑ → move down/up by line"), + Line::from(" w → forward to next word start"), + Line::from(" e → forward to word end"), + Line::from(" b → backward to previous word"), + Line::from(" 0 / Home → start of line"), + Line::from(" ^ → first non-blank character"), + Line::from(" $ / End → end of line"), + Line::from(" gg → jump to top"), + 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+f/b → scroll full page down/up"), + Line::from(" PageUp/Down → scroll full page"), + ], + 1 => vec![ // Editing + Line::from(""), + 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(" a → append after cursor"), + Line::from(" A → append at end 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 above and enter insert mode"), + Line::from(""), + Line::from(vec![ + Span::styled("INSERT MODE KEYS", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)) + ]), + Line::from(" Enter → send message"), + Line::from(" Ctrl+J → insert newline (multiline message)"), + Line::from(" Ctrl+↑/↓ → navigate input history"), + Line::from(" Ctrl+A → jump to start of line"), + Line::from(" Ctrl+E → jump to end of line"), + Line::from(" Ctrl+W → word forward"), + Line::from(" Ctrl+B → word backward"), + Line::from(" Esc → return to normal mode"), + Line::from(""), + 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(""), + 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(" w → extend to next word start"), + Line::from(" e → extend to word end"), + Line::from(" b → extend backward to previous word"), + 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(""), + 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(" :q, :quit → quit application"), + Line::from(""), + Line::from(vec![ + Span::styled("MODEL MANAGEMENT", Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan)) + ]), + Line::from(" :m, :model → open model selector"), + Line::from(""), + 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![], + }; + + // Create layout for tabs and content + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Tab bar + Constraint::Min(0), // Content + 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) + .border_style(Style::default().fg(Color::Rgb(95, 20, 135))); + let nav_para = Paragraph::new(nav_hint) + .block(nav_block) + .alignment(Alignment::Center); + frame.render_widget(nav_para, layout[2]); } fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) { -- 2.52.0 From 5c5953912024e76611736b28a1c75fb724d2e876 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Thu, 2 Oct 2025 02:09:26 +0200 Subject: [PATCH 2/2] Bump version to 0.1.7 in PKGBUILD, Cargo.toml, and README --- Cargo.toml | 2 +- PKGBUILD | 2 +- README.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f12251..7504159 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ exclude = [] [workspace.package] -version = "0.1.5" +version = "0.1.7" edition = "2021" authors = ["Owlibou"] license = "AGPL-3.0" diff --git a/PKGBUILD b/PKGBUILD index b27cf48..3fea9ce 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: vikingowl pkgname=owlen -pkgver=0.1.4 +pkgver=0.1.7 pkgrel=1 pkgdesc="Terminal User Interface LLM client for Ollama with chat and code assistance features" arch=('x86_64') diff --git a/README.md b/README.md index e049967..d4b341a 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ > Terminal-native assistant for running local language models with a comfortable TUI. ![Status](https://img.shields.io/badge/status-alpha-yellow) -![Version](https://img.shields.io/badge/version-0.1.0-blue) +![Version](https://img.shields.io/badge/version-0.1.7-blue) ![Rust](https://img.shields.io/badge/made_with-Rust-ffc832?logo=rust&logoColor=white) ![License](https://img.shields.io/badge/license-AGPL--3.0-blue) ## Alpha Status -- This project is currently in **alpha** (v0.1.5) and under active development. +- This project is currently in **alpha** (v0.1.7) and under active development. - Core features are functional but expect occasional bugs and missing polish. - Breaking changes may occur between releases as we refine the API. - Feedback, bug reports, and contributions are very welcome! -- 2.52.0