2026-08-02 19:04:30 -04:00
|
|
|
//! QuickSearch binary: `quicksearch <query>` searches from the terminal;
|
2026-08-23 00:33:53 -04:00
|
|
|
//! without a query it opens the egui desktop app. On Windows this is the GUI
|
|
|
|
|
//! only (window-subsystem, so no console flashes); terminal search there is
|
|
|
|
|
//! `quicksearch-cli`. A query passed here still seeds the search box.
|
2026-08-02 19:04:30 -04:00
|
|
|
#![cfg_attr(windows, windows_subsystem = "windows")]
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
// This line puts the app on mimalloc: a library cannot choose an allocator
|
|
|
|
|
// for its dependents. See `platform::Allocator` for why it is not glibc's.
|
2026-08-25 01:04:36 -04:00
|
|
|
#[global_allocator]
|
|
|
|
|
static GLOBAL: quicksearch_core::platform::Allocator = quicksearch_core::platform::Allocator;
|
|
|
|
|
|
2026-09-04 21:39:54 -04:00
|
|
|
mod activate;
|
2026-08-02 19:04:30 -04:00
|
|
|
mod app;
|
|
|
|
|
mod backend;
|
2026-08-04 23:33:28 -04:00
|
|
|
#[cfg(feature = "capture")]
|
|
|
|
|
mod capture;
|
2026-08-02 19:04:30 -04:00
|
|
|
#[cfg(not(windows))]
|
|
|
|
|
mod cli;
|
2026-08-09 02:58:13 -04:00
|
|
|
mod color;
|
2026-08-02 19:04:30 -04:00
|
|
|
mod duplicates_tab;
|
2026-08-17 22:30:43 -04:00
|
|
|
mod fonts;
|
2026-08-02 19:04:30 -04:00
|
|
|
mod format;
|
2026-08-02 20:21:19 -04:00
|
|
|
mod help_tab;
|
2026-08-09 02:58:13 -04:00
|
|
|
mod hotkey;
|
2026-08-02 20:21:19 -04:00
|
|
|
mod keychain;
|
2026-08-02 19:04:30 -04:00
|
|
|
mod logs_tab;
|
|
|
|
|
mod manage_tab;
|
|
|
|
|
mod platform;
|
|
|
|
|
mod query_highlight;
|
|
|
|
|
mod search_tab;
|
2026-08-17 19:26:18 -04:00
|
|
|
mod settings_tab;
|
2026-09-05 01:55:59 -04:00
|
|
|
mod shortcut_setup;
|
2026-08-23 00:33:53 -04:00
|
|
|
mod spotlight;
|
2026-08-05 18:05:04 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test_ui;
|
|
|
|
|
mod tips;
|
2026-08-02 19:04:30 -04:00
|
|
|
mod tracker;
|
2026-08-17 19:26:18 -04:00
|
|
|
mod tutorial;
|
2026-08-02 20:21:19 -04:00
|
|
|
mod ui_util;
|
2026-08-02 22:21:39 -04:00
|
|
|
mod unlock;
|
2026-08-04 23:33:28 -04:00
|
|
|
mod version;
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
use quicksearch_core::config::Config;
|
2026-08-20 02:34:08 -04:00
|
|
|
use quicksearch_core::platform::{IndexLock, LockError};
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// X11 takes these pixels directly; Wayland ignores them and looks the app
|
|
|
|
|
/// id up in `/usr/share/applications/`, so the id below must match the
|
|
|
|
|
/// installed `quicksearch.desktop`.
|
2026-08-02 19:04:30 -04:00
|
|
|
fn app_icon() -> egui::IconData {
|
|
|
|
|
eframe::icon_data::from_png_bytes(include_bytes!("../assets/icons/quicksearch-256.png"))
|
|
|
|
|
.expect("bundled icon is a valid PNG")
|
|
|
|
|
}
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// Positional arguments, joined, to seed the search box; flags are dropped
|
|
|
|
|
/// unparsed (eframe and winit take some of their own).
|
2026-08-02 19:04:30 -04:00
|
|
|
fn seed_query() -> Option<String> {
|
|
|
|
|
let terms: Vec<String> = std::env::args()
|
|
|
|
|
.skip(1)
|
|
|
|
|
.filter(|a| !a.starts_with('-'))
|
|
|
|
|
.collect();
|
2026-09-25 14:58:51 -04:00
|
|
|
(!terms.is_empty()).then(|| terms.join(" "))
|
2026-04-21 23:00:47 -04:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 21:39:54 -04:00
|
|
|
/// What the desktop's search shortcut runs. Not a query flag: it is checked
|
|
|
|
|
/// before terminal mode, because `--toggle` is about the window.
|
|
|
|
|
const TOGGLE_FLAG: &str = "--toggle";
|
|
|
|
|
|
|
|
|
|
fn wants_toggle() -> bool {
|
|
|
|
|
std::env::args().skip(1).any(|a| a == TOGGLE_FLAG)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
fn main() {
|
2026-08-23 00:33:53 -04:00
|
|
|
// First: printing without a stdio handle panics rather than failing quietly.
|
2026-08-02 19:04:30 -04:00
|
|
|
#[cfg(windows)]
|
|
|
|
|
platform::redirect_null_stdio();
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-09-04 21:39:54 -04:00
|
|
|
let toggle = wants_toggle();
|
|
|
|
|
|
|
|
|
|
// Before terminal mode, which would read `--toggle` as an unknown flag.
|
2026-08-02 19:04:30 -04:00
|
|
|
#[cfg(not(windows))]
|
2026-09-04 21:39:54 -04:00
|
|
|
if !toggle {
|
|
|
|
|
if let Some(code) = cli::maybe_run_cli() {
|
|
|
|
|
std::process::exit(code);
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
// A broken config must never keep the window from opening.
|
2026-08-02 19:04:30 -04:00
|
|
|
let (config, config_error) = match Config::load() {
|
|
|
|
|
Ok(c) => (c, None),
|
|
|
|
|
Err(e) => (Config::default(), Some(e)),
|
2026-04-21 23:00:47 -04:00
|
|
|
};
|
2026-09-04 21:39:54 -04:00
|
|
|
|
|
|
|
|
// The desktop's shortcut, on an app that is already up: hand the
|
2026-09-25 14:58:51 -04:00
|
|
|
// activation over and get out of the way. Failure is the normal case
|
|
|
|
|
// (nothing is running) and falls through to starting the GUI, so one
|
|
|
|
|
// binding both raises and launches.
|
2026-09-04 21:39:54 -04:00
|
|
|
if toggle && activate::signal(&Config::config_path()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-08-25 01:04:36 -04:00
|
|
|
// Before any search connection exists: the ceiling is applied at open, and
|
|
|
|
|
// `0` leaves it derived from the index.
|
|
|
|
|
quicksearch_core::db::set_search_cache_override(
|
|
|
|
|
(config.search.cache_size_mib != 0).then_some(config.search.cache_size_mib as i64),
|
|
|
|
|
);
|
2026-08-02 19:04:30 -04:00
|
|
|
let initial_query = seed_query();
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
// After the CLI early-exit, deliberately: the CLI only reads. Two windows
|
|
|
|
|
// on one index are the problem: two indexers writing, and an attach that
|
|
|
|
|
// truncates the wal-index under a live mapping. Held for the life of the
|
|
|
|
|
// process; the kernel releases it however the exit happens.
|
2026-08-20 02:34:08 -04:00
|
|
|
match IndexLock::hold(&config.resolved_database_path()) {
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(LockError::Held { pid }) => {
|
2026-09-25 14:58:51 -04:00
|
|
|
// A second launch, or one that lost the race to an instance that
|
|
|
|
|
// came up after the signal above: either way there is a window to
|
|
|
|
|
// show. Retried because the winner holds the lock as soon as
|
2026-09-05 01:55:59 -04:00
|
|
|
// `main` reaches it but only listens once eframe's creation
|
2026-09-25 14:58:51 -04:00
|
|
|
// closure has run; two seconds outlasts that gap by orders of
|
|
|
|
|
// magnitude, and a wedged instance still gets the dialog.
|
2026-09-05 01:55:59 -04:00
|
|
|
for _ in 0..20 {
|
|
|
|
|
if activate::signal(&Config::config_path()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
2026-09-04 21:39:54 -04:00
|
|
|
}
|
2026-08-20 02:34:08 -04:00
|
|
|
let msg = format!(
|
|
|
|
|
"QuickSearch is already running{}.\n\nOnly one window can use \
|
|
|
|
|
the index at a time. Switch to the running window, or close \
|
|
|
|
|
it and try again.",
|
2026-09-25 14:58:51 -04:00
|
|
|
app::held_by(pid)
|
2026-08-20 02:34:08 -04:00
|
|
|
);
|
|
|
|
|
eprintln!("{}", msg);
|
2026-08-23 00:33:53 -04:00
|
|
|
// Launched from a desktop icon, nothing watches stderr; show a dialog.
|
2026-08-20 02:34:08 -04:00
|
|
|
rfd::MessageDialog::new()
|
|
|
|
|
.set_level(rfd::MessageLevel::Info)
|
|
|
|
|
.set_title("QuickSearch")
|
|
|
|
|
.set_description(&msg)
|
|
|
|
|
.show();
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-08-23 00:33:53 -04:00
|
|
|
// The filesystem could not answer: a convenience guard is never a
|
|
|
|
|
// good enough reason to refuse to open.
|
2026-08-20 02:34:08 -04:00
|
|
|
Err(LockError::Unsupported(why)) => {
|
|
|
|
|
eprintln!("warning: cannot lock the index ({}); starting anyway", why);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
// Try the keychain before the window opens; a verified key means no
|
|
|
|
|
// prompt at all. `None` starts locked.
|
2026-08-03 03:06:19 -04:00
|
|
|
let key_source = if !config.security.password_protected {
|
|
|
|
|
Some(unlock::KeySource::Unprotected)
|
|
|
|
|
} else if unlock::try_keychain_unlock(&config) {
|
|
|
|
|
Some(unlock::KeySource::Keychain)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2026-08-02 20:21:19 -04:00
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
let native_options = eframe::NativeOptions {
|
|
|
|
|
viewport: egui::ViewportBuilder::default()
|
|
|
|
|
.with_title("QuickSearch")
|
|
|
|
|
.with_app_id("quicksearch")
|
|
|
|
|
.with_icon(app_icon())
|
|
|
|
|
.with_inner_size([1000.0, 700.0])
|
|
|
|
|
.with_min_inner_size([640.0, 400.0]),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
let result = eframe::run_native(
|
|
|
|
|
"QuickSearch",
|
|
|
|
|
native_options,
|
|
|
|
|
Box::new(move |cc| {
|
2026-09-25 14:58:51 -04:00
|
|
|
// Everything here runs before frame 1 and before the unlock gate,
|
|
|
|
|
// so the unlock screen already has fonts, theme and working
|
|
|
|
|
// shortcuts.
|
2026-08-17 22:30:43 -04:00
|
|
|
fonts::install(&cc.egui_ctx);
|
2026-09-25 14:58:51 -04:00
|
|
|
// Safe to bind now: we hold the index lock, so no other instance
|
|
|
|
|
// is listening.
|
2026-09-04 21:39:54 -04:00
|
|
|
activate::listen(&cc.egui_ctx, &Config::config_path());
|
2026-09-25 14:58:51 -04:00
|
|
|
// Needs the running event loop, which starts here.
|
2026-08-09 02:58:13 -04:00
|
|
|
hotkey::init(&cc.egui_ctx, &config.ui.search_hotkey);
|
|
|
|
|
app::apply_theme(&cc.egui_ctx, &config.ui.color_scheme);
|
2026-09-04 21:39:54 -04:00
|
|
|
color::apply_text_contrast(&cc.egui_ctx);
|
2026-08-03 03:06:19 -04:00
|
|
|
let gate = match key_source {
|
|
|
|
|
Some(source) => {
|
|
|
|
|
unlock::Gate::running(&cc.egui_ctx, config, config_error, initial_query, source)
|
|
|
|
|
.map_err(Box::<dyn std::error::Error + Send + Sync>::from)?
|
|
|
|
|
}
|
|
|
|
|
None => unlock::Gate::locked(config, config_error, initial_query),
|
2026-08-02 20:21:19 -04:00
|
|
|
};
|
|
|
|
|
Ok(Box::new(gate) as Box<dyn eframe::App>)
|
2026-08-02 19:04:30 -04:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
if let Err(e) = result {
|
|
|
|
|
eprintln!("failed to start GUI: {}", e);
|
|
|
|
|
std::process::exit(1);
|
2026-04-21 23:00:47 -04:00
|
|
|
}
|
|
|
|
|
}
|