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, Serialize, Deserialize)]
pub struct Settings {
pub syntax_theme: SyntaxTheme,
}
impl Default for Settings {
fn default() -> Self {
Settings {
syntax_theme: SyntaxTheme::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,
}
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,
};
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(())
}
}