use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::ui::state::{AppState, ExportField, ImportField};
use crate::ui::theme::Theme;
pub(super) fn render_confirm_delete_connection(
frame: &mut Frame,
theme: &Theme,
area: Rect,
conn_name: &str,
) {
let width = 50_u16;
let height = 5_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Delete Connection ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.error_fg))
.style(Style::default().bg(theme.dialog_bg));
let text = vec![
Line::from(""),
Line::from(vec![
Span::raw(" Delete "),
Span::styled(
conn_name,
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
),
Span::raw("? "),
Span::styled(
"y",
Style::default()
.fg(theme.error_fg)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default()
.fg(theme.conn_connected)
.add_modifier(Modifier::BOLD),
),
]),
];
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(text).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_confirm_close(frame: &mut Frame, theme: &Theme, area: Rect) {
let width = 44_u16;
let height = 5_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Unsaved Changes ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.conn_connecting))
.style(Style::default().bg(theme.dialog_bg));
let text = vec![
Line::from(""),
Line::from(vec![
Span::raw(" Save before closing? "),
Span::styled(
"y",
Style::default()
.fg(theme.conn_connected)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default()
.fg(theme.error_fg)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled("Esc", Style::default().fg(theme.dim)),
]),
];
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(text).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_confirm_quit(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
let unsaved: Vec<String> = state
.tabs
.iter()
.filter(|t| {
t.editor.as_ref().is_some_and(|e| e.modified)
|| t.body_editor.as_ref().is_some_and(|e| e.modified)
|| t.decl_editor.as_ref().is_some_and(|e| e.modified)
})
.map(|t| {
format!(
" {} {} ({})",
t.kind.icon(),
t.kind.display_name(),
t.kind.kind_label()
)
})
.collect();
let list_height = unsaved.len() as u16;
let width = 50_u16;
let height = 3 + list_height + 2 + 1;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Unsaved Changes ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.conn_connecting))
.style(Style::default().bg(theme.dialog_bg));
let mut lines = vec![Line::from("")];
for name in &unsaved {
lines.push(Line::from(Span::styled(
name.as_str(),
Style::default().fg(theme.error_fg),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::raw(" Quit anyway? "),
Span::styled(
"y",
Style::default()
.fg(theme.conn_connected)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default()
.fg(theme.error_fg)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled("Esc", Style::default().fg(theme.dim)),
]));
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_save_grid_confirm(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
) {
use crate::ui::tabs::RowChange;
let tab = match state.active_tab() {
Some(t) => t,
None => return,
};
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();
let width = 44_u16;
let height = 9_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Save Changes ")
.borders(Borders::ALL)
.border_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.style(Style::default().bg(theme.dialog_bg));
let mut lines = vec![Line::from("")];
if modified > 0 {
lines.push(Line::from(Span::styled(
format!(" {modified} modified row(s)"),
Style::default().fg(Color::Yellow),
)));
}
if new > 0 {
lines.push(Line::from(Span::styled(
format!(" {new} new row(s)"),
Style::default().fg(Color::Green),
)));
}
if deleted > 0 {
lines.push(Line::from(Span::styled(
format!(" {deleted} deleted row(s)"),
Style::default().fg(Color::Red),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::raw(" Save to database? "),
Span::styled(
"y",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
]));
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_confirm_drop(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
let action = match &state.sidebar.pending_action {
Some(a) => a,
None => return,
};
let width = 48_u16;
let height = 7_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Drop Object ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
.style(Style::default().bg(theme.dialog_bg));
let obj_label = format!(" {} {}.{}", action.obj_type, action.schema, action.name);
let lines = vec![
Line::from(""),
Line::from(Span::styled(
obj_label,
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::raw(" Drop? "),
Span::styled(
"y",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
]),
];
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_rename_object(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
let action = match &state.sidebar.pending_action {
Some(a) => a,
None => return,
};
let width = 50_u16;
let height = 7_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(format!(" Rename {} ", action.obj_type))
.borders(Borders::ALL)
.border_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.style(Style::default().bg(theme.dialog_bg));
let old_label = format!(" {}.{} \u{2192}", action.schema, action.name);
let input_text = format!(" {}\u{2588}", state.sidebar.rename_buf);
let lines = vec![
Line::from(""),
Line::from(Span::styled(old_label, Style::default().fg(theme.dim))),
Line::from(Span::styled(
input_text,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_confirm_compile(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
) {
let tab = match state.active_tab() {
Some(t) => t,
None => return,
};
let obj_label = match &tab.kind {
crate::ui::tabs::TabKind::Package { schema, name, .. } => {
format!(" PACKAGE {schema}.{name}")
}
crate::ui::tabs::TabKind::Function { schema, name, .. } => {
format!(" FUNCTION {schema}.{name}")
}
crate::ui::tabs::TabKind::Procedure { schema, name, .. } => {
format!(" PROCEDURE {schema}.{name}")
}
_ => return,
};
let has_decl = tab.decl_editor.as_ref().is_some_and(|e| e.modified);
let has_body = tab.body_editor.as_ref().is_some_and(|e| e.modified);
let has_source = tab.editor.as_ref().is_some_and(|e| e.modified);
let width = 48_u16;
let height = 9_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Compile to Database ")
.borders(Borders::ALL)
.border_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.style(Style::default().bg(theme.dialog_bg));
let mut lines = vec![Line::from("")];
lines.push(Line::from(Span::styled(
obj_label,
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
if has_decl {
lines.push(Line::from(Span::styled(
" \u{270e} Declaration (modified)",
Style::default().fg(Color::Yellow),
)));
}
if has_body {
lines.push(Line::from(Span::styled(
" \u{270e} Body (modified)",
Style::default().fg(Color::Yellow),
)));
}
if has_source {
lines.push(Line::from(Span::styled(
" \u{270e} Source (modified)",
Style::default().fg(Color::Yellow),
)));
}
if !has_decl && !has_body && !has_source {
lines.push(Line::from(Span::styled(
" (no changes detected)",
Style::default().fg(theme.dim),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::raw(" Compile? "),
Span::styled(
"y",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("/"),
Span::styled(
"n",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
]));
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_save_script_name(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
) {
let width = 44_u16;
let height = 5_u16;
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width.min(area.width), height.min(area.height));
let block = Block::default()
.title(" Save Script As ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.conn_connecting))
.style(Style::default().bg(theme.dialog_bg));
let name_buf = state.scripts.save_name.as_deref().unwrap_or("");
let text = vec![
Line::from(""),
Line::from(vec![
Span::raw(" Name: "),
Span::styled(
format!("{name_buf}\u{2588}"),
Style::default()
.fg(theme.conn_connecting)
.add_modifier(Modifier::BOLD),
),
]),
];
frame.render_widget(ratatui::widgets::Clear, popup);
let content = Paragraph::new(text).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_bind_variables(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
) {
let bv = match &state.dialogs.bind_variables {
Some(b) => b,
None => return,
};
let var_count = bv.variables.len();
let width = 50_u16.min(area.width.saturating_sub(4));
let height = (3 + var_count as u16 + 2).min(area.height.saturating_sub(2));
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let popup = Rect::new(x, y, width, height);
let block = Block::default()
.title(" Bind Variables ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.conn_connecting))
.style(Style::default().bg(theme.dialog_bg));
frame.render_widget(ratatui::widgets::Clear, popup);
let inner = block.inner(popup);
frame.render_widget(block, popup);
for (i, (name, value)) in bv.variables.iter().enumerate() {
if i as u16 >= inner.height.saturating_sub(1) {
break;
}
let row_y = inner.y + i as u16;
let is_selected = i == bv.selected_idx;
let label = format!(":{name}");
let display_val = if is_selected {
format!("{value}\u{2588}") } else {
value.clone()
};
let label_style = if is_selected {
Style::default()
.fg(theme.conn_connecting)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.dim)
};
let val_style = if is_selected {
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.status_fg)
};
let line = Line::from(vec![
Span::styled(format!(" {label:<16}"), label_style),
Span::styled(display_val, val_style),
]);
let row_rect = Rect::new(inner.x, row_y, inner.width, 1);
frame.render_widget(Paragraph::new(line), row_rect);
}
let hint_y = inner.y + inner.height.saturating_sub(1);
let hint = Line::from(vec![
Span::styled(" Tab", Style::default().fg(theme.accent)),
Span::styled(" next ", Style::default().fg(theme.dim)),
Span::styled("Enter", Style::default().fg(theme.conn_connected)),
Span::styled(" execute ", Style::default().fg(theme.dim)),
Span::styled("Esc", Style::default().fg(theme.error_fg)),
Span::styled(" cancel", Style::default().fg(theme.dim)),
]);
let hint_rect = Rect::new(inner.x, hint_y, inner.width, 1);
frame.render_widget(Paragraph::new(hint), hint_rect);
}
pub(super) fn render_theme_picker(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
use crate::ui::theme::THEME_NAMES;
let count = THEME_NAMES.len();
let height = (count as u16 + 2).min(area.height);
let width = 30_u16.min(area.width);
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width, height);
frame.render_widget(ratatui::widgets::Clear, popup);
let block = Block::default()
.title(" Theme ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.dialog_bg));
let inner = block.inner(popup);
frame.render_widget(block, popup);
let items: Vec<ratatui::widgets::ListItem> = THEME_NAMES
.iter()
.map(|name| {
let is_current = theme.name == *name;
let icon = if is_current { "\u{25cf} " } else { " " };
ratatui::widgets::ListItem::new(Line::from(vec![
Span::styled(
icon,
Style::default().fg(if is_current {
theme.conn_connected
} else {
theme.dim
}),
),
Span::styled(*name, Style::default().fg(theme.topbar_fg)),
]))
})
.collect();
let mut list_state = ratatui::widgets::ListState::default();
list_state.select(Some(state.dialogs.theme_picker.cursor));
let list = ratatui::widgets::List::new(items)
.highlight_style(
Style::default()
.bg(theme.tree_selected_bg)
.fg(theme.tree_selected_fg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{25b8} ");
frame.render_stateful_widget(list, inner, &mut list_state);
}
pub(super) fn render_export_dialog(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
let dialog = match &state.dialogs.export_dialog {
Some(d) => d,
None => return,
};
let width = 56u16.min(area.width.saturating_sub(4));
let height = 13u16.min(area.height.saturating_sub(2));
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
let dialog_area = Rect::new(x, y, width, height);
let border_color = if dialog.error.is_some() {
theme.error_fg
} else {
theme.border_focused
};
frame.render_widget(ratatui::widgets::Clear, dialog_area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.title(" Export ")
.style(Style::default().bg(theme.dialog_bg));
let inner_block = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let inner = inner_block;
let mut lines = Vec::new();
let focused_fg = theme.border_focused;
let normal_fg = theme.status_fg;
if let Some(ref err) = dialog.error {
lines.push(Line::from(Span::styled(
err.as_str(),
Style::default().fg(theme.error_fg),
)));
}
let path_fg = if dialog.focused == ExportField::Path {
focused_fg
} else {
normal_fg
};
lines.push(Line::from(vec![
Span::styled("Path: ", Style::default().fg(path_fg)),
Span::styled(dialog.path.as_str(), Style::default().fg(path_fg)),
if dialog.focused == ExportField::Path {
Span::styled("\u{2588}", Style::default().fg(path_fg))
} else {
Span::raw("")
},
]));
let cred_fg = if dialog.focused == ExportField::IncludeCredentials {
focused_fg
} else {
normal_fg
};
let cred_val = if dialog.include_credentials {
"[Y] Yes"
} else {
"[N] No "
};
lines.push(Line::from(vec![
Span::styled("Include credentials? ", Style::default().fg(cred_fg)),
Span::styled(
cred_val,
Style::default().fg(cred_fg).add_modifier(Modifier::BOLD),
),
]));
let sp_fg = if dialog.focused == ExportField::ShowPassword {
focused_fg
} else {
normal_fg
};
let sp_val = if dialog.show_password {
"[Y] Yes"
} else {
"[N] No "
};
lines.push(Line::from(vec![
Span::styled("Show password? ", Style::default().fg(sp_fg)),
Span::styled(
sp_val,
Style::default().fg(sp_fg).add_modifier(Modifier::BOLD),
),
]));
let pw_fg = if dialog.focused == ExportField::Password {
focused_fg
} else {
normal_fg
};
let pw_display = if dialog.show_password {
dialog.password.clone()
} else {
"*".repeat(dialog.password.len())
};
lines.push(Line::from(vec![
Span::styled("Password: ", Style::default().fg(pw_fg)),
Span::styled(pw_display, Style::default().fg(pw_fg)),
if dialog.focused == ExportField::Password {
Span::styled("\u{2588}", Style::default().fg(pw_fg))
} else {
Span::raw("")
},
]));
let cf_fg = if dialog.focused == ExportField::Confirm {
focused_fg
} else {
normal_fg
};
let cf_display = if dialog.show_password {
dialog.confirm.clone()
} else {
"*".repeat(dialog.confirm.len())
};
lines.push(Line::from(vec![
Span::styled("Confirm: ", Style::default().fg(cf_fg)),
Span::styled(cf_display, Style::default().fg(cf_fg)),
if dialog.focused == ExportField::Confirm {
Span::styled("\u{2588}", Style::default().fg(cf_fg))
} else {
Span::raw("")
},
]));
lines.push(Line::raw(""));
lines.push(Line::from(vec![
Span::styled("[Enter] Export ", Style::default().fg(normal_fg)),
Span::styled("[Esc] Cancel ", Style::default().fg(normal_fg)),
Span::styled("[Tab] Next field", Style::default().fg(normal_fg)),
]));
let content = Paragraph::new(lines);
frame.render_widget(content, inner);
}
pub(super) fn render_import_dialog(frame: &mut Frame, state: &AppState, theme: &Theme, area: Rect) {
let dialog = match &state.dialogs.import_dialog {
Some(d) => d,
None => return,
};
let width = 56u16.min(area.width.saturating_sub(4));
let height = 10u16.min(area.height.saturating_sub(2));
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
let dialog_area = Rect::new(x, y, width, height);
let border_color = if dialog.error.is_some() {
theme.error_fg
} else {
theme.border_focused
};
frame.render_widget(ratatui::widgets::Clear, dialog_area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.title(" Import ")
.style(Style::default().bg(theme.dialog_bg));
let inner = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let mut lines = Vec::new();
let focused_fg = theme.border_focused;
let normal_fg = theme.status_fg;
if let Some(ref err) = dialog.error {
lines.push(Line::from(Span::styled(
err.as_str(),
Style::default().fg(theme.error_fg),
)));
}
let path_fg = if dialog.focused == ImportField::Path {
focused_fg
} else {
normal_fg
};
lines.push(Line::from(vec![
Span::styled("File: ", Style::default().fg(path_fg)),
Span::styled(dialog.path.as_str(), Style::default().fg(path_fg)),
if dialog.focused == ImportField::Path {
Span::styled("\u{2588}", Style::default().fg(path_fg))
} else {
Span::raw("")
},
]));
let sp_fg = if dialog.focused == ImportField::ShowPassword {
focused_fg
} else {
normal_fg
};
let sp_val = if dialog.show_password {
"[Y] Yes"
} else {
"[N] No "
};
lines.push(Line::from(vec![
Span::styled("Show password? ", Style::default().fg(sp_fg)),
Span::styled(
sp_val,
Style::default().fg(sp_fg).add_modifier(Modifier::BOLD),
),
]));
let pw_fg = if dialog.focused == ImportField::Password {
focused_fg
} else {
normal_fg
};
let pw_display = if dialog.show_password {
dialog.password.clone()
} else {
"*".repeat(dialog.password.len())
};
lines.push(Line::from(vec![
Span::styled("Password: ", Style::default().fg(pw_fg)),
Span::styled(pw_display, Style::default().fg(pw_fg)),
if dialog.focused == ImportField::Password {
Span::styled("\u{2588}", Style::default().fg(pw_fg))
} else {
Span::raw("")
},
]));
lines.push(Line::raw(""));
lines.push(Line::from(vec![
Span::styled("[Enter] Import ", Style::default().fg(normal_fg)),
Span::styled("[Esc] Cancel ", Style::default().fg(normal_fg)),
Span::styled("[Tab] Next field", Style::default().fg(normal_fg)),
]));
let content = Paragraph::new(lines);
frame.render_widget(content, inner);
}
pub(super) fn render_leader_help(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
level: usize,
) {
use crate::keybindings::Context;
use ratatui::style::Color;
let key_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let desc_style = Style::default().fg(theme.status_fg);
let header_style = Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD);
let pk = |ctx: Context, action: &str| state.bindings.primary_key(ctx, action);
let owned: Vec<(String, &str)>;
let title: &str;
match level {
2 => {
title = "Leader > b";
owned = vec![(pk(Context::LeaderBuffer, "close_tab"), "close buffer")];
}
3 => {
title = "Leader > Leader";
owned = vec![("s".to_string(), "compile to DB")];
}
4 => {
title = "Leader > w";
owned = vec![(pk(Context::LeaderWindow, "close_group"), "close group")];
}
5 => {
title = "Leader > s";
owned = vec![
(pk(Context::LeaderSnippet, "snippet_select"), "SELECT"),
(pk(Context::LeaderSnippet, "snippet_update"), "UPDATE"),
(pk(Context::LeaderSnippet, "snippet_delete"), "DELETE"),
(
pk(Context::LeaderSnippet, "snippet_call_proc"),
"CALL/EXEC proc",
),
(
pk(Context::LeaderSnippet, "snippet_select_func"),
"SELECT func",
),
(
pk(Context::LeaderSnippet, "snippet_create_table"),
"CREATE TABLE",
),
];
}
6 => {
title = "Leader > f";
owned = vec![
(
pk(Context::LeaderFile, "export_connections"),
"export connections",
),
(
pk(Context::LeaderFile, "import_connections"),
"import connections",
),
];
}
7 => {
title = "Leader > q";
owned = vec![(pk(Context::LeaderQuit, "quit_app"), "quit app")];
}
_ => {
title = "Leader (Space)";
owned = vec![
(pk(Context::Leader, "execute_query"), "execute query"),
(
pk(Context::Leader, "execute_query_new_tab"),
"execute \u{2192} new tab",
),
(pk(Context::Leader, "toggle_sidebar"), "toggle sidebar"),
(pk(Context::Leader, "vertical_split"), "vertical split"),
(
pk(Context::Leader, "move_tab_to_other_group"),
"move tab to other group",
),
(
pk(Context::Leader, "open_script_connection_picker"),
"connection",
),
(pk(Context::Leader, "open_theme_picker"), "theme"),
(pk(Context::Leader, "toggle_diagnostic_list"), "diagnostics"),
(
pk(Context::Leader, "open_file_submenu"),
"+file (export/import)",
),
(pk(Context::Leader, "open_quit_submenu"), "+quit..."),
(pk(Context::Leader, "open_snippet_submenu"), "+snippets..."),
(pk(Context::Leader, "open_buffer_submenu"), "+buffer..."),
(
pk(Context::Leader, "open_window_submenu"),
"+close group...",
),
("Spc".to_string(), "+compile..."),
];
}
};
let entries: Vec<(&str, &str)> = owned.iter().map(|(k, d)| (k.as_str(), *d)).collect();
let mut lines = vec![
Line::from(Span::styled(format!(" {title}"), header_style)),
Line::from(""),
];
for (key, desc) in &entries {
lines.push(Line::from(vec![
Span::styled(format!(" {key:<8} "), key_style),
Span::styled(*desc, desc_style),
]));
}
let height = (lines.len() as u16 + 2).min(area.height);
let width = 28_u16.min(area.width);
let x = area.width.saturating_sub(width + 1);
let y = area.height.saturating_sub(height + 2);
let popup = Rect::new(x, y, width, height);
frame.render_widget(ratatui::widgets::Clear, popup);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border_focused))
.style(Style::default().bg(Color::Reset));
let content = Paragraph::new(lines).block(block);
frame.render_widget(content, popup);
}
pub(super) fn render_script_conn_picker(
frame: &mut Frame,
state: &AppState,
theme: &Theme,
area: Rect,
) {
use crate::ui::state::PickerItem;
let picker = match &state.dialogs.script_conn_picker {
Some(p) => p,
None => return,
};
let visible = picker.visible_items();
let count = visible.len();
let height = (count as u16 + 2).min(14).min(area.height);
let width = 38_u16.min(area.width);
let x = area.width.saturating_sub(width) / 2;
let y = area.height.saturating_sub(height) / 2;
let popup = Rect::new(x, y, width, height);
frame.render_widget(ratatui::widgets::Clear, popup);
let block = Block::default()
.title(" Select Connection ")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.dialog_bg));
let inner = block.inner(popup);
frame.render_widget(block, popup);
let items: Vec<ratatui::widgets::ListItem> = visible
.iter()
.map(|item| match item {
PickerItem::Active(name) => ratatui::widgets::ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled("\u{25cf} ", Style::default().fg(theme.conn_connected)),
Span::styled(name.as_str(), Style::default().fg(theme.topbar_fg)),
])),
PickerItem::OthersHeader => {
let arrow = if picker.others_expanded {
"\u{25bc}"
} else {
"\u{25b6}"
};
ratatui::widgets::ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("{arrow} Others"),
Style::default()
.fg(theme.dim)
.add_modifier(Modifier::ITALIC),
),
]))
}
PickerItem::Other(name) => ratatui::widgets::ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled("\u{25cb} ", Style::default().fg(theme.dim)),
Span::styled(name.as_str(), Style::default().fg(theme.dim)),
])),
})
.collect();
let mut list_state = ratatui::widgets::ListState::default();
list_state.select(Some(picker.cursor));
let list = ratatui::widgets::List::new(items)
.highlight_style(
Style::default()
.bg(theme.tree_selected_bg)
.fg(theme.tree_selected_fg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{25b8} ");
frame.render_stateful_widget(list, inner, &mut list_state);
}