use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::core::models::DatabaseType;
use crate::ui::state::{AppState, LeafKind, Overlay, TreeNode};
use crate::ui::tabs::TabKind;
use super::Action;
use super::overlays::{handle_group_create, handle_group_rename};
pub(super) fn handle_filter_key(state: &mut AppState) -> Action {
if let Some(idx) = state.selected_tree_index() {
let conn_prefix = state.connection_for_tree_idx(idx).unwrap_or("").to_string();
match &state.sidebar.tree[idx] {
TreeNode::Group { .. } => {}
TreeNode::Connection { .. } | TreeNode::Schema { .. } => {
let schemas = state.schema_names_for_conn(&conn_prefix);
if !schemas.is_empty() {
let key = format!("{conn_prefix}::schemas");
state.sidebar.object_filter.open_for(&key, schemas);
state.overlay = Some(Overlay::ObjectFilter);
}
}
TreeNode::Category { schema, kind, .. } => {
let base_key = kind.filter_key(schema);
let key = format!("{conn_prefix}::{base_key}");
let items = state.leaves_under_category(idx);
if !items.is_empty() {
state.sidebar.object_filter.open_for(&key, items);
state.overlay = Some(Overlay::ObjectFilter);
}
}
TreeNode::Empty => {}
TreeNode::Leaf { schema, kind, .. } => {
let base_key = match kind {
LeafKind::Table => format!("{schema}.Tables"),
LeafKind::View => format!("{schema}.Views"),
LeafKind::MaterializedView => format!("{schema}.MaterializedViews"),
LeafKind::Index => format!("{schema}.Indexes"),
LeafKind::Sequence => format!("{schema}.Sequences"),
LeafKind::Type => format!("{schema}.Types"),
LeafKind::Trigger => format!("{schema}.Triggers"),
LeafKind::Package => format!("{schema}.Packages"),
LeafKind::Procedure => format!("{schema}.Procedures"),
LeafKind::Function => format!("{schema}.Functions"),
LeafKind::Event => format!("{schema}.Events"),
};
let cat_key = format!("{conn_prefix}::{base_key}");
let mut walk = idx;
while walk > 0 {
walk -= 1;
if matches!(&state.sidebar.tree[walk], TreeNode::Category { .. }) {
let items = state.leaves_under_category(walk);
if !items.is_empty() {
state.sidebar.object_filter.open_for(&cat_key, items);
state.overlay = Some(Overlay::ObjectFilter);
}
break;
}
}
}
}
} else if !state.sidebar.tree.is_empty() {
let schemas = state.all_schema_names();
if !schemas.is_empty() {
state.sidebar.object_filter.open_for("schemas", schemas);
state.overlay = Some(Overlay::ObjectFilter);
}
}
Action::Render
}
pub(super) fn handle_sidebar_search(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.sidebar.tree_state.search_active = false;
state.sidebar.tree_state.search_query.clear();
state.sidebar.tree_state.search_matches.clear();
Action::Render
}
KeyCode::Enter => {
state.sidebar.tree_state.search_active = false;
Action::Render
}
KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let count = state.visible_tree().len();
state.sidebar.tree_state.next_match(count);
Action::Render
}
KeyCode::Backspace => {
state.sidebar.tree_state.search_query.pop();
update_search_and_jump(state);
Action::Render
}
KeyCode::Char(c) => {
state.sidebar.tree_state.search_query.push(c);
update_search_and_jump(state);
Action::Render
}
_ => Action::None,
}
}
pub(super) fn update_search_and_jump(state: &mut AppState) {
let query = state.sidebar.tree_state.search_query.to_lowercase();
let visible = state.visible_tree();
let mut matches = Vec::new();
for (vis_idx, (_, node, _)) in visible.iter().enumerate() {
if !query.is_empty() && node.display_name().to_lowercase().contains(&query) {
matches.push(vis_idx);
}
}
let count = visible.len();
drop(visible);
state.sidebar.tree_state.search_matches = matches;
state.sidebar.tree_state.search_match_idx = 0;
if let Some(&first) = state.sidebar.tree_state.search_matches.first() {
state.sidebar.tree_state.cursor = first;
state.sidebar.tree_state.center_scroll(count);
}
}
pub(super) fn handle_sidebar(state: &mut AppState, key: KeyEvent) -> Action {
if state.dialogs.group_renaming.is_some() {
return handle_group_rename(state, key);
}
if state.dialogs.group_creating {
return handle_group_create(state, key);
}
let visible_count = state.visible_tree().len();
if visible_count == 0 {
return Action::None;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('d') => {
state.sidebar.tree_state.half_page_down(visible_count);
return Action::Render;
}
KeyCode::Char('u') => {
state.sidebar.tree_state.half_page_up(visible_count);
return Action::Render;
}
_ => {}
}
}
if key.code != KeyCode::Char('d') {
state.sidebar.tree_state.pending_d = false;
}
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
state.sidebar.tree_state.move_down(visible_count);
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.sidebar.tree_state.move_up();
Action::Render
}
KeyCode::Char('g') => {
state.sidebar.tree_state.go_top();
Action::Render
}
KeyCode::Char('G') => {
state.sidebar.tree_state.go_bottom(visible_count);
Action::Render
}
KeyCode::Char('l') | KeyCode::Enter => {
if let Some(idx) = state.selected_tree_index() {
handle_tree_action(state, idx)
} else {
Action::None
}
}
KeyCode::Char('h') | KeyCode::Left => {
if let Some(idx) = state.selected_tree_index() {
if state.sidebar.tree[idx].is_expanded() {
state.sidebar.tree[idx].toggle_expand();
} else {
let child_depth = state.sidebar.tree[idx].depth();
if child_depth > 0 {
let mut walk = idx;
while walk > 0 {
walk -= 1;
if state.sidebar.tree[walk].depth() < child_depth {
if state.sidebar.tree[walk].is_expanded() {
state.sidebar.tree[walk].toggle_expand();
}
let vis_info = {
let visible = state.visible_tree();
visible
.iter()
.position(|(vi, _, _)| *vi == walk)
.map(|p| (p, visible.len()))
};
if let Some((vis_pos, vis_len)) = vis_info {
state.sidebar.tree_state.cursor = vis_pos;
state.sidebar.tree_state.adjust_scroll(vis_len);
}
break;
}
}
}
}
}
Action::Render
}
KeyCode::Char('d') => {
if state.sidebar.tree_state.pending_d {
state.sidebar.tree_state.pending_d = false;
if let Some(idx) = state.selected_tree_index() {
match &state.sidebar.tree[idx] {
TreeNode::Connection { name, .. } => {
state.overlay =
Some(Overlay::ConfirmDeleteConnection { name: name.clone() });
return Action::Render;
}
TreeNode::Leaf {
name, schema, kind, ..
} if matches!(
kind,
LeafKind::Table | LeafKind::View | LeafKind::Package
) =>
{
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
LeafKind::Package => "PACKAGE",
_ => unreachable!(),
};
let conn_name = find_conn_name_for(state, idx);
state.sidebar.pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: schema.clone(),
name: name.clone(),
obj_type: obj_type.to_string(),
conn_name,
});
state.overlay = Some(Overlay::ConfirmDropObject);
return Action::Render;
}
_ => {}
}
}
Action::Render
} else {
state.sidebar.tree_state.pending_d = true;
Action::Render
}
}
KeyCode::Char('r') => {
if let Some(idx) = state.selected_tree_index() {
match &state.sidebar.tree[idx] {
TreeNode::Connection { name, .. } => {
state.sidebar.rename_buf = name.clone();
state.sidebar.pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: String::new(),
name: name.clone(),
obj_type: "CONNECTION".to_string(),
conn_name: name.clone(),
});
state.overlay = Some(Overlay::RenameObject);
return Action::Render;
}
TreeNode::Leaf {
name, schema, kind, ..
} if matches!(kind, LeafKind::Table | LeafKind::View) => {
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
_ => unreachable!(),
};
let conn_name = find_conn_name_for(state, idx);
state.sidebar.rename_buf = name.clone();
state.sidebar.pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: schema.clone(),
name: name.clone(),
obj_type: obj_type.to_string(),
conn_name,
});
state.overlay = Some(Overlay::RenameObject);
return Action::Render;
}
_ => {}
}
}
Action::Render
}
KeyCode::Char('y') => {
if let Some(idx) = state.selected_tree_index() {
let mut walk = idx;
loop {
if let TreeNode::Connection { name, .. } = &state.sidebar.tree[walk] {
state.sidebar.yank_conn = Some(name.clone());
state.status_message = format!("Yanked connection: {name}");
break;
}
if walk == 0 {
break;
}
walk -= 1;
}
}
Action::Render
}
KeyCode::Char('p') => {
if let Some(ref source) = state.sidebar.yank_conn.clone() {
let group = if let Some(idx) = state.selected_tree_index() {
let mut walk = idx;
loop {
if let TreeNode::Group { name, .. } = &state.sidebar.tree[walk] {
break name.clone();
}
if walk == 0 {
break "Default".to_string();
}
walk -= 1;
}
} else {
"Default".to_string()
};
return Action::DuplicateConnection {
source_name: source.clone(),
target_group: group,
};
}
Action::Render
}
KeyCode::Char('o') | KeyCode::Char('i') => {
if let Some(idx) = state.selected_tree_index() {
match &state.sidebar.tree[idx] {
TreeNode::Connection { .. } | TreeNode::Group { .. } => {
state.dialogs.connection_form =
crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
return Action::Render;
}
TreeNode::Category { schema, kind, .. } => {
let obj_type = match kind {
crate::ui::state::CategoryKind::Tables => "TABLE",
crate::ui::state::CategoryKind::Views => "VIEW",
crate::ui::state::CategoryKind::Packages => "PACKAGE",
_ => return Action::Render,
};
let conn_name = find_conn_name_for(state, idx);
return Action::CreateFromTemplate {
conn_name,
schema: schema.clone(),
obj_type: obj_type.to_string(),
};
}
TreeNode::Leaf { schema, kind, .. } => {
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
LeafKind::Package => "PACKAGE",
_ => return Action::Render,
};
let conn_name = find_conn_name_for(state, idx);
return Action::CreateFromTemplate {
conn_name,
schema: schema.clone(),
obj_type: obj_type.to_string(),
};
}
_ => {
state.dialogs.connection_form =
crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
return Action::Render;
}
}
} else {
state.dialogs.connection_form = crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
}
Action::Render
}
KeyCode::Char('m') => {
if let Some(idx) = state.selected_tree_index() {
if let TreeNode::Group { name, .. } = &state.sidebar.tree[idx] {
let group_name = name.clone();
let has_children = idx + 1 < state.sidebar.tree.len()
&& state.sidebar.tree[idx + 1].depth() > state.sidebar.tree[idx].depth();
state.dialogs.group_menu.group_name = group_name;
state.dialogs.group_menu.cursor = 0;
state.dialogs.group_menu.is_empty = !has_children;
state.overlay = Some(Overlay::GroupMenu);
return Action::Render;
}
let mut walk = idx;
loop {
if let TreeNode::Connection { name, status, .. } = &state.sidebar.tree[walk] {
let conn_name = name.clone();
state.dialogs.conn_menu.conn_name = conn_name;
state.dialogs.conn_menu.cursor = 0;
state.dialogs.conn_menu.is_connected =
*status == crate::ui::state::ConnStatus::Connected;
state.overlay = Some(Overlay::ConnectionMenu);
return Action::Render;
}
if walk == 0 {
break;
}
walk -= 1;
}
}
Action::None
}
KeyCode::Char('/') => {
state.sidebar.tree_state.search_active = true;
state.sidebar.tree_state.search_query.clear();
state.sidebar.tree_state.search_matches.clear();
Action::Render
}
_ => Action::None,
}
}
pub(super) fn find_conn_name_for(state: &AppState, mut idx: usize) -> String {
loop {
if let TreeNode::Connection { name, .. } = &state.sidebar.tree[idx] {
return name.clone();
}
if idx == 0 {
break;
}
idx -= 1;
}
state.conn.name.clone().unwrap_or_default()
}
pub(super) fn handle_tree_action(state: &mut AppState, idx: usize) -> Action {
if idx >= state.sidebar.tree.len() {
return Action::None;
}
let node = &state.sidebar.tree[idx];
match node {
TreeNode::Connection { expanded, name, .. } if !expanded => {
let conn_name = name.clone();
state.sidebar.tree[idx].toggle_expand();
Action::LoadSchemas { conn_name }
}
TreeNode::Schema { expanded, name, .. } if !expanded => {
let schema = name.clone();
state.sidebar.tree[idx].toggle_expand();
let has_children = idx + 1 < state.sidebar.tree.len()
&& state.sidebar.tree[idx + 1].depth() > state.sidebar.tree[idx].depth();
if !has_children {
insert_categories(state, idx, &schema);
}
Action::Render
}
TreeNode::Category {
expanded,
schema,
label,
..
} if !expanded => {
let schema = schema.clone();
let label = label.clone();
state.sidebar.tree[idx].toggle_expand();
Action::LoadChildren {
schema,
kind: label,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Table | LeafKind::View | LeafKind::MaterializedView,
..
} => {
let schema = schema.clone();
let table = name.clone();
let conn_name = find_conn_name_for(state, idx);
state.conn.current_schema = Some(schema.clone());
let tab_id = state.open_or_focus_tab(TabKind::Table {
conn_name,
schema: schema.clone(),
table: table.clone(),
});
Action::LoadTableData {
tab_id,
schema,
table,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Package,
..
} => {
let schema = schema.clone();
let pkg_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Package {
conn_name,
schema: schema.clone(),
name: pkg_name.clone(),
});
Action::LoadPackageContent {
tab_id,
schema,
name: pkg_name,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Function,
..
} => {
let schema = schema.clone();
let func_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Function {
conn_name,
schema: schema.clone(),
name: func_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: func_name,
obj_type: "FUNCTION".to_string(),
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Procedure,
..
} => {
let schema = schema.clone();
let proc_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Procedure {
conn_name,
schema: schema.clone(),
name: proc_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: proc_name,
obj_type: "PROCEDURE".to_string(),
}
}
TreeNode::Leaf {
schema, name, kind, ..
} if matches!(kind, LeafKind::Index | LeafKind::Sequence | LeafKind::Event) => {
let schema = schema.clone();
let obj_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let obj_type = match kind {
LeafKind::Index => "INDEX",
LeafKind::Sequence => "SEQUENCE",
LeafKind::Event => "EVENT",
_ => unreachable!(),
};
let tab_id = state.open_or_focus_tab(TabKind::Function {
conn_name,
schema: schema.clone(),
name: obj_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: obj_name,
obj_type: obj_type.to_string(),
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Type,
..
} => {
let schema = schema.clone();
let type_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::DbType {
conn_name,
schema: schema.clone(),
name: type_name.clone(),
});
Action::LoadTypeInfo {
tab_id,
schema,
name: type_name,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Trigger,
..
} => {
let schema = schema.clone();
let trigger_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Trigger {
conn_name,
schema: schema.clone(),
name: trigger_name.clone(),
});
Action::LoadTriggerInfo {
tab_id,
schema,
name: trigger_name,
}
}
_ => {
state.sidebar.tree[idx].toggle_expand();
Action::Render
}
}
}
pub(super) fn insert_categories(state: &mut AppState, parent_idx: usize, schema: &str) {
use crate::ui::state::CategoryKind;
let categories: Vec<(&str, CategoryKind)> = match state.conn.db_type {
Some(DatabaseType::Oracle) => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Materialized Views", CategoryKind::MaterializedViews),
("Indexes", CategoryKind::Indexes),
("Sequences", CategoryKind::Sequences),
("Types", CategoryKind::Types),
("Triggers", CategoryKind::Triggers),
("Packages", CategoryKind::Packages),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
Some(DatabaseType::MySQL) => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Indexes", CategoryKind::Indexes),
("Triggers", CategoryKind::Triggers),
("Events", CategoryKind::Events),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
Some(DatabaseType::PostgreSQL) | None => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Materialized Views", CategoryKind::MaterializedViews),
("Indexes", CategoryKind::Indexes),
("Sequences", CategoryKind::Sequences),
("Triggers", CategoryKind::Triggers),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
};
let insert_pos = parent_idx + 1;
for (i, (label, kind)) in categories.into_iter().enumerate() {
state.sidebar.tree.insert(
insert_pos + i,
TreeNode::Category {
label: label.to_string(),
schema: schema.to_string(),
kind,
expanded: false,
},
);
}
}