use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::ui::state::{AppState, Mode, Overlay};
use crate::ui::tabs::{CellEdit, RowChange, TabKind, WorkspaceTab};
use super::Action;
pub(super) fn handle_tab_data_grid(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let is_script = matches!(state.tabs[tab_idx].kind, TabKind::Script { .. });
if is_script {
let tab = &mut state.tabs[tab_idx];
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
let rt = &tab.result_tabs[idx];
tab.query_result = Some(rt.result.clone());
tab.grid_scroll_row = rt.scroll_row;
tab.grid_selected_row = rt.selected_row;
tab.grid_selected_col = rt.selected_col;
tab.grid_selection_anchor = rt.selection_anchor;
}
match key.code {
KeyCode::Char('}') => {
if tab.result_tabs.len() > 1 {
sync_grid_to_result_tab(tab);
tab.active_result_idx = (tab.active_result_idx + 1) % tab.result_tabs.len();
}
return Action::Render;
}
KeyCode::Char('{') => {
if tab.result_tabs.len() > 1 {
sync_grid_to_result_tab(tab);
tab.active_result_idx = if tab.active_result_idx == 0 {
tab.result_tabs.len() - 1
} else {
tab.active_result_idx - 1
};
}
return Action::Render;
}
_ => {}
}
}
let tab = &mut state.tabs[tab_idx];
if tab.grid_editing.is_some() && state.mode == Mode::Insert {
return handle_grid_cell_edit(state, key);
}
if tab.grid_error_editor.is_some() && key.code == KeyCode::Esc {
tab.grid_error_editor = None;
tab.grid_query_editor = None;
return Action::Render;
}
let is_table_tab = matches!(tab.kind, TabKind::Table { .. });
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let vh = tab.grid_visible_height.max(1);
let visual = tab.grid_visual_mode;
let action = match key.code {
KeyCode::Char('i') if is_table_tab && !visual => {
if row_count > 0 && col_count > 0 {
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
}
return Action::Render;
}
KeyCode::Char('o') if is_table_tab && !visual => {
if let Some(ref mut qr) = tab.query_result {
let new_row: Vec<String> = qr.columns.iter().map(|_| "NULL".to_string()).collect();
let insert_pos = (tab.grid_selected_row + 1).min(qr.rows.len());
qr.rows.insert(insert_pos, new_row.clone());
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k >= insert_pos {
shifted.insert(k + 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
tab.grid_changes
.insert(insert_pos, RowChange::New { values: new_row });
tab.grid_selected_row = insert_pos;
tab.grid_selected_col = 0;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh - 1);
}
}
return Action::Render;
}
KeyCode::Char('d')
if is_table_tab && !visual && !key.modifiers.contains(KeyModifiers::CONTROL) =>
{
if state.pending_d {
state.pending_d = false;
if row_count > 0 {
let row = tab.grid_selected_row;
if matches!(tab.grid_changes.get(&row), Some(RowChange::New { .. })) {
tab.grid_changes.remove(&row);
if let Some(ref mut qr) = tab.query_result
&& row < qr.rows.len()
{
qr.rows.remove(row);
}
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k > row {
shifted.insert(k - 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
let new_count =
tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if tab.grid_selected_row >= new_count && new_count > 0 {
tab.grid_selected_row = new_count - 1;
}
} else {
tab.grid_changes.insert(row, RowChange::Deleted);
}
}
return Action::Render;
} else {
state.pending_d = true;
return Action::Render;
}
}
KeyCode::Char('u')
if is_table_tab && !visual && !key.modifiers.contains(KeyModifiers::CONTROL) =>
{
if !tab.grid_changes.is_empty() {
tab.grid_changes.clear();
state.status_message = "Changes discarded".to_string();
return Action::ReloadTableData;
}
return Action::Render;
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) && is_table_tab => {
if !tab.grid_changes.is_empty() {
let modified = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Modified { .. }))
.count();
let new = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::New { .. }))
.count();
let deleted = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Deleted))
.count();
state.status_message =
format!("Save: {modified} modified, {new} new, {deleted} deleted — y/n?");
state.overlay = Some(Overlay::SaveGridChanges);
} else {
state.status_message = "No pending changes".to_string();
}
return Action::Render;
}
KeyCode::Char('v') => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
} else {
tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((tab.grid_selected_row, tab.grid_selected_col));
}
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
if tab.grid_on_header {
tab.grid_on_header = false;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
if tab.grid_on_header {
} else if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
} else {
tab.grid_on_header = true;
}
Action::Render
}
KeyCode::Char('h') | KeyCode::Left => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
}
Action::Render
}
KeyCode::Char('l') | KeyCode::Right => {
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
}
Action::Render
}
KeyCode::Char('e') => {
if col_count > 0 {
if tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
}
Action::Render
}
KeyCode::Char('b') => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
} else if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
tab.grid_selected_col = col_count.saturating_sub(1);
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
}
Action::Render
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = vh / 2;
tab.grid_selected_row = (tab.grid_selected_row + half).min(row_count.saturating_sub(1));
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = vh / 2;
tab.grid_selected_row = tab.grid_selected_row.saturating_sub(half);
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
KeyCode::Char('g') => {
tab.grid_selected_row = 0;
tab.grid_selected_col = 0;
tab.grid_scroll_row = 0;
tab.grid_on_header = true;
Action::Render
}
KeyCode::Char('G') => {
tab.grid_on_header = false;
if row_count > 0 {
tab.grid_selected_row = row_count - 1;
tab.grid_scroll_row = row_count.saturating_sub(vh);
}
Action::Render
}
KeyCode::Char('y') => {
grid_yank(tab);
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
Action::Render
}
KeyCode::Esc => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
} else {
tab.grid_focused = false;
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
}
Action::Render
}
_ => Action::None,
};
if matches!(key.code, KeyCode::Char('y')) {
state.status_message = "Copied to clipboard".to_string();
}
if is_script {
let tab = &mut state.tabs[tab_idx];
sync_grid_to_result_tab(tab);
}
action
}
pub(super) fn handle_grid_cell_edit(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
match key.code {
KeyCode::Esc | KeyCode::Enter => {
if let Some((row, col)) = tab.grid_editing.take() {
let new_val = if tab.grid_edit_buffer.is_empty() {
"NULL".to_string()
} else {
tab.grid_edit_buffer.clone()
};
let original = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
if let Some(ref mut qr) = tab.query_result
&& let Some(r) = qr.rows.get_mut(row)
&& let Some(cell) = r.get_mut(col)
{
*cell = new_val.clone();
}
if new_val != original {
match tab.grid_changes.get_mut(&row) {
Some(RowChange::Modified { edits }) => {
if let Some(e) = edits.iter_mut().find(|e| e.col == col) {
e.value = new_val;
} else {
edits.push(CellEdit {
col,
original,
value: new_val,
});
}
}
Some(RowChange::New { values }) => {
if let Some(v) = values.get_mut(col) {
*v = new_val;
}
}
Some(RowChange::Deleted) => {}
None => {
tab.grid_changes.insert(
row,
RowChange::Modified {
edits: vec![CellEdit {
col,
original,
value: new_val,
}],
},
);
}
}
}
}
tab.grid_edit_buffer.clear();
tab.grid_edit_cursor = 0;
state.mode = Mode::Normal;
Action::Render
}
KeyCode::Tab => {
let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let _ = handle_grid_cell_edit(state, esc_event);
let tab = &mut state.tabs[tab_idx];
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
}
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
Action::Render
}
KeyCode::Backspace => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Delete => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Left => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
}
Action::Render
}
KeyCode::Right => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_cursor += 1;
}
Action::Render
}
KeyCode::Home => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = 0;
Action::Render
}
KeyCode::End => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = tab.grid_edit_buffer.len();
Action::Render
}
KeyCode::Char(c) => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_buffer.insert(tab.grid_edit_cursor, c);
tab.grid_edit_cursor += 1;
Action::Render
}
_ => Action::None,
}
}
pub(super) fn handle_table_error_editor(
state: &mut AppState,
key: KeyEvent,
is_query: bool,
) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
if key.code == KeyCode::Esc {
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_ref()
} else {
tab.grid_error_editor.as_ref()
};
let in_normal =
editor.is_some_and(|e| matches!(e.mode, vimltui::VimMode::Normal) && !e.search.active);
if in_normal {
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('l') | KeyCode::Right => {
if !is_query {
let tab = &mut state.tabs[tab_idx];
if tab.grid_query_editor.is_some() {
tab.sub_focus = crate::ui::tabs::SubFocus::QueryView;
}
}
return Action::Render;
}
KeyCode::Char('h') | KeyCode::Left => {
if is_query {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Results;
}
return Action::Render;
}
KeyCode::Char('k') | KeyCode::Up => {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
_ => {}
}
}
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_mut()
} else {
tab.grid_error_editor.as_mut()
};
if let Some(ed) = editor {
let _ = ed.handle_key(key);
}
Action::Render
}
pub(super) fn handle_save_grid_confirm(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
state.overlay = None;
Action::SaveGridChanges
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.overlay = None;
state.status_message = "Save cancelled".to_string();
Action::Render
}
_ => Action::None,
}
}
pub(super) fn sync_grid_to_result_tab(tab: &mut WorkspaceTab) {
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
tab.result_tabs[idx].scroll_row = tab.grid_scroll_row;
tab.result_tabs[idx].selected_row = tab.grid_selected_row;
tab.result_tabs[idx].selected_col = tab.grid_selected_col;
tab.result_tabs[idx].selection_anchor = tab.grid_selection_anchor;
}
}
pub(super) fn grid_yank(tab: &WorkspaceTab) {
let result = match &tab.query_result {
Some(r) => r,
None => return,
};
if tab.grid_on_header {
let vals: Vec<&str> = result.columns.iter().map(|c| c.as_str()).collect();
let text = vals.join(" ");
copy_to_clipboard(&text);
return;
}
if result.rows.is_empty() {
return;
}
let (sr, sc, er, ec) = match tab.grid_selection_anchor {
Some((ar, ac)) => {
let r1 = ar.min(tab.grid_selected_row);
let r2 = ar.max(tab.grid_selected_row);
let c1 = ac.min(tab.grid_selected_col);
let c2 = ac.max(tab.grid_selected_col);
(r1, c1, r2, c2)
}
None => {
let col_count = result.columns.len().saturating_sub(1);
(tab.grid_selected_row, 0, tab.grid_selected_row, col_count)
}
};
let mut text = String::new();
for row_idx in sr..=er {
if let Some(row_data) = result.rows.get(row_idx) {
if !text.is_empty() {
text.push('\n');
}
let vals: Vec<&str> = (sc..=ec)
.filter_map(|c| row_data.get(c).map(|v| v.as_str()))
.collect();
text.push_str(&vals.join(" "));
}
}
if !text.is_empty() {
copy_to_clipboard(&text);
}
}
pub(super) fn copy_to_clipboard(text: &str) {
use std::io::Write;
let b64 = simple_base64_encode(text.as_bytes());
let osc = format!("\x1b]52;c;{b64}\x07");
let _ = std::io::stdout().write_all(osc.as_bytes());
let _ = std::io::stdout().flush();
let cmds: &[(&str, &[&str])] = &[
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
];
for (cmd, args) in cmds {
if let Ok(mut child) = std::process::Command::new(cmd)
.args(*args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
return;
}
}
}
fn simple_base64_encode(input: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
out.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(CHARS[(n & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}