Upload FastSeekWpf/App.xaml.cs
Browse files- 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 |
-
|
| 41 |
-
private static extern IntPtr GetConsoleWindow();
|
| 42 |
|
| 43 |
private void OnStartup(object sender, StartupEventArgs e)
|
| 44 |
{
|
| 45 |
-
|
| 46 |
-
if (e.Args.Contains("--clear-cache"))
|
| 47 |
{
|
| 48 |
-
|
| 49 |
-
ClearCacheCli();
|
| 50 |
-
Console.WriteLine("\nPress Enter to exit...");
|
| 51 |
-
Console.ReadLine();
|
| 52 |
-
FreeConsole();
|
| 53 |
Shutdown();
|
| 54 |
return;
|
| 55 |
}
|
| 56 |
|
| 57 |
-
|
| 58 |
-
if (e.Args.Contains("--diag") || e.Args.Contains("--cli"))
|
| 59 |
{
|
| 60 |
-
|
| 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
|
| 126 |
{
|
| 127 |
-
|
| 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 |
-
|
| 144 |
}
|
| 145 |
-
|
| 146 |
{
|
| 147 |
-
|
| 148 |
}
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
-
private static void
|
| 152 |
{
|
| 153 |
-
|
|
|
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
// 1.
|
| 158 |
-
PrintSection("1. Drive Discovery");
|
| 159 |
var drives = DriveDiscovery.GetNtfsDrives();
|
| 160 |
-
|
| 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
|
|
|
|
| 168 |
return;
|
| 169 |
}
|
| 170 |
|
| 171 |
-
// 2.
|
| 172 |
-
|
| 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 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
index.PopulateFromScan(scan, drive.Root);
|
| 184 |
totalRecords += scan.Records.Count;
|
| 185 |
-
|
| 186 |
}
|
| 187 |
catch (Exception ex)
|
| 188 |
{
|
| 189 |
-
|
| 190 |
}
|
| 191 |
}
|
|
|
|
| 192 |
index.CompleteIndex();
|
| 193 |
sw.Stop();
|
| 194 |
|
| 195 |
-
|
| 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
|
| 202 |
-
|
| 203 |
-
sw.Restart();
|
| 204 |
CacheManager.SaveCache(index);
|
| 205 |
-
|
| 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 |
-
|
| 211 |
-
PrintMetric("Cache size", cacheSize, "bytes", null);
|
| 212 |
-
PrintDetail($"→ {FormatBytes(cacheSize)} ({cacheSize:N0} bytes)");
|
| 213 |
Console.WriteLine();
|
| 214 |
|
| 215 |
-
// 4.
|
| 216 |
-
|
| 217 |
-
|
| 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 =
|
| 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 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
{
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
-
|
| 252 |
|
| 253 |
-
double
|
| 254 |
-
|
| 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 |
-
//
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
Console.WriteLine();
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
}
|
| 280 |
|
| 281 |
-
|
| 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.
|
| 289 |
-
Console.WriteLine(
|
| 290 |
-
Console.WriteLine($"║{new string(' ', pad)}{title}{new string(' ', width - pad - title.Length)}║");
|
| 291 |
-
Console.WriteLine($"╚{line}╝");
|
| 292 |
Console.ResetColor();
|
| 293 |
-
Console.
|
|
|
|
| 294 |
}
|
| 295 |
|
| 296 |
-
|
|
|
|
| 297 |
{
|
| 298 |
-
Console.ForegroundColor = ConsoleColor.
|
| 299 |
-
Console.WriteLine($"
|
| 300 |
Console.ResetColor();
|
| 301 |
}
|
| 302 |
|
| 303 |
-
private static void
|
| 304 |
{
|
| 305 |
-
Console.Write(" ");
|
| 306 |
Console.ForegroundColor = ConsoleColor.DarkGray;
|
| 307 |
-
Console.
|
| 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
|
| 324 |
{
|
| 325 |
Console.ForegroundColor = ConsoleColor.DarkGray;
|
| 326 |
-
Console.
|
|
|
|
|
|
|
| 327 |
Console.ResetColor();
|
| 328 |
}
|
| 329 |
|
| 330 |
-
private static void
|
| 331 |
{
|
| 332 |
-
Console.ForegroundColor = ConsoleColor.
|
| 333 |
-
Console.WriteLine(
|
| 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 |
-
|
| 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 |
-
|
| 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 |
}
|