finder-wpf / FastSeekWpf /MainWindow.xaml.cs
anshdadhich's picture
MainWindow.xaml.cs: Update to use IndexStore.Finalize() API matching Rust. Remove CompleteIndex() call that no longer exists.
6b2f56b verified
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<NtfsDrive> _drives = new();
private readonly List<UsnWatcher> _watchers = new();
private readonly object _indexLock = new();
private readonly System.Collections.Concurrent.ConcurrentQueue<IndexEvent> _eventQueue = new();
private CancellationTokenSource? _cts;
private List<string> _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<ResultItem> _results = new();
public List<ResultItem> 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<JournalCheckpoint>(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<ResultItem>();
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<ResultItem>();
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<SearchResult> 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; }
}