feat: add read-only PostgreSQL inspection TUI
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
mod app;
|
||||
mod db;
|
||||
|
||||
use std::{error::Error, io::IsTerminal, process::ExitCode};
|
||||
|
||||
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
||||
|
||||
const HELP: &str = r#"dbdash — read-only PostgreSQL inspection
|
||||
|
||||
Usage:
|
||||
dbdash [POSTGRES_CONNECTION_STRING]
|
||||
dbdash --help
|
||||
|
||||
With no argument, the connection is read from DATABASE_URL.
|
||||
Prefer the environment variable to keep passwords out of process arguments:
|
||||
export DATABASE_URL='postgresql://user@localhost:5432/database'
|
||||
cargo run
|
||||
|
||||
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 terminal must be at least 70 columns wide and 12 lines high.
|
||||
|
||||
Navigation:
|
||||
Tab / Esc Switch focus / return to relations
|
||||
Up/Down, j/k Move selection (Enter opens a relation)
|
||||
Home/End, g/G First / last item
|
||||
s Toggle rows and column schema
|
||||
Left/Right Scroll columns (h/l also work)
|
||||
n/p, PgDn/Up Next / previous 100-row page
|
||||
r Refresh relations and the active relation
|
||||
q / Ctrl-C Quit
|
||||
|
||||
Reads are enforced read-only by PostgreSQL and have a 5-second query timeout.
|
||||
The interface waits for reads to complete. NULL cells are dimmed.
|
||||
Pages use primary-key order, or physical order for keyless tables.
|
||||
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 connection = match args.next() {
|
||||
Some(argument) if argument == "--help" || argument == "-h" => {
|
||||
print!("{HELP}");
|
||||
return Ok(());
|
||||
}
|
||||
Some(argument) => argument
|
||||
.into_string()
|
||||
.map_err(|_| "connection string must be UTF-8")?,
|
||||
None => std::env::var("DATABASE_URL")
|
||||
.map_err(|_| "set DATABASE_URL or pass a connection string; see dbdash --help")?,
|
||||
};
|
||||
if args.next().is_some() {
|
||||
return Err("expected one connection string; 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 mut app = app::App::new(db::Database::connect(&connection)?)?;
|
||||
let result = (|| {
|
||||
let mut terminal = ratatui::try_init()?;
|
||||
app.run(&mut terminal)
|
||||
})();
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user