use anyhow::Result;
use base64::Engine as _;
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind};
use crate::clipboard;
use crate::tui::App;
#[derive(Debug, Clone)]
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(3); Ok(EventAction::Continue)
},
MouseEventKind::ScrollDown => {
app.scroll_down(3); 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 {
if mouse.row == area_y && !app.attachment_state.is_empty() {
if let Some(path) = app.attachment_state.last_temp_path() {
let _ = std::process::Command::new("xdg-open")
.arg(path)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
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) {
if let Some(ref images) = msg.images {
if 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 = format!("/tmp/mermaid-view-{}-{}.{}", msg_idx, img_idx, ext);
if std::fs::write(&path, &bytes).is_ok() {
let _ = std::process::Command::new("xdg-open")
.arg(&path)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
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),
KeyCode::Tab => handle_tab(app, key.modifiers),
KeyCode::BackTab => handle_backtab(app),
_ => EventAction::Continue,
};
Ok(action)
}
fn handle_escape_key(app: &mut App) -> EventAction {
if app.app_state.is_generating() {
if let Some(abort) = app.abort_generation() {
abort.abort();
}
if !app.current_response.is_empty() {
use crate::models::MessageRole;
app.add_message(MessageRole::Assistant, app.current_response.clone());
app.current_response.clear();
}
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('\n', " ").replace('\r', "");
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
if !collapsed.is_empty() {
if app.session_state.history_index.is_some() {
app.session_state.history_index = None;
app.session_state.history_buffer.clear();
}
app.input.insert_str(&collapsed);
}
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('\n', " ").replace('\r', "");
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
if !collapsed.is_empty() {
if app.session_state.history_index.is_some() {
app.session_state.history_index = None;
app.session_state.history_buffer.clear();
}
app.input.insert_str(&collapsed);
}
}
}
return EventAction::Continue;
}
if c == 'o' && modifiers == KeyModifiers::CONTROL {
if let Some(path) = app.attachment_state.last_temp_path() {
let _ = std::process::Command::new("xdg-open")
.arg(path)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
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.session_state.history_index.is_some() {
app.session_state.history_index = None;
app.session_state.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.session_state.input_history.is_empty() {
return;
}
match app.session_state.history_index {
None => {
app.session_state.history_buffer = app.input.get().to_string();
app.session_state.history_index = Some(app.session_state.input_history.len() - 1);
app.input.set(&app.session_state.input_history[app.session_state.history_index.unwrap()]);
}
Some(idx) if idx > 0 => {
app.session_state.history_index = Some(idx - 1);
app.input.set(&app.session_state.input_history[idx - 1]);
}
Some(0) => {
}
_ => {}
}
}
fn navigate_history_forward(app: &mut App) {
match app.session_state.history_index {
Some(idx) if idx < app.session_state.input_history.len() - 1 => {
app.session_state.history_index = Some(idx + 1);
app.input.set(&app.session_state.input_history[idx + 1]);
}
Some(_) => {
app.session_state.history_index = None;
app.input.set(&app.session_state.history_buffer);
}
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.session_state.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.session_state.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
}
fn handle_tab(_app: &mut App, _modifiers: KeyModifiers) -> EventAction {
EventAction::Continue
}
fn handle_backtab(_app: &mut App) -> EventAction {
EventAction::Continue
}