Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdda669d4d | |||
| 108070db4b | |||
| 08ba04e99f | |||
| 5c59539120 | |||
| c725bb1ce6 |
@@ -39,37 +39,58 @@ matrix:
|
||||
EXT: ".exe"
|
||||
|
||||
steps:
|
||||
- name: install-deps
|
||||
image: *rust_image
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-mingw-w64-x86-64 zip
|
||||
|
||||
- name: build
|
||||
image: *rust_image
|
||||
commands:
|
||||
# Install cross-compilation tools
|
||||
- apt-get update
|
||||
- apt-get install -y musl-tools gcc-aarch64-linux-gnu g++-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf mingw-w64 zip
|
||||
|
||||
# Verify cross-compilers are installed
|
||||
- which aarch64-linux-gnu-gcc || echo "aarch64-linux-gnu-gcc not found!"
|
||||
- which arm-linux-gnueabihf-gcc || echo "arm-linux-gnueabihf-gcc not found!"
|
||||
- which x86_64-w64-mingw32-gcc || echo "x86_64-w64-mingw32-gcc not found!"
|
||||
|
||||
# Add rust target
|
||||
- rustup target add ${TARGET}
|
||||
|
||||
# Set up cross-compilation environment variables and build
|
||||
- |
|
||||
case "${TARGET}" in
|
||||
aarch64-unknown-linux-gnu)
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CXX_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-g++
|
||||
export AR_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-ar
|
||||
;;
|
||||
aarch64-unknown-linux-musl)
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CXX_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-g++
|
||||
export AR_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-ar
|
||||
;;
|
||||
armv7-unknown-linux-gnueabihf)
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CXX_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-g++
|
||||
export AR_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-ar
|
||||
;;
|
||||
armv7-unknown-linux-musleabihf)
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CXX_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-g++
|
||||
export AR_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-ar
|
||||
;;
|
||||
x86_64-pc-windows-gnu)
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=/usr/bin/x86_64-w64-mingw32-gcc
|
||||
export CC_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-gcc
|
||||
export CXX_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-g++
|
||||
export AR_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-ar
|
||||
;;
|
||||
esac
|
||||
- cargo build --release --all-features --target ${TARGET}
|
||||
|
||||
# Build the project
|
||||
cargo build --release --all-features --target ${TARGET}
|
||||
|
||||
- name: package
|
||||
image: *rust_image
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
exclude = []
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.5"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
authors = ["Owlibou"]
|
||||
license = "AGPL-3.0"
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -1,6 +1,6 @@
|
||||
# Maintainer: vikingowl <christian@nachtigall.dev>
|
||||
pkgname=owlen
|
||||
pkgver=0.1.4
|
||||
pkgver=0.1.8
|
||||
pkgrel=1
|
||||
pkgdesc="Terminal User Interface LLM client for Ollama with chat and code assistance features"
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
> Terminal-native assistant for running local language models with a comfortable TUI.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Alpha Status
|
||||
|
||||
- This project is currently in **alpha** (v0.1.5) and under active development.
|
||||
- This project is currently in **alpha** (v0.1.8) 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!
|
||||
|
||||
@@ -51,6 +51,8 @@ pub struct ChatApp {
|
||||
pending_key: Option<char>, // For multi-key sequences like gg, dd
|
||||
clipboard: String, // Vim-style clipboard for yank/paste
|
||||
command_buffer: String, // Buffer for command mode input
|
||||
command_suggestions: Vec<String>, // Filtered command suggestions based on current input
|
||||
selected_suggestion: usize, // Index of selected suggestion
|
||||
visual_start: Option<(usize, usize)>, // Visual mode selection start (row, col) for Input panel
|
||||
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
||||
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
||||
@@ -60,6 +62,7 @@ pub struct ChatApp {
|
||||
saved_sessions: Vec<SessionMeta>, // 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 {
|
||||
@@ -99,6 +102,8 @@ impl ChatApp {
|
||||
pending_key: None,
|
||||
clipboard: String::new(),
|
||||
command_buffer: String::new(),
|
||||
command_suggestions: Vec::new(),
|
||||
selected_suggestion: 0,
|
||||
visual_start: None,
|
||||
visual_end: None,
|
||||
focused_panel: FocusedPanel::Input,
|
||||
@@ -108,6 +113,7 @@ impl ChatApp {
|
||||
saved_sessions: Vec::new(),
|
||||
selected_session_index: 0,
|
||||
save_name_buffer: String::new(),
|
||||
help_tab_index: 0,
|
||||
};
|
||||
|
||||
(app, session_rx)
|
||||
@@ -204,6 +210,76 @@ impl ChatApp {
|
||||
&self.command_buffer
|
||||
}
|
||||
|
||||
pub fn command_suggestions(&self) -> &[String] {
|
||||
&self.command_suggestions
|
||||
}
|
||||
|
||||
pub fn selected_suggestion(&self) -> usize {
|
||||
self.selected_suggestion
|
||||
}
|
||||
|
||||
/// Returns all available commands with their aliases
|
||||
fn get_all_commands() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("quit", "Exit the application"),
|
||||
("q", "Alias for quit"),
|
||||
("clear", "Clear the conversation"),
|
||||
("c", "Alias for clear"),
|
||||
("w", "Alias for write"),
|
||||
("save", "Alias for write"),
|
||||
("load", "Load a saved conversation"),
|
||||
("open", "Alias for load"),
|
||||
("o", "Alias for load"),
|
||||
("sessions", "List saved sessions"),
|
||||
("ls", "Alias for sessions"),
|
||||
("help", "Show help documentation"),
|
||||
("h", "Alias for help"),
|
||||
("model", "Select a model"),
|
||||
("m", "Alias for model"),
|
||||
("new", "Start a new conversation"),
|
||||
("n", "Alias for new"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Update command suggestions based on current input
|
||||
fn update_command_suggestions(&mut self) {
|
||||
let input = self.command_buffer.trim();
|
||||
|
||||
if input.is_empty() {
|
||||
// Show all commands when input is empty
|
||||
self.command_suggestions = Self::get_all_commands()
|
||||
.iter()
|
||||
.map(|(cmd, _)| cmd.to_string())
|
||||
.collect();
|
||||
} else {
|
||||
// Filter commands that start with the input
|
||||
self.command_suggestions = Self::get_all_commands()
|
||||
.iter()
|
||||
.filter_map(|(cmd, _)| {
|
||||
if cmd.starts_with(input) {
|
||||
Some(cmd.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Reset selection if out of bounds
|
||||
if self.selected_suggestion >= self.command_suggestions.len() {
|
||||
self.selected_suggestion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the current command with the selected suggestion
|
||||
fn complete_command(&mut self) {
|
||||
if let Some(suggestion) = self.command_suggestions.get(self.selected_suggestion) {
|
||||
self.command_buffer = suggestion.clone();
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_panel(&self) -> FocusedPanel {
|
||||
self.focused_panel
|
||||
}
|
||||
@@ -232,6 +308,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 => {
|
||||
@@ -410,6 +490,8 @@ impl ChatApp {
|
||||
(KeyCode::Char(':'), KeyModifiers::NONE) => {
|
||||
self.mode = InputMode::Command;
|
||||
self.command_buffer.clear();
|
||||
self.selected_suggestion = 0;
|
||||
self.update_command_suggestions();
|
||||
self.status = ":".to_string();
|
||||
}
|
||||
// Enter editing mode
|
||||
@@ -994,8 +1076,28 @@ impl ChatApp {
|
||||
(KeyCode::Esc, _) => {
|
||||
self.mode = InputMode::Normal;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
self.reset_status();
|
||||
}
|
||||
(KeyCode::Tab, _) => {
|
||||
// Tab completion
|
||||
self.complete_command();
|
||||
}
|
||||
(KeyCode::Up, _) | (KeyCode::Char('k'), KeyModifiers::CONTROL) => {
|
||||
// Navigate up in suggestions
|
||||
if !self.command_suggestions.is_empty() {
|
||||
self.selected_suggestion = self
|
||||
.selected_suggestion
|
||||
.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
(KeyCode::Down, _) | (KeyCode::Char('j'), KeyModifiers::CONTROL) => {
|
||||
// Navigate down in suggestions
|
||||
if !self.command_suggestions.is_empty() {
|
||||
self.selected_suggestion = (self.selected_suggestion + 1)
|
||||
.min(self.command_suggestions.len().saturating_sub(1));
|
||||
}
|
||||
}
|
||||
(KeyCode::Enter, _) => {
|
||||
// Execute command
|
||||
let cmd = self.command_buffer.trim();
|
||||
@@ -1041,7 +1143,7 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
"load" | "open" => {
|
||||
"load" | "open" | "o" => {
|
||||
// Load saved sessions and enter browser mode
|
||||
match self.storage.list_sessions() {
|
||||
Ok(sessions) => {
|
||||
@@ -1049,6 +1151,7 @@ impl ChatApp {
|
||||
self.selected_session_index = 0;
|
||||
self.mode = InputMode::SessionBrowser;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1064,6 +1167,7 @@ impl ChatApp {
|
||||
self.selected_session_index = 0;
|
||||
self.mode = InputMode::SessionBrowser;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1074,12 +1178,14 @@ impl ChatApp {
|
||||
"h" | "help" => {
|
||||
self.mode = InputMode::Help;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
"m" | "model" => {
|
||||
self.refresh_models().await?;
|
||||
self.mode = InputMode::ProviderSelection;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
"n" | "new" => {
|
||||
@@ -1091,15 +1197,18 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
self.mode = InputMode::Normal;
|
||||
}
|
||||
(KeyCode::Char(c), KeyModifiers::NONE)
|
||||
| (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
||||
self.command_buffer.push(c);
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
(KeyCode::Backspace, _) => {
|
||||
self.command_buffer.pop();
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
_ => {}
|
||||
@@ -1175,9 +1284,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 {
|
||||
|
||||
@@ -80,8 +80,9 @@ 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),
|
||||
InputMode::Command => render_command_suggestions(frame, app),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1083,96 +1084,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) {
|
||||
@@ -1309,6 +1465,63 @@ fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
frame.render_widget(footer, layout[1]);
|
||||
}
|
||||
|
||||
fn render_command_suggestions(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
let suggestions = app.command_suggestions();
|
||||
|
||||
// Only show suggestions if there are any
|
||||
if suggestions.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a small popup near the status bar (bottom of screen)
|
||||
let frame_height = frame.area().height;
|
||||
let suggestion_count = suggestions.len().min(8); // Show max 8 suggestions
|
||||
let popup_height = (suggestion_count as u16) + 2; // +2 for borders
|
||||
|
||||
// Position the popup above the status bar
|
||||
let popup_area = Rect {
|
||||
x: 1,
|
||||
y: frame_height.saturating_sub(popup_height + 3), // 3 for status bar height
|
||||
width: 40.min(frame.area().width - 2),
|
||||
height: popup_height,
|
||||
};
|
||||
|
||||
frame.render_widget(Clear, popup_area);
|
||||
|
||||
let items: Vec<ListItem> = suggestions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, cmd)| {
|
||||
let is_selected = idx == app.selected_suggestion();
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
ListItem::new(Span::styled(cmd.to_string(), style))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
" Commands (Tab to complete) ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow)),
|
||||
);
|
||||
|
||||
frame.render_widget(list, popup_area);
|
||||
}
|
||||
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
||||
let vertical = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
|
||||
Reference in New Issue
Block a user