anshdadhich commited on
Commit
f4ce227
·
verified ·
1 Parent(s): b090aa8

Delete fastsearch-tauri

Browse files
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 &nbsp; <kbd>Enter</kbd> Open &nbsp; <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
- }