mod app; mod connection; mod db; use std::{error::Error, io::IsTerminal, path::Path, process::ExitCode}; type Result> = std::result::Result; const HELP: &str = r#"dbdash — read-only PostgreSQL and SQLite inspection Usage: dbdash [POSTGRES_CONNECTION_STRING] dbdash --sqlite PATH dbdash --help Run without a connection argument to open the saved database list: cargo run Use the in-app password field to keep credentials out of process arguments. Open an existing SQLite database file (read-only; missing files are not created): cargo run -- --sqlite ./database.sqlite SQLite paths can be relative or absolute; quote paths containing spaces. SQLite paths expand leading ~, $VAR, and ${VAR}; undefined variables are errors. Expansion never runs shell commands. Quote CLI paths to let dbdash expand them. PostgreSQL URLs and libpq-style key=value strings are accepted. Use sslmode=require for mandatory TLS; certificates and hostnames are verified. Use sslmode=disable only for trusted local connections. The inspector needs 70×12 terminal cells; the PostgreSQL window needs 74×20. Navigation: Tab/Shift-Tab Switch between explorer and rows/schema (Esc returns to explorer) Up/Down, j/k Move selection; Enter connects/expands a database or opens a relation Home/End, g/G First / last item s Toggle rows and column schema Left/Right Collapse/expand in explorer; scroll columns in rows/schema (h/l too) n/p, PgDn/Up Next / previous 100-row page c Add a configured database in a floating window r Refresh relations and the active relation Connected databases show their tables and views beneath them in the explorer. Enter on the connected database collapses/expands it without reconnecting. q / Ctrl-C Quit 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. Enter saves without connecting, including when the database is offline. Select a saved database and press Enter to connect. PostgreSQL prompts for a password; Esc cancels. Passwords are masked, never saved, and clear on success or cancellation. Ctrl-U clears a field; pasting is supported. Enter the database user explicitly; the operating-system username is not used. Configurations persist in $XDG_CONFIG_HOME/dbdash/connections.sqlite3, falling back to ~/.config/dbdash/connections.sqlite3. SQLite paths are saved absolute. CLI connections are not saved. Failed switches keep the current database open. PostgreSQL reads are enforced read-only and have a 5-second query timeout. SQLite files are opened read-only, with a 5-second lock wait but no query timeout. The interface waits for reads to complete. NULL cells are dimmed; SQLite BLOBs are shown as X'hex'. Only SQLite's main database is inspected. Pages use primary-key order, or physical row order for keyless tables when available. Keyless views have no guaranteed order; concurrent changes can shift pages. "#; fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("dbdash: {}", error_text(error.as_ref())); ExitCode::FAILURE } } } fn run() -> Result<()> { let mut args = std::env::args_os().skip(1); let argument = args.next(); if argument .as_ref() .is_some_and(|arg| arg == "--help" || arg == "-h") { print!("{HELP}"); return Ok(()); } let sqlite = argument.as_ref().is_some_and(|arg| arg == "--sqlite"); let connection = if sqlite { Some( args.next() .ok_or("--sqlite requires a database file path; see dbdash --help")?, ) } else { argument }; if args.next().is_some() { return Err("unexpected extra arguments; see dbdash --help".into()); } if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() { return Err("an interactive terminal is required; see dbdash --help".into()); } let database = connection .map(|connection| { if sqlite { db::Database::open_sqlite(Path::new(&connection)) } else { db::Database::connect( connection .to_str() .ok_or("connection string must be UTF-8")?, ) } }) .transpose()?; let result = (|| { let mut terminal = ratatui::try_init()?; crossterm::execute!(std::io::stdout(), crossterm::event::EnableBracketedPaste)?; connection::run(&mut terminal, database) })(); let paste_result = crossterm::execute!(std::io::stdout(), crossterm::event::DisableBracketedPaste); ratatui::restore(); result?; paste_result?; Ok(()) } fn error_text(error: &(dyn Error + 'static)) -> String { let mut text = error.to_string(); let mut source = error.source(); while let Some(error) = source { text.push_str(": "); text.push_str(&error.to_string()); source = error.source(); } db::display_text(&text) }