mod app;
mod ui;
mod utils;
use crate::app::App;
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, time::Duration};
fn matches_key(key: &crossterm::event::KeyEvent, kc: &app::KeyCombination) -> bool {
let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let is_shift = key.modifiers.contains(KeyModifiers::SHIFT);
if kc.ctrl != is_ctrl {
return false;
}
let code_matches = match key.code {
KeyCode::Char(c) => {
if kc.shift == is_shift {
c.to_string() == kc.key_code || c.to_string().to_uppercase() == kc.key_code.to_uppercase()
} else if kc.shift && !is_shift {
c.to_uppercase().to_string() == kc.key_code.to_uppercase()
} else {
c.to_string().to_lowercase() == kc.key_code.to_lowercase()
}
}
KeyCode::Enter => kc.key_code.eq_ignore_ascii_case("Enter") || kc.key_code.eq_ignore_ascii_case("Return"),
KeyCode::Backspace => kc.key_code.eq_ignore_ascii_case("Backspace"),
KeyCode::Delete => kc.key_code.eq_ignore_ascii_case("Delete"),
KeyCode::Up => kc.key_code.eq_ignore_ascii_case("Up"),
KeyCode::Down => kc.key_code.eq_ignore_ascii_case("Down"),
KeyCode::Left => kc.key_code.eq_ignore_ascii_case("Left"),
KeyCode::Right => kc.key_code.eq_ignore_ascii_case("Right"),
KeyCode::Home => kc.key_code.eq_ignore_ascii_case("Home"),
KeyCode::End => kc.key_code.eq_ignore_ascii_case("End"),
KeyCode::PageUp => kc.key_code.eq_ignore_ascii_case("PageUp"),
KeyCode::PageDown => kc.key_code.eq_ignore_ascii_case("PageDown"),
KeyCode::Esc => kc.key_code.eq_ignore_ascii_case("Esc") || kc.key_code.eq_ignore_ascii_case("Escape"),
_ => false,
};
code_matches && (kc.shift == is_shift || !kc.shift)
}
#[tokio::main]
async fn main() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new().await;
let mut should_quit = false;
terminal.draw(|f| ui::ui(f, &mut app))?;
while !should_quit {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
if app.debug_keys {
app.debug_last_key = Some(format!("{:?}", key));
}
let kb = &app.settings.keybindings;
if app.show_settings_dialog {
let themes = app::SyntaxTheme::all();
let bindings = app::KeyBindings::all();
let theme_count = themes.len();
let binding_count = bindings.len();
let selectable = ui::get_settings_selectable_indices(theme_count, binding_count);
let selectable_count = selectable.len();
let current_sel_pos = selectable.iter().position(|&i| i == app.settings_selection)
.unwrap_or(0);
let theme_sel_end = theme_count.saturating_sub(1); let binding_sel_start = theme_count;
if let Some(recording_name) = &app.settings_recording_binding.clone() {
if matches_key(&key, &kb.close_dialog) {
app.settings_recording_binding = None;
} else {
let kc = key_event_to_key_combination(&key);
app.settings.keybindings.set(recording_name, kc);
app.settings_recording_binding = None;
let _ = utils::save_settings(&app.settings);
}
terminal.draw(|f| ui::ui(f, &mut app))?;
continue;
}
if matches_key(&key, &kb.close_dialog) {
app.show_settings_dialog = false;
continue;
} else if matches_key(&key, &kb.dialog_up) {
if current_sel_pos > 0 {
app.settings_selection = selectable[current_sel_pos - 1];
} else {
app.settings_selection = selectable[selectable_count - 1];
}
} else if matches_key(&key, &kb.dialog_down) {
if current_sel_pos + 1 < selectable_count {
app.settings_selection = selectable[current_sel_pos + 1];
} else {
app.settings_selection = selectable[0];
}
} else if matches_key(&key, &kb.dialog_apply) {
if current_sel_pos <= theme_sel_end {
if let Some(theme) = themes.get(current_sel_pos) {
app.settings.syntax_theme = theme.clone();
let _ = utils::save_settings(&app.settings);
}
}
else if current_sel_pos >= binding_sel_start {
let binding_idx = current_sel_pos - binding_sel_start;
if let Some((name, _)) = bindings.get(binding_idx) {
app.settings_recording_binding = Some(name.to_string());
}
}
}
terminal.draw(|f| ui::ui(f, &mut app))?;
continue; }
if matches_key(&key, &kb.quit) {
should_quit = true;
}
else if matches_key(&key, &kb.clear_history) {
app.history.clear();
app.scroll = 0;
app.autoscroll = true;
app.save_current_model_buffers();
}
else if matches_key(&key, &kb.copy) {
if let Err(e) = app.copy_selection() {
if app.debug_keys {
app.debug_last_key = Some(format!("Copy failed: {}", e));
}
}
}
else if matches_key(&key, &kb.paste) {
if let Err(e) = app.paste_from_clipboard() {
if app.debug_keys {
app.debug_last_key = Some(format!("Paste failed: {}", e));
}
}
}
else if matches_key(&key, &kb.toggle_autoscroll) {
app.autoscroll = !app.autoscroll;
}
else if matches_key(&key, &kb.open_settings) {
app.show_settings_dialog = !app.show_settings_dialog;
if app.show_settings_dialog {
app.settings_selection = 1;
}
}
else if matches_key(&key, &kb.cursor_word_left_select) {
app.move_cursor_word_left_with_selection();
}
else if matches_key(&key, &kb.cursor_word_right_select) {
app.move_cursor_word_right_with_selection();
}
else if matches_key(&key, &kb.cursor_left_select) {
app.move_cursor_left_with_selection();
}
else if matches_key(&key, &kb.cursor_right_select) {
app.move_cursor_right_with_selection();
}
else if matches_key(&key, &kb.cursor_home_select) {
app.move_cursor_home_with_selection();
}
else if matches_key(&key, &kb.cursor_end_select) {
app.move_cursor_end_with_selection();
}
else if matches_key(&key, &kb.cursor_word_left) {
app.move_cursor_word_left();
}
else if matches_key(&key, &kb.cursor_word_right) {
app.move_cursor_word_right();
}
else if matches_key(&key, &kb.cursor_home) {
app.move_cursor_home();
}
else if matches_key(&key, &kb.cursor_end) {
app.move_cursor_end();
}
else if matches_key(&key, &kb.select_previous_model) {
app.select_previous_model();
}
else if matches_key(&key, &kb.select_next_model) {
app.select_next_model();
}
else if matches_key(&key, &kb.cursor_up) {
app.move_cursor_up();
}
else if matches_key(&key, &kb.cursor_down) {
app.move_cursor_down();
}
else if matches_key(&key, &kb.page_up) {
app.autoscroll = false;
app.scroll = app.scroll.saturating_sub(5);
}
else if matches_key(&key, &kb.page_down) {
app.autoscroll = false;
app.scroll = app.scroll.saturating_add(5);
}
else if matches_key(&key, &kb.cursor_left) {
app.move_cursor_left();
}
else if matches_key(&key, &kb.cursor_right) {
app.move_cursor_right();
}
else if matches_key(&key, &kb.delete_word_left) {
app.delete_word_left();
}
else if matches_key(&key, &kb.delete_word_right) {
app.delete_word_right();
}
else if matches_key(&key, &kb.delete_forward) {
app.delete_forward();
}
else if matches_key(&key, &kb.backspace) {
app.backspace();
}
else if matches_key(&key, &kb.insert_newline) {
app.insert_char('\n');
}
else if matches_key(&key, &kb.send_query) {
if !app.input.is_empty() && !app.is_loading {
app.send_query(&mut terminal).await?;
}
}
else if let KeyCode::Char(c) = key.code {
if !is_system_key(&key) {
app.insert_char(c);
}
}
terminal.draw(|f| ui::ui(f, &mut app))?;
}
} else if app.is_loading {
terminal.draw(|f| ui::ui(f, &mut app))?;
} else if app.update_cursor_blink() {
terminal.draw(|f| ui::ui(f, &mut app))?;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
app.save_current_model_buffers();
utils::save_history_to_file(&app.history)?;
utils::save_model_histories(&app.model_histories)?;
Ok(())
}
fn key_event_to_key_combination(key: &crossterm::event::KeyEvent) -> app::KeyCombination {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let key_code = match key.code {
KeyCode::Char(c) => {
if ctrl || shift {
c.to_uppercase().to_string()
} else {
c.to_string()
}
}
KeyCode::Enter => "Enter".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Delete => "Delete".to_string(),
KeyCode::Up => "Up".to_string(),
KeyCode::Down => "Down".to_string(),
KeyCode::Left => "Left".to_string(),
KeyCode::Right => "Right".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PageUp".to_string(),
KeyCode::PageDown => "PageDown".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Tab => "Tab".to_string(),
_ => return app::KeyCombination::parse("C-q").unwrap(), };
app::KeyCombination { key_code, ctrl, shift }
}
fn is_system_key(key: &crossterm::event::KeyEvent) -> bool {
let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
is_ctrl
}