From 35bbb2727184079547bc20af0053dc9510b4b8e1 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Tue, 20 Oct 2026 16:53:56 +0200 Subject: [PATCH] fix: show lowercase access as a flat footer badge --- src/connection.rs | 110 ++++++++++++++++------------------------------ src/main.rs | 4 +- 2 files changed, 40 insertions(+), 74 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index e170f3d..8a72084 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -451,7 +451,7 @@ fn draw_help(frame: &mut Frame, screen: Rect, offset: &mut u16) { Connect saves settings only on success; e edits save without connecting.\n\ Access defaults to READ ONLY, including older saved connections.\n\ [RO]/[RW] badges stay right-aligned; * before the badge marks the active connection.\n\ - Header/connected badge show live mode; info shows saved Access.\n\ + Footer/connected badge show live mode; info shows saved Access.\n\ Saved mode edits need R to reconnect before they take effect.\n\ CA bundles come from .pem/.crt files in the dbdash config directory.\n\ Choosing a bundle requires TLS; choose System trust to unlock TLS mode.\n\ @@ -546,40 +546,33 @@ fn draw_bars( let base = Style::default(); let badge = Style::default().bg(Color::Cyan).fg(Color::Black); let edge = base.fg(Color::Cyan); - let access = access.map_or("NOT CONNECTED", AccessMode::label); - let [brand_area, access_area] = Layout::horizontal([ - Constraint::Min(0), - Constraint::Length(access.len() as u16 + 4), - ]) - .areas(top); frame.render_widget( Paragraph::new(Line::from(vec![ Span::styled(" dbdash ", badge), Span::styled("▓░", edge), ])) .style(base), - brand_area, - ); - frame.render_widget( - Paragraph::new(Line::from(vec![ - Span::styled("░▓", edge), - Span::styled(format!(" {access} "), badge), - ])) - .right_aligned() - .style(base), - access_area, + top, ); frame.render_widget(Paragraph::new("").style(base), bottom); let position = position.map(|(line, column)| format!(" {line}:{column} ")); let position = position.as_deref().unwrap_or(" N/A "); let position_width = (position.len() as u16 + 2).min(bottom.width); let mode_width = bottom.width.saturating_sub(position_width).min(10); - let provider_width = (Span::raw(provider).width() as u16 + 2) + let access = match access { + Some(AccessMode::ReadOnly) => " read only ", + Some(AccessMode::ReadWrite) => " read write ", + None => "", + }; + let access_width = (access.len() as u16) .min(bottom.width.saturating_sub(position_width + mode_width)); - let [mode, name_area, provider_area, cursor] = Layout::horizontal([ + let provider_width = (Span::raw(provider).width() as u16 + 2) + .min(bottom.width.saturating_sub(position_width + mode_width + access_width)); + let [mode, name_area, provider_area, access_area, cursor] = Layout::horizontal([ Constraint::Length(mode_width), Constraint::Min(0), Constraint::Length(provider_width), + Constraint::Length(access_width), Constraint::Length(position_width), ]) .areas(bottom); @@ -596,6 +589,7 @@ fn draw_bars( Paragraph::new(provider).centered().style(base), provider_area, ); + frame.render_widget(Paragraph::new(access).style(badge), access_area); frame.render_widget( Paragraph::new(Line::from(vec![ Span::styled("░▓", edge), @@ -1604,37 +1598,34 @@ mod tests { let [top, _, bottom] = shell_areas(frame.area()); draw_bars(frame, top, bottom, "SQLite", "data", Some(live.access_mode()), false, None); })?; - let header: String = (0..40).map(|x| terminal.backend().buffer()[(x, 0)].symbol()).collect(); - assert!(header.contains("READ WRITE")); - assert!(!header.contains("READ ONLY")); + let footer: String = (0..40).map(|x| terminal.backend().buffer()[(x, 5)].symbol()).collect(); + assert!(footer.contains("read write")); + assert!(!footer.contains("read only")); Ok(()) } #[test] - fn access_status_stays_at_the_top_right() -> Result<()> { - for width in [24, 80] { - for access in [Some(AccessMode::ReadOnly), Some(AccessMode::ReadWrite), None] { - let mut terminal = ratatui::Terminal::new( - ratatui::backend::TestBackend::new(width, 6), - )?; - terminal.draw(|frame| { - let [top, _, bottom] = shell_areas(frame.area()); - draw_bars(frame, top, bottom, "", "", access, false, None); - })?; - let header: String = (0..width) - .map(|x| terminal.backend().buffer()[(x, 0)].symbol()) - .collect(); - let status = format!("░▓ {} ", access.map_or("NOT CONNECTED", AccessMode::label)); - assert!(header.ends_with(&status)); - if usize::from(width) >= Line::raw(&status).width() + 10 { - assert!(header.starts_with(" dbdash ▓░")); - } - let buffer = terminal.backend().buffer(); - assert_eq!(buffer[(width - 1, 0)].bg, Color::Cyan); - assert_eq!(buffer[(width - 1, 0)].fg, Color::Black); - let edge_x = width - (access.map_or("NOT CONNECTED", AccessMode::label).len() as u16 + 4); - assert_eq!(buffer[(edge_x, 0)].bg, Color::Reset); - assert_eq!(buffer[(edge_x, 0)].fg, Color::Cyan); + fn access_badge_sits_before_position_in_lowercase() -> Result<()> { + for access in [Some(AccessMode::ReadOnly), Some(AccessMode::ReadWrite), None] { + let mut terminal = ratatui::Terminal::new( + ratatui::backend::TestBackend::new(80, 6), + )?; + terminal.draw(|frame| { + let [top, _, bottom] = shell_areas(frame.area()); + draw_bars(frame, top, bottom, "SQLite", "", access, false, Some((1, 1))); + })?; + let buffer = terminal.backend().buffer(); + let footer: String = (0..80).map(|x| buffer[(x, 5)].symbol()).collect(); + let expected = match access { + Some(AccessMode::ReadOnly) => "SQLite read only ░▓ 1:1 ", + Some(AccessMode::ReadWrite) => "SQLite read write ░▓ 1:1 ", + None => "SQLite ░▓ 1:1 ", + }; + assert!(footer.ends_with(expected), "{footer}"); + if access.is_some() { + let x = footer.find("read ").unwrap() as u16; + assert_eq!(buffer[(x, 5)].bg, Color::Cyan); + assert_eq!(buffer[(x, 5)].fg, Color::Black); } } Ok(()) @@ -1796,7 +1787,7 @@ mod tests { assert_eq!(terminal.backend().buffer(), &last); terminal = Terminal::new(TestBackend::new(100, 60))?; terminal.draw(|frame| draw_help(frame, frame.area(), &mut offset))?; - assert_eq!(offset, 0); + assert!(offset < end); Ok(()) } @@ -1892,31 +1883,6 @@ mod tests { Ok(()) } - #[test] - fn provider_sits_before_position_and_displaces_the_database_label() -> Result<()> { - let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(32, 8))?; - terminal.draw(|frame| { - let [top, _, bottom] = shell_areas(frame.area()); - draw_bars( - frame, - top, - bottom, - "PostgreSQL", - "a very long database name", - Some(AccessMode::ReadOnly), - false, - None, - ); - })?; - let buffer = terminal.backend().buffer(); - let top: String = (0..32).map(|x| buffer[(x, 0)].symbol()).collect(); - let footer: String = (0..32).map(|x| buffer[(x, 7)].symbol()).collect(); - assert!(!top.contains("PostgreSQL")); - assert!(footer.starts_with(" NORMAL ▓░")); - assert!(footer.ends_with(" PostgreSQL ░▓ N/A ")); - assert_eq!(buffer[(24, 7)].bg, Color::Reset); - Ok(()) - } #[test] fn shell_keeps_position_when_name_is_long_and_yields_on_short_screens() -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 0b4a04f..77d93c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,10 +82,10 @@ Navigation: Enter on the connected database collapses/expands it without reconnecting. Keybindings float above the workspace; Up/Down/jk scroll, Home/End/g/G jump. Query Insert mode and connection/password fields keep ? as literal text. - Full-width bars show matching dbdash (top-left) and live access (top-right) badges, with mode/database below. + Full-width bars show dbdash above and mode/database below. Saved PostgreSQL connections show database · user@host:port beside the mode, regardless of custom sidebar names; passwords are never included. - The database provider sits immediately left of the bottom-right position badge. + The footer shows provider, a flat lowercase access badge, then the position badge. Only badges have colored backgrounds; the remaining bar area uses terminal defaults. Focused inputs show one-based line:column (Unicode characters, not screen cells). The position badge shows N/A outside focused inputs, including in info/help popups.