mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
Add native desktop spotlight search
This commit is contained in:
parent
ce8c960586
commit
53c10e2126
8 changed files with 506 additions and 13 deletions
|
|
@ -14,9 +14,10 @@ import {
|
|||
SendHorizontal,
|
||||
Sparkles,
|
||||
} from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { listDocuments, type DocumentWithMemories } from "@/lib/api"
|
||||
import type { SearchResult } from "@/lib/search"
|
||||
import { OPEN_MEMORY_EVENT, type SpotlightMemory } from "@/lib/spotlight"
|
||||
import { SearchCommand, useCommandK } from "@/components/search-command"
|
||||
|
||||
type MemoryPreview = {
|
||||
|
|
@ -51,6 +52,27 @@ export default function DashboardPage() {
|
|||
}, [documentsQuery.isPending, totalCount])
|
||||
const visibleDocuments = documents.slice(0, 5)
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined
|
||||
|
||||
import("@tauri-apps/api/event")
|
||||
.then(({ listen }) =>
|
||||
listen<SpotlightMemory>(OPEN_MEMORY_EVENT, (event) => {
|
||||
setSelectedMemory(spotlightMemoryToPreview(event.payload))
|
||||
}),
|
||||
)
|
||||
.then((handler) => {
|
||||
unlisten = handler
|
||||
})
|
||||
.catch(() => {
|
||||
unlisten = undefined
|
||||
})
|
||||
|
||||
return () => {
|
||||
unlisten?.()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col px-4 pb-10 md:px-6">
|
||||
<div className="mx-auto flex min-h-[calc(100vh-8.5rem)] w-full max-w-5xl flex-col justify-center py-8">
|
||||
|
|
@ -273,6 +295,19 @@ function searchResultToPreview(result: SearchResult): MemoryPreview {
|
|||
}
|
||||
}
|
||||
|
||||
function spotlightMemoryToPreview(memory: SpotlightMemory): MemoryPreview {
|
||||
return {
|
||||
id: memory.id,
|
||||
title: memory.title,
|
||||
summary: memory.summary,
|
||||
content: memory.content,
|
||||
raw: memory.raw,
|
||||
url: memory.url,
|
||||
type: memory.type,
|
||||
createdAt: memory.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value: string | Date) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
|
|
|
|||
|
|
@ -1,23 +1,198 @@
|
|||
"use client"
|
||||
|
||||
import { Search } from "lucide-react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { FileText, Loader2, Search, Sparkles } from "lucide-react"
|
||||
import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { searchMemories, type SearchResult } from "@/lib/search"
|
||||
import {
|
||||
hideSpotlight,
|
||||
openSpotlightResult,
|
||||
SPOTLIGHT_SHOWN_EVENT,
|
||||
type SpotlightMemory,
|
||||
} from "@/lib/spotlight"
|
||||
|
||||
// Phase 1 stub for the frameless spotlight window. Phase 5 wires the Rust
|
||||
// global-shortcut, window show/hide on blur/Esc, input focus on show, and real
|
||||
// /v3/search results.
|
||||
export default function SpotlightPage() {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [query, setQuery] = useState("")
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const trimmedQuery = query.trim()
|
||||
const searchQuery = useQuery({
|
||||
queryKey: ["spotlight-search", trimmedQuery],
|
||||
queryFn: () => searchMemories(trimmedQuery),
|
||||
enabled: trimmedQuery.length > 0,
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
const results = useMemo(
|
||||
() => searchQuery.data?.results.slice(0, 6) ?? [],
|
||||
[searchQuery.data?.results],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined
|
||||
|
||||
const focusInput = () => {
|
||||
setTimeout(() => inputRef.current?.focus(), 0)
|
||||
}
|
||||
|
||||
focusInput()
|
||||
import("@tauri-apps/api/event")
|
||||
.then(({ listen }) => listen(SPOTLIGHT_SHOWN_EVENT, focusInput))
|
||||
.then((handler) => {
|
||||
unlisten = handler
|
||||
})
|
||||
.catch(() => {
|
||||
unlisten = undefined
|
||||
})
|
||||
|
||||
return () => {
|
||||
unlisten?.()
|
||||
}
|
||||
}, [])
|
||||
|
||||
function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
void hideSpotlight()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
setActiveIndex((index) => Math.min(index + 1, results.length - 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
setActiveIndex((index) => Math.max(index - 1, 0))
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && results[activeIndex]) {
|
||||
event.preventDefault()
|
||||
void openResult(results[activeIndex])
|
||||
}
|
||||
}
|
||||
|
||||
async function openResult(result: SearchResult) {
|
||||
await openSpotlightResult(searchResultToSpotlightMemory(result))
|
||||
setQuery("")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-start justify-center bg-transparent p-3">
|
||||
<div className="w-full overflow-hidden rounded-xl border border-border/60 bg-popover/95 shadow-2xl backdrop-blur">
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<Search className="size-4 shrink-0 text-muted-foreground" />
|
||||
<div className="w-full overflow-hidden rounded-2xl border border-white/[0.10] bg-[#090D14]/96 shadow-[0_26px_90px_rgba(0,0,0,0.44),inset_1px_1px_1px_rgba(255,255,255,0.06)] backdrop-blur-2xl">
|
||||
<div className="flex items-center gap-3 border-white/[0.06] border-b px-4 py-3">
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-[#4BA0FA]/12 text-[#8BC6FF]">
|
||||
<Search className="size-4" />
|
||||
</div>
|
||||
<input
|
||||
aria-label="Search your memories"
|
||||
placeholder="Search your memories…"
|
||||
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
ref={inputRef}
|
||||
aria-label="Ask or search your memories"
|
||||
value={query}
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value)
|
||||
setActiveIndex(0)
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Ask or search your memories..."
|
||||
className="min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-[#6F7885]"
|
||||
/>
|
||||
<div className="hidden items-center gap-1.5 rounded-full border border-white/[0.06] bg-white/[0.03] px-2.5 py-1 text-[11px] text-fg-subtle sm:flex">
|
||||
<Sparkles className="size-3 text-[#4BA0FA]" />
|
||||
Spotlight
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[220px] p-2">
|
||||
{!trimmedQuery ? (
|
||||
<div className="flex h-[220px] flex-col items-center justify-center text-center">
|
||||
<p className="font-medium text-fg-primary text-sm">
|
||||
Search by meaning, title, or question.
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-fg-subtle">
|
||||
Press Enter to open a result in the main window.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{searchQuery.isFetching ? (
|
||||
<div className="flex items-center gap-2 px-3 py-2.5 text-fg-subtle text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Searching...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{searchQuery.isError ? (
|
||||
<div className="px-3 py-2.5 text-destructive text-sm">
|
||||
Search failed. Check your token and API URL.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{trimmedQuery && !searchQuery.isFetching && results.length === 0 ? (
|
||||
<div className="px-3 py-2.5 text-fg-subtle text-sm">
|
||||
No results found.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ul className="space-y-0.5">
|
||||
{results.map((result, index) => {
|
||||
const title = result.title ?? result.documentId
|
||||
const preview =
|
||||
result.summary ??
|
||||
result.content ??
|
||||
result.chunks.find((chunk) => chunk.isRelevant)?.content
|
||||
const active = index === activeIndex
|
||||
|
||||
return (
|
||||
<li key={result.documentId}>
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={() => setActiveIndex(index)}
|
||||
onClick={() => void openResult(result)}
|
||||
className={[
|
||||
"group flex w-full items-start gap-3 rounded-xl px-3 py-2.5 text-left transition-colors",
|
||||
active ? "bg-white/[0.07]" : "hover:bg-white/[0.04]",
|
||||
].join(" ")}
|
||||
>
|
||||
<span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg bg-surface-card ring-1 ring-surface-border">
|
||||
<FileText className="size-3.5 text-fg-subtle" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium text-fg-primary text-sm">
|
||||
{title}
|
||||
</span>
|
||||
{preview ? (
|
||||
<span className="mt-0.5 line-clamp-2 block text-[12px] text-fg-subtle leading-relaxed">
|
||||
{preview}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function searchResultToSpotlightMemory(result: SearchResult): SpotlightMemory {
|
||||
return {
|
||||
id: result.documentId,
|
||||
title: result.title,
|
||||
summary: result.summary,
|
||||
content:
|
||||
result.content ??
|
||||
result.chunks.find((chunk) => chunk.isRelevant)?.content ??
|
||||
null,
|
||||
type: result.type,
|
||||
createdAt: normalizeDate(result.createdAt),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDate(value: string | Date) {
|
||||
return value instanceof Date ? value.toISOString() : value
|
||||
}
|
||||
|
|
|
|||
29
apps/desktop/lib/spotlight.ts
Normal file
29
apps/desktop/lib/spotlight.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client"
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
|
||||
export type SpotlightMemory = {
|
||||
id: string
|
||||
title: string | null
|
||||
summary?: string | null
|
||||
content?: string | null
|
||||
raw?: string | null
|
||||
url?: string | null
|
||||
type?: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const OPEN_MEMORY_EVENT = "nav:open-memory"
|
||||
export const SPOTLIGHT_SHOWN_EVENT = "spotlight:shown"
|
||||
|
||||
export function showSpotlight() {
|
||||
return invoke("spotlight_show")
|
||||
}
|
||||
|
||||
export function hideSpotlight() {
|
||||
return invoke("spotlight_hide")
|
||||
}
|
||||
|
||||
export function openSpotlightResult(memory: SpotlightMemory) {
|
||||
return invoke("spotlight_open_result", { memory })
|
||||
}
|
||||
112
apps/desktop/src-tauri/Cargo.lock
generated
112
apps/desktop/src-tauri/Cargo.lock
generated
|
|
@ -724,6 +724,16 @@ dependencies = [
|
|||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
|
|
@ -993,6 +1003,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
|
|
@ -1116,6 +1136,24 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "global-hotkey"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"keyboard-types",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.59.0",
|
||||
"x11rb",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
|
|
@ -1683,6 +1721,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
|
|
@ -2598,6 +2642,19 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.41"
|
||||
|
|
@ -3061,6 +3118,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-global-shortcut",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3298,6 +3356,37 @@ dependencies = [
|
|||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
"plist",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.3"
|
||||
|
|
@ -4672,6 +4761,29 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ keyring = "3"
|
|||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Core capabilities granted to the main window.",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "spotlight"],
|
||||
"permissions": ["core:default", "core:window:allow-start-dragging"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod auth;
|
||||
mod spotlight;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -43,15 +44,42 @@ async fn auth_whoami() -> Result<auth::AuthSession, String> {
|
|||
auth::whoami().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn spotlight_show(app: tauri::AppHandle) -> Result<(), String> {
|
||||
spotlight::show(&app).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn spotlight_hide(app: tauri::AppHandle) -> Result<(), String> {
|
||||
spotlight::hide(&app).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn spotlight_open_result(
|
||||
app: tauri::AppHandle,
|
||||
memory: spotlight::SpotlightMemory,
|
||||
) -> Result<(), String> {
|
||||
spotlight::open_result(&app, memory).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.setup(|app| {
|
||||
spotlight::create_window(app)?;
|
||||
spotlight::register_shortcut(app);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
app_info,
|
||||
auth_store_token,
|
||||
auth_get_token,
|
||||
auth_clear,
|
||||
auth_whoami
|
||||
auth_whoami,
|
||||
spotlight_show,
|
||||
spotlight_hide,
|
||||
spotlight_open_result
|
||||
])
|
||||
// Bootstrap failure is unrecoverable (no window, no app), so we abort
|
||||
// loudly here. This is the one sanctioned `expect` — see roadmap quality bar.
|
||||
|
|
|
|||
113
apps/desktop/src-tauri/src/spotlight.rs
Normal file
113
apps/desktop/src-tauri/src/spotlight.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
App, AppHandle, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
||||
WindowEvent,
|
||||
};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
|
||||
|
||||
const MAIN_LABEL: &str = "main";
|
||||
const SPOTLIGHT_LABEL: &str = "spotlight";
|
||||
const OPEN_MEMORY_EVENT: &str = "nav:open-memory";
|
||||
const SHOWN_EVENT: &str = "spotlight:shown";
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpotlightMemory {
|
||||
id: String,
|
||||
title: Option<String>,
|
||||
summary: Option<String>,
|
||||
content: Option<String>,
|
||||
raw: Option<String>,
|
||||
url: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
memory_type: Option<String>,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
pub fn shortcut() -> Shortcut {
|
||||
Shortcut::new(Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyM)
|
||||
}
|
||||
|
||||
pub fn create_window(app: &App) -> tauri::Result<WebviewWindow> {
|
||||
if let Some(window) = app.get_webview_window(SPOTLIGHT_LABEL) {
|
||||
return Ok(window);
|
||||
}
|
||||
|
||||
let window = WebviewWindowBuilder::new(
|
||||
app,
|
||||
SPOTLIGHT_LABEL,
|
||||
WebviewUrl::App("spotlight/".into()),
|
||||
)
|
||||
.title("Supermemory Spotlight")
|
||||
.inner_size(760.0, 420.0)
|
||||
.min_inner_size(560.0, 220.0)
|
||||
.decorations(false)
|
||||
.always_on_top(true)
|
||||
.skip_taskbar(true)
|
||||
.visible(false)
|
||||
.center()
|
||||
.build()?;
|
||||
|
||||
let window_for_blur = window.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if matches!(event, WindowEvent::Focused(false)) {
|
||||
let _ = window_for_blur.hide();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
pub fn register_shortcut(app: &App) {
|
||||
let result = app
|
||||
.global_shortcut()
|
||||
.on_shortcut(shortcut(), |app, shortcut, event| {
|
||||
if event.state == ShortcutState::Pressed && shortcut.matches(
|
||||
Modifiers::SUPER | Modifiers::SHIFT,
|
||||
Code::KeyM,
|
||||
) {
|
||||
let _ = toggle(app);
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(error) = result {
|
||||
eprintln!("failed to register Supermemory spotlight shortcut: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(app: &AppHandle) -> tauri::Result<()> {
|
||||
let window = spotlight_window(app)?;
|
||||
if window.is_visible()? {
|
||||
window.hide()
|
||||
} else {
|
||||
show(app)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &AppHandle) -> tauri::Result<()> {
|
||||
let window = spotlight_window(app)?;
|
||||
window.center()?;
|
||||
window.show()?;
|
||||
window.set_focus()?;
|
||||
window.emit(SHOWN_EVENT, ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(app: &AppHandle) -> tauri::Result<()> {
|
||||
spotlight_window(app)?.hide()
|
||||
}
|
||||
|
||||
pub fn open_result(app: &AppHandle, memory: SpotlightMemory) -> tauri::Result<()> {
|
||||
if let Some(main) = app.get_webview_window(MAIN_LABEL) {
|
||||
main.show()?;
|
||||
main.set_focus()?;
|
||||
main.emit(OPEN_MEMORY_EVENT, memory)?;
|
||||
}
|
||||
|
||||
hide(app)
|
||||
}
|
||||
|
||||
fn spotlight_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
|
||||
app.get_webview_window(SPOTLIGHT_LABEL)
|
||||
.ok_or_else(|| tauri::Error::WindowNotFound)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue