Delete fastsearch-tauri
Browse files- fastsearch-tauri/create_icons.py +0 -101
- fastsearch-tauri/src-tauri/Cargo.toml +0 -28
- fastsearch-tauri/src-tauri/build.rs +0 -3
- fastsearch-tauri/src-tauri/capabilities/main.json +0 -19
- fastsearch-tauri/src-tauri/src/main.rs +0 -439
- fastsearch-tauri/src-tauri/tauri.conf.json +0 -50
- fastsearch-tauri/ui/app.js +0 -262
- fastsearch-tauri/ui/index.html +0 -85
- fastsearch-tauri/ui/styles.css +0 -387
fastsearch-tauri/create_icons.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Create minimal placeholder icon files for Tauri Windows build.
|
| 3 |
-
|
| 4 |
-
Tauri v2's tauri-build requires icons/ directory with at least icon.ico
|
| 5 |
-
for Windows resource embedding. Run this script before building.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
cd fastsearch-tauri
|
| 9 |
-
python create_icons.py
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import struct
|
| 13 |
-
import os
|
| 14 |
-
import zlib
|
| 15 |
-
|
| 16 |
-
def create_png(width, height):
|
| 17 |
-
"""Create a minimal valid transparent PNG."""
|
| 18 |
-
# IHDR
|
| 19 |
-
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0)
|
| 20 |
-
ihdr_crc = struct.pack('>I', zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff)
|
| 21 |
-
ihdr = struct.pack('>I', 13) + b'IHDR' + ihdr_data + ihdr_crc
|
| 22 |
-
|
| 23 |
-
# IDAT - transparent black RGBA
|
| 24 |
-
raw = b''
|
| 25 |
-
for _ in range(height):
|
| 26 |
-
raw += b'\x00' # Filter: None
|
| 27 |
-
raw += b'\x00\x00\x00\x00' * width # Transparent RGBA
|
| 28 |
-
|
| 29 |
-
compressed = zlib.compress(raw)
|
| 30 |
-
idat_crc = struct.pack('>I', zlib.crc32(b'IDAT' + compressed) & 0xffffffff)
|
| 31 |
-
idat = struct.pack('>I', len(compressed)) + b'IDAT' + compressed + idat_crc
|
| 32 |
-
|
| 33 |
-
# IEND
|
| 34 |
-
iend_crc = struct.pack('>I', zlib.crc32(b'IEND') & 0xffffffff)
|
| 35 |
-
iend = struct.pack('>I', 0) + b'IEND' + iend_crc
|
| 36 |
-
|
| 37 |
-
return b'\x89PNG\r\n\x1a\n' + ihdr + idat + iend
|
| 38 |
-
|
| 39 |
-
def create_ico(width=32, height=32):
|
| 40 |
-
"""Create a minimal valid transparent ICO file."""
|
| 41 |
-
# ICO header
|
| 42 |
-
header = struct.pack('<HHH', 0, 1, 1)
|
| 43 |
-
|
| 44 |
-
# BITMAPINFOHEADER (40 bytes)
|
| 45 |
-
infoheader = struct.pack('<IiiHHIIiiII', 40, width, height * 2, 1, 32, 0, 0, 0, 0, 0, 0)
|
| 46 |
-
|
| 47 |
-
# XOR mask (BGRA pixels) + AND mask (1bpp)
|
| 48 |
-
xor_pixels = b'\x00\x00\x00\x00' * (width * height)
|
| 49 |
-
|
| 50 |
-
row_bytes = (width + 7) // 8
|
| 51 |
-
pad = (4 - (row_bytes % 4)) % 4
|
| 52 |
-
and_mask = b''
|
| 53 |
-
for _ in range(height):
|
| 54 |
-
and_mask += b'\x00' * row_bytes + b'\x00' * pad
|
| 55 |
-
|
| 56 |
-
img_data = infoheader + xor_pixels + and_mask
|
| 57 |
-
img_size = len(img_data)
|
| 58 |
-
|
| 59 |
-
# ICONDIRENTRY
|
| 60 |
-
entry = struct.pack('<BBBBHHII',
|
| 61 |
-
width if width < 256 else 0,
|
| 62 |
-
height if height < 256 else 0,
|
| 63 |
-
0, 0, 1, 32,
|
| 64 |
-
img_size,
|
| 65 |
-
22 # Offset = header(6) + entry(16)
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
return header + entry + img_data
|
| 69 |
-
|
| 70 |
-
def main():
|
| 71 |
-
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 72 |
-
out_dir = os.path.join(script_dir, 'src-tauri', 'icons')
|
| 73 |
-
os.makedirs(out_dir, exist_ok=True)
|
| 74 |
-
|
| 75 |
-
# Create PNG icons
|
| 76 |
-
for size in [32, 128]:
|
| 77 |
-
data = create_png(size, size)
|
| 78 |
-
path = os.path.join(out_dir, f'{size}x{size}.png')
|
| 79 |
-
with open(path, 'wb') as f:
|
| 80 |
-
f.write(data)
|
| 81 |
-
print(f"Created {path}")
|
| 82 |
-
|
| 83 |
-
# Create 128@2x (256x256)
|
| 84 |
-
data = create_png(256, 256)
|
| 85 |
-
path = os.path.join(out_dir, '128x128@2x.png')
|
| 86 |
-
with open(path, 'wb') as f:
|
| 87 |
-
f.write(data)
|
| 88 |
-
print(f"Created {path}")
|
| 89 |
-
|
| 90 |
-
# Create ICO
|
| 91 |
-
data = create_ico(32, 32)
|
| 92 |
-
path = os.path.join(out_dir, 'icon.ico')
|
| 93 |
-
with open(path, 'wb') as f:
|
| 94 |
-
f.write(data)
|
| 95 |
-
print(f"Created {path}")
|
| 96 |
-
|
| 97 |
-
print(f"\nAll placeholder icons created in {out_dir}")
|
| 98 |
-
print("Replace these with real icons for production.")
|
| 99 |
-
|
| 100 |
-
if __name__ == '__main__':
|
| 101 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/src-tauri/Cargo.toml
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
[package]
|
| 2 |
-
name = "fastsearch-tauri"
|
| 3 |
-
version = "0.1.0"
|
| 4 |
-
description = "FastSeek - macOS Spotlight-style file search for Windows"
|
| 5 |
-
authors = ["anshdadhich"]
|
| 6 |
-
license = "MIT"
|
| 7 |
-
edition = "2021"
|
| 8 |
-
rust-version = "1.77.2"
|
| 9 |
-
|
| 10 |
-
[build-dependencies]
|
| 11 |
-
tauri-build = { version = "2", features = [] }
|
| 12 |
-
|
| 13 |
-
[dependencies]
|
| 14 |
-
tauri = { version = "2", features = ["tray-icon"] }
|
| 15 |
-
tauri-plugin-global-shortcut = "2"
|
| 16 |
-
tauri-plugin-opener = "2"
|
| 17 |
-
serde = { version = "1", features = ["derive"] }
|
| 18 |
-
serde_json = "1"
|
| 19 |
-
once_cell = "1.19"
|
| 20 |
-
parking_lot = "0.12"
|
| 21 |
-
crossbeam-channel = "0.5"
|
| 22 |
-
lz4_flex = "0.11"
|
| 23 |
-
bincode = "1.3"
|
| 24 |
-
fastsearch-core = { path = "../../fastsearch-core" }
|
| 25 |
-
|
| 26 |
-
[features]
|
| 27 |
-
default = ["custom-protocol"]
|
| 28 |
-
custom-protocol = ["tauri/custom-protocol"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/src-tauri/build.rs
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
fn main() {
|
| 2 |
-
tauri_build::build()
|
| 3 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/src-tauri/capabilities/main.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"identifier": "main",
|
| 3 |
-
"description": "Main FastSeek window capabilities",
|
| 4 |
-
"windows": ["main"],
|
| 5 |
-
"permissions": [
|
| 6 |
-
"core:default",
|
| 7 |
-
"core:window:allow-show",
|
| 8 |
-
"core:window:allow-hide",
|
| 9 |
-
"core:window:allow-set-focus",
|
| 10 |
-
"core:window:allow-set-always-on-top",
|
| 11 |
-
"core:window:allow-set-position",
|
| 12 |
-
"core:window:allow-close",
|
| 13 |
-
"core:window:allow-is-visible",
|
| 14 |
-
"global-shortcut:allow-register",
|
| 15 |
-
"global-shortcut:allow-is-registered",
|
| 16 |
-
"global-shortcut:allow-unregister",
|
| 17 |
-
"opener:default"
|
| 18 |
-
]
|
| 19 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/src-tauri/src/main.rs
DELETED
|
@@ -1,439 +0,0 @@
|
|
| 1 |
-
// Prevents additional console window on Windows in release
|
| 2 |
-
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
| 3 |
-
|
| 4 |
-
use std::sync::Arc;
|
| 5 |
-
use parking_lot::RwLock;
|
| 6 |
-
use crossbeam_channel::unbounded;
|
| 7 |
-
use once_cell::sync::Lazy;
|
| 8 |
-
use tauri::{
|
| 9 |
-
menu::{MenuBuilder, MenuItemBuilder},
|
| 10 |
-
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
| 11 |
-
Manager, PhysicalPosition,
|
| 12 |
-
};
|
| 13 |
-
use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};
|
| 14 |
-
use serde::{Serialize, Deserialize};
|
| 15 |
-
|
| 16 |
-
use fastsearch_core::index::store::IndexStore;
|
| 17 |
-
use fastsearch_core::index::search::search;
|
| 18 |
-
use fastsearch_core::mft::reader::MftReader;
|
| 19 |
-
use fastsearch_core::mft::watcher::UsnWatcher;
|
| 20 |
-
use fastsearch_core::mft::types::{IndexEvent, JournalCheckpoint};
|
| 21 |
-
use fastsearch_core::utils::drives::get_ntfs_drives;
|
| 22 |
-
|
| 23 |
-
// -- Global state --------------------------------------------------
|
| 24 |
-
static INDEX: Lazy<Arc<RwLock<IndexStore>>> = Lazy::new(|| {
|
| 25 |
-
Arc::new(RwLock::new(IndexStore::new()))
|
| 26 |
-
});
|
| 27 |
-
|
| 28 |
-
static LIVE_CHECKPOINTS: Lazy<Arc<parking_lot::Mutex<Vec<JournalCheckpoint>>>> = Lazy::new(|| {
|
| 29 |
-
Arc::new(parking_lot::Mutex::new(Vec::new()))
|
| 30 |
-
});
|
| 31 |
-
|
| 32 |
-
static EXCLUDED_DIRS: Lazy<Arc<RwLock<Vec<String>>>> = Lazy::new(|| {
|
| 33 |
-
Arc::new(RwLock::new(load_exclusions()))
|
| 34 |
-
});
|
| 35 |
-
|
| 36 |
-
// -- Search response types ----------------------------------------
|
| 37 |
-
#[derive(Serialize, Clone)]
|
| 38 |
-
struct SearchResponse {
|
| 39 |
-
results: Vec<SearchItem>,
|
| 40 |
-
elapsed_ms: f64,
|
| 41 |
-
total_indexed: usize,
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
#[derive(Serialize, Clone)]
|
| 45 |
-
struct SearchItem {
|
| 46 |
-
name: String,
|
| 47 |
-
path: String,
|
| 48 |
-
is_dir: bool,
|
| 49 |
-
rank: u8,
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
#[derive(Deserialize)]
|
| 53 |
-
struct SearchRequest {
|
| 54 |
-
query: String,
|
| 55 |
-
#[serde(default)]
|
| 56 |
-
limit: usize,
|
| 57 |
-
#[serde(default)]
|
| 58 |
-
case_sensitive: bool,
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
// -- Tauri Commands ------------------------------------------------
|
| 62 |
-
#[tauri::command]
|
| 63 |
-
fn cmd_search(request: SearchRequest) -> SearchResponse {
|
| 64 |
-
let store = INDEX.read();
|
| 65 |
-
let excluded = EXCLUDED_DIRS.read();
|
| 66 |
-
let limit = if request.limit == 0 { 50 } else { request.limit };
|
| 67 |
-
|
| 68 |
-
let start = std::time::Instant::now();
|
| 69 |
-
let raw = search(&store, &request.query, limit * 2, request.case_sensitive, &excluded);
|
| 70 |
-
let elapsed = start.elapsed();
|
| 71 |
-
|
| 72 |
-
let results: Vec<SearchItem> = raw.into_iter()
|
| 73 |
-
.take(limit)
|
| 74 |
-
.map(|r| SearchItem {
|
| 75 |
-
name: r.name,
|
| 76 |
-
path: r.full_path.to_string_lossy().to_string(),
|
| 77 |
-
is_dir: r.is_dir,
|
| 78 |
-
rank: r.rank,
|
| 79 |
-
})
|
| 80 |
-
.collect();
|
| 81 |
-
|
| 82 |
-
SearchResponse {
|
| 83 |
-
results,
|
| 84 |
-
elapsed_ms: elapsed.as_secs_f64() * 1000.0,
|
| 85 |
-
total_indexed: store.len(),
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
#[tauri::command]
|
| 90 |
-
fn cmd_get_stats() -> serde_json::Value {
|
| 91 |
-
let store = INDEX.read();
|
| 92 |
-
serde_json::json!({
|
| 93 |
-
"total_indexed": store.len(),
|
| 94 |
-
"checkpoints": store.checkpoints.len(),
|
| 95 |
-
})
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
#[tauri::command]
|
| 99 |
-
fn cmd_rescan() {
|
| 100 |
-
let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
|
| 101 |
-
let _ = std::fs::remove_file(&cache_path);
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
#[tauri::command]
|
| 105 |
-
fn cmd_toggle_window(window: tauri::WebviewWindow) {
|
| 106 |
-
toggle_window(&window);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
#[tauri::command]
|
| 110 |
-
fn cmd_open_file(path: String) {
|
| 111 |
-
let _ = std::process::Command::new("explorer")
|
| 112 |
-
.arg("/select,")
|
| 113 |
-
.arg(&path)
|
| 114 |
-
.spawn();
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
#[tauri::command]
|
| 118 |
-
fn cmd_open_folder(path: String) {
|
| 119 |
-
let _ = std::process::Command::new("explorer")
|
| 120 |
-
.arg(&path)
|
| 121 |
-
.spawn();
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
#[tauri::command]
|
| 125 |
-
fn cmd_add_exclusion(path: String) {
|
| 126 |
-
let mut excluded = EXCLUDED_DIRS.write();
|
| 127 |
-
let path = path.to_lowercase();
|
| 128 |
-
let path = if path.ends_with('\\') || path.ends_with('/') {
|
| 129 |
-
path
|
| 130 |
-
} else {
|
| 131 |
-
format!("{}\\", path)
|
| 132 |
-
};
|
| 133 |
-
if !excluded.contains(&path) {
|
| 134 |
-
excluded.push(path.clone());
|
| 135 |
-
save_exclusions(&excluded);
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
#[tauri::command]
|
| 140 |
-
fn cmd_remove_exclusion(path: String) {
|
| 141 |
-
let mut excluded = EXCLUDED_DIRS.write();
|
| 142 |
-
let path = path.to_lowercase();
|
| 143 |
-
excluded.retain(|d| d != &path);
|
| 144 |
-
save_exclusions(&excluded);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
#[tauri::command]
|
| 148 |
-
fn cmd_get_exclusions() -> Vec<String> {
|
| 149 |
-
EXCLUDED_DIRS.read().clone()
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
// -- Window helpers ------------------------------------------------
|
| 153 |
-
fn toggle_window(window: &tauri::WebviewWindow) {
|
| 154 |
-
if window.is_visible().unwrap_or(false) {
|
| 155 |
-
let _ = window.hide();
|
| 156 |
-
} else {
|
| 157 |
-
if let Some(monitor) = window.primary_monitor().unwrap_or(None) {
|
| 158 |
-
let size = monitor.size();
|
| 159 |
-
let win_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(800, 520));
|
| 160 |
-
let x = (size.width as i32 - win_size.width as i32) / 2;
|
| 161 |
-
let y = (size.height as i32 - win_size.height as i32) / 3;
|
| 162 |
-
let _ = window.set_position(PhysicalPosition::new(x, y));
|
| 163 |
-
}
|
| 164 |
-
let _ = window.show();
|
| 165 |
-
let _ = window.set_focus();
|
| 166 |
-
}
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
fn show_window(window: &tauri::WebviewWindow) {
|
| 170 |
-
if !window.is_visible().unwrap_or(false) {
|
| 171 |
-
let _ = window.show();
|
| 172 |
-
let _ = window.set_focus();
|
| 173 |
-
}
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
// -- Config helpers ------------------------------------------------
|
| 177 |
-
fn config_dir() -> std::path::PathBuf {
|
| 178 |
-
let dir = std::env::var("APPDATA")
|
| 179 |
-
.map(std::path::PathBuf::from)
|
| 180 |
-
.unwrap_or_else(|_| std::env::temp_dir())
|
| 181 |
-
.join("fastseek");
|
| 182 |
-
let _ = std::fs::create_dir_all(&dir);
|
| 183 |
-
dir
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
fn load_exclusions() -> Vec<String> {
|
| 187 |
-
let path = config_dir().join("exclusions.txt");
|
| 188 |
-
std::fs::read_to_string(&path)
|
| 189 |
-
.unwrap_or_default()
|
| 190 |
-
.lines()
|
| 191 |
-
.map(|l| l.trim().to_lowercase())
|
| 192 |
-
.filter(|l| !l.is_empty())
|
| 193 |
-
.collect()
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
fn save_exclusions(dirs: &[String]) {
|
| 197 |
-
let path = config_dir().join("exclusions.txt");
|
| 198 |
-
let content = dirs.join("\n");
|
| 199 |
-
let _ = std::fs::write(&path, content);
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
// -- Index initialization (background thread) -------------------
|
| 203 |
-
fn init_index() {
|
| 204 |
-
std::thread::spawn(|| {
|
| 205 |
-
let drives = get_ntfs_drives();
|
| 206 |
-
if drives.is_empty() {
|
| 207 |
-
eprintln!("No NTFS drives found. Run as Administrator.");
|
| 208 |
-
return;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
let (tx, rx) = unbounded();
|
| 212 |
-
let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
|
| 213 |
-
|
| 214 |
-
// Try loading from cache
|
| 215 |
-
let cache_loaded = if cache_path.exists() {
|
| 216 |
-
match std::fs::read(&cache_path) {
|
| 217 |
-
Ok(compressed) => {
|
| 218 |
-
match lz4_flex::decompress_size_prepended(&compressed) {
|
| 219 |
-
Ok(bytes) => {
|
| 220 |
-
match bincode::deserialize::<fastsearch_core::index::store::CacheData>(&bytes) {
|
| 221 |
-
Ok(cache) => {
|
| 222 |
-
let checkpoints = cache.checkpoints.clone();
|
| 223 |
-
*INDEX.write() = IndexStore::from_cache(cache);
|
| 224 |
-
|
| 225 |
-
// Delta catch-up
|
| 226 |
-
if !checkpoints.is_empty() {
|
| 227 |
-
let (delta_tx, delta_rx) = unbounded::<IndexEvent>();
|
| 228 |
-
let mut journal_ok = true;
|
| 229 |
-
|
| 230 |
-
for drive in &drives {
|
| 231 |
-
let cp = checkpoints.iter()
|
| 232 |
-
.find(|c| c.drive_letter == drive.letter);
|
| 233 |
-
|
| 234 |
-
if let Some(cp) = cp {
|
| 235 |
-
match UsnWatcher::new_from(drive, delta_tx.clone(), Some(cp)) {
|
| 236 |
-
Ok(mut watcher) => {
|
| 237 |
-
watcher.drain();
|
| 238 |
-
let new_cp = watcher.checkpoint();
|
| 239 |
-
let mut store = INDEX.write();
|
| 240 |
-
store.checkpoints.retain(|c| c.drive_letter != drive.letter);
|
| 241 |
-
store.checkpoints.push(new_cp);
|
| 242 |
-
}
|
| 243 |
-
Err(_) => {
|
| 244 |
-
let _ = std::fs::remove_file(&cache_path);
|
| 245 |
-
journal_ok = false;
|
| 246 |
-
break;
|
| 247 |
-
}
|
| 248 |
-
}
|
| 249 |
-
} else {
|
| 250 |
-
let _ = std::fs::remove_file(&cache_path);
|
| 251 |
-
journal_ok = false;
|
| 252 |
-
break;
|
| 253 |
-
}
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
drop(delta_tx);
|
| 257 |
-
|
| 258 |
-
if journal_ok {
|
| 259 |
-
let mut store = INDEX.write();
|
| 260 |
-
for event in delta_rx {
|
| 261 |
-
match event {
|
| 262 |
-
IndexEvent::Created(r) => store.insert(r),
|
| 263 |
-
IndexEvent::Deleted(id) => store.remove(id),
|
| 264 |
-
IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record),
|
| 265 |
-
IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
|
| 266 |
-
store.apply_move(file_ref, new_parent_ref, name, kind);
|
| 267 |
-
}
|
| 268 |
-
}
|
| 269 |
-
}
|
| 270 |
-
true
|
| 271 |
-
} else {
|
| 272 |
-
false
|
| 273 |
-
}
|
| 274 |
-
} else {
|
| 275 |
-
true
|
| 276 |
-
}
|
| 277 |
-
}
|
| 278 |
-
Err(_) => false
|
| 279 |
-
}
|
| 280 |
-
}
|
| 281 |
-
Err(_) => false
|
| 282 |
-
}
|
| 283 |
-
}
|
| 284 |
-
Err(_) => false
|
| 285 |
-
}
|
| 286 |
-
} else {
|
| 287 |
-
false
|
| 288 |
-
};
|
| 289 |
-
|
| 290 |
-
// Full MFT scan if no cache
|
| 291 |
-
if !cache_loaded {
|
| 292 |
-
{
|
| 293 |
-
let mut store = INDEX.write();
|
| 294 |
-
for drive in &drives {
|
| 295 |
-
let (dummy_tx, _) = unbounded::<IndexEvent>();
|
| 296 |
-
if let Ok(w) = UsnWatcher::new(drive, dummy_tx) {
|
| 297 |
-
store.checkpoints.push(w.checkpoint());
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
let index_clone = Arc::clone(&INDEX);
|
| 303 |
-
let drives_clone = drives.clone();
|
| 304 |
-
|
| 305 |
-
for drive in &drives_clone {
|
| 306 |
-
let reader = match MftReader::open(drive) {
|
| 307 |
-
Ok(r) => r,
|
| 308 |
-
Err(_) => continue,
|
| 309 |
-
};
|
| 310 |
-
|
| 311 |
-
let scan = match reader.scan_direct() {
|
| 312 |
-
Some(s) => s,
|
| 313 |
-
None => reader.scan(),
|
| 314 |
-
};
|
| 315 |
-
|
| 316 |
-
let mut store = index_clone.write();
|
| 317 |
-
store.populate_from_scan(scan, &drive.root);
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
{
|
| 321 |
-
let mut store = index_clone.write();
|
| 322 |
-
store.finalize();
|
| 323 |
-
|
| 324 |
-
let cache = store.to_cache();
|
| 325 |
-
if let Ok(bytes) = bincode::serialize(&cache) {
|
| 326 |
-
let compressed = lz4_flex::compress_prepend_size(&bytes);
|
| 327 |
-
let _ = std::fs::write(&cache_path, &compressed);
|
| 328 |
-
}
|
| 329 |
-
}
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
// Start USN watchers for live updates
|
| 333 |
-
let live_cps = Arc::clone(&LIVE_CHECKPOINTS);
|
| 334 |
-
for drive in &drives {
|
| 335 |
-
let tx_clone = tx.clone();
|
| 336 |
-
let drive_clone = drive.clone();
|
| 337 |
-
let cps = Arc::clone(&live_cps);
|
| 338 |
-
std::thread::spawn(move || {
|
| 339 |
-
if let Ok(mut watcher) = UsnWatcher::new(&drive_clone, tx_clone) {
|
| 340 |
-
watcher.run_shared(cps);
|
| 341 |
-
}
|
| 342 |
-
});
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
// Apply live updates
|
| 346 |
-
let index_live = Arc::clone(&INDEX);
|
| 347 |
-
std::thread::spawn(move || {
|
| 348 |
-
for event in rx {
|
| 349 |
-
let mut store = index_live.write();
|
| 350 |
-
match event {
|
| 351 |
-
IndexEvent::Created(r) => store.insert(r),
|
| 352 |
-
IndexEvent::Deleted(id) => store.remove(id),
|
| 353 |
-
IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record),
|
| 354 |
-
IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
|
| 355 |
-
store.apply_move(file_ref, new_parent_ref, name, kind);
|
| 356 |
-
}
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
});
|
| 360 |
-
});
|
| 361 |
-
}
|
| 362 |
-
|
| 363 |
-
// -- Main ----------------------------------------------------------
|
| 364 |
-
fn main() {
|
| 365 |
-
tauri::Builder::default()
|
| 366 |
-
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
| 367 |
-
.plugin(tauri_plugin_opener::init())
|
| 368 |
-
.setup(|app| {
|
| 369 |
-
// Initialize index in background
|
| 370 |
-
init_index();
|
| 371 |
-
|
| 372 |
-
// -- Tray Icon -----------------------------------------
|
| 373 |
-
let show = MenuItemBuilder::with_id("show", "Show").build(app)?;
|
| 374 |
-
let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?;
|
| 375 |
-
let menu = MenuBuilder::new(app).items(&[&show, &quit]).build()?;
|
| 376 |
-
|
| 377 |
-
let _tray = TrayIconBuilder::new()
|
| 378 |
-
.menu(&menu)
|
| 379 |
-
.on_menu_event(|app, event| match event.id().as_ref() {
|
| 380 |
-
"show" => {
|
| 381 |
-
if let Some(win) = app.get_webview_window("main") {
|
| 382 |
-
let _ = win.show();
|
| 383 |
-
let _ = win.set_focus();
|
| 384 |
-
}
|
| 385 |
-
}
|
| 386 |
-
"quit" => {
|
| 387 |
-
app.exit(0);
|
| 388 |
-
}
|
| 389 |
-
_ => {}
|
| 390 |
-
})
|
| 391 |
-
.on_tray_icon_event(|tray, event| {
|
| 392 |
-
if let TrayIconEvent::Click {
|
| 393 |
-
button: MouseButton::Left,
|
| 394 |
-
button_state: MouseButtonState::Up,
|
| 395 |
-
..
|
| 396 |
-
} = event {
|
| 397 |
-
let app = tray.app_handle();
|
| 398 |
-
if let Some(win) = app.get_webview_window("main") {
|
| 399 |
-
toggle_window(&win);
|
| 400 |
-
}
|
| 401 |
-
}
|
| 402 |
-
})
|
| 403 |
-
.build(app)?;
|
| 404 |
-
|
| 405 |
-
// -- Global Shortcut (Win+Space) ---------------------
|
| 406 |
-
app.handle().plugin(
|
| 407 |
-
tauri_plugin_global_shortcut::Builder::new()
|
| 408 |
-
.with_handler(|app, shortcut, event| {
|
| 409 |
-
if shortcut.matches(tauri_plugin_global_shortcut::Modifiers::SUPER, tauri_plugin_global_shortcut::Code::Space) {
|
| 410 |
-
if event.state == ShortcutState::Pressed {
|
| 411 |
-
if let Some(win) = app.get_webview_window("main") {
|
| 412 |
-
toggle_window(&win);
|
| 413 |
-
}
|
| 414 |
-
}
|
| 415 |
-
}
|
| 416 |
-
})
|
| 417 |
-
.build(),
|
| 418 |
-
)?;
|
| 419 |
-
|
| 420 |
-
// Register Win+Space shortcut
|
| 421 |
-
let shortcut_manager = app.global_shortcut();
|
| 422 |
-
let _ = shortcut_manager.register("Super+Space");
|
| 423 |
-
|
| 424 |
-
Ok(())
|
| 425 |
-
})
|
| 426 |
-
.invoke_handler(tauri::generate_handler![
|
| 427 |
-
cmd_search,
|
| 428 |
-
cmd_get_stats,
|
| 429 |
-
cmd_rescan,
|
| 430 |
-
cmd_toggle_window,
|
| 431 |
-
cmd_open_file,
|
| 432 |
-
cmd_open_folder,
|
| 433 |
-
cmd_add_exclusion,
|
| 434 |
-
cmd_remove_exclusion,
|
| 435 |
-
cmd_get_exclusions
|
| 436 |
-
])
|
| 437 |
-
.run(tauri::generate_context!())
|
| 438 |
-
.expect("error while running tauri application");
|
| 439 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/src-tauri/tauri.conf.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"productName": "FastSeek",
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
-
"identifier": "com.fastseek.app",
|
| 5 |
-
"mainBinaryName": "FastSeek",
|
| 6 |
-
"build": {
|
| 7 |
-
"frontendDist": "../ui"
|
| 8 |
-
},
|
| 9 |
-
"app": {
|
| 10 |
-
"trayIcon": {
|
| 11 |
-
"id": "tray-main",
|
| 12 |
-
"iconPath": "icons/32x32.png"
|
| 13 |
-
},
|
| 14 |
-
"security": {
|
| 15 |
-
"csp": null,
|
| 16 |
-
"capabilities": ["main"]
|
| 17 |
-
},
|
| 18 |
-
"windows": [
|
| 19 |
-
{
|
| 20 |
-
"label": "main",
|
| 21 |
-
"title": "FastSeek",
|
| 22 |
-
"width": 800,
|
| 23 |
-
"height": 520,
|
| 24 |
-
"resizable": false,
|
| 25 |
-
"decorations": false,
|
| 26 |
-
"alwaysOnTop": true,
|
| 27 |
-
"skipTaskbar": true,
|
| 28 |
-
"visible": false,
|
| 29 |
-
"center": true,
|
| 30 |
-
"focus": true
|
| 31 |
-
}
|
| 32 |
-
]
|
| 33 |
-
},
|
| 34 |
-
"bundle": {
|
| 35 |
-
"active": true,
|
| 36 |
-
"category": "DeveloperTool",
|
| 37 |
-
"icon": [
|
| 38 |
-
"icons/32x32.png",
|
| 39 |
-
"icons/128x128.png",
|
| 40 |
-
"icons/128x128@2x.png",
|
| 41 |
-
"icons/icon.ico"
|
| 42 |
-
],
|
| 43 |
-
"targets": ["msi", "nsis"],
|
| 44 |
-
"windows": {
|
| 45 |
-
"certificateThumbprint": null,
|
| 46 |
-
"digestAlgorithm": "sha256",
|
| 47 |
-
"timestampUrl": ""
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/ui/app.js
DELETED
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
// ── Tauri v2 API wrapper ───────────────────────────────────────────
|
| 2 |
-
const TAURI = typeof window.__TAURI__ !== 'undefined' ? window.__TAURI__ : null;
|
| 3 |
-
|
| 4 |
-
// v2: __TAURI__.core.invoke | v1 fallback: __TAURI__.invoke
|
| 5 |
-
const invoke = (TAURI && TAURI.core && TAURI.core.invoke)
|
| 6 |
-
? TAURI.core.invoke
|
| 7 |
-
: (TAURI ? TAURI.invoke : null);
|
| 8 |
-
|
| 9 |
-
// v2: __TAURI__.event.listen | v1 fallback: __TAURI__.event.listen
|
| 10 |
-
const listen = (TAURI && TAURI.event && TAURI.event.listen)
|
| 11 |
-
? TAURI.event.listen
|
| 12 |
-
: (TAURI && TAURI.event ? TAURI.event.listen : null);
|
| 13 |
-
|
| 14 |
-
// ── DOM Elements ───────────────────────────────────────────────────
|
| 15 |
-
const searchInput = document.getElementById('searchInput');
|
| 16 |
-
const loadingSpinner = document.getElementById('loadingSpinner');
|
| 17 |
-
const resultsContainer = document.getElementById('resultsContainer');
|
| 18 |
-
const resultsList = document.getElementById('resultsList');
|
| 19 |
-
const footerStats = document.getElementById('footerStats');
|
| 20 |
-
const emptyState = document.getElementById('emptyState');
|
| 21 |
-
const noResults = document.getElementById('noResults');
|
| 22 |
-
|
| 23 |
-
// ── State ──────────────────────────────────────────────────────────
|
| 24 |
-
let results = [];
|
| 25 |
-
let selectedIndex = -1;
|
| 26 |
-
let searchTimeout = null;
|
| 27 |
-
let totalIndexed = 0;
|
| 28 |
-
|
| 29 |
-
// ── Icon helpers ───────────────────────────────────────────────────
|
| 30 |
-
function getFileIcon(name, isDir) {
|
| 31 |
-
if (isDir) return '📁';
|
| 32 |
-
const ext = name.split('.').pop()?.toLowerCase() || '';
|
| 33 |
-
const iconMap = {
|
| 34 |
-
exe: '⚙️', lnk: '🔗', pdf: '📄',
|
| 35 |
-
doc: '📝', docx: '📝', xls: '📊', xlsx: '📊', ppt: '📊', pptx: '📊',
|
| 36 |
-
jpg: '🖼️', jpeg: '🖼️', png: '🖼️', gif: '🖼️', bmp: '🖼️', svg: '🖼️', webp: '🖼️',
|
| 37 |
-
mp4: '🎬', mov: '🎬', avi: '🎬', mkv: '🎬',
|
| 38 |
-
mp3: '🎵', wav: '🎵', flac: '🎵', aac: '🎵',
|
| 39 |
-
zip: '📦', rar: '📦', '7z': '📦', tar: '📦', gz: '📦',
|
| 40 |
-
js: '📜', ts: '📜', jsx: '📜', tsx: '📜',
|
| 41 |
-
html: '🌐', css: '🎨', json: '📋', xml: '📋',
|
| 42 |
-
py: '🐍', rs: '🦀', go: '🐹', java: '☕', cpp: '⚙️', c: '⚙️',
|
| 43 |
-
txt: '📃', md: '📃', log: '📃',
|
| 44 |
-
db: '🗄️', sql: '🗄️',
|
| 45 |
-
ini: '⚙️', cfg: '⚙️', conf: '⚙️',
|
| 46 |
-
};
|
| 47 |
-
return iconMap[ext] || '📄';
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function getFileClass(name, isDir) {
|
| 51 |
-
if (isDir) return 'folder';
|
| 52 |
-
const ext = name.split('.').pop()?.toLowerCase() || '';
|
| 53 |
-
if (['exe', 'lnk', 'msi', 'bat', 'cmd'].includes(ext)) return 'app';
|
| 54 |
-
return 'file';
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// ── Highlight query in result name ────────────────────────────────
|
| 58 |
-
function highlightMatch(name, query) {
|
| 59 |
-
if (!query) return name;
|
| 60 |
-
const q = query.toLowerCase();
|
| 61 |
-
const n = name.toLowerCase();
|
| 62 |
-
const idx = n.indexOf(q);
|
| 63 |
-
if (idx === -1) return name;
|
| 64 |
-
return (
|
| 65 |
-
name.slice(0, idx) +
|
| 66 |
-
'<span class="highlight">' +
|
| 67 |
-
name.slice(idx, idx + query.length) +
|
| 68 |
-
'</span>' +
|
| 69 |
-
name.slice(idx + query.length)
|
| 70 |
-
);
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
function escapeHtml(text) {
|
| 74 |
-
const div = document.createElement('div');
|
| 75 |
-
div.textContent = text;
|
| 76 |
-
return div.innerHTML;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
// ── Render results ────────────────────────────────────────────────
|
| 80 |
-
function renderResults() {
|
| 81 |
-
resultsList.innerHTML = '';
|
| 82 |
-
results.forEach((item, idx) => {
|
| 83 |
-
const el = document.createElement('div');
|
| 84 |
-
el.className = 'result-item' + (idx === selectedIndex ? ' selected' : '');
|
| 85 |
-
el.dataset.index = idx;
|
| 86 |
-
el.addEventListener('click', () => selectAndOpen(idx));
|
| 87 |
-
el.addEventListener('dblclick', () => openItem(idx, true));
|
| 88 |
-
|
| 89 |
-
const iconClass = getFileClass(item.name, item.is_dir);
|
| 90 |
-
const icon = getFileIcon(item.name, item.is_dir);
|
| 91 |
-
const query = searchInput.value.trim();
|
| 92 |
-
const highlightedName = highlightMatch(item.name, query);
|
| 93 |
-
|
| 94 |
-
let displayPath = item.path;
|
| 95 |
-
if (displayPath.length > 80) {
|
| 96 |
-
displayPath = '...' + displayPath.slice(-77);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
el.innerHTML = `
|
| 100 |
-
<div class="result-icon ${iconClass}">${icon}</div>
|
| 101 |
-
<div class="result-info">
|
| 102 |
-
<div class="result-name">${highlightedName}</div>
|
| 103 |
-
<div class="result-path">${escapeHtml(displayPath)}</div>
|
| 104 |
-
</div>
|
| 105 |
-
<div class="result-meta">${item.is_dir ? 'DIR' : 'FILE'}</div>
|
| 106 |
-
`;
|
| 107 |
-
resultsList.appendChild(el);
|
| 108 |
-
});
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
function updateSelection() {
|
| 112 |
-
const items = resultsList.querySelectorAll('.result-item');
|
| 113 |
-
items.forEach((el, idx) => {
|
| 114 |
-
el.classList.toggle('selected', idx === selectedIndex);
|
| 115 |
-
});
|
| 116 |
-
if (selectedIndex >= 0) {
|
| 117 |
-
const selected = items[selectedIndex];
|
| 118 |
-
if (selected) {
|
| 119 |
-
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
| 120 |
-
}
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
function selectNext() {
|
| 125 |
-
if (results.length === 0) return;
|
| 126 |
-
selectedIndex = (selectedIndex + 1) % results.length;
|
| 127 |
-
updateSelection();
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
function selectPrev() {
|
| 131 |
-
if (results.length === 0) return;
|
| 132 |
-
selectedIndex = (selectedIndex - 1 + results.length) % results.length;
|
| 133 |
-
updateSelection();
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
function selectAndOpen(index) {
|
| 137 |
-
selectedIndex = index;
|
| 138 |
-
updateSelection();
|
| 139 |
-
openItem(index, false);
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
// ── Open item ────────────────────────────────────────────────────
|
| 143 |
-
function openItem(index, reveal) {
|
| 144 |
-
if (index < 0 || index >= results.length) return;
|
| 145 |
-
const item = results[index];
|
| 146 |
-
if (invoke) {
|
| 147 |
-
if (reveal || item.is_dir) {
|
| 148 |
-
invoke('cmd_open_folder', { path: item.path });
|
| 149 |
-
} else {
|
| 150 |
-
invoke('cmd_open_file', { path: item.path });
|
| 151 |
-
}
|
| 152 |
-
} else {
|
| 153 |
-
console.log('Open:', item.path);
|
| 154 |
-
}
|
| 155 |
-
setTimeout(() => {
|
| 156 |
-
if (invoke) invoke('cmd_toggle_window');
|
| 157 |
-
}, 100);
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
// ── Perform search ───────────────────────────────────────────────
|
| 161 |
-
function performSearch(query) {
|
| 162 |
-
if (!query) { showEmptyState(); return; }
|
| 163 |
-
loadingSpinner.classList.add('active');
|
| 164 |
-
|
| 165 |
-
if (invoke) {
|
| 166 |
-
invoke('cmd_search', {
|
| 167 |
-
request: { query: query, limit: 50, case_sensitive: false }
|
| 168 |
-
}).then(response => {
|
| 169 |
-
loadingSpinner.classList.remove('active');
|
| 170 |
-
results = response.results || [];
|
| 171 |
-
totalIndexed = response.total_indexed;
|
| 172 |
-
if (results.length > 0) {
|
| 173 |
-
showResults();
|
| 174 |
-
selectedIndex = 0;
|
| 175 |
-
renderResults();
|
| 176 |
-
updateFooter(response.elapsed_ms);
|
| 177 |
-
} else {
|
| 178 |
-
showNoResults();
|
| 179 |
-
}
|
| 180 |
-
}).catch(err => {
|
| 181 |
-
loadingSpinner.classList.remove('active');
|
| 182 |
-
console.error('Search error:', err);
|
| 183 |
-
});
|
| 184 |
-
} else {
|
| 185 |
-
loadingSpinner.classList.remove('active');
|
| 186 |
-
showEmptyState();
|
| 187 |
-
}
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
// ── View states ───────────────────────────────────────────────────
|
| 191 |
-
function showResults() {
|
| 192 |
-
resultsContainer.classList.add('visible');
|
| 193 |
-
emptyState.classList.add('hidden');
|
| 194 |
-
noResults.classList.remove('visible');
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
function showEmptyState() {
|
| 198 |
-
resultsContainer.classList.remove('visible');
|
| 199 |
-
emptyState.classList.remove('hidden');
|
| 200 |
-
noResults.classList.remove('visible');
|
| 201 |
-
results = [];
|
| 202 |
-
selectedIndex = -1;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
function showNoResults() {
|
| 206 |
-
resultsContainer.classList.remove('visible');
|
| 207 |
-
emptyState.classList.add('hidden');
|
| 208 |
-
noResults.classList.add('visible');
|
| 209 |
-
results = [];
|
| 210 |
-
selectedIndex = -1;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
function updateFooter(elapsedMs) {
|
| 214 |
-
footerStats.textContent = `${results.length} of ${totalIndexed.toLocaleString()} files · ${elapsedMs.toFixed(2)}ms`;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
// ── Search input handler ─────────────────────────────────────────
|
| 218 |
-
searchInput.addEventListener('input', (e) => {
|
| 219 |
-
const query = e.target.value.trim();
|
| 220 |
-
clearTimeout(searchTimeout);
|
| 221 |
-
if (!query) { showEmptyState(); return; }
|
| 222 |
-
searchTimeout = setTimeout(() => performSearch(query), 50);
|
| 223 |
-
});
|
| 224 |
-
|
| 225 |
-
// ── Keyboard navigation ──────────────────────────────────────────
|
| 226 |
-
document.addEventListener('keydown', (e) => {
|
| 227 |
-
if (e.key === 'Escape') {
|
| 228 |
-
if (invoke) invoke('cmd_toggle_window');
|
| 229 |
-
return;
|
| 230 |
-
}
|
| 231 |
-
if (e.key === 'ArrowDown') { e.preventDefault(); selectNext(); return; }
|
| 232 |
-
if (e.key === 'ArrowUp') { e.preventDefault(); selectPrev(); return; }
|
| 233 |
-
if (e.key === 'Enter') {
|
| 234 |
-
e.preventDefault();
|
| 235 |
-
openItem(selectedIndex, e.ctrlKey || e.metaKey);
|
| 236 |
-
return;
|
| 237 |
-
}
|
| 238 |
-
if (e.key === 'Tab') {
|
| 239 |
-
e.preventDefault();
|
| 240 |
-
searchInput.focus();
|
| 241 |
-
return;
|
| 242 |
-
}
|
| 243 |
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
| 244 |
-
if (document.activeElement !== searchInput) searchInput.focus();
|
| 245 |
-
}
|
| 246 |
-
});
|
| 247 |
-
|
| 248 |
-
// ── Reset & focus ──────────────────────────────────────────────────
|
| 249 |
-
function resetSearch() {
|
| 250 |
-
searchInput.value = '';
|
| 251 |
-
searchInput.focus();
|
| 252 |
-
showEmptyState();
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
// ── Listen for toggle event from Rust ────────────────────────────
|
| 256 |
-
if (listen) {
|
| 257 |
-
listen('toggle-search', () => resetSearch());
|
| 258 |
-
invoke('cmd_get_stats').then(stats => { totalIndexed = stats.total_indexed; });
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// Focus input on load
|
| 262 |
-
searchInput.focus();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/ui/index.html
DELETED
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>FastSeek</title>
|
| 7 |
-
<link rel="stylesheet" href="styles.css">
|
| 8 |
-
</head>
|
| 9 |
-
<body>
|
| 10 |
-
<div id="app">
|
| 11 |
-
<div class="search-container" id="searchContainer">
|
| 12 |
-
<div class="search-bar">
|
| 13 |
-
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 14 |
-
<circle cx="11" cy="11" r="8"></circle>
|
| 15 |
-
<path d="m21 21-4.3-4.3"></path>
|
| 16 |
-
</svg>
|
| 17 |
-
<input
|
| 18 |
-
type="text"
|
| 19 |
-
id="searchInput"
|
| 20 |
-
placeholder="Search files..."
|
| 21 |
-
autocomplete="off"
|
| 22 |
-
spellcheck="false"
|
| 23 |
-
/>
|
| 24 |
-
<div class="loading-spinner" id="loadingSpinner"></div>
|
| 25 |
-
<div class="shortcut-hint">ESC</div>
|
| 26 |
-
</div>
|
| 27 |
-
|
| 28 |
-
<div class="results-container" id="resultsContainer">
|
| 29 |
-
<div class="results-list" id="resultsList"></div>
|
| 30 |
-
<div class="results-footer" id="resultsFooter">
|
| 31 |
-
<span class="footer-hint">
|
| 32 |
-
<kbd>↑</kbd><kbd>↓</kbd> Navigate <kbd>Enter</kbd> Open <kbd>Ctrl</kbd>+<kbd>Enter</kbd> Reveal
|
| 33 |
-
</span>
|
| 34 |
-
<span class="footer-stats" id="footerStats"></span>
|
| 35 |
-
</div>
|
| 36 |
-
</div>
|
| 37 |
-
|
| 38 |
-
<div class="empty-state" id="emptyState">
|
| 39 |
-
<div class="empty-icon">
|
| 40 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 41 |
-
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
| 42 |
-
</svg>
|
| 43 |
-
</div>
|
| 44 |
-
<p>Start typing to search files</p>
|
| 45 |
-
<div class="empty-shortcuts">
|
| 46 |
-
<div class="shortcut-row">
|
| 47 |
-
<span class="key">Win</span><span class="key">Space</span>
|
| 48 |
-
<span class="desc">Open search</span>
|
| 49 |
-
</div>
|
| 50 |
-
<div class="shortcut-row">
|
| 51 |
-
<span class="key">Esc</span>
|
| 52 |
-
<span class="desc">Close</span>
|
| 53 |
-
</div>
|
| 54 |
-
<div class="shortcut-row">
|
| 55 |
-
<span class="key">↑↓</span>
|
| 56 |
-
<span class="desc">Navigate results</span>
|
| 57 |
-
</div>
|
| 58 |
-
<div class="shortcut-row">
|
| 59 |
-
<span class="key">Enter</span>
|
| 60 |
-
<span class="desc">Open file</span>
|
| 61 |
-
</div>
|
| 62 |
-
<div class="shortcut-row">
|
| 63 |
-
<span class="key">Ctrl</span><span class="key">Enter</span>
|
| 64 |
-
<span class="desc">Show in folder</span>
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
-
|
| 69 |
-
<div class="no-results" id="noResults" style="display: none;">
|
| 70 |
-
<div class="no-results-icon">
|
| 71 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 72 |
-
<circle cx="11" cy="11" r="8"></circle>
|
| 73 |
-
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
| 74 |
-
<line x1="8" y1="8" x2="14" y2="14"></line>
|
| 75 |
-
</svg>
|
| 76 |
-
</div>
|
| 77 |
-
<p>No results found</p>
|
| 78 |
-
<span class="no-results-sub">Try a different search term</span>
|
| 79 |
-
</div>
|
| 80 |
-
</div>
|
| 81 |
-
</div>
|
| 82 |
-
|
| 83 |
-
<script src="app.js"></script>
|
| 84 |
-
</body>
|
| 85 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fastsearch-tauri/ui/styles.css
DELETED
|
@@ -1,387 +0,0 @@
|
|
| 1 |
-
:root {
|
| 2 |
-
--bg-primary: rgba(30, 30, 35, 0.97);
|
| 3 |
-
--bg-secondary: rgba(50, 50, 60, 0.9);
|
| 4 |
-
--bg-hover: rgba(70, 70, 85, 0.9);
|
| 5 |
-
--bg-selected: rgba(10, 132, 255, 0.4);
|
| 6 |
-
--text-primary: #ffffff;
|
| 7 |
-
--text-secondary: rgba(255, 255, 255, 0.6);
|
| 8 |
-
--text-tertiary: rgba(255, 255, 255, 0.35);
|
| 9 |
-
--accent: #0a84ff;
|
| 10 |
-
--accent-hover: #0077e6;
|
| 11 |
-
--border: rgba(255, 255, 255, 0.08);
|
| 12 |
-
--shadow: 0 25px 80px rgba(0, 0, 0, 0.5), 0 10px 30px rgba(0, 0, 0, 0.3);
|
| 13 |
-
--radius: 18px;
|
| 14 |
-
--radius-sm: 10px;
|
| 15 |
-
--transition: 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
* {
|
| 19 |
-
margin: 0;
|
| 20 |
-
padding: 0;
|
| 21 |
-
box-sizing: border-box;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
body {
|
| 25 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
| 26 |
-
background: #1a1a24;
|
| 27 |
-
overflow: hidden;
|
| 28 |
-
color: var(--text-primary);
|
| 29 |
-
-webkit-font-smoothing: antialiased;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
#app {
|
| 33 |
-
width: 100vw;
|
| 34 |
-
height: 100vh;
|
| 35 |
-
display: flex;
|
| 36 |
-
align-items: flex-start;
|
| 37 |
-
justify-content: center;
|
| 38 |
-
padding-top: 80px;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
/* -- Search Container ------------------------------------------------ */
|
| 42 |
-
.search-container {
|
| 43 |
-
width: 720px;
|
| 44 |
-
max-width: 90vw;
|
| 45 |
-
background: var(--bg-primary);
|
| 46 |
-
border: 1px solid var(--border);
|
| 47 |
-
border-radius: var(--radius);
|
| 48 |
-
box-shadow: var(--shadow);
|
| 49 |
-
overflow: hidden;
|
| 50 |
-
transition: all var(--transition);
|
| 51 |
-
animation: slideDown 0.25s ease-out;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
@keyframes slideDown {
|
| 55 |
-
from {
|
| 56 |
-
opacity: 0;
|
| 57 |
-
transform: translateY(-20px) scale(0.95);
|
| 58 |
-
}
|
| 59 |
-
to {
|
| 60 |
-
opacity: 1;
|
| 61 |
-
transform: translateY(0) scale(1);
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
/* -- Search Bar ---------------------------------------------------- */
|
| 66 |
-
.search-bar {
|
| 67 |
-
display: flex;
|
| 68 |
-
align-items: center;
|
| 69 |
-
padding: 16px 20px;
|
| 70 |
-
gap: 12px;
|
| 71 |
-
border-bottom: 1px solid var(--border);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.search-icon {
|
| 75 |
-
width: 22px;
|
| 76 |
-
height: 22px;
|
| 77 |
-
color: var(--text-tertiary);
|
| 78 |
-
flex-shrink: 0;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
.search-bar input {
|
| 82 |
-
flex: 1;
|
| 83 |
-
background: transparent;
|
| 84 |
-
border: none;
|
| 85 |
-
outline: none;
|
| 86 |
-
color: var(--text-primary);
|
| 87 |
-
font-size: 22px;
|
| 88 |
-
font-weight: 400;
|
| 89 |
-
letter-spacing: -0.01em;
|
| 90 |
-
caret-color: var(--accent);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
.search-bar input::placeholder {
|
| 94 |
-
color: var(--text-tertiary);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
.loading-spinner {
|
| 98 |
-
width: 18px;
|
| 99 |
-
height: 18px;
|
| 100 |
-
border: 2px solid var(--text-tertiary);
|
| 101 |
-
border-top-color: var(--accent);
|
| 102 |
-
border-radius: 50%;
|
| 103 |
-
animation: spin 0.8s linear infinite;
|
| 104 |
-
display: none;
|
| 105 |
-
flex-shrink: 0;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
.loading-spinner.active {
|
| 109 |
-
display: block;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
@keyframes spin {
|
| 113 |
-
to { transform: rotate(360deg); }
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.shortcut-hint {
|
| 117 |
-
font-size: 11px;
|
| 118 |
-
color: var(--text-tertiary);
|
| 119 |
-
background: var(--bg-secondary);
|
| 120 |
-
padding: 4px 8px;
|
| 121 |
-
border-radius: 6px;
|
| 122 |
-
border: 1px solid var(--border);
|
| 123 |
-
font-family: monospace;
|
| 124 |
-
flex-shrink: 0;
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
/* -- Results Container --------------------------------------------- */
|
| 128 |
-
.results-container {
|
| 129 |
-
max-height: 420px;
|
| 130 |
-
overflow-y: auto;
|
| 131 |
-
display: none;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
.results-container.visible {
|
| 135 |
-
display: block;
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
.results-container::-webkit-scrollbar {
|
| 139 |
-
width: 6px;
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
.results-container::-webkit-scrollbar-track {
|
| 143 |
-
background: transparent;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.results-container::-webkit-scrollbar-thumb {
|
| 147 |
-
background: rgba(255, 255, 255, 0.15);
|
| 148 |
-
border-radius: 3px;
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
.results-list {
|
| 152 |
-
padding: 6px 8px;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
/* -- Result Item --------------------------------------------------- */
|
| 156 |
-
.result-item {
|
| 157 |
-
display: flex;
|
| 158 |
-
align-items: center;
|
| 159 |
-
gap: 14px;
|
| 160 |
-
padding: 10px 14px;
|
| 161 |
-
border-radius: var(--radius-sm);
|
| 162 |
-
cursor: pointer;
|
| 163 |
-
transition: background var(--transition);
|
| 164 |
-
user-select: none;
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
.result-item:hover {
|
| 168 |
-
background: var(--bg-hover);
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
.result-item.selected {
|
| 172 |
-
background: var(--bg-selected);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
.result-item.selected .result-name,
|
| 176 |
-
.result-item.selected .result-path {
|
| 177 |
-
color: var(--text-primary);
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
.result-icon {
|
| 181 |
-
width: 40px;
|
| 182 |
-
height: 40px;
|
| 183 |
-
border-radius: var(--radius-sm);
|
| 184 |
-
display: flex;
|
| 185 |
-
align-items: center;
|
| 186 |
-
justify-content: center;
|
| 187 |
-
flex-shrink: 0;
|
| 188 |
-
font-size: 18px;
|
| 189 |
-
background: var(--bg-secondary);
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
.result-icon.folder {
|
| 193 |
-
color: #ff9f0a;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
.result-icon.file {
|
| 197 |
-
color: #0a84ff;
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
.result-icon.app {
|
| 201 |
-
color: #32d74b;
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
.result-info {
|
| 205 |
-
flex: 1;
|
| 206 |
-
min-width: 0;
|
| 207 |
-
overflow: hidden;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
.result-name {
|
| 211 |
-
font-size: 14px;
|
| 212 |
-
font-weight: 500;
|
| 213 |
-
color: var(--text-primary);
|
| 214 |
-
white-space: nowrap;
|
| 215 |
-
overflow: hidden;
|
| 216 |
-
text-overflow: ellipsis;
|
| 217 |
-
line-height: 1.4;
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
.result-name .highlight {
|
| 221 |
-
color: var(--accent);
|
| 222 |
-
font-weight: 600;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
.result-path {
|
| 226 |
-
font-size: 12px;
|
| 227 |
-
color: var(--text-tertiary);
|
| 228 |
-
white-space: nowrap;
|
| 229 |
-
overflow: hidden;
|
| 230 |
-
text-overflow: ellipsis;
|
| 231 |
-
margin-top: 2px;
|
| 232 |
-
line-height: 1.3;
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
.result-meta {
|
| 236 |
-
font-size: 11px;
|
| 237 |
-
color: var(--text-tertiary);
|
| 238 |
-
background: var(--bg-secondary);
|
| 239 |
-
padding: 3px 8px;
|
| 240 |
-
border-radius: 4px;
|
| 241 |
-
flex-shrink: 0;
|
| 242 |
-
font-family: monospace;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
/* -- Results Footer ------------------------------------------------ */
|
| 246 |
-
.results-footer {
|
| 247 |
-
display: flex;
|
| 248 |
-
align-items: center;
|
| 249 |
-
justify-content: space-between;
|
| 250 |
-
padding: 8px 16px;
|
| 251 |
-
border-top: 1px solid var(--border);
|
| 252 |
-
font-size: 11px;
|
| 253 |
-
color: var(--text-tertiary);
|
| 254 |
-
background: rgba(0, 0, 0, 0.15);
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
.footer-hint kbd {
|
| 258 |
-
display: inline-block;
|
| 259 |
-
padding: 2px 5px;
|
| 260 |
-
background: var(--bg-secondary);
|
| 261 |
-
border: 1px solid var(--border);
|
| 262 |
-
border-radius: 4px;
|
| 263 |
-
font-family: monospace;
|
| 264 |
-
font-size: 10px;
|
| 265 |
-
color: var(--text-secondary);
|
| 266 |
-
margin: 0 1px;
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
.footer-stats {
|
| 270 |
-
font-family: monospace;
|
| 271 |
-
font-size: 11px;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
/* -- Empty State --------------------------------------------------- */
|
| 275 |
-
.empty-state {
|
| 276 |
-
padding: 40px 20px;
|
| 277 |
-
text-align: center;
|
| 278 |
-
display: block;
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
.empty-state.hidden {
|
| 282 |
-
display: none;
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
.empty-icon {
|
| 286 |
-
width: 56px;
|
| 287 |
-
height: 56px;
|
| 288 |
-
margin: 0 auto 16px;
|
| 289 |
-
color: var(--text-tertiary);
|
| 290 |
-
opacity: 0.6;
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
.empty-state p {
|
| 294 |
-
font-size: 15px;
|
| 295 |
-
color: var(--text-secondary);
|
| 296 |
-
font-weight: 500;
|
| 297 |
-
margin-bottom: 24px;
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
.empty-shortcuts {
|
| 301 |
-
display: flex;
|
| 302 |
-
flex-direction: column;
|
| 303 |
-
gap: 8px;
|
| 304 |
-
align-items: center;
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
.shortcut-row {
|
| 308 |
-
display: flex;
|
| 309 |
-
align-items: center;
|
| 310 |
-
gap: 8px;
|
| 311 |
-
font-size: 12px;
|
| 312 |
-
color: var(--text-tertiary);
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
.shortcut-row .key {
|
| 316 |
-
display: inline-flex;
|
| 317 |
-
align-items: center;
|
| 318 |
-
justify-content: center;
|
| 319 |
-
min-width: 28px;
|
| 320 |
-
padding: 3px 7px;
|
| 321 |
-
background: var(--bg-secondary);
|
| 322 |
-
border: 1px solid var(--border);
|
| 323 |
-
border-radius: 5px;
|
| 324 |
-
font-family: monospace;
|
| 325 |
-
font-size: 11px;
|
| 326 |
-
color: var(--text-secondary);
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
.shortcut-row .desc {
|
| 330 |
-
min-width: 130px;
|
| 331 |
-
text-align: left;
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
/* -- No Results ---------------------------------------------------- */
|
| 335 |
-
.no-results {
|
| 336 |
-
padding: 40px 20px;
|
| 337 |
-
text-align: center;
|
| 338 |
-
display: none;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
.no-results.visible {
|
| 342 |
-
display: block;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
.no-results-icon {
|
| 346 |
-
width: 48px;
|
| 347 |
-
height: 48px;
|
| 348 |
-
margin: 0 auto 16px;
|
| 349 |
-
color: var(--text-tertiary);
|
| 350 |
-
opacity: 0.5;
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
.no-results p {
|
| 354 |
-
font-size: 15px;
|
| 355 |
-
color: var(--text-secondary);
|
| 356 |
-
font-weight: 500;
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
.no-results-sub {
|
| 360 |
-
font-size: 12px;
|
| 361 |
-
color: var(--text-tertiary);
|
| 362 |
-
margin-top: 4px;
|
| 363 |
-
display: block;
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
/* -- Animations ---------------------------------------------------- */
|
| 367 |
-
.result-item {
|
| 368 |
-
animation: fadeIn 0.1s ease-out;
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
@keyframes fadeIn {
|
| 372 |
-
from {
|
| 373 |
-
opacity: 0;
|
| 374 |
-
transform: translateY(4px);
|
| 375 |
-
}
|
| 376 |
-
to {
|
| 377 |
-
opacity: 1;
|
| 378 |
-
transform: translateY(0);
|
| 379 |
-
}
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
/* -- Responsive ---------------------------------------------------- */
|
| 383 |
-
@media (max-width: 800px) {
|
| 384 |
-
.search-container {
|
| 385 |
-
width: 95vw;
|
| 386 |
-
}
|
| 387 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|