using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using FastSeekWpf.Core; using FastSeekWpf.NativeInterop; namespace FastSeekWpf; public partial class MainWindow : Window, INotifyPropertyChanged { public const uint WM_TOGGLE_WINDOW = Win32Api.WM_USER + 3; private IndexStore _index = new(); private List _drives = new(); private readonly List _watchers = new(); private readonly object _indexLock = new(); private readonly System.Collections.Concurrent.ConcurrentQueue _eventQueue = new(); private CancellationTokenSource? _cts; private List _excludedDirs = new(); private int _selectedIndex; private bool _initialized; private bool _isVisible; private CancellationTokenSource? _searchDebounceCts; private readonly object _searchLock = new(); public event PropertyChangedEventHandler? PropertyChanged; private List _results = new(); public List Results { get => _results; set { _results = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Results))); } } public MainWindow() { Logger.Log("MainWindow constructor"); InitializeComponent(); DataContext = this; _excludedDirs = CacheManager.LoadExclusions(); Loaded += (s, e) => { var helper = new WindowInteropHelper(this); var hwnd = helper.Handle; HwndSource.FromHwnd(hwnd)?.AddHook(WndProc); EnableDwmRoundedCorners(hwnd); }; } private void EnableDwmRoundedCorners(IntPtr hwnd) { try { int cornerPref = Win32Api.DWMWCP_ROUND; Win32Api.DwmSetWindowAttribute(hwnd, Win32Api.DWMWA_WINDOW_CORNER_PREFERENCE, ref cornerPref, Marshal.SizeOf(cornerPref)); int darkMode = 1; Win32Api.DwmSetWindowAttribute(hwnd, Win32Api.DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, Marshal.SizeOf(darkMode)); Logger.Log("DWM rounded corners + dark mode enabled."); } catch (Exception ex) { Logger.Log($"DWM setup failed: {ex.Message}"); } } private void Window_Loaded(object sender, RoutedEventArgs e) { Logger.Log("Window_Loaded"); if (_initialized) return; _initialized = true; CenterWindow(); ShowEmptyState(); Task.Run(async () => await InitializeAsync()); } private void ShowEmptyState() { Dispatcher.Invoke(() => { ResultsScroll.Visibility = Visibility.Collapsed; EmptyHint.Visibility = Visibility.Visible; EmptyHint.Text = "Indexing..."; StatusBar.Visibility = Visibility.Visible; StatusText.Text = "Starting scan..."; SepLine.Visibility = Visibility.Collapsed; }); } private async Task InitializeAsync() { Logger.Log("InitializeAsync start"); try { _drives = DriveDiscovery.GetNtfsDrives(); Logger.Log($"Found {_drives.Count} NTFS drives"); if (_drives.Count == 0) { Dispatcher.Invoke(() => { EmptyHint.Text = "No NTFS drives found.\nRun as Administrator."; StatusText.Text = "Error: no drives"; }); return; } bool needFullScan = true; var cache = CacheManager.LoadCache(); if (cache != null) { Logger.Log($"Cache loaded: {cache.Entries.Count} entries"); lock (_indexLock) { _index = IndexStore.FromCache(cache); } Logger.Log($"Loaded cache: {_index.Count} files"); var checkpoints = new List(cache.Checkpoints); bool deltaOk = true; foreach (var drive in _drives) { var cp = checkpoints.FirstOrDefault(c => c.DriveLetter == drive.Letter); if (cp != null) { try { var watcher = UsnWatcher.CreateForCheckpoint(drive, evt => _eventQueue.Enqueue(evt), cp); watcher.Drain(); lock (_indexLock) { _index.Checkpoints.RemoveAll(c => c.DriveLetter == drive.Letter); _index.Checkpoints.Add(watcher.Checkpoint()); } _watchers.Add(watcher); Logger.Log($"Delta catch-up OK for {drive.Letter}"); } catch (Exception ex) { Logger.Log($"Delta catch-up failed: {ex.Message}"); deltaOk = false; break; } } } if (deltaOk) { needFullScan = false; while (_eventQueue.TryDequeue(out var evt)) ApplyEvent(evt); Logger.Log("Delta events applied."); } else { Logger.Log("Delta failed, clearing cache."); CacheManager.ClearCache(); _watchers.Clear(); _eventQueue.Clear(); } } if (needFullScan) { Logger.Log("Starting full MFT scan..."); lock (_indexLock) { _index = new IndexStore(); long totalRecords = 0; foreach (var drive in _drives) { try { Logger.Log($"Scanning {drive.Letter}..."); Dispatcher.Invoke(() => StatusText.Text = $"Scanning {drive.Letter}: ..."); 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; Logger.Log($"Scanned {drive.Letter}: {scan.Records.Count} files ({method})"); Dispatcher.Invoke(() => StatusText.Text = $"Scanned {drive.Letter}: {scan.Records.Count:N0} files"); } else { Logger.Log($"Scan returned 0 records for {drive.Letter}"); Dispatcher.Invoke(() => StatusText.Text = $"Scan failed for {drive.Letter}"); } } catch (Exception ex) { Logger.Log($"Scan failed for {drive.Letter}: {ex}"); } } _index.Finalize(); Logger.Log($"Full scan complete. Total indexed: {_index.Count}"); CacheManager.SaveCache(_index); Logger.Log("Cache saved."); } foreach (var drive in _drives) { try { var watcher = new UsnWatcher(drive, evt => _eventQueue.Enqueue(evt), null); _watchers.Add(watcher); _ = watcher.RunAsync(CancellationToken.None); Logger.Log($"USN watcher started for {drive.Letter}"); } catch (Exception ex) { Logger.Log($"USN watcher failed for {drive.Letter}: {ex.Message}"); } } } _cts = new CancellationTokenSource(); _ = Task.Run(() => LiveUpdateLoop(_cts.Token)); Dispatcher.Invoke(() => { if (_index.Count == 0) { EmptyHint.Text = "Index is empty.\nRun CLI diagnostics: FastSeekWpf.exe --cli"; StatusText.Text = "0 files indexed — check diagnostics"; } else { EmptyHint.Text = "Start typing to search your files..."; StatusText.Text = $"{_index.Count:N0} files indexed"; } StatusBar.Visibility = Visibility.Visible; }); Logger.Log("InitializeAsync done."); } catch (Exception ex) { Logger.Log($"InitializeAsync fatal: {ex}"); Dispatcher.Invoke(() => { EmptyHint.Text = $"Initialization failed:\n{ex.Message}\n\nCheck log at %LOCALAPPDATA%\\FastSeek\\log.txt"; StatusText.Text = "Error — check log"; }); } } private void LiveUpdateLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { int processed = 0; while (_eventQueue.TryDequeue(out var evt)) { ApplyEvent(evt); processed++; if (processed > 100) break; } if (processed > 50) CacheManager.SaveCache(_index); Thread.Sleep(100); } } private void ApplyEvent(IndexEvent evt) { lock (_indexLock) { switch (evt) { case IndexEvent.Created c: _index.Insert(c.Record); break; case IndexEvent.Deleted d: _index.Remove(d.FileRef); break; case IndexEvent.Renamed r: _index.Rename(r.OldRef, r.NewRecord); break; case IndexEvent.Moved m: _index.ApplyMove(m.FileRef, m.NewParentRef, m.Name, m.Kind); break; } } } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_TOGGLE_WINDOW) { Dispatcher.Invoke(ToggleVisibility); handled = true; } return IntPtr.Zero; } private void Window_Deactivated(object sender, EventArgs e) => FadeOutAndHide(); private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) DragMove(); } private void FadeOutAndHide() { if (Resources["FadeAnim"] is Storyboard fade) { fade.Completed += (s, e) => { Hide(); _isVisible = false; }; fade.Begin(this); } else { Hide(); _isVisible = false; } } private void ToggleVisibility() { if (_isVisible) FadeOutAndHide(); else { CenterWindow(); SearchBox.Clear(); Results = new List(); ShowEmptyState(); Show(); Activate(); SearchBox.Focus(); if (Resources["AppearAnim"] is Storyboard appear) appear.Begin(this); else Opacity = 1; _isVisible = true; } } private void CenterWindow() { var screenW = SystemParameters.PrimaryScreenWidth; var screenH = SystemParameters.PrimaryScreenHeight; Left = (screenW - Width) / 2; Top = screenH * 0.18; } private void Window_KeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: FadeOutAndHide(); e.Handled = true; break; case Key.Down: if (ResultsList.Items.Count > 0 && ResultsList.SelectedIndex + 1 < ResultsList.Items.Count) { ResultsList.SelectedIndex++; ResultsList.ScrollIntoView(ResultsList.SelectedItem); e.Handled = true; } break; case Key.Up: if (ResultsList.SelectedIndex > 0) { ResultsList.SelectedIndex--; ResultsList.ScrollIntoView(ResultsList.SelectedItem); e.Handled = true; } break; case Key.Enter: OpenResult(e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl)); e.Handled = true; break; } } private void SearchBox_Loaded(object sender, RoutedEventArgs e) => SearchBox.Focus(); private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) { var query = SearchBox.Text.Trim(); Logger.Log($"SearchBox query: '{query}' (index={_index.Count:N0})"); if (string.IsNullOrEmpty(query)) { CancelPendingSearch(); Dispatcher.Invoke(() => { Results = new List(); ResultsScroll.Visibility = Visibility.Collapsed; EmptyHint.Visibility = Visibility.Visible; EmptyHint.Text = _index.Count == 0 ? "Index is empty.\nRun CLI diagnostics: FastSeekWpf.exe --cli" : "Start typing to search your files..."; SepLine.Visibility = Visibility.Collapsed; StatusText.Text = $"{_index.Count:N0} files indexed"; }); return; } CancelPendingSearch(); var cts = new CancellationTokenSource(); lock (_searchLock) { _searchDebounceCts = cts; } Task.Run(async () => { try { await Task.Delay(25, cts.Token); } catch (TaskCanceledException) { return; } if (cts.IsCancellationRequested) return; List results; lock (_indexLock) { results = SearchEngine.Search(_index, query, 50, false, _excludedDirs); } Logger.Log($"Search '{query}': {results.Count} results returned to UI"); var items = results.Select((r, i) => new ResultItem { FullPath = r.FullPath, DisplayName = Path.GetFileNameWithoutExtension(r.FullPath), IsSelected = i == 0, IsHovered = false, Kind = r.Kind, IconGlyph = GetIconGlyph(r.Kind), BadgeLabel = GetBadgeLabel(r.Kind), BadgeColor = GetBadgeColor(r.Kind) }).ToList(); Dispatcher.Invoke(() => { if (cts.IsCancellationRequested) return; _selectedIndex = 0; Results = items; if (items.Count > 0) { ResultsScroll.Visibility = Visibility.Visible; EmptyHint.Visibility = Visibility.Collapsed; SepLine.Visibility = Visibility.Visible; ResultsList.SelectedIndex = 0; } else { ResultsScroll.Visibility = Visibility.Collapsed; EmptyHint.Visibility = Visibility.Visible; EmptyHint.Text = $"No results for \"{query}\""; SepLine.Visibility = Visibility.Collapsed; } StatusText.Text = $"{items.Count} results"; StatusBar.Visibility = Visibility.Visible; }); }); } private void ResultsList_SelectionChanged(object sender, SelectionChangedEventArgs e) { _selectedIndex = ResultsList.SelectedIndex; } private void CancelPendingSearch() { lock (_searchLock) { _searchDebounceCts?.Cancel(); _searchDebounceCts?.Dispose(); _searchDebounceCts = null; } } private void ResultItem_Click(object sender, MouseButtonEventArgs e) { if (sender is Border border && border.DataContext is ResultItem item) { int idx = Results.IndexOf(item); if (idx >= 0) { ResultsList.SelectedIndex = idx; OpenResult(false); } } } private void OpenResult(bool folderOnly) { if (ResultsList.SelectedItem is not ResultItem item) return; string target = folderOnly ? (Directory.Exists(item.FullPath) ? item.FullPath : Path.GetDirectoryName(item.FullPath) ?? item.FullPath) : item.FullPath; Win32Api.ShellExecuteW(IntPtr.Zero, "open", target, null, null, 1); FadeOutAndHide(); } private static string GetIconGlyph(ResultKind kind) => kind switch { ResultKind.App => "\uE7E8", ResultKind.Document => "\uE8A5", ResultKind.Image => "\uE91B", ResultKind.Video => "\uE714", ResultKind.Audio => "\uE8D6", ResultKind.Archive => "\uE7B8", ResultKind.Folder => "\uE8B7", _ => "\uE7C3" }; 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 static Color GetBadgeColor(ResultKind kind) => kind switch { ResultKind.App => Color.FromRgb(60, 200, 120), ResultKind.Document => Color.FromRgb(240, 140, 60), ResultKind.Image => Color.FromRgb(180, 80, 220), ResultKind.Video => Color.FromRgb(220, 60, 80), ResultKind.Audio => Color.FromRgb(60, 160, 240), ResultKind.Archive => Color.FromRgb(200, 160, 40), ResultKind.Folder => Color.FromRgb(80, 120, 220), _ => Color.FromRgb(100, 100, 130) }; protected override void OnClosing(CancelEventArgs e) { _cts?.Cancel(); CancelPendingSearch(); foreach (var w in _watchers) w.Dispose(); _watchers.Clear(); CacheManager.SaveCache(_index); base.OnClosing(e); } } public record ResultItem { public string FullPath { get; init; } = ""; public string DisplayName { get; init; } = ""; public bool IsSelected { get; init; } public bool IsHovered { get; init; } public ResultKind Kind { get; init; } public string IconGlyph { get; init; } = ""; public string BadgeLabel { get; init; } = ""; public Color BadgeColor { get; init; } }