133 lines
5.1 KiB
Rust
133 lines
5.1 KiB
Rust
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)
|
||
}
|