use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::core::models::DatabaseType;
use crate::keybindings::Context;
use crate::ui::state::{AppState, LeafKind, Overlay, TreeNode};
use crate::ui::tabs::TabKind;
use super::Action;
use super::overlays::{handle_conn_rename, 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);
}
if state.dialogs.conn_renaming.is_some() {
return handle_conn_rename(state, key);
}
let visible_count = state.visible_tree().len();
if visible_count == 0 {
if state.bindings.matches(Context::Sidebar, "create_new", &key) {
state.dialogs.group_creating = true;
state.dialogs.group_rename_buf.clear();
return Action::Render;
}
return Action::None;
}
if state
.bindings
.matches(Context::Sidebar, "half_page_down", &key)
{
state.sidebar.tree_state.half_page_down(visible_count);
return Action::Render;
}
if state
.bindings
.matches(Context::Sidebar, "half_page_up", &key)
{
state.sidebar.tree_state.half_page_up(visible_count);
return Action::Render;
}
if !state
.bindings
.matches(Context::Sidebar, "delete_pending", &key)
{
state.sidebar.tree_state.pending_d = false;
}
let b = &state.bindings;
if b.matches(Context::Sidebar, "scroll_down", &key) {
state.sidebar.tree_state.move_down(visible_count);
return Action::Render;
}
if b.matches(Context::Sidebar, "scroll_up", &key) {
state.sidebar.tree_state.move_up();
return Action::Render;
}
if b.matches(Context::Sidebar, "scroll_top", &key) {
state.sidebar.tree_state.go_top();
return Action::Render;
}
if b.matches(Context::Sidebar, "scroll_bottom", &key) {
state.sidebar.tree_state.go_bottom(visible_count);
return Action::Render;
}
if b.matches(Context::Sidebar, "expand_or_open", &key) {
return if let Some(idx) = state.selected_tree_index() {
handle_tree_action(state, idx)
} else {
Action::None
};
}
if b.matches(Context::Sidebar, "start_search", &key) {
state.sidebar.tree_state.search_active = true;
state.sidebar.tree_state.search_query.clear();
state.sidebar.tree_state.search_matches.clear();
return Action::Render;
}
if state
.bindings
.matches(Context::Sidebar, "collapse_or_parent", &key)
{
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;
}
}
}
}
}
return Action::Render;
}
match key.code {
_ if state
.bindings
.matches(Context::Sidebar, "delete_pending", &key) =>
{
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
}
}
_ if state
.bindings
.matches(Context::Sidebar, "rename_or_refresh", &key) =>
{
if let Some(idx) = state.selected_tree_index() {
match &state.sidebar.tree[idx] {
TreeNode::Group { name, .. } => {
state.dialogs.group_renaming = Some(name.clone());
state.dialogs.group_rename_buf = name.clone();
return Action::Render;
}
TreeNode::Connection { name, .. } => {
state.dialogs.conn_renaming = Some(name.clone());
state.dialogs.conn_rename_buf = name.clone();
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;
}
TreeNode::Category { schema, label, .. } => {
let schema = schema.clone();
let label = label.clone();
let depth = state.sidebar.tree[idx].depth();
let mut end = idx + 1;
while end < state.sidebar.tree.len()
&& state.sidebar.tree[end].depth() > depth
{
end += 1;
}
if end > idx + 1 {
state.sidebar.tree.drain(idx + 1..end);
}
state.status_message = format!("Refreshing {label}...");
return Action::LoadChildren {
schema,
kind: label,
};
}
TreeNode::Schema {
name: schema_name, ..
} => {
let schema = schema_name.clone();
let depth = state.sidebar.tree[idx].depth();
let mut categories: Vec<String> = Vec::new();
let mut i = idx + 1;
while i < state.sidebar.tree.len() && state.sidebar.tree[i].depth() > depth
{
if let TreeNode::Category {
label, expanded, ..
} = &state.sidebar.tree[i]
&& *expanded
{
categories.push(label.clone());
}
i += 1;
}
for label in &categories {
if let Some(cat_idx) = state.sidebar.tree.iter().position(|n| {
matches!(n,
TreeNode::Category { schema: s, label: l, .. }
if s == &schema && l == label)
}) {
let cdepth = state.sidebar.tree[cat_idx].depth();
let mut cend = cat_idx + 1;
while cend < state.sidebar.tree.len()
&& state.sidebar.tree[cend].depth() > cdepth
{
cend += 1;
}
if cend > cat_idx + 1 {
state.sidebar.tree.drain(cat_idx + 1..cend);
}
}
}
state.status_message = format!("Refreshing schema {schema}...");
return Action::RefreshSchema {
schema,
kinds: categories,
};
}
_ => {}
}
}
Action::Render
}
_ if state
.bindings
.matches(Context::Sidebar, "yank_pending", &key) =>
{
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
}
_ if state.bindings.matches(Context::Sidebar, "paste", &key) => {
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
}
_ if state.bindings.matches(Context::Sidebar, "create_new", &key) => {
if let Some(idx) = state.selected_tree_index() {
match &state.sidebar.tree[idx] {
TreeNode::Group { expanded, .. } => {
if *expanded {
let group_name =
if let TreeNode::Group { name, .. } = &state.sidebar.tree[idx] {
name.clone()
} else {
"Default".to_string()
};
state.dialogs.connection_form =
crate::ui::state::ConnectionFormState::new();
state.dialogs.connection_form.group = group_name;
state.dialogs.connection_form.group_options = state.available_groups();
state.overlay = Some(Overlay::ConnectionDialog);
} else {
state.dialogs.group_creating = true;
state.dialogs.group_rename_buf.clear();
}
return Action::Render;
}
TreeNode::Connection { .. } => {
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.group_creating = true;
state.dialogs.group_rename_buf.clear();
}
Action::Render
}
_ if state.bindings.matches(Context::Sidebar, "group_menu", &key) => {
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
}
_ => 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,
},
);
}
}