anshdadhich commited on
Commit
26e3416
·
verified ·
1 Parent(s): 83f4ac4

Upload FastSeekWpf/App.xaml.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/App.xaml.cs +168 -205
FastSeekWpf/App.xaml.cs CHANGED
@@ -23,10 +23,6 @@ public partial class App : Application
23
  private volatile bool _shutdown;
24
  private MainWindow? _mainWindow;
25
  private Thread? _hotkeyThread;
26
- private IntPtr _hotkeyWakeEvent;
27
-
28
- // Console attachment
29
- private const int ATTACH_PARENT_PROCESS = -1;
30
 
31
  [DllImport("kernel32.dll", SetLastError = true)]
32
  private static extern bool AttachConsole(int dwProcessId);
@@ -37,66 +33,40 @@ public partial class App : Application
37
  [DllImport("kernel32.dll", SetLastError = true)]
38
  private static extern bool FreeConsole();
39
 
40
- [DllImport("kernel32.dll")]
41
- private static extern IntPtr GetConsoleWindow();
42
 
43
  private void OnStartup(object sender, StartupEventArgs e)
44
  {
45
- // --- CLI: Clear cache ---
46
- if (e.Args.Contains("--clear-cache"))
47
  {
48
- EnsureConsole();
49
- ClearCacheCli();
50
- Console.WriteLine("\nPress Enter to exit...");
51
- Console.ReadLine();
52
- FreeConsole();
53
  Shutdown();
54
  return;
55
  }
56
 
57
- // --- CLI DIAGNOSTIC MODE ---
58
- if (e.Args.Contains("--diag") || e.Args.Contains("--cli"))
59
  {
60
- EnsureConsole();
61
- try
62
- {
63
- RunDiagnostics();
64
- }
65
- catch (Exception ex)
66
- {
67
- Console.ForegroundColor = ConsoleColor.Red;
68
- Console.WriteLine($"\nFatal error: {ex}");
69
- Console.ResetColor();
70
- }
71
- Console.WriteLine("\nPress Enter to exit...");
72
- Console.ReadLine();
73
- FreeConsole();
74
  Shutdown();
75
  return;
76
  }
77
 
78
  Logger.Log("=== App startup ===");
79
 
80
- // Single-instance guard
81
  _mutex = new Mutex(true, MutexName, out bool createdNew);
82
  if (!createdNew)
83
  {
84
- Logger.Log("Another instance is running. Sending toggle message.");
85
  IntPtr hwnd = Win32Api.FindWindowW(null, "FastSeek");
86
  if (hwnd != IntPtr.Zero)
87
  Win32Api.PostMessageW(hwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
88
- else
89
- Logger.Log("Could not find existing window to toggle.");
90
  Shutdown();
91
  return;
92
  }
93
  _ownsMutex = true;
94
- Logger.Log("Mutex acquired. Starting main window.");
95
 
96
  _mainWindow = new MainWindow();
97
  _mainWindow.Show();
98
 
99
- // Start global hotkey thread (Ctrl+Shift+Space = MOD_CONTROL|MOD_SHIFT, VK_SPACE)
100
  _hotkeyThread = new Thread(HotkeyLoop)
101
  {
102
  IsBackground = true,
@@ -108,229 +78,240 @@ public partial class App : Application
108
 
109
  private static void EnsureConsole()
110
  {
111
- // Try attaching to parent console first (for dotnet run in PowerShell)
112
  if (!AttachConsole(ATTACH_PARENT_PROCESS))
113
- {
114
- // Fallback: allocate a new console
115
  AllocConsole();
116
- }
117
  Console.OutputEncoding = Encoding.UTF8;
118
- // Redirect standard output to the attached console
119
  var stdout = Console.OpenStandardOutput();
120
  var writer = new StreamWriter(stdout, Encoding.UTF8) { AutoFlush = true };
121
  Console.SetOut(writer);
122
  Console.SetError(writer);
123
  }
124
 
125
- private static void ClearCacheCli()
126
  {
127
- PrintBanner("Cache Cleanup");
128
  var cachePath = Path.Combine(
129
  Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
130
  "FastSeek", "cache.dat");
131
-
132
- if (!File.Exists(cachePath))
133
- {
134
- PrintInfo("No cache file found.");
135
- PrintPath(cachePath);
136
- return;
137
- }
138
-
139
- var size = new FileInfo(cachePath).Length;
140
- try
141
  {
 
142
  File.Delete(cachePath);
143
- PrintSuccess($"Cache deleted ({FormatBytes(size)})");
144
  }
145
- catch (Exception ex)
146
  {
147
- PrintError($"Failed to delete cache: {ex.Message}");
148
  }
 
 
 
149
  }
150
 
151
- private static void RunDiagnostics()
152
  {
153
- PrintBanner("FastSeek Diagnostic Mode");
 
154
 
155
- var sw = Stopwatch.StartNew();
 
 
156
 
157
- // 1. Drive discovery
158
- PrintSection("1. Drive Discovery");
159
  var drives = DriveDiscovery.GetNtfsDrives();
160
- PrintMetric("NTFS drives", drives.Count, "found", sw.ElapsedMilliseconds);
161
- foreach (var d in drives)
162
- PrintDetail($"→ {d.Letter}: {d.Root} ({d.DevicePath})");
163
  Console.WriteLine();
164
 
165
  if (drives.Count == 0)
166
  {
167
- PrintError("No NTFS drives found. Run as Administrator.");
 
168
  return;
169
  }
170
 
171
- // 2. MFT Scan
172
- PrintSection("2. MFT Scan");
173
- sw.Restart();
174
  var index = new IndexStore();
175
  long totalRecords = 0;
 
 
176
  foreach (var drive in drives)
177
  {
178
  try
179
  {
180
  using var reader = new MftReader(drive);
181
- var scan = reader.ScanDirect();
182
- if (scan == null) scan = reader.Scan();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  index.PopulateFromScan(scan, drive.Root);
184
  totalRecords += scan.Records.Count;
185
- PrintDetail($"{drive.Letter}: {scan.Records.Count:N0} records");
186
  }
187
  catch (Exception ex)
188
  {
189
- PrintDetail($"{drive.Letter}: ERROR - {ex.Message}");
190
  }
191
  }
 
192
  index.CompleteIndex();
193
  sw.Stop();
194
 
195
- double recPerMs = totalRecords / Math.Max(sw.ElapsedMilliseconds, 1);
196
- PrintMetric("Total records", totalRecords, "records", sw.ElapsedMilliseconds);
197
- PrintMetric("Scan speed", recPerMs, "rec/ms", null);
198
- PrintMetric("Final index", index.Count, "entries", null);
199
  Console.WriteLine();
200
 
201
- // 3. Cache save
202
- PrintSection("3. Cache Save");
203
- sw.Restart();
204
  CacheManager.SaveCache(index);
205
- sw.Stop();
206
- var cachePath = Path.Combine(
207
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
208
- "FastSeek", "cache.dat");
209
  long cacheSize = File.Exists(cachePath) ? new FileInfo(cachePath).Length : 0;
210
- PrintMetric("Save time", sw.ElapsedMilliseconds, "ms", null);
211
- PrintMetric("Cache size", cacheSize, "bytes", null);
212
- PrintDetail($"→ {FormatBytes(cacheSize)} ({cacheSize:N0} bytes)");
213
  Console.WriteLine();
214
 
215
- // 4. Cache load
216
- PrintSection("4. Cache Load");
217
- sw.Restart();
218
- var loadedCache = CacheManager.LoadCache();
219
- IndexStore? loadedIndex = null;
220
- if (loadedCache != null)
221
- loadedIndex = IndexStore.FromCache(loadedCache);
222
- sw.Stop();
223
- PrintMetric("Load time", sw.ElapsedMilliseconds, "ms", null);
224
- PrintMetric("Entries", loadedIndex?.Count ?? 0, "entries", null);
225
- Console.WriteLine();
226
-
227
- // 5. Search benchmark
228
- PrintSection("5. Search Benchmark");
229
- Console.WriteLine(" Running 1,000 random queries...");
230
  var rnd = new Random(42);
231
- var names = loadedIndex?.NameCache ?? index.NameCache;
232
- var queries = Enumerable.Range(0, 1000)
233
- .Select(_ =>
234
- {
235
- if (names.Count == 0) return "test";
236
- var name = names[rnd.Next(names.Count)];
237
- if (name.Length < 2) return name;
238
- int start = rnd.Next(name.Length - 1);
239
- int len = rnd.Next(1, Math.Min(name.Length - start, 6));
240
- return name.Substring(start, len).ToLowerInvariant();
241
- }).ToList();
242
-
243
  var exclusions = CacheManager.LoadExclusions();
244
- sw.Restart();
245
- long resultCount = 0;
246
- foreach (var q in queries)
247
  {
248
- var r = SearchEngine.Search(loadedIndex ?? index, q, 50, false, exclusions);
249
- resultCount += r.Count;
 
 
 
 
 
 
250
  }
251
- sw.Stop();
252
 
253
- double avgMs = sw.ElapsedMilliseconds / 1000.0;
254
- double avgResults = resultCount / 1000.0;
255
- PrintMetric("Total time", sw.ElapsedMilliseconds, "ms", null);
256
- PrintMetric("Avg/query", avgMs, "ms", null);
257
- PrintMetric("Avg results", avgResults, "results/query", null);
258
- PrintMetric("Throughput", 1000.0 / Math.Max(sw.ElapsedMilliseconds, 1) * 1000, "queries/sec", null);
259
  Console.WriteLine();
260
-
261
- // Memory
262
- PrintSection("6. Memory Usage");
263
- GC.Collect();
264
- GC.WaitForPendingFinalizers();
265
- GC.Collect();
266
- long memBytes = GC.GetTotalMemory(false);
267
- PrintMetric("Working set", memBytes, "bytes", null);
268
- PrintDetail($"→ {FormatBytes(memBytes)}");
269
  Console.WriteLine();
270
 
271
- // Log
272
- PrintSection("7. Log File");
273
- PrintPath(Path.Combine(
274
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
275
- "FastSeek", "log.txt"));
 
 
 
 
 
 
 
276
  Console.WriteLine();
277
 
278
- PrintSuccess("Diagnostic complete.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }
280
 
281
- // --- Pretty-print helpers ---
282
- private static void PrintBanner(string title)
283
  {
284
- int width = 50;
285
- string line = new('═', width);
286
- int pad = (width - title.Length) / 2;
287
  Console.WriteLine();
288
- Console.ForegroundColor = ConsoleColor.Cyan;
289
- Console.WriteLine($"╔{line}╗");
290
- Console.WriteLine($"║{new string(' ', pad)}{title}{new string(' ', width - pad - title.Length)}║");
291
- Console.WriteLine($"╚{line}╝");
292
  Console.ResetColor();
293
- Console.WriteLine();
 
294
  }
295
 
296
- private static void PrintSection(string title)
 
297
  {
298
- Console.ForegroundColor = ConsoleColor.Yellow;
299
- Console.WriteLine($" {title}");
300
  Console.ResetColor();
301
  }
302
 
303
- private static void PrintMetric(string label, double value, string unit, long? ms)
304
  {
305
- Console.Write(" ");
306
  Console.ForegroundColor = ConsoleColor.DarkGray;
307
- Console.Write($"{label,-16}");
308
  Console.ResetColor();
309
- Console.Write(": ");
310
- Console.ForegroundColor = ConsoleColor.White;
311
- Console.Write(value.ToString("N0").PadLeft(12));
312
- Console.ResetColor();
313
- Console.Write($" {unit}");
314
- if (ms.HasValue)
315
- {
316
- Console.ForegroundColor = ConsoleColor.DarkGray;
317
- Console.Write($" ({ms.Value} ms)");
318
- Console.ResetColor();
319
- }
320
- Console.WriteLine();
321
  }
322
 
323
- private static void PrintDetail(string text)
324
  {
325
  Console.ForegroundColor = ConsoleColor.DarkGray;
326
- Console.WriteLine($" {text}");
 
 
327
  Console.ResetColor();
328
  }
329
 
330
- private static void PrintSuccess(string text)
331
  {
332
- Console.ForegroundColor = ConsoleColor.Green;
333
- Console.WriteLine($"✓ {text}");
334
  Console.ResetColor();
335
  }
336
 
@@ -348,13 +329,6 @@ public partial class App : Application
348
  Console.ResetColor();
349
  }
350
 
351
- private static void PrintPath(string path)
352
- {
353
- Console.ForegroundColor = ConsoleColor.DarkGray;
354
- Console.WriteLine($" {path}");
355
- Console.ResetColor();
356
- }
357
-
358
  private static string FormatBytes(long bytes)
359
  {
360
  if (bytes < 1024) return $"{bytes} B";
@@ -363,37 +337,39 @@ public partial class App : Application
363
  return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
364
  }
365
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  private void OnExit(object sender, ExitEventArgs e)
367
  {
368
  _shutdown = true;
369
- if (_hotkeyWakeEvent != IntPtr.Zero)
370
- SetEvent(_hotkeyWakeEvent);
371
- _hotkeyThread?.Join(1000);
372
  if (_ownsMutex)
373
  _mutex?.ReleaseMutex();
374
  _mutex?.Dispose();
375
- if (_hotkeyWakeEvent != IntPtr.Zero)
376
- CloseHandle(_hotkeyWakeEvent);
377
  Logger.Log("=== App exit ===");
378
  }
379
 
380
  private void HotkeyLoop(object? param)
381
  {
382
  IntPtr targetHwnd = (IntPtr)(param ?? IntPtr.Zero);
383
- if (targetHwnd == IntPtr.Zero)
384
- {
385
- Logger.Log("HotkeyLoop: targetHwnd is zero, exiting.");
386
- return;
387
- }
388
 
389
  uint modifiers = 0x0002 | 0x0004;
390
  if (!Win32Api.RegisterHotKey(IntPtr.Zero, 1, modifiers, 0x20))
391
  {
392
- int err = Marshal.GetLastWin32Error();
393
- Logger.Log($"RegisterHotKey failed: error {err}. Hotkey will not work.");
394
  return;
395
  }
396
- Logger.Log("Hotkey registered: Ctrl+Shift+Space");
397
 
398
  try
399
  {
@@ -403,10 +379,7 @@ public partial class App : Application
403
  if (result == 0 || result == -1) break;
404
 
405
  if (msg.message == Win32Api.WM_HOTKEY)
406
- {
407
- Logger.Log("Hotkey pressed, toggling window.");
408
  Win32Api.PostMessageW(targetHwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
409
- }
410
  else
411
  {
412
  Win32Api.TranslateMessage(ref msg);
@@ -417,16 +390,6 @@ public partial class App : Application
417
  finally
418
  {
419
  Win32Api.UnregisterHotKey(IntPtr.Zero, 1);
420
- Logger.Log("Hotkey unregistered.");
421
  }
422
  }
423
-
424
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
425
- private static extern IntPtr CreateEventW(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string? lpName);
426
-
427
- [DllImport("kernel32.dll", SetLastError = true)]
428
- private static extern bool SetEvent(IntPtr hEvent);
429
-
430
- [DllImport("kernel32.dll", SetLastError = true)]
431
- private static extern bool CloseHandle(IntPtr hObject);
432
  }
 
23
  private volatile bool _shutdown;
24
  private MainWindow? _mainWindow;
25
  private Thread? _hotkeyThread;
 
 
 
 
26
 
27
  [DllImport("kernel32.dll", SetLastError = true)]
28
  private static extern bool AttachConsole(int dwProcessId);
 
33
  [DllImport("kernel32.dll", SetLastError = true)]
34
  private static extern bool FreeConsole();
35
 
36
+ private const int ATTACH_PARENT_PROCESS = -1;
 
37
 
38
  private void OnStartup(object sender, StartupEventArgs e)
39
  {
40
+ if (e.Args.Contains("--cli") || e.Args.Contains("--diag"))
 
41
  {
42
+ RunCliMode();
 
 
 
 
43
  Shutdown();
44
  return;
45
  }
46
 
47
+ if (e.Args.Contains("--clear-cache"))
 
48
  {
49
+ RunClearCache();
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  Shutdown();
51
  return;
52
  }
53
 
54
  Logger.Log("=== App startup ===");
55
 
 
56
  _mutex = new Mutex(true, MutexName, out bool createdNew);
57
  if (!createdNew)
58
  {
 
59
  IntPtr hwnd = Win32Api.FindWindowW(null, "FastSeek");
60
  if (hwnd != IntPtr.Zero)
61
  Win32Api.PostMessageW(hwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
 
 
62
  Shutdown();
63
  return;
64
  }
65
  _ownsMutex = true;
 
66
 
67
  _mainWindow = new MainWindow();
68
  _mainWindow.Show();
69
 
 
70
  _hotkeyThread = new Thread(HotkeyLoop)
71
  {
72
  IsBackground = true,
 
78
 
79
  private static void EnsureConsole()
80
  {
 
81
  if (!AttachConsole(ATTACH_PARENT_PROCESS))
 
 
82
  AllocConsole();
 
83
  Console.OutputEncoding = Encoding.UTF8;
 
84
  var stdout = Console.OpenStandardOutput();
85
  var writer = new StreamWriter(stdout, Encoding.UTF8) { AutoFlush = true };
86
  Console.SetOut(writer);
87
  Console.SetError(writer);
88
  }
89
 
90
+ private static void RunClearCache()
91
  {
92
+ EnsureConsole();
93
  var cachePath = Path.Combine(
94
  Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
95
  "FastSeek", "cache.dat");
96
+ if (File.Exists(cachePath))
 
 
 
 
 
 
 
 
 
97
  {
98
+ var size = new FileInfo(cachePath).Length;
99
  File.Delete(cachePath);
100
+ Console.WriteLine($"Cache cleared ({size:N0} bytes)");
101
  }
102
+ else
103
  {
104
+ Console.WriteLine("No cache found.");
105
  }
106
+ Console.WriteLine("Press Enter...");
107
+ Console.ReadLine();
108
+ FreeConsole();
109
  }
110
 
111
+ private static void RunCliMode()
112
  {
113
+ EnsureConsole();
114
+ Console.Clear();
115
 
116
+ // Compact header
117
+ PrintHeader("FastSeek CLI");
118
+ Console.WriteLine();
119
 
120
+ // 1. Drives
 
121
  var drives = DriveDiscovery.GetNtfsDrives();
122
+ PrintLine(ConsoleColor.Cyan, "Drives", $"{drives.Count} NTFS found");
123
+ foreach (var d in drives) PrintDetail($" {d.Letter}: {d.Root}");
 
124
  Console.WriteLine();
125
 
126
  if (drives.Count == 0)
127
  {
128
+ PrintError("No NTFS drives. Run as Administrator.");
129
+ WaitExit();
130
  return;
131
  }
132
 
133
+ // 2. Scan with per-drive debug
134
+ PrintLine(ConsoleColor.Cyan, "Scan", "Reading MFT...");
 
135
  var index = new IndexStore();
136
  long totalRecords = 0;
137
+ var sw = Stopwatch.StartNew();
138
+
139
  foreach (var drive in drives)
140
  {
141
  try
142
  {
143
  using var reader = new MftReader(drive);
144
+ ScanResult? scan = null;
145
+ string method = "direct";
146
+
147
+ try { scan = reader.ScanDirect(); }
148
+ catch (Exception ex1)
149
+ {
150
+ PrintDetail($" {drive.Letter}: direct scan failed: {ex1.Message}");
151
+ method = "fallback";
152
+ }
153
+
154
+ if (scan == null)
155
+ {
156
+ try { scan = reader.Scan(); method = "fallback"; }
157
+ catch (Exception ex2)
158
+ {
159
+ PrintError($" {drive.Letter}: fallback scan failed: {ex2.Message}");
160
+ continue;
161
+ }
162
+ }
163
+
164
  index.PopulateFromScan(scan, drive.Root);
165
  totalRecords += scan.Records.Count;
166
+ PrintLine(ConsoleColor.Green, $" {drive.Letter}", $"{scan.Records.Count:N0} records ({method})");
167
  }
168
  catch (Exception ex)
169
  {
170
+ PrintError($" {drive.Letter}: {ex.Message}");
171
  }
172
  }
173
+
174
  index.CompleteIndex();
175
  sw.Stop();
176
 
177
+ PrintLine(ConsoleColor.White, "Index", $"{index.Count:N0} entries | {sw.ElapsedMilliseconds}ms | {totalRecords / Math.Max(sw.ElapsedMilliseconds, 1):N0} rec/ms");
 
 
 
178
  Console.WriteLine();
179
 
180
+ // 3. Cache
181
+ var cacheSw = Stopwatch.StartNew();
 
182
  CacheManager.SaveCache(index);
183
+ cacheSw.Stop();
184
+ var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FastSeek", "cache.dat");
 
 
185
  long cacheSize = File.Exists(cachePath) ? new FileInfo(cachePath).Length : 0;
186
+ PrintLine(ConsoleColor.Cyan, "Cache", $"saved {FormatBytes(cacheSize)} in {cacheSw.ElapsedMilliseconds}ms");
 
 
187
  Console.WriteLine();
188
 
189
+ // 4. Benchmark
190
+ PrintLine(ConsoleColor.Cyan, "Benchmark", "1,000 random queries...");
191
+ var benchSw = Stopwatch.StartNew();
 
 
 
 
 
 
 
 
 
 
 
 
192
  var rnd = new Random(42);
193
+ var names = index.NameCache;
 
 
 
 
 
 
 
 
 
 
 
194
  var exclusions = CacheManager.LoadExclusions();
195
+ long benchResults = 0;
196
+
197
+ for (int i = 0; i < 1000; i++)
198
  {
199
+ if (names.Count == 0) break;
200
+ var name = names[rnd.Next(names.Count)];
201
+ if (name.Length < 2) continue;
202
+ int start = rnd.Next(name.Length - 1);
203
+ int len = rnd.Next(1, Math.Min(name.Length - start, 6));
204
+ var q = name.Substring(start, len).ToLowerInvariant();
205
+ var r = SearchEngine.Search(index, q, 50, false, exclusions);
206
+ benchResults += r.Count;
207
  }
208
+ benchSw.Stop();
209
 
210
+ double qps = 1000.0 / Math.Max(benchSw.ElapsedMilliseconds, 1) * 1000;
211
+ PrintLine(ConsoleColor.White, "Result", $"{benchSw.ElapsedMilliseconds}ms total | {qps:N0} qps | {benchResults / 1000.0:F1} avg results");
 
 
 
 
212
  Console.WriteLine();
213
+ PrintDivider();
 
 
 
 
 
 
 
 
214
  Console.WriteLine();
215
 
216
+ // 5. Interactive search
217
+ if (index.Count == 0)
218
+ {
219
+ PrintError("INDEX IS EMPTY — GUI will show no results.");
220
+ PrintInfo("Possible causes:");
221
+ PrintDetail(" • Not running as Administrator");
222
+ PrintDetail(" • MFT direct read blocked (try chkdsk)");
223
+ PrintDetail(" • FSCTL_ENUM_USN_DATA not supported on this volume");
224
+ Console.WriteLine();
225
+ }
226
+
227
+ PrintHeader("Interactive Search (type 'exit' to quit)");
228
  Console.WriteLine();
229
 
230
+ while (true)
231
+ {
232
+ Console.ForegroundColor = ConsoleColor.DarkGray;
233
+ Console.Write("> ");
234
+ Console.ResetColor();
235
+ var query = Console.ReadLine()?.Trim();
236
+ if (string.IsNullOrEmpty(query)) continue;
237
+ if (query.Equals("exit", StringComparison.OrdinalIgnoreCase)) break;
238
+
239
+ var searchSw = Stopwatch.StartNew();
240
+ var results = SearchEngine.Search(index, query, 20, false, exclusions);
241
+ searchSw.Stop();
242
+
243
+ Console.ForegroundColor = ConsoleColor.DarkGray;
244
+ Console.WriteLine($" {results.Count} results | {searchSw.Elapsed.TotalMilliseconds:F3}ms");
245
+ Console.ResetColor();
246
+
247
+ foreach (var r in results.Take(10))
248
+ {
249
+ var color = r.Kind switch
250
+ {
251
+ ResultKind.App => ConsoleColor.Green,
252
+ ResultKind.Document => ConsoleColor.Yellow,
253
+ ResultKind.Image => ConsoleColor.Magenta,
254
+ ResultKind.Video => ConsoleColor.Red,
255
+ ResultKind.Audio => ConsoleColor.Blue,
256
+ ResultKind.Archive => ConsoleColor.DarkYellow,
257
+ ResultKind.Folder => ConsoleColor.Cyan,
258
+ _ => ConsoleColor.Gray
259
+ };
260
+ Console.ForegroundColor = color;
261
+ Console.Write($" [{GetBadgeLabel(r.Kind),-4}] ");
262
+ Console.ResetColor();
263
+ Console.WriteLine($"{r.Name} → {r.FullPath}");
264
+ }
265
+ if (results.Count > 10)
266
+ {
267
+ Console.ForegroundColor = ConsoleColor.DarkGray;
268
+ Console.WriteLine($" ... and {results.Count - 10} more");
269
+ Console.ResetColor();
270
+ }
271
+ Console.WriteLine();
272
+ }
273
+
274
+ WaitExit();
275
  }
276
 
277
+ private static void WaitExit()
 
278
  {
 
 
 
279
  Console.WriteLine();
280
+ Console.ForegroundColor = ConsoleColor.DarkGray;
281
+ Console.WriteLine("Press Enter to exit...");
 
 
282
  Console.ResetColor();
283
+ Console.ReadLine();
284
+ FreeConsole();
285
  }
286
 
287
+ // --- Formatting helpers ---
288
+ private static void PrintHeader(string text)
289
  {
290
+ Console.ForegroundColor = ConsoleColor.Cyan;
291
+ Console.WriteLine($"━━━ {text} ━━━");
292
  Console.ResetColor();
293
  }
294
 
295
+ private static void PrintDivider()
296
  {
 
297
  Console.ForegroundColor = ConsoleColor.DarkGray;
298
+ Console.WriteLine(new string('─', 60));
299
  Console.ResetColor();
 
 
 
 
 
 
 
 
 
 
 
 
300
  }
301
 
302
+ private static void PrintLine(ConsoleColor color, string label, string value)
303
  {
304
  Console.ForegroundColor = ConsoleColor.DarkGray;
305
+ Console.Write($"{label,-10}: ");
306
+ Console.ForegroundColor = color;
307
+ Console.WriteLine(value);
308
  Console.ResetColor();
309
  }
310
 
311
+ private static void PrintDetail(string text)
312
  {
313
+ Console.ForegroundColor = ConsoleColor.DarkGray;
314
+ Console.WriteLine(text);
315
  Console.ResetColor();
316
  }
317
 
 
329
  Console.ResetColor();
330
  }
331
 
 
 
 
 
 
 
 
332
  private static string FormatBytes(long bytes)
333
  {
334
  if (bytes < 1024) return $"{bytes} B";
 
337
  return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
338
  }
339
 
340
+ private static string GetBadgeLabel(ResultKind kind) => kind switch
341
+ {
342
+ ResultKind.App => "APP",
343
+ ResultKind.Document => "DOC",
344
+ ResultKind.Image => "IMG",
345
+ ResultKind.Video => "VID",
346
+ ResultKind.Audio => "AUD",
347
+ ResultKind.Archive => "ZIP",
348
+ ResultKind.Folder => "DIR",
349
+ _ => "FILE"
350
+ };
351
+
352
  private void OnExit(object sender, ExitEventArgs e)
353
  {
354
  _shutdown = true;
355
+ _hotkeyThread?.Join(500);
 
 
356
  if (_ownsMutex)
357
  _mutex?.ReleaseMutex();
358
  _mutex?.Dispose();
 
 
359
  Logger.Log("=== App exit ===");
360
  }
361
 
362
  private void HotkeyLoop(object? param)
363
  {
364
  IntPtr targetHwnd = (IntPtr)(param ?? IntPtr.Zero);
365
+ if (targetHwnd == IntPtr.Zero) return;
 
 
 
 
366
 
367
  uint modifiers = 0x0002 | 0x0004;
368
  if (!Win32Api.RegisterHotKey(IntPtr.Zero, 1, modifiers, 0x20))
369
  {
370
+ Logger.Log($"RegisterHotKey failed: {Marshal.GetLastWin32Error()}");
 
371
  return;
372
  }
 
373
 
374
  try
375
  {
 
379
  if (result == 0 || result == -1) break;
380
 
381
  if (msg.message == Win32Api.WM_HOTKEY)
 
 
382
  Win32Api.PostMessageW(targetHwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
 
383
  else
384
  {
385
  Win32Api.TranslateMessage(ref msg);
 
390
  finally
391
  {
392
  Win32Api.UnregisterHotKey(IntPtr.Zero, 1);
 
393
  }
394
  }
 
 
 
 
 
 
 
 
 
395
  }