anshdadhich commited on
Commit
2379df8
Β·
verified Β·
1 Parent(s): 030d4a2

Upload fastsearch-tauri/src-tauri/src/main.rs

Browse files
fastsearch-tauri/src-tauri/src/main.rs ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, CustomMenuItem, WindowBuilder, WindowUrl, WindowEvent, PhysicalPosition};
9
+ use serde::{Serialize, Deserialize};
10
+
11
+ use fastsearch_core::index::store::IndexStore;
12
+ use fastsearch_core::index::search::search;
13
+ use fastsearch_core::mft::reader::MftReader;
14
+ use fastsearch_core::mft::watcher::UsnWatcher;
15
+ use fastsearch_core::mft::types::{IndexEvent, JournalCheckpoint};
16
+ use fastsearch_core::utils::drives::get_ntfs_drives;
17
+
18
+ // ── Global state ───────────────────────────────────────────────────
19
+ static INDEX: Lazy<Arc<RwLock<IndexStore>>> = Lazy::new(|| {
20
+ Arc::new(RwLock::new(IndexStore::new()))
21
+ });
22
+
23
+ static LIVE_CHECKPOINTS: Lazy<Arc<parking_lot::Mutex<Vec<JournalCheckpoint>>>> = Lazy::new(|| {
24
+ Arc::new(parking_lot::Mutex::new(Vec::new()))
25
+ });
26
+
27
+ static EXCLUDED_DIRS: Lazy<Arc<RwLock<Vec<String>>>> = Lazy::new(|| {
28
+ Arc::new(RwLock::new(load_exclusions()))
29
+ });
30
+
31
+ // ── Search response types ─────────────────────────────────────────
32
+ #[derive(Serialize, Clone)]
33
+ struct SearchResponse {
34
+ results: Vec<SearchItem>,
35
+ elapsed_ms: f64,
36
+ total_indexed: usize,
37
+ }
38
+
39
+ #[derive(Serialize, Clone)]
40
+ struct SearchItem {
41
+ name: String,
42
+ path: String,
43
+ is_dir: bool,
44
+ rank: u8,
45
+ }
46
+
47
+ #[derive(Deserialize)]
48
+ struct SearchRequest {
49
+ query: String,
50
+ #[serde(default)]
51
+ limit: usize,
52
+ #[serde(default)]
53
+ case_sensitive: bool,
54
+ }
55
+
56
+ // ── Tauri Commands ─────────────────────────────────────────────────
57
+ #[tauri::command]
58
+ fn cmd_search(request: SearchRequest) -> SearchResponse {
59
+ let store = INDEX.read();
60
+ let excluded = EXCLUDED_DIRS.read();
61
+ let limit = if request.limit == 0 { 50 } else { request.limit };
62
+
63
+ let start = std::time::Instant::now();
64
+ let raw = search(&store, &request.query, limit * 2, request.case_sensitive, &excluded);
65
+ let elapsed = start.elapsed();
66
+
67
+ let results: Vec<SearchItem> = raw.into_iter()
68
+ .take(limit)
69
+ .map(|r| SearchItem {
70
+ name: r.name,
71
+ path: r.full_path.to_string_lossy().to_string(),
72
+ is_dir: r.is_dir,
73
+ rank: r.rank,
74
+ })
75
+ .collect();
76
+
77
+ SearchResponse {
78
+ results,
79
+ elapsed_ms: elapsed.as_secs_f64() * 1000.0,
80
+ total_indexed: store.len(),
81
+ }
82
+ }
83
+
84
+ #[tauri::command]
85
+ fn cmd_get_stats() -> serde_json::Value {
86
+ let store = INDEX.read();
87
+ serde_json::json!({
88
+ "total_indexed": store.len(),
89
+ "checkpoints": store.checkpoints.len(),
90
+ })
91
+ }
92
+
93
+ #[tauri::command]
94
+ fn cmd_rescan() {
95
+ let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
96
+ let _ = std::fs::remove_file(&cache_path);
97
+ }
98
+
99
+ #[tauri::command]
100
+ fn cmd_toggle_window(window: tauri::Window) {
101
+ toggle_window(&window);
102
+ }
103
+
104
+ #[tauri::command]
105
+ fn cmd_open_file(path: String) {
106
+ let _ = std::process::Command::new("explorer")
107
+ .arg("/select,")
108
+ .arg(&path)
109
+ .spawn();
110
+ }
111
+
112
+ #[tauri::command]
113
+ fn cmd_open_folder(path: String) {
114
+ let _ = std::process::Command::new("explorer")
115
+ .arg(&path)
116
+ .spawn();
117
+ }
118
+
119
+ #[tauri::command]
120
+ fn cmd_add_exclusion(path: String) {
121
+ let mut excluded = EXCLUDED_DIRS.write();
122
+ let path = path.to_lowercase();
123
+ let path = if path.ends_with('\\') || path.ends_with('/') {
124
+ path
125
+ } else {
126
+ format!("{}\\", path)
127
+ };
128
+ if !excluded.contains(&path) {
129
+ excluded.push(path.clone());
130
+ save_exclusions(&excluded);
131
+ }
132
+ }
133
+
134
+ #[tauri::command]
135
+ fn cmd_remove_exclusion(path: String) {
136
+ let mut excluded = EXCLUDED_DIRS.write();
137
+ let path = path.to_lowercase();
138
+ excluded.retain(|d| d != &path);
139
+ save_exclusions(&excluded);
140
+ }
141
+
142
+ #[tauri::command]
143
+ fn cmd_get_exclusions() -> Vec<String> {
144
+ EXCLUDED_DIRS.read().clone()
145
+ }
146
+
147
+ // ── Window helpers ───────────────────────────────────────────────
148
+ fn toggle_window(window: &tauri::Window) {
149
+ if window.is_visible().unwrap_or(false) {
150
+ let _ = window.hide();
151
+ } else {
152
+ if let Some(monitor) = window.primary_monitor().unwrap_or(None) {
153
+ let size = monitor.size();
154
+ let win_size = window.outer_size().unwrap_or(tauri::PhysicalSize::new(800, 520));
155
+ let x = (size.width as i32 - win_size.width as i32) / 2;
156
+ let y = (size.height as i32 - win_size.height as i32) / 3;
157
+ let _ = window.set_position(PhysicalPosition::new(x, y));
158
+ }
159
+ let _ = window.show();
160
+ let _ = window.set_focus();
161
+ }
162
+ }
163
+
164
+ fn show_window(window: &tauri::Window) {
165
+ if !window.is_visible().unwrap_or(false) {
166
+ let _ = window.show();
167
+ let _ = window.set_focus();
168
+ }
169
+ }
170
+
171
+ // ── Config helpers ───────────────────────────────────────────────
172
+ fn config_dir() -> std::path::PathBuf {
173
+ let dir = std::env::var("APPDATA")
174
+ .map(std::path::PathBuf::from)
175
+ .unwrap_or_else(|_| std::env::temp_dir())
176
+ .join("fastseek");
177
+ let _ = std::fs::create_dir_all(&dir);
178
+ dir
179
+ }
180
+
181
+ fn load_exclusions() -> Vec<String> {
182
+ let path = config_dir().join("exclusions.txt");
183
+ std::fs::read_to_string(&path)
184
+ .unwrap_or_default()
185
+ .lines()
186
+ .map(|l| l.trim().to_lowercase())
187
+ .filter(|l| !l.is_empty())
188
+ .collect()
189
+ }
190
+
191
+ fn save_exclusions(dirs: &[String]) {
192
+ let path = config_dir().join("exclusions.txt");
193
+ let content = dirs.join("\n");
194
+ let _ = std::fs::write(&path, content);
195
+ }
196
+
197
+ // ── Index initialization (background thread) ──────────────────
198
+ fn init_index() {
199
+ std::thread::spawn(|| {
200
+ let drives = get_ntfs_drives();
201
+ if drives.is_empty() {
202
+ eprintln!("No NTFS drives found. Run as Administrator.");
203
+ return;
204
+ }
205
+
206
+ let (tx, rx) = unbounded();
207
+ let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
208
+
209
+ // Try loading from cache
210
+ let cache_loaded = if cache_path.exists() {
211
+ match std::fs::read(&cache_path) {
212
+ Ok(compressed) => {
213
+ match lz4_flex::decompress_size_prepended(&compressed) {
214
+ Ok(bytes) => {
215
+ match bincode::deserialize::<fastsearch_core::index::store::CacheData>(&bytes) {
216
+ Ok(cache) => {
217
+ let checkpoints = cache.checkpoints.clone();
218
+ *INDEX.write() = IndexStore::from_cache(cache);
219
+
220
+ // Delta catch-up
221
+ if !checkpoints.is_empty() {
222
+ let (delta_tx, delta_rx) = unbounded::<IndexEvent>();
223
+ let mut journal_ok = true;
224
+
225
+ for drive in &drives {
226
+ let cp = checkpoints.iter()
227
+ .find(|c| c.drive_letter == drive.letter);
228
+
229
+ if let Some(cp) = cp {
230
+ match UsnWatcher::new_from(drive, delta_tx.clone(), Some(cp)) {
231
+ Ok(mut watcher) => {
232
+ watcher.drain();
233
+ let new_cp = watcher.checkpoint();
234
+ let mut store = INDEX.write();
235
+ store.checkpoints.retain(|c| c.drive_letter != drive.letter);
236
+ store.checkpoints.push(new_cp);
237
+ }
238
+ Err(_) => {
239
+ let _ = std::fs::remove_file(&cache_path);
240
+ journal_ok = false;
241
+ break;
242
+ }
243
+ }
244
+ } else {
245
+ let _ = std::fs::remove_file(&cache_path);
246
+ journal_ok = false;
247
+ break;
248
+ }
249
+ }
250
+
251
+ drop(delta_tx);
252
+
253
+ if journal_ok {
254
+ let mut store = INDEX.write();
255
+ for event in delta_rx {
256
+ match event {
257
+ IndexEvent::Created(r) => store.insert(r),
258
+ IndexEvent::Deleted(id) => store.remove(id),
259
+ IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record),
260
+ IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
261
+ store.apply_move(file_ref, new_parent_ref, name, kind);
262
+ }
263
+ }
264
+ }
265
+ true
266
+ } else {
267
+ false
268
+ }
269
+ } else {
270
+ true
271
+ }
272
+ }
273
+ Err(_) => false
274
+ }
275
+ }
276
+ Err(_) => false
277
+ }
278
+ }
279
+ Err(_) => false
280
+ }
281
+ } else {
282
+ false
283
+ };
284
+
285
+ // Full MFT scan if no cache
286
+ if !cache_loaded {
287
+ {
288
+ let mut store = INDEX.write();
289
+ for drive in &drives {
290
+ let (dummy_tx, _) = unbounded::<IndexEvent>();
291
+ if let Ok(w) = UsnWatcher::new(drive, dummy_tx) {
292
+ store.checkpoints.push(w.checkpoint());
293
+ }
294
+ }
295
+ }
296
+
297
+ let index_clone = Arc::clone(&INDEX);
298
+ let drives_clone = drives.clone();
299
+
300
+ for drive in &drives_clone {
301
+ let reader = match MftReader::open(drive) {
302
+ Ok(r) => r,
303
+ Err(_) => continue,
304
+ };
305
+
306
+ let scan = match reader.scan_direct() {
307
+ Some(s) => s,
308
+ None => reader.scan(),
309
+ };
310
+
311
+ let mut store = index_clone.write();
312
+ store.populate_from_scan(scan, &drive.root);
313
+ }
314
+
315
+ {
316
+ let mut store = index_clone.write();
317
+ store.finalize();
318
+
319
+ let cache = store.to_cache();
320
+ if let Ok(bytes) = bincode::serialize(&cache) {
321
+ let compressed = lz4_flex::compress_prepend_size(&bytes);
322
+ let _ = std::fs::write(&cache_path, &compressed);
323
+ }
324
+ }
325
+ }
326
+
327
+ // Start USN watchers for live updates
328
+ let live_cps = Arc::clone(&LIVE_CHECKPOINTS);
329
+ for drive in &drives {
330
+ let tx_clone = tx.clone();
331
+ let drive_clone = drive.clone();
332
+ let cps = Arc::clone(&live_cps);
333
+ std::thread::spawn(move || {
334
+ if let Ok(mut watcher) = UsnWatcher::new(&drive_clone, tx_clone) {
335
+ watcher.run_shared(cps);
336
+ }
337
+ });
338
+ }
339
+
340
+ // Apply live updates
341
+ let index_live = Arc::clone(&INDEX);
342
+ std::thread::spawn(move || {
343
+ for event in rx {
344
+ let mut store = index_live.write();
345
+ match event {
346
+ IndexEvent::Created(r) => store.insert(r),
347
+ IndexEvent::Deleted(id) => store.remove(id),
348
+ IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record),
349
+ IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
350
+ store.apply_move(file_ref, new_parent_ref, name, kind);
351
+ }
352
+ }
353
+ }
354
+ });
355
+ });
356
+ }
357
+
358
+ // ── Hotkey listener (Win+Space) ──────────────────────────────────
359
+ fn setup_hotkey_listener(app_handle: tauri::AppHandle) {
360
+ std::thread::spawn(move || {
361
+ use rdev::{listen, EventType, Key};
362
+
363
+ let mut win_pressed = false;
364
+
365
+ let callback = move |event: rdev::Event| {
366
+ match event.event_type {
367
+ EventType::KeyPress(Key::MetaLeft) | EventType::KeyPress(Key::MetaRight) => {
368
+ win_pressed = true;
369
+ }
370
+ EventType::KeyRelease(Key::MetaLeft) | EventType::KeyRelease(Key::MetaRight) => {
371
+ win_pressed = false;
372
+ }
373
+ EventType::KeyPress(Key::Space) => {
374
+ if win_pressed {
375
+ if let Some(window) = app_handle.get_window("main") {
376
+ let _ = window.emit("toggle-search", "");
377
+ }
378
+ }
379
+ }
380
+ _ => {}
381
+ }
382
+ };
383
+
384
+ if let Err(e) = listen(callback) {
385
+ eprintln!("Hotkey listener error: {:?}", e);
386
+ }
387
+ });
388
+ }
389
+
390
+ // ── Main ───────────────────────────────────────────────────────────
391
+ fn main() {
392
+ let tray_menu = SystemTrayMenu::new()
393
+ .add_item(CustomMenuItem::new("show", "Show"))
394
+ .add_native_item(SystemTrayMenuItem::Separator)
395
+ .add_item(CustomMenuItem::new("quit", "Quit"));
396
+
397
+ let system_tray = SystemTray::new().with_menu(tray_menu);
398
+
399
+ tauri::Builder::default()
400
+ .setup(|app| {
401
+ init_index();
402
+
403
+ let app_handle = app.handle();
404
+ setup_hotkey_listener(app_handle.clone());
405
+
406
+ let window = WindowBuilder::new(
407
+ app,
408
+ "main",
409
+ WindowUrl::App("index.html".into())
410
+ )
411
+ .title("FastSeek")
412
+ .inner_size(800.0, 520.0)
413
+ .decorations(false)
414
+ .transparent(true)
415
+ .always_on_top(true)
416
+ .skip_taskbar(true)
417
+ .visible(false)
418
+ .build()?;
419
+
420
+ // Center window initially
421
+ if let Some(monitor) = window.primary_monitor()? {
422
+ let size = monitor.size();
423
+ let win_size = window.outer_size()?;
424
+ let x = (size.width as i32 - win_size.width as i32) / 2;
425
+ let y = (size.height as i32 - win_size.height as i32) / 3;
426
+ window.set_position(PhysicalPosition::new(x, y))?;
427
+ }
428
+
429
+ Ok(())
430
+ })
431
+ .system_tray(system_tray)
432
+ .on_system_tray_event(|app, event| {
433
+ match event {
434
+ SystemTrayEvent::LeftClick { .. } => {
435
+ if let Some(window) = app.get_window("main") {
436
+ toggle_window(&window);
437
+ }
438
+ }
439
+ SystemTrayEvent::MenuItemClick { id, .. } => {
440
+ match id.as_str() {
441
+ "show" => {
442
+ if let Some(window) = app.get_window("main") {
443
+ show_window(&window);
444
+ }
445
+ }
446
+ "quit" => {
447
+ std::process::exit(0);
448
+ }
449
+ _ => {}
450
+ }
451
+ }
452
+ _ => {}
453
+ }
454
+ })
455
+ .on_window_event(|event| {
456
+ match event.event() {
457
+ WindowEvent::Focused(false) => {
458
+ // Optional: hide on blur (click outside)
459
+ // event.window().hide().ok();
460
+ }
461
+ _ => {}
462
+ }
463
+ })
464
+ .invoke_handler(tauri::generate_handler![
465
+ cmd_search,
466
+ cmd_get_stats,
467
+ cmd_rescan,
468
+ cmd_toggle_window,
469
+ cmd_open_file,
470
+ cmd_open_folder,
471
+ cmd_add_exclusion,
472
+ cmd_remove_exclusion,
473
+ cmd_get_exclusions
474
+ ])
475
+ .run(tauri::generate_context!())
476
+ .expect("error while running tauri application");
477
+ }