4 Commits

Author SHA1 Message Date
7c186882dc Merge pull request 'dev' (#25) from dev into main
Reviewed-on: #25
2025-10-02 03:00:29 +02:00
bdda669d4d Bump version to 0.1.8 in PKGBUILD, Cargo.toml, and README
Some checks failed
ci/someci/tag/woodpecker/1 Pipeline was successful
ci/someci/tag/woodpecker/2 Pipeline was successful
ci/someci/tag/woodpecker/3 Pipeline was successful
ci/someci/tag/woodpecker/4 Pipeline was successful
ci/someci/tag/woodpecker/5 Pipeline was successful
ci/someci/tag/woodpecker/6 Pipeline was successful
ci/someci/tag/woodpecker/7 Pipeline failed
2025-10-02 03:00:00 +02:00
108070db4b Update Woodpecker CI: improve cross-compilation setup and refine build steps 2025-10-02 02:58:13 +02:00
08ba04e99f Add command suggestions and enhancements to Command mode
- Introduce `command_suggestions` feature for autocompletion in Command mode.
- Implement `render_command_suggestions` to display filtered suggestions in a popup.
- Enable navigation through suggestions using Up/Down keys and Tab for completion.
- Add dynamic filtering of suggestions based on input buffer.
- Improve input handling, ensuring suggestion state resets appropriately when exiting Command mode.
2025-10-02 02:48:36 +02:00
6 changed files with 200 additions and 18 deletions

View File

@@ -39,37 +39,58 @@ matrix:
EXT: ".exe" EXT: ".exe"
steps: 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 - name: build
image: *rust_image image: *rust_image
commands: 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} - rustup target add ${TARGET}
# Set up cross-compilation environment variables and build
- | - |
case "${TARGET}" in case "${TARGET}" in
aarch64-unknown-linux-gnu) 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) aarch64-unknown-linux-musl)
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/usr/bin/aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_musl=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) 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) armv7-unknown-linux-musleabihf)
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
export CC_armv7_unknown_linux_musleabihf=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) 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 esac
- cargo build --release --all-features --target ${TARGET}
# Build the project
cargo build --release --all-features --target ${TARGET}
- name: package - name: package
image: *rust_image image: *rust_image

View File

@@ -9,7 +9,7 @@ members = [
exclude = [] exclude = []
[workspace.package] [workspace.package]
version = "0.1.7" version = "0.1.8"
edition = "2021" edition = "2021"
authors = ["Owlibou"] authors = ["Owlibou"]
license = "AGPL-3.0" license = "AGPL-3.0"

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlen pkgname=owlen
pkgver=0.1.7 pkgver=0.1.8
pkgrel=1 pkgrel=1
pkgdesc="Terminal User Interface LLM client for Ollama with chat and code assistance features" pkgdesc="Terminal User Interface LLM client for Ollama with chat and code assistance features"
arch=('x86_64') arch=('x86_64')

View File

@@ -3,13 +3,13 @@
> Terminal-native assistant for running local language models with a comfortable TUI. > Terminal-native assistant for running local language models with a comfortable TUI.
![Status](https://img.shields.io/badge/status-alpha-yellow) ![Status](https://img.shields.io/badge/status-alpha-yellow)
![Version](https://img.shields.io/badge/version-0.1.7-blue) ![Version](https://img.shields.io/badge/version-0.1.8-blue)
![Rust](https://img.shields.io/badge/made_with-Rust-ffc832?logo=rust&logoColor=white) ![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) ![License](https://img.shields.io/badge/license-AGPL--3.0-blue)
## Alpha Status ## Alpha Status
- This project is currently in **alpha** (v0.1.7) 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. - Core features are functional but expect occasional bugs and missing polish.
- Breaking changes may occur between releases as we refine the API. - Breaking changes may occur between releases as we refine the API.
- Feedback, bug reports, and contributions are very welcome! - Feedback, bug reports, and contributions are very welcome!

View File

@@ -51,6 +51,8 @@ pub struct ChatApp {
pending_key: Option<char>, // For multi-key sequences like gg, dd pending_key: Option<char>, // For multi-key sequences like gg, dd
clipboard: String, // Vim-style clipboard for yank/paste clipboard: String, // Vim-style clipboard for yank/paste
command_buffer: String, // Buffer for command mode input 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_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 visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
focused_panel: FocusedPanel, // Currently focused panel for scrolling focused_panel: FocusedPanel, // Currently focused panel for scrolling
@@ -100,6 +102,8 @@ impl ChatApp {
pending_key: None, pending_key: None,
clipboard: String::new(), clipboard: String::new(),
command_buffer: String::new(), command_buffer: String::new(),
command_suggestions: Vec::new(),
selected_suggestion: 0,
visual_start: None, visual_start: None,
visual_end: None, visual_end: None,
focused_panel: FocusedPanel::Input, focused_panel: FocusedPanel::Input,
@@ -206,6 +210,76 @@ impl ChatApp {
&self.command_buffer &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 { pub fn focused_panel(&self) -> FocusedPanel {
self.focused_panel self.focused_panel
} }
@@ -416,6 +490,8 @@ impl ChatApp {
(KeyCode::Char(':'), KeyModifiers::NONE) => { (KeyCode::Char(':'), KeyModifiers::NONE) => {
self.mode = InputMode::Command; self.mode = InputMode::Command;
self.command_buffer.clear(); self.command_buffer.clear();
self.selected_suggestion = 0;
self.update_command_suggestions();
self.status = ":".to_string(); self.status = ":".to_string();
} }
// Enter editing mode // Enter editing mode
@@ -1000,8 +1076,28 @@ impl ChatApp {
(KeyCode::Esc, _) => { (KeyCode::Esc, _) => {
self.mode = InputMode::Normal; self.mode = InputMode::Normal;
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
self.reset_status(); 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, _) => { (KeyCode::Enter, _) => {
// Execute command // Execute command
let cmd = self.command_buffer.trim(); let cmd = self.command_buffer.trim();
@@ -1055,6 +1151,7 @@ impl ChatApp {
self.selected_session_index = 0; self.selected_session_index = 0;
self.mode = InputMode::SessionBrowser; self.mode = InputMode::SessionBrowser;
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
return Ok(AppState::Running); return Ok(AppState::Running);
} }
Err(e) => { Err(e) => {
@@ -1070,6 +1167,7 @@ impl ChatApp {
self.selected_session_index = 0; self.selected_session_index = 0;
self.mode = InputMode::SessionBrowser; self.mode = InputMode::SessionBrowser;
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
return Ok(AppState::Running); return Ok(AppState::Running);
} }
Err(e) => { Err(e) => {
@@ -1080,12 +1178,14 @@ impl ChatApp {
"h" | "help" => { "h" | "help" => {
self.mode = InputMode::Help; self.mode = InputMode::Help;
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
return Ok(AppState::Running); return Ok(AppState::Running);
} }
"m" | "model" => { "m" | "model" => {
self.refresh_models().await?; self.refresh_models().await?;
self.mode = InputMode::ProviderSelection; self.mode = InputMode::ProviderSelection;
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
return Ok(AppState::Running); return Ok(AppState::Running);
} }
"n" | "new" => { "n" | "new" => {
@@ -1097,15 +1197,18 @@ impl ChatApp {
} }
} }
self.command_buffer.clear(); self.command_buffer.clear();
self.command_suggestions.clear();
self.mode = InputMode::Normal; self.mode = InputMode::Normal;
} }
(KeyCode::Char(c), KeyModifiers::NONE) (KeyCode::Char(c), KeyModifiers::NONE)
| (KeyCode::Char(c), KeyModifiers::SHIFT) => { | (KeyCode::Char(c), KeyModifiers::SHIFT) => {
self.command_buffer.push(c); self.command_buffer.push(c);
self.update_command_suggestions();
self.status = format!(":{}", self.command_buffer); self.status = format!(":{}", self.command_buffer);
} }
(KeyCode::Backspace, _) => { (KeyCode::Backspace, _) => {
self.command_buffer.pop(); self.command_buffer.pop();
self.update_command_suggestions();
self.status = format!(":{}", self.command_buffer); self.status = format!(":{}", self.command_buffer);
} }
_ => {} _ => {}

View File

@@ -82,6 +82,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
InputMode::ModelSelection => render_model_selector(frame, app), InputMode::ModelSelection => render_model_selector(frame, app),
InputMode::Help => render_help(frame, app), InputMode::Help => render_help(frame, app),
InputMode::SessionBrowser => render_session_browser(frame, app), InputMode::SessionBrowser => render_session_browser(frame, app),
InputMode::Command => render_command_suggestions(frame, app),
_ => {} _ => {}
} }
} }
@@ -1464,6 +1465,63 @@ fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
frame.render_widget(footer, layout[1]); 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 { fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::default() let vertical = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)