fix: place help below both panes and status in table border

This commit is contained in:
Valdemar Lorenzen
2026-10-19 13:46:59 +02:00
parent de7d373cca
commit 0ffcef494f
3 changed files with 69 additions and 44 deletions
+61 -43
View File
@@ -152,18 +152,17 @@ impl App {
} }
pub(crate) fn draw(&mut self, frame: &mut Frame, area: Rect, focused: bool) { pub(crate) fn draw(&mut self, frame: &mut Frame, area: Rect, focused: bool) {
if area.width < 38 || area.height < 12 { if area.width < 38 || area.height < 10 {
frame.render_widget( frame.render_widget(
Paragraph::new("Enlarge terminal to at least 70×12. q to quit."), Paragraph::new("Enlarge terminal to at least 70×12. q to quit."),
area, area,
); );
return; return;
} }
let [header, body, status, help] = Layout::vertical([ let [header, body, error_area] = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Min(4), Constraint::Min(4),
Constraint::Length(2), Constraint::Length(1),
Constraint::Length(2),
]) ])
.areas(area); .areas(area);
frame.render_widget( frame.render_widget(
@@ -181,7 +180,6 @@ impl App {
} else { } else {
" Rows · s for schema " " Rows · s for schema "
}; };
let content_block = panel(title, focused);
let count = if self.show_schema { let count = if self.show_schema {
5 5
} else { } else {
@@ -197,6 +195,40 @@ impl App {
Constraint::Length(20) Constraint::Length(20)
} }
}); });
let description = if let Some(index) = self.active {
let relation = &self.relations[index];
if self.show_schema {
format!(
" {} [{}] · {} columns · fields {}–{}/5",
relation.label,
relation.kind,
self.columns.len(),
self.column_offset + 1,
self.column_offset + visible
)
} else {
let start = if self.page.rows.is_empty() {
0
} else {
self.page_index * PAGE_SIZE + 1
};
let end = self.page_index * PAGE_SIZE + self.page.rows.len();
format!(
" {} [{}] · rows {start}–{end} · page {}{} · columns {}–{}/{}",
relation.label,
relation.kind,
self.page_index + 1,
if self.page.has_next { " (more)" } else { "" },
if count > 0 { self.column_offset + 1 } else { 0 },
self.column_offset + visible,
count
)
}
} else {
" No readable relations. r to refresh.".into()
};
let content_block =
panel(title, focused).title_bottom(Line::from(description).left_aligned());
if self.active.is_none() { if self.active.is_none() {
frame.render_widget( frame.render_widget(
Paragraph::new("Choose a relation and press Enter.").block(content_block), Paragraph::new("Choose a relation and press Enter.").block(content_block),
@@ -266,50 +298,12 @@ impl App {
.highlight_symbol("› "); .highlight_symbol("› ");
frame.render_stateful_widget(table, content, &mut self.rows_state); frame.render_stateful_widget(table, content, &mut self.rows_state);
} }
let description = if let Some(index) = self.active {
let relation = &self.relations[index];
if self.show_schema {
format!(
" {} [{}] · {} columns · fields {}–{}/5",
relation.label,
relation.kind,
self.columns.len(),
self.column_offset + 1,
self.column_offset + visible
)
} else {
let start = if self.page.rows.is_empty() {
0
} else {
self.page_index * PAGE_SIZE + 1
};
let end = self.page_index * PAGE_SIZE + self.page.rows.len();
format!(
" {} [{}] · rows {start}–{end} · page {}{} · columns {}–{}/{}",
relation.label,
relation.kind,
self.page_index + 1,
if self.page.has_next { " (more)" } else { "" },
if count > 0 { self.column_offset + 1 } else { 0 },
self.column_offset + visible,
count
)
}
} else {
" No readable relations. r to refresh.".into()
};
let [description_area, error_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(status);
frame.render_widget(Paragraph::new(description), description_area);
if let Some(error) = &self.error { if let Some(error) = &self.error {
frame.render_widget( frame.render_widget(
Paragraph::new(error.as_str()).style(Style::default().fg(Color::Red)), Paragraph::new(error.as_str()).style(Style::default().fg(Color::Red)),
error_area, error_area,
); );
} }
frame.render_widget(Paragraph::new(
" Tab focus · Esc explorer · ↑↓/jk move · Enter open · s schema/rows\n ←→/hl columns · n/p page · r refresh · c add database · q quit"
).style(Style::default().fg(Color::DarkGray)), help);
} }
} }
@@ -358,4 +352,28 @@ mod tests {
assert_eq!(moved_index(Some(1), 3, isize::MIN), Some(0)); assert_eq!(moved_index(Some(1), 3, isize::MIN), Some(0));
assert_eq!(moved_index(Some(1), 3, isize::MAX), Some(2)); assert_eq!(moved_index(Some(1), 3, isize::MAX), Some(2));
} }
#[test]
fn relation_status_is_in_the_bottom_border() -> crate::Result<()> {
let file = tempfile::NamedTempFile::new()?;
rusqlite::Connection::open(file.path())?.execute_batch(
"CREATE TABLE comments (id INTEGER); INSERT INTO comments VALUES (1);",
)?;
let mut app = super::App::new(crate::db::Database::open_sqlite(file.path())?)?;
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 20))?;
for schema in [false, true] {
app.show_schema = schema;
terminal.draw(|frame| app.draw(frame, frame.area(), true))?;
let buffer = terminal.backend().buffer();
let border: String = (0..100).map(|x| buffer[(x, 18)].symbol()).collect();
assert!(border.starts_with("└ main.comments [table]"), "{border}");
assert!(border.ends_with('┘'), "{border}");
assert!(border.contains(if schema {
"fields 1–4/5"
} else {
"rows 1–1"
}));
}
Ok(())
}
} }
+6 -1
View File
@@ -34,8 +34,10 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
expanded, expanded,
); );
terminal.draw(|frame| { terminal.draw(|frame| {
let [main, help] =
Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(frame.area());
let [sidebar, content] = Layout::horizontal([Constraint::Length(32), Constraint::Min(1)]) let [sidebar, content] = Layout::horizontal([Constraint::Length(32), Constraint::Min(1)])
.areas(frame.area()); .areas(main);
let items = entries.iter().map(|entry| { let items = entries.iter().map(|entry| {
ListItem::new(match *entry { ListItem::new(match *entry {
ExplorerEntry::Database(index) => { ExplorerEntry::Database(index) => {
@@ -62,6 +64,9 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
if let Some(error) = &error { if let Some(error) = &error {
frame.render_widget(Paragraph::new(error.as_str()).style(Style::default().fg(Color::Red)), Rect::new(content.x, content.bottom().saturating_sub(1), content.width, 1)); frame.render_widget(Paragraph::new(error.as_str()).style(Style::default().fg(Color::Red)), Rect::new(content.x, content.bottom().saturating_sub(1), content.width, 1));
} }
frame.render_widget(Paragraph::new(
" Tab focus · Esc explorer · ↑↓/jk move · Enter open · s schema/rows\n ←→/hl columns · n/p page · r refresh · c add database · q quit"
).style(Style::default().fg(Color::DarkGray)), help);
if show_form { if show_form {
form.draw(frame); form.draw(frame);
} }
+2
View File
@@ -39,6 +39,8 @@ Navigation:
r Refresh relations and the active relation r Refresh relations and the active relation
Connected databases show their tables and views beneath them in the explorer. Connected databases show their tables and views beneath them in the explorer.
Enter on the connected database collapses/expands it without reconnecting. Enter on the connected database collapses/expands it without reconnecting.
Navigation help spans the bottom of the terminal beneath both panes.
Relation, page, and column status appears in the rows/schema bottom border.
q / Ctrl-C Quit q / Ctrl-C Quit
New database window: Up/Down chooses a provider; Enter starts editing. New database window: Up/Down chooses a provider; Enter starts editing.
Tab/Shift-Tab changes fields; Left/Right changes TLS when that field is focused. Tab/Shift-Tab changes fields; Left/Right changes TLS when that field is focused.