Keep searchi resident with a global search shortcut and tray
This commit is contained in:
1 parent
54de583430
commit
24e80edaf9
9 files changed
+152
-12
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"dependencies": {
|
||||
"@fluentui/svg-icons": "1.1.344",
|
||||
"@fontsource-variable/inter": "5.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3799,7 +3799,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "searchi-desktop"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"image",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1555,6 +1555,19 @@ document.addEventListener("keydown", (event) => {
|
||||
query.select();
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
if (isNativeDesktop()) {
|
||||
event.preventDefault();
|
||||
const popovers =
|
||||
document.querySelectorAll<HTMLDetailsElement>(".popover[open]");
|
||||
if (popovers.length) {
|
||||
popovers.forEach((popover) => {
|
||||
popover.open = false;
|
||||
});
|
||||
} else {
|
||||
void invoke("hide_search").catch(showError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearMedia();
|
||||
document
|
||||
.querySelectorAll<HTMLDetailsElement>(".popover[open]")
|
||||
@@ -1618,6 +1631,11 @@ async function initializeSurface() {
|
||||
queueSemanticRefresh(0);
|
||||
}).catch(showError);
|
||||
if (isNativeDesktop()) {
|
||||
void listen("search-hidden", () => {
|
||||
element("media-preview")
|
||||
.querySelectorAll<HTMLMediaElement>("audio,video")
|
||||
.forEach((player) => player.pause());
|
||||
}).catch(showError);
|
||||
void listen("folder-indexing-changed", () => {
|
||||
void refreshDesktopStatus().catch(showError);
|
||||
queueSemanticRefresh(0);
|
||||
|
||||
Reference in new issue
Block a user