use anyhow::Result;
use base64::Engine as _;
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind};
use crate::clipboard;
use crate::constants::UI_MOUSE_SCROLL_LINES;
use crate::tui::App;
use crate::utils::open_file;
#[derive(Debug, Clone)]
#[must_use]
pub enum EventAction {
Continue,
Quit,
SubmitMessage(String),
ExecuteCommand(String),
}
pub fn handle_event(app: &mut App, event: Event, viewport_height: u16) -> Result<EventAction> {
match event {
Event::Mouse(mouse) => handle_mouse_event(app, mouse, viewport_height),
Event::Key(key) => handle_key_event(app, key, viewport_height),
Event::Paste(text) => handle_paste(app, &text),
_ => Ok(EventAction::Continue), }
}
fn handle_mouse_event(
app: &mut App,
mouse: crossterm::event::MouseEvent,
_viewport_height: u16,
) -> Result<EventAction> {
match mouse.kind {
MouseEventKind::ScrollUp => {
app.scroll_up(UI_MOUSE_SCROLL_LINES); Ok(EventAction::Continue)
},
MouseEventKind::ScrollDown => {
app.scroll_down(UI_MOUSE_SCROLL_LINES); Ok(EventAction::Continue)
},
MouseEventKind::Down(crossterm::event::MouseButton::Left)
if mouse.modifiers.contains(KeyModifiers::CONTROL) =>
{
if let Some(area_y) = app.ui_state.attachment_area_y
&& mouse.row == area_y
&& !app.attachment_state.is_empty()
{
if let Some(path) = app.attachment_state.last_temp_path() {
open_file(path);
app.set_status("Opening image preview...");
}
return Ok(EventAction::Continue);
}
if let Some(target) = app.ui_state.chat_state.find_image_at_screen_pos(mouse.row) {
let msg_idx = target.message_index;
let img_idx = target.image_index;
if let Some(msg) = app.session_state.messages.get(msg_idx)
&& let Some(ref images) = msg.images
&& let Some(base64_data) = images.get(img_idx)
{
if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(base64_data)
{
let ext = if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
"png"
} else {
"jpeg"
};
let path = std::env::temp_dir()
.join(format!("mermaid-view-{}-{}.{}", msg_idx, img_idx, ext));
if std::fs::write(&path, &bytes).is_ok() {
open_file(&path);
app.set_status(format!("Opening Image #{}", img_idx + 1));
}
}
}
}
Ok(EventAction::Continue)
},
_ => Ok(EventAction::Continue),
}
}
fn handle_key_event(
app: &mut App,
key: crossterm::event::KeyEvent,
_viewport_height: u16,
) -> Result<EventAction> {
if key.kind != KeyEventKind::Press {
return Ok(EventAction::Continue);
}
if app.ui_state.attachment_focused {
let action = match key.code {
KeyCode::Down | KeyCode::Enter => {
app.ui_state.attachment_focused = false;
EventAction::Continue
},
KeyCode::Left => {
if app.ui_state.selected_attachment > 0 {
app.ui_state.selected_attachment -= 1;
}
EventAction::Continue
},
KeyCode::Right => {
if app.ui_state.selected_attachment + 1 < app.attachment_state.len() {
app.ui_state.selected_attachment += 1;
}
EventAction::Continue
},
KeyCode::Backspace | KeyCode::Delete => {
let idx = app.ui_state.selected_attachment;
if idx < app.attachment_state.len() {
app.attachment_state.remove_at(idx);
let remaining = app.attachment_state.len();
if remaining == 0 {
app.ui_state.attachment_focused = false;
app.ui_state.selected_attachment = 0;
app.set_status("All images removed");
} else {
if app.ui_state.selected_attachment >= remaining {
app.ui_state.selected_attachment = remaining - 1;
}
app.set_status(format!("Image removed ({} remaining)", remaining));
}
}
EventAction::Continue
},
KeyCode::Esc => {
app.ui_state.attachment_focused = false;
EventAction::Continue
},
KeyCode::Char(c) => {
app.ui_state.attachment_focused = false;
handle_char_input(app, c, key.modifiers)
},
_ => EventAction::Continue,
};
return Ok(action);
}
let action = match key.code {
KeyCode::Esc => handle_escape_key(app),
KeyCode::Enter => handle_enter_key(app)?,
KeyCode::Char(c) => handle_char_input(app, c, key.modifiers),
KeyCode::Backspace => handle_backspace(app),
KeyCode::Delete => handle_delete(app),
KeyCode::Left => handle_left_arrow(app),
KeyCode::Right => handle_right_arrow(app),
KeyCode::Home => handle_home(app),
KeyCode::End => handle_end(app),
KeyCode::Up => handle_up_arrow(app),
KeyCode::Down => handle_down_arrow(app),
KeyCode::PageUp => handle_page_up(app),
KeyCode::PageDown => handle_page_down(app),
_ => EventAction::Continue,
};
Ok(action)
}
fn handle_escape_key(app: &mut App) -> EventAction {
if app.app_state.is_generating() {
let (abort, partial) = app.abort_generation();
if let Some(h) = abort {
h.abort();
}
if !partial.is_empty() {
use crate::models::MessageRole;
app.add_message(MessageRole::Assistant, partial);
}
app.set_status("Generation stopped");
} else if !app.input.is_empty() {
app.input.clear();
app.set_status("Input cleared");
}
EventAction::Continue
}
fn handle_enter_key(app: &mut App) -> Result<EventAction> {
if app.input.is_empty() {
return Ok(EventAction::Continue);
}
if app.input.get().starts_with(':') {
if !app.app_state.is_generating() {
let command = app.input.get().trim_start_matches(':').to_string();
app.clear_input();
return Ok(EventAction::ExecuteCommand(command));
}
return Ok(EventAction::Continue);
}
if app.app_state.is_generating() {
let input = app.input.get().to_string();
app.operation_state.queue_message(input);
app.clear_input();
app.set_status("Message queued - will be sent before next action");
return Ok(EventAction::Continue);
}
let input = app.input.get().to_string();
app.clear_input();
Ok(EventAction::SubmitMessage(input))
}
fn handle_paste(app: &mut App, text: &str) -> Result<EventAction> {
let cleaned = text.replace('\r', "");
if !cleaned.is_empty() {
if app.input.history_index.is_some() {
app.input.history_index = None;
app.input.history_buffer.clear();
}
app.input.insert_str(&cleaned);
}
Ok(EventAction::Continue)
}
fn handle_char_input(app: &mut App, c: char, modifiers: KeyModifiers) -> EventAction {
if c == 'c' && modifiers == KeyModifiers::CONTROL {
app.auto_save_conversation();
app.quit();
return EventAction::Quit;
}
if c == 'v' && modifiers == KeyModifiers::CONTROL {
if app.model_state.vision_supported != Some(false) && clipboard::has_image() {
match clipboard::read_image_bytes() {
Ok((bytes, format)) => {
let num = app.attachment_state.add(&bytes, format);
app.set_status(format!(
"Image #{} attached (Ctrl+O to preview, Backspace to remove)",
num
));
},
Err(e) => {
app.set_status(format!("Failed to read clipboard image: {}", e));
},
}
} else {
if let Ok(text) = clipboard::read_text() {
let cleaned = text.replace('\r', "");
if !cleaned.is_empty() {
if app.input.history_index.is_some() {
app.input.history_index = None;
app.input.history_buffer.clear();
}
app.input.insert_str(&cleaned);
}
}
}
return EventAction::Continue;
}
if c == 'o' && modifiers == KeyModifiers::CONTROL {
if let Some(path) = app.attachment_state.last_temp_path() {
open_file(path);
app.set_status("Opening image preview...");
}
return EventAction::Continue;
}
if c == 't' && modifiers == KeyModifiers::ALT {
match app.model_state.toggle_thinking() {
Some(true) => app.set_status("Thinking mode enabled"),
Some(false) => app.set_status("Thinking mode disabled"),
None => app.set_status("Model does not support thinking"),
}
return EventAction::Continue;
}
if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT {
if app.input.history_index.is_some() {
app.input.history_index = None;
app.input.history_buffer.clear();
}
app.input.insert(c);
}
EventAction::Continue
}
fn handle_backspace(app: &mut App) -> EventAction {
if app.input.is_empty() && !app.attachment_state.is_empty() {
app.attachment_state.remove_last();
let remaining = app.attachment_state.len();
if remaining > 0 {
app.set_status(format!("Image removed ({} remaining)", remaining));
} else {
app.set_status("Image removed");
}
} else {
app.input.backspace();
}
EventAction::Continue
}
fn handle_delete(app: &mut App) -> EventAction {
app.input.delete();
EventAction::Continue
}
fn handle_left_arrow(app: &mut App) -> EventAction {
app.input.move_left();
EventAction::Continue
}
fn handle_right_arrow(app: &mut App) -> EventAction {
app.input.move_right();
EventAction::Continue
}
fn handle_home(app: &mut App) -> EventAction {
app.input.move_home();
EventAction::Continue
}
fn handle_end(app: &mut App) -> EventAction {
app.input.move_end();
EventAction::Continue
}
fn navigate_history_backward(app: &mut App) {
if app.input.history.is_empty() {
return;
}
match app.input.history_index {
None => {
app.input.history_buffer = app.input.get().to_string();
let idx = app.input.history.len() - 1;
app.input.history_index = Some(idx);
let entry = app.input.history[idx].clone();
app.input.set(&entry);
},
Some(idx) if idx > 0 => {
app.input.history_index = Some(idx - 1);
let entry = app.input.history[idx - 1].clone();
app.input.set(&entry);
},
Some(0) => {
},
_ => {},
}
}
fn navigate_history_forward(app: &mut App) {
match app.input.history_index {
Some(idx) if idx < app.input.history.len() - 1 => {
app.input.history_index = Some(idx + 1);
let entry = app.input.history[idx + 1].clone();
app.input.set(&entry);
},
Some(_) => {
app.input.history_index = None;
let buf = app.input.history_buffer.clone();
app.input.set(&buf);
},
None => {
},
}
}
fn handle_up_arrow(app: &mut App) -> EventAction {
if !app.ui_state.chat_state.is_manually_scrolling()
&& !app.attachment_state.is_empty()
&& app.input.cursor_position == 0
{
app.ui_state.attachment_focused = true;
app.ui_state.selected_attachment = app.attachment_state.len().saturating_sub(1);
return EventAction::Continue;
}
if !app.ui_state.chat_state.is_manually_scrolling()
&& !app.input.history.is_empty()
{
navigate_history_backward(app);
return EventAction::Continue;
}
app.scroll_up(1);
EventAction::Continue
}
fn handle_down_arrow(app: &mut App) -> EventAction {
if !app.ui_state.chat_state.is_manually_scrolling()
&& !app.input.history.is_empty()
{
navigate_history_forward(app);
return EventAction::Continue;
}
app.scroll_down(1);
EventAction::Continue
}
fn handle_page_up(app: &mut App) -> EventAction {
app.scroll_up(10);
EventAction::Continue
}
fn handle_page_down(app: &mut App) -> EventAction {
app.scroll_down(10);
EventAction::Continue
}