Files
dbx/src-tauri/src/commands/external_db.rs
T
t8y2 ef4baba57a feat: support double-clicking .db/.sqlite/.duckdb files to open in IDE
Add OS file association for database files (.db, .sqlite, .sqlite3, .duckdb)
so double-clicking them in Finder/Explorer automatically creates a connection.
If the same file path is already connected, prompts the user to switch instead.

Also fix 42 pre-existing clippy warnings in dbx-core (needless_borrow,
too_many_arguments, derivable_impls, manual_div_ceil, type_complexity, etc.).

Closes #527
2026-05-29 17:25:52 +08:00

114 lines
3.2 KiB
Rust

use std::path::{Path, PathBuf};
use std::sync::Mutex;
const DB_EXTENSIONS: &[&str] = &["db", "sqlite", "sqlite3", "duckdb"];
#[derive(Default)]
pub struct ExternalDbOpenState {
pending: Mutex<Vec<String>>,
}
impl ExternalDbOpenState {
pub fn push(&self, paths: Vec<String>) {
if paths.is_empty() {
return;
}
if let Ok(mut pending) = self.pending.lock() {
pending.extend(paths);
}
}
fn drain(&self) -> Vec<String> {
self.pending.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default()
}
}
#[tauri::command]
pub fn pending_open_db_files(state: tauri::State<'_, ExternalDbOpenState>) -> Vec<String> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut paths = db_file_paths_from_args(std::env::args().skip(1), &cwd);
paths.extend(state.drain());
dedupe_paths(paths)
}
pub fn db_file_paths_from_args<I, S>(args: I, cwd: &Path) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
args.into_iter().filter_map(|arg| db_file_path_from_arg(arg.as_ref(), cwd)).collect()
}
fn db_file_path_from_arg(arg: &str, cwd: &Path) -> Option<String> {
if arg.starts_with('-') {
return None;
}
let path = PathBuf::from(arg);
if !is_db_file_path(&path) {
return None;
}
let resolved = if path.is_absolute() { path } else { cwd.join(path) };
Some(resolved.to_string_lossy().to_string())
}
pub fn is_db_file_path(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| DB_EXTENSIONS.iter().any(|e| e.eq_ignore_ascii_case(ext)))
.unwrap_or(false)
}
fn dedupe_paths(paths: Vec<String>) -> Vec<String> {
let mut unique = Vec::new();
for path in paths {
if !unique.contains(&path) {
unique.push(path);
}
}
unique
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_db_file_args_case_insensitively() {
let paths = db_file_paths_from_args(
["/tmp/a.db", "--flag", "/tmp/b.SQLITE", "/tmp/c.sqlite3", "/tmp/d.duckdb", "/tmp/e.txt"],
Path::new("/work"),
);
assert_eq!(paths, vec!["/tmp/a.db", "/tmp/b.SQLITE", "/tmp/c.sqlite3", "/tmp/d.duckdb"]);
}
#[test]
fn resolves_relative_db_file_args_against_cwd() {
let paths = db_file_paths_from_args(["data/mydb.db"], Path::new("/work"));
assert_eq!(paths, vec!["/work/data/mydb.db"]);
}
#[test]
fn drains_pending_db_file_paths_once() {
let state = ExternalDbOpenState::default();
state.push(vec!["/tmp/a.db".to_string()]);
assert_eq!(state.drain(), vec!["/tmp/a.db"]);
assert!(state.drain().is_empty());
}
#[test]
fn is_db_file_path_recognizes_extensions() {
assert!(is_db_file_path(Path::new("test.db")));
assert!(is_db_file_path(Path::new("test.sqlite")));
assert!(is_db_file_path(Path::new("test.sqlite3")));
assert!(is_db_file_path(Path::new("test.duckdb")));
assert!(is_db_file_path(Path::new("test.DB")));
assert!(!is_db_file_path(Path::new("test.sql")));
assert!(!is_db_file_path(Path::new("test.txt")));
}
}