finder-wpf / FastSeekWpf /App.xaml.cs
anshdadhich's picture
App.xaml.cs: Update to use IndexStore.Finalize() and index.Name(entry) APIs. Fix benchmark to use Entries list instead of removed NameCache property.
cee26fa verified
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
using FastSeekWpf.Core;
using FastSeekWpf.NativeInterop;
namespace FastSeekWpf;
public partial class App : Application
{
private const string MutexName = "FastSeekWpf_SingleInstance_Mutex";
private const uint WM_TOGGLE_WINDOW = Win32Api.WM_USER + 3;
private Mutex? _mutex;
private bool _ownsMutex;
private volatile bool _shutdown;
private MainWindow? _mainWindow;
private Thread? _hotkeyThread;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
private const int ATTACH_PARENT_PROCESS = -1;
private void OnStartup(object sender, StartupEventArgs e)
{
if (e.Args.Contains("--cli"))
{
RunCliMode();
Shutdown();
return;
}
if (e.Args.Contains("--clear-cache"))
{
RunClearCache();
Shutdown();
return;
}
Logger.Log("=== App startup ===");
_mutex = new Mutex(true, MutexName, out bool createdNew);
if (!createdNew)
{
IntPtr hwnd = Win32Api.FindWindowW(null, "FastSeek");
if (hwnd != IntPtr.Zero)
Win32Api.PostMessageW(hwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
Shutdown();
return;
}
_ownsMutex = true;
_mainWindow = new MainWindow();
_mainWindow.Show();
_hotkeyThread = new Thread(HotkeyLoop)
{
IsBackground = true,
Name = "HotkeyThread"
};
var helper = new WindowInteropHelper(_mainWindow);
_hotkeyThread.Start(helper.Handle);
}
private static void EnsureConsole()
{
if (!AttachConsole(ATTACH_PARENT_PROCESS))
AllocConsole();
Console.OutputEncoding = Encoding.UTF8;
var stdout = Console.OpenStandardOutput();
var writer = new StreamWriter(stdout, Encoding.UTF8) { AutoFlush = true };
Console.SetOut(writer);
Console.SetError(writer);
}
private static void RunClearCache()
{
EnsureConsole();
var cachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"FastSeek", "cache.dat");
if (File.Exists(cachePath))
{
var size = new FileInfo(cachePath).Length;
File.Delete(cachePath);
Console.WriteLine($"Cache cleared ({size:N0} bytes)");
}
else
{
Console.WriteLine("No cache found.");
}
Console.WriteLine("Press Enter...");
Console.ReadLine();
FreeConsole();
}
private static void RunCliMode()
{
EnsureConsole();
Console.Clear();
PrintHeader("FastSeek CLI");
Console.WriteLine();
bool elevated = Elevation.IsElevated();
if (elevated)
PrintLine(ConsoleColor.Green, "Elevation", "Administrator βœ“");
else
{
PrintLine(ConsoleColor.Red, "Elevation", $"{Elevation.StatusText()} βœ—");
PrintError("NOT RUNNING AS ADMINISTRATOR β€” MFT read will fail.");
PrintInfo("To fix: Build the project, then run the compiled .exe:");
PrintDetail(" dotnet publish -c Release -r win-x64");
PrintDetail(" .\\bin\\Release\\net8.0-windows\\win-x64\\FastSeekWpf.exe --cli");
Console.WriteLine();
}
var drives = DriveDiscovery.GetNtfsDrives();
PrintLine(ConsoleColor.Cyan, "Drives", $"{drives.Count} NTFS found");
foreach (var d in drives) PrintDetail($" {d.Letter}: {d.Root}");
Console.WriteLine();
if (drives.Count == 0)
{
PrintError("No NTFS drives found.");
if (!elevated)
PrintInfo("This is likely because you are not elevated. Try running as Administrator.");
WaitExit();
return;
}
if (!elevated)
PrintInfo("Attempting scan anyway (will likely fail without elevation)...");
PrintLine(ConsoleColor.Cyan, "Scan", "Reading MFT...");
var index = new IndexStore();
long totalRecords = 0;
var sw = Stopwatch.StartNew();
foreach (var drive in drives)
{
try
{
using var reader = new MftReader(drive);
var (scan, method) = reader.ScanAny();
if (scan.Records.Count > 0)
{
index.PopulateFromScan(scan, drive.Root);
totalRecords += scan.Records.Count;
PrintLine(ConsoleColor.Green, $" {drive.Letter}", $"{scan.Records.Count:N0} records ({method})");
}
else
{
PrintError($" {drive.Letter}: 0 records");
if (!elevated)
PrintInfo(" ↑ Expected when not running as Administrator.");
}
}
catch (Exception ex)
{
PrintError($" {drive.Letter}: {ex.Message}");
}
}
index.Finalize();
sw.Stop();
PrintLine(ConsoleColor.White, "Index", $"{index.Count:N0} entries | {sw.ElapsedMilliseconds}ms | {totalRecords / Math.Max(sw.ElapsedMilliseconds, 1):N0} rec/ms");
Console.WriteLine();
if (index.Count > 0)
{
var cacheSw = Stopwatch.StartNew();
CacheManager.SaveCache(index);
cacheSw.Stop();
var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FastSeek", "cache.dat");
long cacheSize = File.Exists(cachePath) ? new FileInfo(cachePath).Length : 0;
PrintLine(ConsoleColor.Cyan, "Cache", $"saved {FormatBytes(cacheSize)} in {cacheSw.ElapsedMilliseconds}ms");
}
else
PrintLine(ConsoleColor.DarkGray, "Cache", "skipped (empty index)");
Console.WriteLine();
if (index.Count > 0)
{
PrintLine(ConsoleColor.Cyan, "Benchmark", "1,000 random queries...");
var benchSw = Stopwatch.StartNew();
var rnd = new Random(42);
var exclusions = CacheManager.LoadExclusions();
long benchResults = 0;
for (int i = 0; i < 1000; i++)
{
if (index.Entries.Count == 0) break;
var entry = index.Entries[rnd.Next(index.Entries.Count)];
var name = index.Name(entry);
if (name.Length < 2) continue;
int start = rnd.Next(name.Length - 1);
int len = rnd.Next(1, Math.Min(name.Length - start, 6));
var q = name.Substring(start, len).ToLowerInvariant();
var r = SearchEngine.Search(index, q, 50, false, exclusions);
benchResults += r.Count;
}
benchSw.Stop();
double qps = 1000.0 / Math.Max(benchSw.ElapsedMilliseconds, 1) * 1000;
PrintLine(ConsoleColor.White, "Result", $"{benchSw.ElapsedMilliseconds}ms total | {qps:N0} qps | {benchResults / 1000.0:F1} avg results");
}
else
PrintLine(ConsoleColor.DarkGray, "Benchmark", "skipped (empty index)");
Console.WriteLine();
PrintDivider();
Console.WriteLine();
if (index.Count == 0)
{
PrintError("INDEX IS EMPTY β€” GUI will show no results.");
if (!elevated)
{
PrintInfo("ROOT CAUSE: You must run the COMPILED .exe as Administrator.");
PrintInfo("'dotnet run' does NOT trigger UAC elevation.");
PrintInfo("Steps:");
PrintDetail(" 1. dotnet publish -c Release -r win-x64");
PrintDetail(" 2. .\\bin\\Release\\net8.0-windows\\win-x64\\FastSeekWpf.exe --cli");
PrintDetail(" 3. Accept the UAC prompt");
}
else
{
PrintInfo("Possible causes:");
PrintDetail(" β€’ MFT direct read blocked by antivirus/EDR");
PrintDetail(" β€’ Volume is ReFS/BitLocker/VM shared folder");
PrintDetail(" β€’ chkdsk / Windows Update holding volume lock");
}
Console.WriteLine();
}
if (index.Count > 0)
{
PrintHeader("Interactive Search (type 'exit' to quit)");
Console.WriteLine();
while (true)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write("> ");
Console.ResetColor();
var query = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(query)) continue;
if (query.Equals("exit", StringComparison.OrdinalIgnoreCase)) break;
var searchSw = Stopwatch.StartNew();
var exclusions = CacheManager.LoadExclusions();
var results = SearchEngine.Search(index, query, 20, false, exclusions);
searchSw.Stop();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" {results.Count} results | {searchSw.Elapsed.TotalMilliseconds:F3}ms");
Console.ResetColor();
foreach (var r in results.Take(10))
{
var color = r.Kind switch
{
ResultKind.App => ConsoleColor.Green,
ResultKind.Document => ConsoleColor.Yellow,
ResultKind.Image => ConsoleColor.Magenta,
ResultKind.Video => ConsoleColor.Red,
ResultKind.Audio => ConsoleColor.Blue,
ResultKind.Archive => ConsoleColor.DarkYellow,
ResultKind.Folder => ConsoleColor.Cyan,
_ => ConsoleColor.Gray
};
Console.ForegroundColor = color;
Console.Write($" [{GetBadgeLabel(r.Kind),-4}] ");
Console.ResetColor();
Console.WriteLine($"{r.Name} β†’ {r.FullPath}");
}
if (results.Count > 10)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" ... and {results.Count - 10} more");
Console.ResetColor();
}
Console.WriteLine();
}
}
WaitExit();
}
private static void WaitExit()
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine("Press Enter to exit...");
Console.ResetColor();
Console.ReadLine();
FreeConsole();
}
private static void PrintHeader(string text)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"━━━ {text} ━━━");
Console.ResetColor();
}
private static void PrintDivider()
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(new string('─', 60));
Console.ResetColor();
}
private static void PrintLine(ConsoleColor color, string label, string value)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write($"{label,-10}: ");
Console.ForegroundColor = color;
Console.WriteLine(value);
Console.ResetColor();
}
private static void PrintDetail(string text)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(text);
Console.ResetColor();
}
private static void PrintError(string text)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"βœ— {text}");
Console.ResetColor();
}
private static void PrintInfo(string text)
{
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine($"β„Ή {text}");
Console.ResetColor();
}
private static string FormatBytes(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
}
private static string GetBadgeLabel(ResultKind kind) => kind switch
{
ResultKind.App => "APP",
ResultKind.Document => "DOC",
ResultKind.Image => "IMG",
ResultKind.Video => "VID",
ResultKind.Audio => "AUD",
ResultKind.Archive => "ZIP",
ResultKind.Folder => "DIR",
_ => "FILE"
};
private void OnExit(object sender, ExitEventArgs e)
{
_shutdown = true;
_hotkeyThread?.Join(500);
if (_ownsMutex)
_mutex?.ReleaseMutex();
_mutex?.Dispose();
Logger.Log("=== App exit ===");
}
private void HotkeyLoop(object? param)
{
IntPtr targetHwnd = (IntPtr)(param ?? IntPtr.Zero);
if (targetHwnd == IntPtr.Zero) return;
uint modifiers = 0x0002 | 0x0004;
if (!Win32Api.RegisterHotKey(IntPtr.Zero, 1, modifiers, 0x20))
{
Logger.Log($"RegisterHotKey failed: {Marshal.GetLastWin32Error()}");
return;
}
try
{
while (!_shutdown)
{
int result = Win32Api.GetMessageW(out Win32Api.MSG msg, IntPtr.Zero, 0, 0);
if (result == 0 || result == -1) break;
if (msg.message == Win32Api.WM_HOTKEY)
Win32Api.PostMessageW(targetHwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero);
else
{
Win32Api.TranslateMessage(ref msg);
Win32Api.DispatchMessageW(ref msg);
}
}
}
finally
{
Win32Api.UnregisterHotKey(IntPtr.Zero, 1);
}
}
}