use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use crate::app::{App, AppMode, Panel};
const WHEEL_ROWS: isize = 3;
pub fn handle_key(app: &mut App, key: KeyEvent) {
if matches!(app.mode, AppMode::Help | AppMode::Info) {
let toggle = if app.mode == AppMode::Help { '?' } else { 'i' };
if matches!(
key.code,
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q')
) || key.code == KeyCode::Char(toggle)
{
app.mode = AppMode::Normal;
}
return;
}
if key.code == KeyCode::Char('q') && app.mode == AppMode::Normal {
app.should_quit = true;
return;
}
match &app.mode {
AppMode::QueryInput => handle_query_input(app, key),
AppMode::Normal => handle_normal(app, key),
AppMode::Help | AppMode::Info => {}
}
}
fn handle_normal(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Char('?') => {
app.mode = AppMode::Help;
}
KeyCode::Char('i') => {
app.mode = AppMode::Info;
}
KeyCode::Tab | KeyCode::BackTab => app.cycle_panel(),
KeyCode::Char('+') | KeyCode::Char('=') if app.query_visible() => {
app.query_expanded = true;
}
KeyCode::Char('-') if app.query_visible() => {
app.query_expanded = false;
}
KeyCode::Char('/') | KeyCode::Char(':') => {
app.mode = AppMode::QueryInput;
app.active_panel = Panel::Query;
}
KeyCode::Esc => {
app.dismiss_query();
if app.active_panel == Panel::Query {
app.active_panel = Panel::Data;
}
}
KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.sidebar_collapsed = !app.sidebar_collapsed;
if app.sidebar_collapsed && app.active_panel == Panel::Sidebar {
app.active_panel = Panel::Data;
}
}
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let exported = if app.active_panel == Panel::Query && app.query_result.is_some() {
app.export_query_csv()
} else {
app.export_table_csv()
};
match exported {
Ok(path) => app.set_status(format!("Exported to {}", path)),
Err(e) => app.set_status(format!("Export failed: {}", e)),
}
}
_ => match app.active_panel {
Panel::Sidebar => handle_sidebar(app, key),
Panel::Data => handle_data(app, key),
Panel::Query => handle_query_results(app, key),
},
}
}
fn handle_sidebar(app: &mut App, key: KeyEvent) {
if app.sidebar_len() == 0 {
return;
}
match key.code {
KeyCode::Up | KeyCode::Char('k') => app.move_sidebar(-1),
KeyCode::Down | KeyCode::Char('j') => app.move_sidebar(1),
KeyCode::PageUp => app.move_sidebar(-10),
KeyCode::PageDown => app.move_sidebar(10),
KeyCode::Home | KeyCode::Char('g') => app.select_sidebar(0),
KeyCode::End | KeyCode::Char('G') => app.select_sidebar(usize::MAX),
KeyCode::Enter => {
app.flush_pending_load();
app.active_panel = Panel::Data;
}
_ => {}
}
}
fn handle_data(app: &mut App, key: KeyEvent) {
if app.table_view.is_none() {
return;
}
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('d') if ctrl => {
let step = app.half_viewport();
app.move_row(step);
}
KeyCode::Char('u') if ctrl => {
let step = app.half_viewport();
app.move_row(-step);
}
KeyCode::Up | KeyCode::Char('k') => app.move_row(-1),
KeyCode::Down | KeyCode::Char('j') => app.move_row(1),
KeyCode::Left if shift => app.pan_columns(-1),
KeyCode::Right if shift => app.pan_columns(1),
KeyCode::Char('H') => app.pan_columns(-1),
KeyCode::Char('L') => app.pan_columns(1),
KeyCode::Left | KeyCode::Char('h') => app.move_cursor_col(-1),
KeyCode::Right | KeyCode::Char('l') => app.move_cursor_col(1),
KeyCode::Char('s') => app.sort_cursor_column(),
KeyCode::Home => app.select_col(0),
KeyCode::End => app.select_col(usize::MAX),
KeyCode::PageUp | KeyCode::Char('[') => {
app.prev_page();
}
KeyCode::PageDown | KeyCode::Char(']') => {
app.next_page();
}
KeyCode::Char('g') => app.select_row(0),
KeyCode::Char('G') => app.select_last_row(),
_ => {}
}
}
fn handle_query_results(app: &mut App, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('u') if ctrl => app.clear_query(),
KeyCode::Up | KeyCode::Char('k') => app.move_query_row(-1),
KeyCode::Down | KeyCode::Char('j') => app.move_query_row(1),
KeyCode::PageUp => app.move_query_row(-10),
KeyCode::PageDown => app.move_query_row(10),
KeyCode::Home | KeyCode::Char('g') => app.move_query_row(isize::MIN / 2),
KeyCode::End | KeyCode::Char('G') => app.move_query_row(isize::MAX / 2),
_ => {
app.mode = AppMode::QueryInput;
handle_query_input(app, key);
}
}
}
fn handle_query_input(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc | KeyCode::Tab | KeyCode::BackTab => {
app.mode = AppMode::Normal;
app.active_panel = if app.sidebar_collapsed {
Panel::Data
} else {
Panel::Sidebar
};
}
KeyCode::Enter => {
app.run_query();
app.mode = AppMode::Normal;
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.clear_query();
}
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
app.query_input.insert(app.query_cursor, c);
app.query_cursor += 1;
}
KeyCode::Backspace => {
if app.query_cursor > 0 {
app.query_cursor -= 1;
app.query_input.remove(app.query_cursor);
}
}
KeyCode::Delete => {
if app.query_cursor < app.query_input.len() {
app.query_input.remove(app.query_cursor);
}
}
KeyCode::Left => {
if app.query_cursor > 0 {
app.query_cursor -= 1;
}
}
KeyCode::Right => {
if app.query_cursor < app.query_input.len() {
app.query_cursor += 1;
}
}
KeyCode::Home => {
app.query_cursor = 0;
}
KeyCode::End => {
app.query_cursor = app.query_input.len();
}
_ => {}
}
}
fn contains(area: Rect, x: u16, y: u16) -> bool {
area.width > 0
&& area.height > 0
&& x >= area.x
&& x < area.x + area.width
&& y >= area.y
&& y < area.y + area.height
}
pub fn handle_mouse(app: &mut App, mouse: MouseEvent) -> bool {
if app.mode != AppMode::Normal {
return false;
}
let (x, y) = (mouse.column, mouse.row);
let in_sidebar = contains(app.sidebar_area, x, y);
let in_data = contains(app.data_area, x, y);
let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
match mouse.kind {
MouseEventKind::ScrollUp if in_data && shift => {
app.pan_columns(-1);
true
}
MouseEventKind::ScrollDown if in_data && shift => {
app.pan_columns(1);
true
}
MouseEventKind::ScrollUp if in_sidebar => {
app.move_sidebar(-1);
true
}
MouseEventKind::ScrollDown if in_sidebar => {
app.move_sidebar(1);
true
}
MouseEventKind::ScrollUp if in_data => {
app.move_row(-WHEEL_ROWS);
true
}
MouseEventKind::ScrollDown if in_data => {
app.move_row(WHEEL_ROWS);
true
}
MouseEventKind::ScrollLeft if in_data => {
app.pan_columns(-1);
true
}
MouseEventKind::ScrollRight if in_data => {
app.pan_columns(1);
true
}
MouseEventKind::Down(MouseButton::Left) if in_sidebar => {
click_sidebar(app, y);
true
}
MouseEventKind::Down(MouseButton::Left) if in_data => {
click_data(app, x, y);
true
}
_ => false,
}
}
fn click_sidebar(app: &mut App, y: u16) {
app.active_panel = Panel::Sidebar;
let Some(row) = y.checked_sub(app.sidebar_area.y + 1) else {
return;
};
let rendered = row as usize + app.sidebar_state.offset();
if let Some(index) = app.sidebar_index_from_render(rendered) {
app.select_sidebar(index);
}
}
fn click_data(app: &mut App, x: u16, y: u16) {
app.active_panel = Panel::Data;
let hit = app
.table_view
.as_ref()
.and_then(|tv| tv.col_spans.iter().find(|(_, cx, w)| x >= *cx && x < cx + w))
.map(|(i, _, _)| *i);
if let Some(col) = hit {
app.select_col(col);
}
let Some(row) = y.checked_sub(app.data_area.y + 2) else {
return;
};
let offset = app
.table_view
.as_ref()
.map(|tv| tv.table_state.offset())
.unwrap_or(0);
app.select_row(row as usize + offset);
}