Files
dbdash/src/main.rs
T
2026-10-19 13:39:08 +02:00

133 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
mod app;
mod connection;
mod db;
use std::{error::Error, io::IsTerminal, path::Path, process::ExitCode};
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
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)
}