use anyhow::Result;
use ollama_rs::{generation::completion::request::GenerationRequest, Ollama};
use ratatui::{backend::CrosstermBackend, widgets::ListState, Terminal};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::io;
use std::time::Instant;
use tokio_stream::StreamExt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SyntaxTheme {
Base16OceanDark,
Monokai,
SolarizedDark,
Dracula,
Nord,
Base16OceanLight,
SolarizedLight,
InspiredGitHub,
MonokaiLight,
GruvboxLight,
}
impl SyntaxTheme {
pub fn as_str(&self) -> &'static str {
match self {
SyntaxTheme::Base16OceanDark => "base16-ocean.dark",
SyntaxTheme::Monokai => "base16-mocha.dark",
SyntaxTheme::SolarizedDark => "Solarized (dark)",
SyntaxTheme::Dracula => "base16-twilight.dark",
SyntaxTheme::Nord => "base16-ocean.dark", SyntaxTheme::Base16OceanLight => "base16-ocean.light",
SyntaxTheme::SolarizedLight => "Solarized (light)",
SyntaxTheme::InspiredGitHub => "InspiredGitHub",
SyntaxTheme::MonokaiLight => "base16-mocha.light",
SyntaxTheme::GruvboxLight => "base16-eighties.light",
}
}
pub fn display_name(&self) -> &'static str {
match self {
SyntaxTheme::Base16OceanDark => "Ocean Dark",
SyntaxTheme::Monokai => "Monokai",
SyntaxTheme::SolarizedDark => "Solarized Dark",
SyntaxTheme::Dracula => "Dracula",
SyntaxTheme::Nord => "Nord",
SyntaxTheme::Base16OceanLight => "Ocean Light",
SyntaxTheme::SolarizedLight => "Solarized Light",
SyntaxTheme::InspiredGitHub => "GitHub",
SyntaxTheme::MonokaiLight => "Monokai Light",
SyntaxTheme::GruvboxLight => "Gruvbox Light",
}
}
pub fn all() -> Vec<SyntaxTheme> {
vec![
SyntaxTheme::Base16OceanDark,
SyntaxTheme::Monokai,
SyntaxTheme::SolarizedDark,
SyntaxTheme::Dracula,
SyntaxTheme::Nord,
SyntaxTheme::Base16OceanLight,
SyntaxTheme::SolarizedLight,
SyntaxTheme::InspiredGitHub,
SyntaxTheme::MonokaiLight,
SyntaxTheme::GruvboxLight,
]
}
pub fn is_dark(&self) -> bool {
matches!(self,
SyntaxTheme::Base16OceanDark |
SyntaxTheme::Monokai |
SyntaxTheme::SolarizedDark |
SyntaxTheme::Dracula |
SyntaxTheme::Nord
)
}
}
impl Default for SyntaxTheme {
fn default() -> Self {
SyntaxTheme::Base16OceanDark
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct KeyCombination {
pub key_code: String,
pub ctrl: bool,
pub shift: bool,
}
impl KeyCombination {
pub fn parse(s: &str) -> Result<Self, String> {
let mut ctrl = false;
let mut shift = false;
let mut parts: Vec<&str> = s.split('-').collect();
while let Some(part) = parts.first() {
match *part {
"C" => {
ctrl = true;
parts.remove(0);
}
"S" => {
shift = true;
parts.remove(0);
}
_ => break,
}
}
if parts.is_empty() {
return Err(format!("Invalid key combination: '{}'", s));
}
let key_code = parts.join("-");
Ok(KeyCombination { key_code, ctrl, shift })
}
pub fn display(&self) -> String {
let mut result = String::new();
if self.ctrl {
result.push_str("C-");
}
if self.shift {
result.push_str("S-");
}
result.push_str(&self.key_code);
result
}
pub fn short_display(&self) -> String {
let mut result = String::new();
if self.ctrl {
result.push('^');
}
if self.shift {
result.push_str("S-");
}
match self.key_code.as_str() {
"Enter" => result.push_str("⏎"),
"Backspace" => result.push_str("⌫"),
"Delete" => result.push_str("⌦"),
"Escape" | "Esc" => result.push_str("Esc"),
"PageUp" => result.push_str("Pg↑"),
"PageDown" => result.push_str("Pg↓"),
"Up" => result.push_str("↑"),
"Down" => result.push_str("↓"),
"Left" => result.push_str("←"),
"Right" => result.push_str("→"),
"Home" => result.push_str("↖"),
"End" => result.push_str("↘"),
other => result.push_str(other),
}
result
}
}
impl TryFrom<String> for KeyCombination {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
KeyCombination::parse(&s)
}
}
impl From<KeyCombination> for String {
fn from(kc: KeyCombination) -> String {
kc.display()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindings {
pub quit: KeyCombination,
pub clear_history: KeyCombination,
pub copy: KeyCombination,
pub paste: KeyCombination,
pub toggle_autoscroll: KeyCombination,
pub open_settings: KeyCombination,
pub select_next_model: KeyCombination,
pub select_previous_model: KeyCombination,
pub send_query: KeyCombination,
pub insert_newline: KeyCombination,
pub cursor_up: KeyCombination,
pub cursor_down: KeyCombination,
pub cursor_left: KeyCombination,
pub cursor_right: KeyCombination,
pub cursor_word_left: KeyCombination,
pub cursor_word_right: KeyCombination,
pub cursor_home: KeyCombination,
pub cursor_end: KeyCombination,
pub cursor_left_select: KeyCombination,
pub cursor_right_select: KeyCombination,
pub cursor_word_left_select: KeyCombination,
pub cursor_word_right_select: KeyCombination,
pub cursor_home_select: KeyCombination,
pub cursor_end_select: KeyCombination,
pub delete_word_left: KeyCombination,
pub delete_word_right: KeyCombination,
pub delete_forward: KeyCombination,
pub backspace: KeyCombination,
pub page_up: KeyCombination,
pub page_down: KeyCombination,
pub close_dialog: KeyCombination,
pub dialog_up: KeyCombination,
pub dialog_down: KeyCombination,
pub dialog_apply: KeyCombination,
}
impl KeyBindings {
pub fn get(&self, name: &str) -> Option<&KeyCombination> {
match name {
"quit" => Some(&self.quit),
"clear_history" => Some(&self.clear_history),
"copy" => Some(&self.copy),
"paste" => Some(&self.paste),
"toggle_autoscroll" => Some(&self.toggle_autoscroll),
"open_settings" => Some(&self.open_settings),
"select_next_model" => Some(&self.select_next_model),
"select_previous_model" => Some(&self.select_previous_model),
"send_query" => Some(&self.send_query),
"insert_newline" => Some(&self.insert_newline),
"cursor_up" => Some(&self.cursor_up),
"cursor_down" => Some(&self.cursor_down),
"cursor_left" => Some(&self.cursor_left),
"cursor_right" => Some(&self.cursor_right),
"cursor_word_left" => Some(&self.cursor_word_left),
"cursor_word_right" => Some(&self.cursor_word_right),
"cursor_home" => Some(&self.cursor_home),
"cursor_end" => Some(&self.cursor_end),
"cursor_left_select" => Some(&self.cursor_left_select),
"cursor_right_select" => Some(&self.cursor_right_select),
"cursor_word_left_select" => Some(&self.cursor_word_left_select),
"cursor_word_right_select" => Some(&self.cursor_word_right_select),
"cursor_home_select" => Some(&self.cursor_home_select),
"cursor_end_select" => Some(&self.cursor_end_select),
"delete_word_left" => Some(&self.delete_word_left),
"delete_word_right" => Some(&self.delete_word_right),
"delete_forward" => Some(&self.delete_forward),
"backspace" => Some(&self.backspace),
"page_up" => Some(&self.page_up),
"page_down" => Some(&self.page_down),
"close_dialog" => Some(&self.close_dialog),
"dialog_up" => Some(&self.dialog_up),
"dialog_down" => Some(&self.dialog_down),
"dialog_apply" => Some(&self.dialog_apply),
_ => None,
}
}
pub fn set(&mut self, name: &str, kc: KeyCombination) {
match name {
"quit" => self.quit = kc,
"clear_history" => self.clear_history = kc,
"copy" => self.copy = kc,
"paste" => self.paste = kc,
"toggle_autoscroll" => self.toggle_autoscroll = kc,
"open_settings" => self.open_settings = kc,
"select_next_model" => self.select_next_model = kc,
"select_previous_model" => self.select_previous_model = kc,
"send_query" => self.send_query = kc,
"insert_newline" => self.insert_newline = kc,
"cursor_up" => self.cursor_up = kc,
"cursor_down" => self.cursor_down = kc,
"cursor_left" => self.cursor_left = kc,
"cursor_right" => self.cursor_right = kc,
"cursor_word_left" => self.cursor_word_left = kc,
"cursor_word_right" => self.cursor_word_right = kc,
"cursor_home" => self.cursor_home = kc,
"cursor_end" => self.cursor_end = kc,
"cursor_left_select" => self.cursor_left_select = kc,
"cursor_right_select" => self.cursor_right_select = kc,
"cursor_word_left_select" => self.cursor_word_left_select = kc,
"cursor_word_right_select" => self.cursor_word_right_select = kc,
"cursor_home_select" => self.cursor_home_select = kc,
"cursor_end_select" => self.cursor_end_select = kc,
"delete_word_left" => self.delete_word_left = kc,
"delete_word_right" => self.delete_word_right = kc,
"delete_forward" => self.delete_forward = kc,
"backspace" => self.backspace = kc,
"page_up" => self.page_up = kc,
"page_down" => self.page_down = kc,
"close_dialog" => self.close_dialog = kc,
"dialog_up" => self.dialog_up = kc,
"dialog_down" => self.dialog_down = kc,
"dialog_apply" => self.dialog_apply = kc,
_ => {}
}
}
pub fn all() -> Vec<(&'static str, &'static str)> {
vec![
("quit", "Quit"),
("clear_history", "Clear History"),
("copy", "Copy Selection"),
("paste", "Paste"),
("toggle_autoscroll", "Toggle Autoscroll"),
("open_settings", "Open Settings"),
("select_next_model", "Next Model"),
("select_previous_model", "Previous Model"),
("send_query", "Send Query"),
("insert_newline", "Insert Newline"),
("cursor_up", "Cursor Up"),
("cursor_down", "Cursor Down"),
("cursor_left", "Cursor Left"),
("cursor_right", "Cursor Right"),
("cursor_word_left", "Word Left"),
("cursor_word_right", "Word Right"),
("cursor_home", "Home"),
("cursor_end", "End"),
("cursor_left_select", "Select Left"),
("cursor_right_select", "Select Right"),
("cursor_word_left_select", "Select Word Left"),
("cursor_word_right_select", "Select Word Right"),
("cursor_home_select", "Select Home"),
("cursor_end_select", "Select End"),
("delete_word_left", "Delete Word Left"),
("delete_word_right", "Delete Word Right"),
("delete_forward", "Delete Forward"),
("backspace", "Backspace"),
("page_up", "Page Up"),
("page_down", "Page Down"),
("close_dialog", "Close Dialog"),
("dialog_up", "Dialog Up"),
("dialog_down", "Dialog Down"),
("dialog_apply", "Dialog Apply"),
]
}
}
impl Default for KeyBindings {
fn default() -> Self {
KeyBindings {
quit: KeyCombination::parse("C-q").unwrap(),
clear_history: KeyCombination::parse("C-c").unwrap(),
copy: KeyCombination::parse("C-S-c").unwrap(),
paste: KeyCombination::parse("C-S-v").unwrap(),
toggle_autoscroll: KeyCombination::parse("C-s").unwrap(),
open_settings: KeyCombination::parse("C-o").unwrap(),
select_next_model: KeyCombination::parse("C-Down").unwrap(),
select_previous_model: KeyCombination::parse("C-Up").unwrap(),
send_query: KeyCombination::parse("Enter").unwrap(),
insert_newline: KeyCombination::parse("S-Enter").unwrap(),
cursor_up: KeyCombination::parse("Up").unwrap(),
cursor_down: KeyCombination::parse("Down").unwrap(),
cursor_left: KeyCombination::parse("Left").unwrap(),
cursor_right: KeyCombination::parse("Right").unwrap(),
cursor_word_left: KeyCombination::parse("C-Left").unwrap(),
cursor_word_right: KeyCombination::parse("C-Right").unwrap(),
cursor_home: KeyCombination::parse("Home").unwrap(),
cursor_end: KeyCombination::parse("End").unwrap(),
cursor_left_select: KeyCombination::parse("S-Left").unwrap(),
cursor_right_select: KeyCombination::parse("S-Right").unwrap(),
cursor_word_left_select: KeyCombination::parse("C-S-Left").unwrap(),
cursor_word_right_select: KeyCombination::parse("C-S-Right").unwrap(),
cursor_home_select: KeyCombination::parse("S-Home").unwrap(),
cursor_end_select: KeyCombination::parse("S-End").unwrap(),
delete_word_left: KeyCombination::parse("C-Backspace").unwrap(),
delete_word_right: KeyCombination::parse("C-Delete").unwrap(),
delete_forward: KeyCombination::parse("Delete").unwrap(),
backspace: KeyCombination::parse("Backspace").unwrap(),
page_up: KeyCombination::parse("PageUp").unwrap(),
page_down: KeyCombination::parse("PageDown").unwrap(),
close_dialog: KeyCombination::parse("Esc").unwrap(),
dialog_up: KeyCombination::parse("Up").unwrap(),
dialog_down: KeyCombination::parse("Down").unwrap(),
dialog_apply: KeyCombination::parse("Enter").unwrap(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub syntax_theme: SyntaxTheme,
#[serde(default)]
pub keybindings: KeyBindings,
}
impl Default for Settings {
fn default() -> Self {
Settings {
syntax_theme: SyntaxTheme::default(),
keybindings: KeyBindings::default(),
}
}
}
pub struct App {
pub models: Vec<String>,
pub list_state: ListState,
pub input: String,
pub history: String,
pub model_inputs: HashMap<String, String>,
pub model_cursors: HashMap<String, usize>,
pub model_selections: HashMap<String, Option<usize>>,
pub model_histories: HashMap<String, String>,
pub model_scrolls: HashMap<String, u16>,
pub scroll: u16,
pub input_scroll: u16,
pub cursor_pos: usize,
pub selection_start: Option<usize>,
pub autoscroll: bool,
pub is_loading: bool,
pub ollama: Ollama,
pub start_time: Instant,
pub last_cursor_blink: Instant,
pub cursor_visible: bool,
pub debug_keys: bool,
pub debug_last_key: Option<String>,
pub render_count: u64,
pub settings: Settings,
pub show_settings_dialog: bool,
pub settings_selection: usize,
pub settings_recording_binding: Option<String>,
}
impl App {
pub async fn new() -> Self {
let ollama = Ollama::default();
let debug_keys = env::var("LAZYLLAMA_DEBUG_KEYS")
.map(|v| v != "0" && v.to_lowercase() != "false")
.unwrap_or(false);
let settings = crate::utils::load_settings();
let mut app = App {
models: Vec::new(),
list_state: ListState::default(),
input: String::new(),
cursor_pos: 0,
selection_start: None,
history: String::new(),
model_inputs: HashMap::new(),
model_cursors: HashMap::new(),
model_selections: HashMap::new(),
model_histories: HashMap::new(),
model_scrolls: HashMap::new(),
scroll: 0,
input_scroll: 0,
autoscroll: true,
is_loading: false,
ollama,
start_time: Instant::now(),
last_cursor_blink: Instant::now(),
cursor_visible: true,
debug_keys,
debug_last_key: None,
render_count: 0,
settings,
show_settings_dialog: false,
settings_selection: 0,
settings_recording_binding: None,
};
app.refresh_models().await;
app
}
pub async fn refresh_models(&mut self) {
if let Ok(models) = self.ollama.list_local_models().await {
self.models = models.into_iter().map(|m| m.name).collect::<Vec<String>>();
for model in &self.models {
self.model_inputs.entry(model.clone()).or_insert_with(String::new);
self.model_cursors.entry(model.clone()).or_insert(0);
self.model_selections.entry(model.clone()).or_insert(None);
self.model_histories.entry(model.clone()).or_insert_with(String::new);
self.model_scrolls.entry(model.clone()).or_insert(0);
}
if !self.models.is_empty() {
self.list_state.select(Some(0));
self.load_current_model_buffers();
}
}
}
pub fn save_current_model_buffers(&mut self) {
if let Some(index) = self.list_state.selected() {
if let Some(model) = self.models.get(index) {
self.model_inputs.insert(model.clone(), self.input.clone());
self.model_cursors.insert(model.clone(), self.cursor_pos);
self.model_selections.insert(model.clone(), self.selection_start);
self.model_histories.insert(model.clone(), self.history.clone());
self.model_scrolls.insert(model.clone(), self.scroll);
}
}
}
pub fn load_current_model_buffers(&mut self) {
if let Some(index) = self.list_state.selected() {
if let Some(model) = self.models.get(index) {
self.input = self.model_inputs.get(model).cloned().unwrap_or_default();
self.cursor_pos = *self.model_cursors.get(model).unwrap_or(&0);
self.selection_start = *self.model_selections.get(model).unwrap_or(&None);
self.history = self.model_histories.get(model).cloned().unwrap_or_default();
self.scroll = *self.model_scrolls.get(model).unwrap_or(&0);
self.clamp_cursor();
}
}
}
pub fn insert_char(&mut self, c: char) {
if self.selection_start.is_some() {
self.delete_selection();
}
let byte_idx = self.char_index_to_byte_index(self.cursor_pos);
self.input.insert(byte_idx, c);
self.cursor_pos = self.cursor_pos.saturating_add(1);
self.reset_cursor_blink();
}
pub fn backspace(&mut self) {
if self.cursor_pos == 0 {
return;
}
let remove_idx = self.cursor_pos - 1;
let byte_idx = self.char_index_to_byte_index(remove_idx);
self.input.remove(byte_idx);
self.cursor_pos = self.cursor_pos.saturating_sub(1);
self.reset_cursor_blink();
}
pub fn delete_word_left(&mut self) {
if self.cursor_pos == 0 {
return;
}
let chars: Vec<char> = self.input.chars().collect();
let mut i = self.cursor_pos.min(chars.len());
while i > 0 && !Self::is_word_char(chars[i - 1]) {
i -= 1;
}
while i > 0 && Self::is_word_char(chars[i - 1]) {
i -= 1;
}
if i != self.cursor_pos {
let start = self.char_index_to_byte_index(i);
let end = self.char_index_to_byte_index(self.cursor_pos);
self.input.replace_range(start..end, "");
self.cursor_pos = i;
self.reset_cursor_blink();
}
}
pub fn delete_forward(&mut self) {
let len = self.input.chars().count();
if self.cursor_pos >= len {
return;
}
let byte_idx = self.char_index_to_byte_index(self.cursor_pos);
self.input.remove(byte_idx);
self.reset_cursor_blink();
}
pub fn delete_word_right(&mut self) {
let chars: Vec<char> = self.input.chars().collect();
let len = chars.len();
if self.cursor_pos >= len {
return;
}
let mut i = self.cursor_pos.min(len);
while i < len && !Self::is_word_char(chars[i]) {
i += 1;
}
while i < len && Self::is_word_char(chars[i]) {
i += 1;
}
if i != self.cursor_pos {
let start = self.char_index_to_byte_index(self.cursor_pos);
let end = self.char_index_to_byte_index(i);
self.input.replace_range(start..end, "");
self.reset_cursor_blink();
}
}
pub fn move_cursor_left(&mut self) {
self.clear_selection();
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.reset_cursor_blink();
}
}
pub fn move_cursor_right(&mut self) {
self.clear_selection();
let len = self.input.chars().count();
if self.cursor_pos < len {
self.cursor_pos += 1;
self.reset_cursor_blink();
}
}
pub fn move_cursor_home(&mut self) {
self.clear_selection();
if self.cursor_pos != 0 {
self.cursor_pos = 0;
self.reset_cursor_blink();
}
}
pub fn move_cursor_end(&mut self) {
self.clear_selection();
let len = self.input.chars().count();
if self.cursor_pos != len {
self.cursor_pos = len;
self.reset_cursor_blink();
}
}
pub fn move_cursor_up(&mut self) {
self.clear_selection();
let chars: Vec<char> = self.input.chars().collect();
let mut line_start = 0;
let mut col = 0;
for i in 0..self.cursor_pos.min(chars.len()) {
if chars[i] == '\n' {
line_start = i + 1;
col = 0;
} else {
col += 1;
}
}
if line_start == 0 {
return;
}
let mut prev_line_start = 0;
for i in 0..line_start - 1 {
if chars[i] == '\n' {
prev_line_start = i + 1;
}
}
let prev_line_len = line_start - prev_line_start - 1; let new_cursor_pos = prev_line_start + col.min(prev_line_len);
if new_cursor_pos != self.cursor_pos {
self.cursor_pos = new_cursor_pos;
self.reset_cursor_blink();
}
}
pub fn move_cursor_down(&mut self) {
self.clear_selection();
let chars: Vec<char> = self.input.chars().collect();
let len = chars.len();
let mut line_start = 0;
let mut col = 0;
for i in 0..self.cursor_pos.min(len) {
if chars[i] == '\n' {
line_start = i + 1;
col = 0;
} else {
col += 1;
}
}
let mut next_line_start = None;
for i in line_start..len {
if chars[i] == '\n' {
next_line_start = Some(i + 1);
break;
}
}
let next_line_start = match next_line_start {
Some(pos) => pos,
None => return,
};
let mut next_line_end = len;
for i in next_line_start..len {
if chars[i] == '\n' {
next_line_end = i;
break;
}
}
let next_line_len = next_line_end - next_line_start;
let new_cursor_pos = next_line_start + col.min(next_line_len);
if new_cursor_pos != self.cursor_pos {
self.cursor_pos = new_cursor_pos;
self.reset_cursor_blink();
}
}
pub fn move_cursor_word_left(&mut self) {
self.clear_selection();
if self.cursor_pos == 0 {
return;
}
let chars: Vec<char> = self.input.chars().collect();
let mut i = self.cursor_pos.min(chars.len());
while i > 0 && !Self::is_word_char(chars[i - 1]) {
i -= 1;
}
while i > 0 && Self::is_word_char(chars[i - 1]) {
i -= 1;
}
if i != self.cursor_pos {
self.cursor_pos = i;
self.reset_cursor_blink();
}
}
pub fn move_cursor_word_right(&mut self) {
self.clear_selection();
let chars: Vec<char> = self.input.chars().collect();
let len = chars.len();
let mut i = self.cursor_pos.min(len);
while i < len && !Self::is_word_char(chars[i]) {
i += 1;
}
while i < len && Self::is_word_char(chars[i]) {
i += 1;
}
if i != self.cursor_pos {
self.cursor_pos = i;
self.reset_cursor_blink();
}
}
pub fn update_cursor_blink(&mut self) -> bool {
if self.last_cursor_blink.elapsed().as_millis() >= 500 {
self.cursor_visible = !self.cursor_visible;
self.last_cursor_blink = Instant::now();
return true;
}
false
}
pub fn reset_cursor_blink(&mut self) {
self.cursor_visible = true;
self.last_cursor_blink = Instant::now();
}
pub fn clamp_cursor(&mut self) {
let len = self.input.chars().count();
if self.cursor_pos > len {
self.cursor_pos = len;
}
}
pub fn char_index_to_byte_index(&self, char_index: usize) -> usize {
self.input
.char_indices()
.nth(char_index)
.map(|(idx, _)| idx)
.unwrap_or_else(|| self.input.len())
}
pub fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub fn clear_selection(&mut self) {
self.selection_start = None;
}
fn start_selection(&mut self) {
if self.selection_start.is_none() {
self.selection_start = Some(self.cursor_pos);
}
}
pub fn get_selection_range(&self) -> Option<(usize, usize)> {
self.selection_start.map(|start| {
if start <= self.cursor_pos {
(start, self.cursor_pos)
} else {
(self.cursor_pos, start)
}
})
}
pub fn get_selected_text(&self) -> Option<String> {
self.get_selection_range().map(|(start, end)| {
let chars: Vec<char> = self.input.chars().collect();
chars[start..end].iter().collect()
})
}
fn delete_selection(&mut self) {
if let Some((start, end)) = self.get_selection_range() {
let start_byte = self.char_index_to_byte_index(start);
let end_byte = self.char_index_to_byte_index(end);
self.input.replace_range(start_byte..end_byte, "");
self.cursor_pos = start;
self.clear_selection();
self.reset_cursor_blink();
}
}
pub fn insert_text_at_cursor(&mut self, text: &str) {
if self.selection_start.is_some() {
self.delete_selection();
}
let byte_idx = self.char_index_to_byte_index(self.cursor_pos);
self.input.insert_str(byte_idx, text);
self.cursor_pos += text.chars().count();
self.reset_cursor_blink();
}
pub fn move_cursor_left_with_selection(&mut self) {
self.start_selection();
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.reset_cursor_blink();
}
}
pub fn move_cursor_right_with_selection(&mut self) {
self.start_selection();
let len = self.input.chars().count();
if self.cursor_pos < len {
self.cursor_pos += 1;
self.reset_cursor_blink();
}
}
pub fn move_cursor_word_left_with_selection(&mut self) {
self.start_selection();
if self.cursor_pos == 0 {
return;
}
let chars: Vec<char> = self.input.chars().collect();
let mut i = self.cursor_pos.min(chars.len());
while i > 0 && !Self::is_word_char(chars[i - 1]) {
i -= 1;
}
while i > 0 && Self::is_word_char(chars[i - 1]) {
i -= 1;
}
if i != self.cursor_pos {
self.cursor_pos = i;
self.reset_cursor_blink();
}
}
pub fn move_cursor_word_right_with_selection(&mut self) {
self.start_selection();
let chars: Vec<char> = self.input.chars().collect();
let len = chars.len();
let mut i = self.cursor_pos.min(len);
while i < len && !Self::is_word_char(chars[i]) {
i += 1;
}
while i < len && Self::is_word_char(chars[i]) {
i += 1;
}
if i != self.cursor_pos {
self.cursor_pos = i;
self.reset_cursor_blink();
}
}
pub fn move_cursor_home_with_selection(&mut self) {
self.start_selection();
if self.cursor_pos != 0 {
self.cursor_pos = 0;
self.reset_cursor_blink();
}
}
pub fn move_cursor_end_with_selection(&mut self) {
self.start_selection();
let len = self.input.chars().count();
if self.cursor_pos != len {
self.cursor_pos = len;
self.reset_cursor_blink();
}
}
pub fn copy_selection(&self) -> Result<()> {
if let Some(text) = self.get_selected_text() {
let mut clipboard = arboard::Clipboard::new()?;
clipboard.set_text(text)?;
Ok(())
} else {
Err(anyhow::anyhow!("No text selected"))
}
}
pub fn paste_from_clipboard(&mut self) -> Result<()> {
let mut clipboard = arboard::Clipboard::new()?;
let text = clipboard.get_text()?;
self.insert_text_at_cursor(&text);
Ok(())
}
pub fn select_next_model(&mut self) {
if self.models.is_empty() {
return;
}
self.save_current_model_buffers();
let i = match self.list_state.selected() {
Some(i) => {
if i >= self.models.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.list_state.select(Some(i));
self.load_current_model_buffers();
}
pub fn select_previous_model(&mut self) {
if self.models.is_empty() {
return;
}
self.save_current_model_buffers();
let i = match self.list_state.selected() {
Some(i) => {
if i == 0 {
self.models.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.list_state.select(Some(i));
self.load_current_model_buffers();
}
pub async fn send_query(
&mut self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<()> {
if let Some(i) = self.list_state.selected() {
let model = self.models[i].clone();
let prompt = self.input.clone();
self.history.push_str(&format!("\nYOU: {}\n\nAI: ", prompt));
self.input.clear();
self.cursor_pos = 0;
self.save_current_model_buffers();
self.is_loading = true;
self.autoscroll = true;
let request = GenerationRequest::new(model.clone(), prompt);
let mut stream = self.ollama.generate_stream(request).await?;
while let Some(res) = stream.next().await {
if let Ok(responses) = res {
for resp in responses {
self.history.push_str(&resp.response);
}
terminal.draw(|f| crate::ui::ui(f, self))?;
}
}
self.history.push_str("\n---\n");
self.is_loading = false;
self.save_current_model_buffers();
}
Ok(())
}
}