Merge pull request 'dev' (#25) from dev into main
Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
2
PKGBUILD
2
PKGBUILD
@@ -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')
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 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!
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user