Keep searchi resident with a global search shortcut and tray

This commit is contained in:
karl committed 2026-10-13 22:18:01 +02:00
1 parent 54de583430
commit 24e80edaf9
9 files changed
+152 -12

No files matched your search

+1 -1
View File
@@ -3799,7 +3799,7 @@ dependencies = [
[[package]]
name = "searchi-desktop"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"image",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "searchi-desktop"
version = "0.1.6"
version = "0.1.7"
edition = "2021"
description = "Local Searchi desktop shell"
license = "MIT"
@@ -16,7 +16,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
anyhow = "1"
notify = "=8.2.0"
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri-plugin-global-shortcut = "2"
+58
View File
@@ -3,6 +3,46 @@ use sha2::{Digest, Sha256};
use std::{ffi::OsString, io, path::PathBuf};
use tauri::{Emitter, Manager};
/// Login launches stay resident without interrupting the user's desktop.
pub fn background_requested(args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> bool {
args.into_iter()
.any(|argument| argument.as_ref() == "--background")
}
pub fn install_tray(app: &tauri::App) -> tauri::Result<()> {
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
let show = MenuItem::with_id(app, "show-search", "Show searchi", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit-searchi", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
let mut tray = TrayIconBuilder::with_id("searchi-resident")
.tooltip("searchi")
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show-search" => focus_search(app),
"quit-searchi" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if matches!(
event,
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
}
) {
focus_search(tray.app_handle());
}
});
if let Some(icon) = app.default_window_icon() {
tray = tray.icon(icon.clone());
}
tray.build(app)?;
Ok(())
}
pub fn profile_identifier(base: &str, profile: Option<OsString>) -> io::Result<String> {
let Some(profile) = profile else {
return Ok(base.to_owned());
@@ -39,10 +79,28 @@ pub fn focus_search(app: &tauri::AppHandle) {
}
}
#[tauri::command]
pub fn hide_search(app: tauri::AppHandle) -> Result<(), String> {
let window = app
.get_webview_window("main")
.ok_or_else(|| "Search window is unavailable.".to_owned())?;
window.hide().map_err(|error| error.to_string())?;
window
.emit("search-hidden", ())
.map_err(|error| error.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn background_flag_is_explicit_and_exact() {
assert!(background_requested(["searchi", "--background"]));
assert!(!background_requested(["searchi", "--background=false"]));
assert!(!background_requested(["searchi", "a--background"]));
assert!(!background_requested(["searchi"]));
}
#[test]
fn default_identity_is_unchanged_and_invalid_profile_rejected() {
assert_eq!(
profile_identifier("dev.searchi.desktop", None).unwrap(),
+34 -2
View File
@@ -553,6 +553,17 @@ fn start_folder_watching(
fn main() {
let mut context = tauri::generate_context!();
if instance::background_requested(std::env::args_os()) {
if let Some(window) = context
.config_mut()
.app
.windows
.iter_mut()
.find(|window| window.label == "main")
{
window.visible = false;
}
}
context.config_mut().identifier = instance::profile_identifier(
&context.config().identifier,
std::env::var_os("SEARCHI_DATA_DIR"),
@@ -562,8 +573,10 @@ fn main() {
let protocol_previews = previews.clone();
tauri::Builder::default()
// Detect duplicates before any model, database, watcher or other plugin starts.
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
instance::focus_search(app);
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if !instance::background_requested(args) {
instance::focus_search(app);
}
}))
.register_asynchronous_uri_scheme_protocol("searchi-media", move |_context, request, responder| {
let Some(admission) = protocol_previews.admit() else {
@@ -584,6 +597,7 @@ fn main() {
}
}).build())
.setup(move |app| {
instance::install_tray(app)?;
let directory = data_directory::resolve(
app.path().app_data_dir()?,
std::env::var_os("SEARCHI_DATA_DIR"),
@@ -716,7 +730,25 @@ speech_worker, directory.join("SpeechModels")),
});
Ok(())
})
.on_window_event(|window, event| {
if window.label() == "main" {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// Keep indexing, prepared readers and the global shortcut alive.
// The tray's explicit Quit action still exits normally.
match window.hide() {
Ok(()) => {
api.prevent_close();
if let Err(error) = window.emit("search-hidden", ()) {
eprintln!("Could not notify hidden searchi: {error}");
}
}
Err(error) => eprintln!("Could not hide searchi: {error}"),
}
}
}
})
.invoke_handler(tauri::generate_handler![
instance::hide_search,
prepare_ai_source,
generate_ai,
search,
+13 -4
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "searchi",
"version": "0.1.6",
"version": "0.1.7",
"identifier": "dev.searchi.desktop",
"build": {
"beforeDevCommand": "npm run dev",
@@ -21,13 +21,22 @@
],
"security": {
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; img-src 'self' data: searchi-media: http://searchi-media.localhost https://searchi-media.localhost; media-src 'self' searchi-media: http://searchi-media.localhost https://searchi-media.localhost",
"assetProtocol": { "enable": false, "scope": [] }
"assetProtocol": {
"enable": false,
"scope": []
}
}
},
"bundle": {
"active": false,
"windows": { "nsis": { "installerHooks": "installer-hooks.nsh" } },
"resources": { "resources/": "" },
"windows": {
"nsis": {
"installerHooks": "installer-hooks.nsh"
}
},
"resources": {
"resources/": ""
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",