diff --git a/App.xaml.cs b/App.xaml.cs
index 06dc022..1513528 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -1,497 +1,469 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-#if DEBUG
-using ThreadPilot.Tests;
-#endif
-using ThreadPilot.Services;
-using ThreadPilot.ViewModels;
-
-namespace ThreadPilot
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows;
- using System.Windows.Threading;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- using ThreadPilot.Helpers;
- using ThreadPilot.Models;
-
- public partial class App : System.Windows.Application
- {
- private const string RegisterLaunchTaskArgument = "--register-launch-task";
- private const string LaunchedViaTaskArgument = "--launched-via-task";
-
- private Mutex? singleInstanceMutex;
- private int uiExceptionDialogOpen;
- private DateTime lastUiExceptionDialogUtc = DateTime.MinValue;
-
- public IServiceProvider ServiceProvider { get; private set; }
-
- public App()
- {
- ServiceCollection services = new ServiceCollection();
-
- // Use the new centralized service configuration
- services.ConfigureApplicationServices();
-
- this.ServiceProvider = services.BuildServiceProvider();
-
- // Validate service configuration
- ServiceConfiguration.ValidateServiceConfiguration(this.ServiceProvider);
- }
-
-
-
- protected override void OnStartup(StartupEventArgs e)
- {
- // Parse command line arguments early so special startup modes can short-circuit normal flow.
- var startupMode = StartupMode.Parse(e.Args);
- bool effectiveStartMinimized = false;
- ApplicationSettingsModel? loadedSettings = null;
-
- effectiveStartMinimized = startupMode.StartMinimized;
-
- if (startupMode.IsSmokeTest)
- {
- var smokeLogger = this.ServiceProvider.GetRequiredService>();
- var smokeTestResult = this.RunSmokeTestWithTimeout(smokeLogger, TimeSpan.FromSeconds(10));
- Environment.ExitCode = smokeTestResult;
- this.Shutdown(smokeTestResult);
- Environment.Exit(smokeTestResult);
- return;
- }
-
- // Set up global exception handlers first
- AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
- this.DispatcherUnhandledException += this.OnDispatcherUnhandledException;
- TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException;
-
- // Check elevation status first
- var elevationService = this.ServiceProvider.GetRequiredService();
- var elevatedTaskService = this.ServiceProvider.GetRequiredService();
- var logger = this.ServiceProvider.GetRequiredService>();
- var isRunningAsAdministrator = elevationService.IsRunningAsAdministrator();
-
- if (isRunningAsAdministrator)
- {
- logger.LogInformation("Application is running with administrator privileges");
-
- var launchTaskEnsured = Task.Run(async () => await elevatedTaskService.EnsureLaunchTaskAsync()).GetAwaiter().GetResult();
- if (!launchTaskEnsured)
- {
- logger.LogWarning("Failed to ensure managed elevated launch task during startup. Future launches may require one-time elevation again.");
- }
- }
- else
- {
- if (startupMode.LaunchedViaTask)
- {
- logger.LogError("Application was launched via managed task marker but is still not elevated.");
- }
-#if DEBUG
- else if (!startupMode.IsTestMode)
-#else
- else
-#endif
- {
- var launchedElevatedInstance = Task.Run(async () => await elevatedTaskService.TryRunLaunchTaskAsync()).GetAwaiter().GetResult();
- if (launchedElevatedInstance)
- {
- logger.LogInformation("Managed elevated launch task started successfully. Exiting current non-elevated instance.");
- this.Shutdown();
- return;
- }
-
- if (!startupMode.RegisterLaunchTask)
- {
- logger.LogInformation("Managed elevated launch task is unavailable. Requesting one-time elevation to bootstrap persistent launch.");
- var restartInitiated = Task.Run(async () => await elevationService.RestartWithElevation(new[] { RegisterLaunchTaskArgument })).GetAwaiter().GetResult();
- if (restartInitiated)
- {
- return;
- }
- }
- }
-
-#if DEBUG
- if (!startupMode.IsTestMode)
-#else
- if (true)
-#endif
- {
- logger.LogError("ThreadPilot requires administrator privileges and cannot continue without elevation.");
- this.ShowElevationRequiredMessage();
- this.Shutdown(1);
- return;
- }
- }
-
- // Enforce single-instance after elevation bootstrap logic to avoid mutex races during handoff.
- bool createdNew;
- this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew);
- if (!createdNew)
- {
- System.Windows.MessageBox.Show(
- "ThreadPilot is already running.",
- "Instance already open",
- MessageBoxButton.OK,
- MessageBoxImage.Information);
-
- this.Shutdown();
- return;
- }
-
- base.OnStartup(e);
-
- // Check for test mode
-#if DEBUG
- if (startupMode.IsTestMode)
- {
- // Run in console test mode
- AllocConsole();
- _ = Task.Run(async () =>
- {
- await TestRunner.RunTests();
- this.Dispatcher.Invoke(() => this.Shutdown());
- });
- return;
- }
-#endif
-
- try
- {
- var settingsService = this.ServiceProvider.GetRequiredService();
- var themeService = this.ServiceProvider.GetRequiredService();
- var localizationService = this.ServiceProvider.GetRequiredService();
-
- Task.Run(async () => await settingsService.LoadSettingsAsync()).GetAwaiter().GetResult();
- var settings = settingsService.Settings;
- loadedSettings = settings;
- localizationService.ApplyLanguage(settings.Language);
- effectiveStartMinimized = startupMode.StartMinimized || settings.StartMinimized;
- var useDarkTheme = settings.HasUserThemePreference
- ? settings.UseDarkTheme
- : themeService.GetSystemUsesDarkTheme();
-
- if (!settings.HasUserThemePreference && settings.UseDarkTheme != useDarkTheme)
- {
- settings.UseDarkTheme = useDarkTheme;
- Task.Run(async () => await settingsService.UpdateSettingsAsync(settings)).GetAwaiter().GetResult();
- }
-
- themeService.ApplyTheme(useDarkTheme);
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to preload theme settings during startup");
- }
-
- var mainWindow = this.ServiceProvider.GetRequiredService();
- this.MainWindow = mainWindow;
-
- // Handle startup behavior with comprehensive error handling
- try
- {
- logger.LogInformation("Attempting to show main window...");
-
- // Ensure the window is properly initialized
- if (mainWindow == null)
- {
- throw new InvalidOperationException("MainWindow could not be created");
- }
-
- var startupWindowBehavior = StartupWindowBehavior.Resolve(startupMode.IsAutostart, effectiveStartMinimized);
- var showStartupSuggestion = loadedSettings != null
- && StartupMinimizedSuggestionPolicy.ShouldShow(loadedSettings, startupWindowBehavior);
- mainWindow.ConfigureStartupMode(
- isSilentStartupMode: !startupWindowBehavior.ShouldShowWindow,
- showStartupMinimizedSuggestionOnReady: showStartupSuggestion);
-
- mainWindow.ShowInTaskbar = startupWindowBehavior.ShowInTaskbar;
- mainWindow.Visibility = startupWindowBehavior.Visibility;
- mainWindow.WindowState = startupWindowBehavior.WindowState;
-
- if (startupWindowBehavior.ShouldShowWindow)
- {
- mainWindow.Show();
-
- if (startupWindowBehavior.HideAfterShow)
- {
- mainWindow.Hide();
- }
- else if (startupWindowBehavior.ActivateAfterShow)
- {
- mainWindow.EnsureDashboardVisibleOnScreen();
- mainWindow.Activate();
- }
- }
-
- logger.LogInformation("Startup window behavior applied successfully");
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Critical error during application startup");
-
- // Show error message and exit gracefully
- var errorMessage = $"ThreadPilot failed to start:\n\n{ex.Message}\n\nStack Trace:\n{ex.StackTrace}";
- System.Windows.MessageBox.Show(errorMessage, "ThreadPilot Startup Error",
- MessageBoxButton.OK, MessageBoxImage.Error);
-
- // Exit the application
- this.Shutdown(1);
- return;
- }
- }
-
- private int RunSmokeTestWithTimeout(ILogger logger, TimeSpan timeout)
- {
- var smokeTestTask = Task.Run(() => this.RunSmokeTest(logger));
- if (smokeTestTask.Wait(timeout))
- {
- return smokeTestTask.GetAwaiter().GetResult();
- }
-
- logger.LogError("ThreadPilot smoke test timed out after {TimeoutSeconds} seconds", timeout.TotalSeconds);
- return 2;
- }
-
- private int RunSmokeTest(ILogger logger)
- {
- try
- {
- logger.LogInformation("Starting ThreadPilot smoke test");
-
- _ = this.ServiceProvider.GetRequiredService();
- _ = this.ServiceProvider.GetRequiredService();
- _ = this.ServiceProvider.GetRequiredService();
- _ = this.ServiceProvider.GetRequiredService();
-
- if (!System.IO.Directory.Exists(AppContext.BaseDirectory))
- {
- throw new InvalidOperationException("Application base directory was not found.");
- }
-
- logger.LogInformation("ThreadPilot smoke test completed successfully");
- return 0;
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "ThreadPilot smoke test failed");
- return 1;
- }
- }
-
- private readonly struct StartupMode
- {
- public bool StartMinimized { get; init; }
-
- public bool IsAutostart { get; init; }
-
- public bool IsSmokeTest { get; init; }
-
- public bool RegisterLaunchTask { get; init; }
-
- public bool LaunchedViaTask { get; init; }
-
- public bool IsTestMode { get; init; }
-
- public static StartupMode Parse(IEnumerable args)
- {
- var mode = default(StartupMode);
- foreach (var arg in args)
- {
- switch (arg.ToLowerInvariant())
- {
- case "--test":
- mode = mode with { IsTestMode = true };
- break;
- case "--smoke-test":
- mode = mode with { IsSmokeTest = true };
- break;
- case "--start-minimized":
- mode = mode with { StartMinimized = true };
- break;
- case "--autostart":
- mode = mode with { IsAutostart = true };
- break;
- case "--startup":
- mode = mode with { IsAutostart = true, StartMinimized = true };
- break;
- case RegisterLaunchTaskArgument:
- mode = mode with { RegisterLaunchTask = true };
- break;
- case LaunchedViaTaskArgument:
- mode = mode with { LaunchedViaTask = true };
- break;
- }
- }
-
- return mode;
- }
- }
-
- protected override void OnExit(ExitEventArgs e)
- {
- AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
- this.DispatcherUnhandledException -= this.OnDispatcherUnhandledException;
- TaskScheduler.UnobservedTaskException -= this.OnUnobservedTaskException;
-
- if (this.singleInstanceMutex != null)
- {
- try
- {
- this.singleInstanceMutex.ReleaseMutex();
- }
- catch
- {
- // Ignore; we just want to clean up quietly
- }
- this.singleInstanceMutex.Dispose();
- this.singleInstanceMutex = null;
- }
-
- base.OnExit(e);
- }
-
-#if DEBUG
- [System.Runtime.InteropServices.DllImport("kernel32.dll")]
- private static extern bool AllocConsole();
-#endif
-
- ///
- /// Shows a message to the user about elevation requirements.
- ///
- private void ShowElevationRequiredMessage()
- {
- // Don't show the message during autostart to avoid interrupting the user
- var args = Environment.GetCommandLineArgs();
- if (args.Any(arg => arg.Equals("--autostart", StringComparison.OrdinalIgnoreCase) ||
- arg.Equals("--startup", StringComparison.OrdinalIgnoreCase)))
- {
- return;
- }
-
- System.Windows.MessageBox.Show(
- "ThreadPilot requires administrator privileges to start.\n\n" +
- "Please relaunch the application and approve the UAC prompt.\n\n" +
- "This instance will now close.",
- "Administrator Privileges Required",
- MessageBoxButton.OK,
- MessageBoxImage.Warning);
- }
-
- ///
- /// Handles unhandled exceptions in the application domain.
- ///
- private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
- {
- var exception = e.ExceptionObject as Exception ?? new InvalidOperationException("Unhandled non-Exception object was raised.");
- this.ReportUnhandledException(exception, "AppDomain.CurrentDomain.UnhandledException", LogLevel.Critical);
-
- var errorMessage = $"A critical error occurred:\n\n{exception?.Message}\n\nThe application will now exit.";
- System.Windows.MessageBox.Show(errorMessage, "Critical Error",
- MessageBoxButton.OK, MessageBoxImage.Error);
- }
-
- ///
- /// Handles unhandled exceptions on the UI thread.
- ///
- private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
- {
- this.ReportUnhandledException(e.Exception, "Application.DispatcherUnhandledException", LogLevel.Error);
-
- if (Interlocked.CompareExchange(ref this.uiExceptionDialogOpen, 1, 0) != 0)
- {
- e.Handled = true;
- return;
- }
-
- if (DateTime.UtcNow - this.lastUiExceptionDialogUtc < TimeSpan.FromSeconds(2))
- {
- e.Handled = true;
- Interlocked.Exchange(ref this.uiExceptionDialogOpen, 0);
- return;
- }
-
- this.lastUiExceptionDialogUtc = DateTime.UtcNow;
-
- var errorMessage = $"An error occurred in the user interface:\n\n{e.Exception.Message}\n\nDo you want to continue?";
- var result = System.Windows.MessageBox.Show(errorMessage, "UI Error",
- MessageBoxButton.YesNo, MessageBoxImage.Error);
-
- if (result == MessageBoxResult.Yes)
- {
- e.Handled = true; // Continue running
- }
- else
- {
- e.Handled = false; // Let the application crash
- }
-
- Interlocked.Exchange(ref this.uiExceptionDialogOpen, 0);
- }
-
- ///
- /// Handles unobserved task exceptions from fire-and-forget tasks that escaped local handlers.
- ///
- private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
- {
- var exception = e.Exception.Flatten();
- this.ReportUnhandledException(exception, "TaskScheduler.UnobservedTaskException", LogLevel.Error);
- e.SetObserved();
- }
-
- private void ReportUnhandledException(Exception exception, string source, LogLevel level)
- {
- var logger = this.ServiceProvider?.GetService>();
- if (level == LogLevel.Critical)
- {
- logger?.LogCritical(exception, "Unhandled exception in {Source}", source);
- }
- else
- {
- logger?.LogError(exception, "Unhandled exception in {Source}", source);
- }
-
- var enhancedLogger = this.ServiceProvider?.GetService();
- if (enhancedLogger == null)
- {
- return;
- }
-
- var errorCode = exception is ThreadPilotException typedException
- ? typedException.ErrorCode.ToString()
- : ErrorCode.Unhandled.ToString();
-
- var context = new Dictionary
- {
- ["Source"] = source,
- [LogProperties.ErrorCode] = errorCode,
- [LogProperties.CorrelationId] = enhancedLogger.GetCurrentCorrelationId() ?? "N/A",
- ["IsTerminatingLevel"] = level == LogLevel.Critical,
- };
-
- TaskSafety.FireAndForget(
- enhancedLogger.LogErrorAsync(exception, source, context),
- logFailure => logger?.LogWarning(logFailure, "Failed to persist unhandled exception report"));
- }
- }
-}
+#if DEBUG
+using ThreadPilot.Tests;
+#endif
+using ThreadPilot.Services;
+using ThreadPilot.ViewModels;
+
+namespace ThreadPilot
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Threading;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using ThreadPilot.Helpers;
+ using ThreadPilot.Models;
+
+ public partial class App : System.Windows.Application
+ {
+ private const string RegisterLaunchTaskArgument = "--register-launch-task";
+ private const string LaunchedViaTaskArgument = "--launched-via-task";
+
+ private Mutex? singleInstanceMutex;
+ private int uiExceptionDialogOpen;
+ private DateTime lastUiExceptionDialogUtc = DateTime.MinValue;
+
+ public IServiceProvider ServiceProvider { get; private set; }
+
+ public App()
+ {
+ ServiceCollection services = new ServiceCollection();
+
+ // Use the new centralized service configuration
+ services.ConfigureApplicationServices();
+
+ this.ServiceProvider = services.BuildServiceProvider();
+
+ // Validate service configuration
+ ServiceConfiguration.ValidateServiceConfiguration(this.ServiceProvider);
+ }
+
+
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ // Parse command line arguments early so special startup modes can short-circuit normal flow.
+ var startupMode = StartupMode.Parse(e.Args);
+ bool effectiveStartMinimized = false;
+ ApplicationSettingsModel? loadedSettings = null;
+
+ effectiveStartMinimized = startupMode.StartMinimized;
+
+ if (startupMode.IsSmokeTest)
+ {
+ var smokeLogger = this.ServiceProvider.GetRequiredService>();
+ var smokeTestResult = this.RunSmokeTestWithTimeout(smokeLogger, TimeSpan.FromSeconds(10));
+ Environment.ExitCode = smokeTestResult;
+ this.Shutdown(smokeTestResult);
+ Environment.Exit(smokeTestResult);
+ return;
+ }
+
+ // Set up global exception handlers first
+ AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
+ this.DispatcherUnhandledException += this.OnDispatcherUnhandledException;
+ TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException;
+
+ // Check elevation status first
+ var elevationService = this.ServiceProvider.GetRequiredService();
+ var elevatedTaskService = this.ServiceProvider.GetRequiredService();
+ var logger = this.ServiceProvider.GetRequiredService>();
+ var isRunningAsAdministrator = elevationService.IsRunningAsAdministrator();
+
+ if (isRunningAsAdministrator)
+ {
+ logger.LogInformation("Application is running with administrator privileges");
+
+ var launchTaskEnsured = Task.Run(async () => await elevatedTaskService.EnsureLaunchTaskAsync()).GetAwaiter().GetResult();
+ if (!launchTaskEnsured)
+ {
+ logger.LogWarning("Failed to ensure managed elevated launch task during startup. Future launches may require one-time elevation again.");
+ }
+ }
+ else
+ {
+ if (startupMode.LaunchedViaTask)
+ {
+ logger.LogError("Application was launched via managed task marker but is still not elevated.");
+ }
+#if DEBUG
+ else if (!startupMode.IsTestMode)
+#else
+ else
+#endif
+ {
+ var launchedElevatedInstance = Task.Run(async () => await elevatedTaskService.TryRunLaunchTaskAsync()).GetAwaiter().GetResult();
+ if (launchedElevatedInstance)
+ {
+ logger.LogInformation("Managed elevated launch task started successfully. Exiting current non-elevated instance.");
+ this.Shutdown();
+ return;
+ }
+
+ if (!startupMode.RegisterLaunchTask)
+ {
+ logger.LogInformation("Managed elevated launch task is unavailable. Requesting one-time elevation to bootstrap persistent launch.");
+ var restartInitiated = Task.Run(async () => await elevationService.RestartWithElevation(new[] { RegisterLaunchTaskArgument })).GetAwaiter().GetResult();
+ if (restartInitiated)
+ {
+ return;
+ }
+ }
+ }
+
+#if DEBUG
+ if (!startupMode.IsTestMode)
+#else
+ if (true)
+#endif
+ {
+ logger.LogError("ThreadPilot requires administrator privileges and cannot continue without elevation.");
+ this.ShowElevationRequiredMessage();
+ this.Shutdown(1);
+ return;
+ }
+ }
+
+ // Enforce single-instance after elevation bootstrap logic to avoid mutex races during handoff.
+ bool createdNew;
+ this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew);
+ if (!createdNew)
+ {
+ System.Windows.MessageBox.Show(
+ "ThreadPilot is already running.",
+ "Instance already open",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+
+ this.Shutdown();
+ return;
+ }
+
+ base.OnStartup(e);
+
+ // Check for test mode
+#if DEBUG
+ if (startupMode.IsTestMode)
+ {
+ // Run in console test mode
+ AllocConsole();
+ _ = Task.Run(async () =>
+ {
+ await TestRunner.RunTests();
+ this.Dispatcher.Invoke(() => this.Shutdown());
+ });
+ return;
+ }
+#endif
+
+ try
+ {
+ var settingsService = this.ServiceProvider.GetRequiredService();
+ var themeService = this.ServiceProvider.GetRequiredService();
+ var localizationService = this.ServiceProvider.GetRequiredService();
+
+ Task.Run(async () => await settingsService.LoadSettingsAsync()).GetAwaiter().GetResult();
+ var settings = settingsService.Settings;
+ loadedSettings = settings;
+ localizationService.ApplyLanguage(settings.Language);
+ effectiveStartMinimized = startupMode.StartMinimized || settings.StartMinimized;
+ var useDarkTheme = settings.HasUserThemePreference
+ ? settings.UseDarkTheme
+ : themeService.GetSystemUsesDarkTheme();
+
+ if (!settings.HasUserThemePreference && settings.UseDarkTheme != useDarkTheme)
+ {
+ settings.UseDarkTheme = useDarkTheme;
+ Task.Run(async () => await settingsService.UpdateSettingsAsync(settings)).GetAwaiter().GetResult();
+ }
+
+ themeService.ApplyTheme(useDarkTheme);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to preload theme settings during startup");
+ }
+
+ var mainWindow = this.ServiceProvider.GetRequiredService();
+ this.MainWindow = mainWindow;
+
+ // Handle startup behavior with comprehensive error handling
+ try
+ {
+ logger.LogInformation("Attempting to show main window...");
+
+ // Ensure the window is properly initialized
+ if (mainWindow == null)
+ {
+ throw new InvalidOperationException("MainWindow could not be created");
+ }
+
+ var startupWindowBehavior = StartupWindowBehavior.Resolve(startupMode.IsAutostart, effectiveStartMinimized);
+ var showStartupSuggestion = loadedSettings != null
+ && StartupMinimizedSuggestionPolicy.ShouldShow(loadedSettings, startupWindowBehavior);
+ mainWindow.ConfigureStartupMode(
+ isSilentStartupMode: !startupWindowBehavior.ShouldShowWindow,
+ showStartupMinimizedSuggestionOnReady: showStartupSuggestion);
+
+ mainWindow.ShowInTaskbar = startupWindowBehavior.ShowInTaskbar;
+ mainWindow.Visibility = startupWindowBehavior.Visibility;
+ mainWindow.WindowState = startupWindowBehavior.WindowState;
+
+ if (startupWindowBehavior.ShouldShowWindow)
+ {
+ mainWindow.Show();
+
+ if (startupWindowBehavior.HideAfterShow)
+ {
+ mainWindow.Hide();
+ }
+ else if (startupWindowBehavior.ActivateAfterShow)
+ {
+ mainWindow.EnsureDashboardVisibleOnScreen();
+ mainWindow.Activate();
+ }
+ }
+
+ logger.LogInformation("Startup window behavior applied successfully");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Critical error during application startup");
+
+ // Show error message and exit gracefully
+ var errorMessage = $"ThreadPilot failed to start:\n\n{ex.Message}\n\nStack Trace:\n{ex.StackTrace}";
+ System.Windows.MessageBox.Show(errorMessage, "ThreadPilot Startup Error",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+
+ // Exit the application
+ this.Shutdown(1);
+ return;
+ }
+ }
+
+ private int RunSmokeTestWithTimeout(ILogger logger, TimeSpan timeout)
+ {
+ var smokeTestTask = Task.Run(() => this.RunSmokeTest(logger));
+ if (smokeTestTask.Wait(timeout))
+ {
+ return smokeTestTask.GetAwaiter().GetResult();
+ }
+
+ logger.LogError("ThreadPilot smoke test timed out after {TimeoutSeconds} seconds", timeout.TotalSeconds);
+ return 2;
+ }
+
+ private int RunSmokeTest(ILogger logger)
+ {
+ try
+ {
+ logger.LogInformation("Starting ThreadPilot smoke test");
+
+ _ = this.ServiceProvider.GetRequiredService();
+ _ = this.ServiceProvider.GetRequiredService();
+ _ = this.ServiceProvider.GetRequiredService();
+ _ = this.ServiceProvider.GetRequiredService();
+
+ if (!System.IO.Directory.Exists(AppContext.BaseDirectory))
+ {
+ throw new InvalidOperationException("Application base directory was not found.");
+ }
+
+ logger.LogInformation("ThreadPilot smoke test completed successfully");
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "ThreadPilot smoke test failed");
+ return 1;
+ }
+ }
+
+ private readonly struct StartupMode
+ {
+ public bool StartMinimized { get; init; }
+
+ public bool IsAutostart { get; init; }
+
+ public bool IsSmokeTest { get; init; }
+
+ public bool RegisterLaunchTask { get; init; }
+
+ public bool LaunchedViaTask { get; init; }
+
+ public bool IsTestMode { get; init; }
+
+ public static StartupMode Parse(IEnumerable args)
+ {
+ var mode = default(StartupMode);
+ foreach (var arg in args)
+ {
+ switch (arg.ToLowerInvariant())
+ {
+ case "--test":
+ mode = mode with { IsTestMode = true };
+ break;
+ case "--smoke-test":
+ mode = mode with { IsSmokeTest = true };
+ break;
+ case "--start-minimized":
+ mode = mode with { StartMinimized = true };
+ break;
+ case "--autostart":
+ mode = mode with { IsAutostart = true };
+ break;
+ case "--startup":
+ mode = mode with { IsAutostart = true, StartMinimized = true };
+ break;
+ case RegisterLaunchTaskArgument:
+ mode = mode with { RegisterLaunchTask = true };
+ break;
+ case LaunchedViaTaskArgument:
+ mode = mode with { LaunchedViaTask = true };
+ break;
+ }
+ }
+
+ return mode;
+ }
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
+ this.DispatcherUnhandledException -= this.OnDispatcherUnhandledException;
+ TaskScheduler.UnobservedTaskException -= this.OnUnobservedTaskException;
+
+ if (this.singleInstanceMutex != null)
+ {
+ try
+ {
+ this.singleInstanceMutex.ReleaseMutex();
+ }
+ catch
+ {
+ // Ignore; we just want to clean up quietly
+ }
+ this.singleInstanceMutex.Dispose();
+ this.singleInstanceMutex = null;
+ }
+
+ base.OnExit(e);
+ }
+
+#if DEBUG
+ [System.Runtime.InteropServices.DllImport("kernel32.dll")]
+ private static extern bool AllocConsole();
+#endif
+
+ private void ShowElevationRequiredMessage()
+ {
+ // Don't show the message during autostart to avoid interrupting the user
+ var args = Environment.GetCommandLineArgs();
+ if (args.Any(arg => arg.Equals("--autostart", StringComparison.OrdinalIgnoreCase) ||
+ arg.Equals("--startup", StringComparison.OrdinalIgnoreCase)))
+ {
+ return;
+ }
+
+ System.Windows.MessageBox.Show(
+ "ThreadPilot requires administrator privileges to start.\n\n" +
+ "Please relaunch the application and approve the UAC prompt.\n\n" +
+ "This instance will now close.",
+ "Administrator Privileges Required",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning);
+ }
+
+ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ var exception = e.ExceptionObject as Exception ?? new InvalidOperationException("Unhandled non-Exception object was raised.");
+ this.ReportUnhandledException(exception, "AppDomain.CurrentDomain.UnhandledException", LogLevel.Critical);
+
+ var errorMessage = $"A critical error occurred:\n\n{exception?.Message}\n\nThe application will now exit.";
+ System.Windows.MessageBox.Show(errorMessage, "Critical Error",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ this.ReportUnhandledException(e.Exception, "Application.DispatcherUnhandledException", LogLevel.Error);
+
+ if (Interlocked.CompareExchange(ref this.uiExceptionDialogOpen, 1, 0) != 0)
+ {
+ e.Handled = true;
+ return;
+ }
+
+ if (DateTime.UtcNow - this.lastUiExceptionDialogUtc < TimeSpan.FromSeconds(2))
+ {
+ e.Handled = true;
+ Interlocked.Exchange(ref this.uiExceptionDialogOpen, 0);
+ return;
+ }
+
+ this.lastUiExceptionDialogUtc = DateTime.UtcNow;
+
+ var errorMessage = $"An error occurred in the user interface:\n\n{e.Exception.Message}\n\nDo you want to continue?";
+ var result = System.Windows.MessageBox.Show(errorMessage, "UI Error",
+ MessageBoxButton.YesNo, MessageBoxImage.Error);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ e.Handled = true; // Continue running
+ }
+ else
+ {
+ e.Handled = false; // Let the application crash
+ }
+
+ Interlocked.Exchange(ref this.uiExceptionDialogOpen, 0);
+ }
+
+ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
+ {
+ var exception = e.Exception.Flatten();
+ this.ReportUnhandledException(exception, "TaskScheduler.UnobservedTaskException", LogLevel.Error);
+ e.SetObserved();
+ }
+
+ private void ReportUnhandledException(Exception exception, string source, LogLevel level)
+ {
+ var logger = this.ServiceProvider?.GetService>();
+ if (level == LogLevel.Critical)
+ {
+ logger?.LogCritical(exception, "Unhandled exception in {Source}", source);
+ }
+ else
+ {
+ logger?.LogError(exception, "Unhandled exception in {Source}", source);
+ }
+
+ var enhancedLogger = this.ServiceProvider?.GetService();
+ if (enhancedLogger == null)
+ {
+ return;
+ }
+
+ var errorCode = exception is ThreadPilotException typedException
+ ? typedException.ErrorCode.ToString()
+ : ErrorCode.Unhandled.ToString();
+
+ var context = new Dictionary
+ {
+ ["Source"] = source,
+ [LogProperties.ErrorCode] = errorCode,
+ [LogProperties.CorrelationId] = enhancedLogger.GetCurrentCorrelationId() ?? "N/A",
+ ["IsTerminatingLevel"] = level == LogLevel.Critical,
+ };
+
+ TaskSafety.FireAndForget(
+ enhancedLogger.LogErrorAsync(exception, source, context),
+ logFailure => logger?.LogWarning(logFailure, "Failed to persist unhandled exception report"));
+ }
+ }
+}
diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs
index 8a7b86f..f5ca8da 100644
--- a/AssemblyInfo.cs
+++ b/AssemblyInfo.cs
@@ -1,30 +1,14 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-using System.Runtime.CompilerServices;
-using System.Windows;
-
-[assembly: InternalsVisibleTo("ThreadPilot.Core.Tests")]
-
-[assembly: ThemeInfo(
- ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located
- // (used if a resource is not found in the page,
- // or application resource dictionaries)
- ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is located
- // (used if a resource is not found in the page,
- // app, or any theme specific resource dictionaries)
-]
-
+using System.Runtime.CompilerServices;
+using System.Windows;
+
+[assembly: InternalsVisibleTo("ThreadPilot.Core.Tests")]
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located
+ // (used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is located
+ // (used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+]
+
diff --git a/Converters/BoolToColorConverter.cs b/Converters/BoolToColorConverter.cs
index 3c64eb3..4538402 100644
--- a/Converters/BoolToColorConverter.cs
+++ b/Converters/BoolToColorConverter.cs
@@ -1,57 +1,41 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Data;
- using System.Windows.Media;
-
- public class BoolToColorConverter : IValueConverter
- {
- public static readonly BoolToColorConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue)
- {
- return boolValue
- ? ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black)
- : ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
- }
-
- return ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-
- private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
- {
- if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
- {
- return brush;
- }
-
- return fallback;
- }
- }
-}
-
+namespace ThreadPilot
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+ using System.Windows.Media;
+
+ public class BoolToColorConverter : IValueConverter
+ {
+ public static readonly BoolToColorConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return boolValue
+ ? ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black)
+ : ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
+ }
+
+ return ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
+ {
+ if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
+ {
+ return brush;
+ }
+
+ return fallback;
+ }
+ }
+}
+
diff --git a/Converters/BoolToFontWeightConverter.cs b/Converters/BoolToFontWeightConverter.cs
index 70fda90..9d51a88 100644
--- a/Converters/BoolToFontWeightConverter.cs
+++ b/Converters/BoolToFontWeightConverter.cs
@@ -1,43 +1,27 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Data;
-
- public class BoolToFontWeightConverter : IValueConverter
- {
- public static readonly BoolToFontWeightConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue && boolValue)
- {
- return FontWeights.Bold;
- }
- return FontWeights.Normal;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
-
+namespace ThreadPilot
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+
+ public class BoolToFontWeightConverter : IValueConverter
+ {
+ public static readonly BoolToFontWeightConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue && boolValue)
+ {
+ return FontWeights.Bold;
+ }
+ return FontWeights.Normal;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
+
diff --git a/Converters/BoolToStringConverter.cs b/Converters/BoolToStringConverter.cs
index 50f5350..36d41eb 100644
--- a/Converters/BoolToStringConverter.cs
+++ b/Converters/BoolToStringConverter.cs
@@ -1,48 +1,29 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows.Data;
-
- ///
- /// Converts boolean values to strings based on parameter format.
- ///
- public class BoolToStringConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue && parameter is string paramString)
- {
- var parts = paramString.Split('|');
- if (parts.Length == 2)
- {
- return boolValue ? parts[0] : parts[1];
- }
- }
-
- return value?.ToString() ?? string.Empty;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
-
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows.Data;
+
+ public class BoolToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue && parameter is string paramString)
+ {
+ var parts = paramString.Split('|');
+ if (parts.Length == 2)
+ {
+ return boolValue ? parts[0] : parts[1];
+ }
+ }
+
+ return value?.ToString() ?? string.Empty;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
+
diff --git a/Converters/BoolToVisibilityConverter.cs b/Converters/BoolToVisibilityConverter.cs
index a2c7a4b..c5effd1 100644
--- a/Converters/BoolToVisibilityConverter.cs
+++ b/Converters/BoolToVisibilityConverter.cs
@@ -1,47 +1,31 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Data;
-
- public class BoolToVisibilityConverter : IValueConverter
- {
- public static readonly BoolToVisibilityConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue && boolValue)
- {
- return Visibility.Visible;
- }
- return Visibility.Collapsed;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is Visibility visibility)
- {
- return visibility == Visibility.Visible;
- }
- return false;
- }
- }
-}
-
+namespace ThreadPilot
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+
+ public class BoolToVisibilityConverter : IValueConverter
+ {
+ public static readonly BoolToVisibilityConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue && boolValue)
+ {
+ return Visibility.Visible;
+ }
+ return Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is Visibility visibility)
+ {
+ return visibility == Visibility.Visible;
+ }
+ return false;
+ }
+ }
+}
+
diff --git a/Converters/BytesToStringConverter.cs b/Converters/BytesToStringConverter.cs
index 0fe952f..f7e8f9b 100644
--- a/Converters/BytesToStringConverter.cs
+++ b/Converters/BytesToStringConverter.cs
@@ -1,64 +1,45 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows.Data;
-
- ///
- /// Converts byte values to human-readable string format.
- ///
- public class BytesToStringConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is long bytes)
- {
- return FormatBytes(bytes);
- }
-
- if (value is int intBytes)
- {
- return FormatBytes(intBytes);
- }
-
- return "0 B";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-
- private static string FormatBytes(long bytes)
- {
- string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
- int counter = 0;
- decimal number = bytes;
-
- while (Math.Round(number / 1024) >= 1)
- {
- number /= 1024;
- counter++;
- }
-
- return $"{number:n1} {suffixes[counter]}";
- }
- }
-}
-
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows.Data;
+
+ public class BytesToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long bytes)
+ {
+ return FormatBytes(bytes);
+ }
+
+ if (value is int intBytes)
+ {
+ return FormatBytes(intBytes);
+ }
+
+ return "0 B";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static string FormatBytes(long bytes)
+ {
+ string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
+ int counter = 0;
+ decimal number = bytes;
+
+ while (Math.Round(number / 1024) >= 1)
+ {
+ number /= 1024;
+ counter++;
+ }
+
+ return $"{number:n1} {suffixes[counter]}";
+ }
+ }
+}
+
diff --git a/Converters/CpuTopologyConverters.cs b/Converters/CpuTopologyConverters.cs
index 0cd88af..3ea0821 100644
--- a/Converters/CpuTopologyConverters.cs
+++ b/Converters/CpuTopologyConverters.cs
@@ -1,212 +1,178 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Data;
- using System.Windows.Media;
- using ThreadPilot.Models;
-
- ///
- /// Converter for CPU core type to color.
- ///
- public class CoreTypeToColorConverter : IMultiValueConverter
- {
- public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
- {
- if (values.Length < 2)
- {
- return ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black);
- }
-
- var coreType = values[0] as CpuCoreType? ?? CpuCoreType.Unknown;
- var isHyperThreaded = values[1] as bool? ?? false;
-
- return coreType switch
- {
- CpuCoreType.PerformanceCore => isHyperThreaded
- ? ResolveBrush("SystemAccentColorSecondaryBrush", System.Windows.Media.Brushes.DodgerBlue)
- : ResolveBrush("SystemAccentColorPrimaryBrush", System.Windows.Media.Brushes.Blue),
- CpuCoreType.EfficiencyCore => isHyperThreaded
- ? ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.DarkGray)
- : ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black),
- CpuCoreType.Zen or CpuCoreType.ZenPlus or CpuCoreType.Zen2 or CpuCoreType.Zen3 or CpuCoreType.Zen4 =>
- isHyperThreaded
- ? ResolveBrush("SystemAccentColorSecondaryBrush", System.Windows.Media.Brushes.DarkOrange)
- : ResolveBrush("SystemAccentColorPrimaryBrush", System.Windows.Media.Brushes.Orange),
- _ => ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray),
- };
- }
-
- public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-
- private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
- {
- if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
- {
- return brush;
- }
-
- return fallback;
- }
- }
-
- ///
- /// Converter for boolean to color (success/failure indication).
- ///
- public class BoolToColorConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool success)
- {
- return success
- ? ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black)
- : ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
- }
- return ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-
- private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
- {
- if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
- {
- return brush;
- }
-
- return fallback;
- }
- }
-
- ///
- /// Converter for boolean to visibility.
- ///
- public class BoolToVisibilityConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool visible)
- {
- return visible ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
- }
- return System.Windows.Visibility.Collapsed;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-
- ///
- /// Converter for affinity mask to readable string.
- ///
- public class AffinityMaskConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is long mask)
- {
- if (mask == 0)
- {
- return "None";
- }
-
- var cores = new System.Collections.Generic.List();
- for (int i = 0; i < 64; i++)
- {
- if ((mask & (1L << i)) != 0)
- {
- cores.Add(i);
- }
- }
-
- if (cores.Count == 0)
- {
- return "None";
- }
-
- if (cores.Count == 1)
- {
- return $"Core {cores[0]}";
- }
-
- if (cores.Count <= 4)
- {
- return $"Cores {string.Join(", ", cores)}";
- }
-
- return $"Cores {cores[0]}-{cores[cores.Count - 1]} ({cores.Count} cores)";
- }
- return "Unknown";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-
- ///
- /// Converter for bytes to megabytes.
- ///
- public class BytesToMbConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is long bytes)
- {
- return (bytes / (1024.0 * 1024.0)).ToString("F1");
- }
- return "0.0";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-
- ///
- /// Converter for string to visibility (empty/null = collapsed).
- ///
- public class StringToVisibilityConverter : IValueConverter
- {
- public static readonly StringToVisibilityConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return string.IsNullOrEmpty(value as string) ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
-
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+ using System.Windows.Media;
+ using ThreadPilot.Models;
+
+ public class CoreTypeToColorConverter : IMultiValueConverter
+ {
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length < 2)
+ {
+ return ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black);
+ }
+
+ var coreType = values[0] as CpuCoreType? ?? CpuCoreType.Unknown;
+ var isHyperThreaded = values[1] as bool? ?? false;
+
+ return coreType switch
+ {
+ CpuCoreType.PerformanceCore => isHyperThreaded
+ ? ResolveBrush("SystemAccentColorSecondaryBrush", System.Windows.Media.Brushes.DodgerBlue)
+ : ResolveBrush("SystemAccentColorPrimaryBrush", System.Windows.Media.Brushes.Blue),
+ CpuCoreType.EfficiencyCore => isHyperThreaded
+ ? ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.DarkGray)
+ : ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black),
+ CpuCoreType.Zen or CpuCoreType.ZenPlus or CpuCoreType.Zen2 or CpuCoreType.Zen3 or CpuCoreType.Zen4 =>
+ isHyperThreaded
+ ? ResolveBrush("SystemAccentColorSecondaryBrush", System.Windows.Media.Brushes.DarkOrange)
+ : ResolveBrush("SystemAccentColorPrimaryBrush", System.Windows.Media.Brushes.Orange),
+ _ => ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray),
+ };
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
+ {
+ if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
+ {
+ return brush;
+ }
+
+ return fallback;
+ }
+ }
+
+ public class BoolToColorConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool success)
+ {
+ return success
+ ? ResolveBrush("TextFillColorPrimaryBrush", System.Windows.Media.Brushes.Black)
+ : ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
+ }
+ return ResolveBrush("TextFillColorSecondaryBrush", System.Windows.Media.Brushes.Gray);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static System.Windows.Media.Brush ResolveBrush(string key, System.Windows.Media.Brush fallback)
+ {
+ if (System.Windows.Application.Current?.TryFindResource(key) is System.Windows.Media.Brush brush)
+ {
+ return brush;
+ }
+
+ return fallback;
+ }
+ }
+
+ public class BoolToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool visible)
+ {
+ return visible ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
+ }
+ return System.Windows.Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class AffinityMaskConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long mask)
+ {
+ if (mask == 0)
+ {
+ return "None";
+ }
+
+ var cores = new System.Collections.Generic.List();
+ for (int i = 0; i < 64; i++)
+ {
+ if ((mask & (1L << i)) != 0)
+ {
+ cores.Add(i);
+ }
+ }
+
+ if (cores.Count == 0)
+ {
+ return "None";
+ }
+
+ if (cores.Count == 1)
+ {
+ return $"Core {cores[0]}";
+ }
+
+ if (cores.Count <= 4)
+ {
+ return $"Cores {string.Join(", ", cores)}";
+ }
+
+ return $"Cores {cores[0]}-{cores[cores.Count - 1]} ({cores.Count} cores)";
+ }
+ return "Unknown";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class BytesToMbConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long bytes)
+ {
+ return (bytes / (1024.0 * 1024.0)).ToString("F1");
+ }
+ return "0.0";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class StringToVisibilityConverter : IValueConverter
+ {
+ public static readonly StringToVisibilityConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return string.IsNullOrEmpty(value as string) ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
+
diff --git a/Converters/InverseBooleanConverter.cs b/Converters/InverseBooleanConverter.cs
index 8578c25..e9fba2d 100644
--- a/Converters/InverseBooleanConverter.cs
+++ b/Converters/InverseBooleanConverter.cs
@@ -1,49 +1,30 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows.Data;
-
- ///
- /// Converter to invert boolean values.
- ///
- public class InverseBooleanConverter : IValueConverter
- {
- public static readonly InverseBooleanConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue)
- {
- return !boolValue;
- }
- return true;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is bool boolValue)
- {
- return !boolValue;
- }
- return false;
- }
- }
-}
-
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows.Data;
+
+ public class InverseBooleanConverter : IValueConverter
+ {
+ public static readonly InverseBooleanConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return !boolValue;
+ }
+ return true;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return !boolValue;
+ }
+ return false;
+ }
+ }
+}
+
diff --git a/Converters/ItemIndexConverter.cs b/Converters/ItemIndexConverter.cs
index 657a1d6..8982551 100644
--- a/Converters/ItemIndexConverter.cs
+++ b/Converters/ItemIndexConverter.cs
@@ -1,51 +1,32 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Data;
-
- ///
- /// Converter to get the index of an item in an ItemsControl.
- ///
- public class ItemIndexConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is DependencyObject item)
- {
- var itemsControl = ItemsControl.ItemsControlFromItemContainer(item);
- if (itemsControl != null)
- {
- int index = itemsControl.ItemContainerGenerator.IndexFromContainer(item);
- return index;
- }
- }
-
- return -1;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
-
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Data;
+
+ public class ItemIndexConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is DependencyObject item)
+ {
+ var itemsControl = ItemsControl.ItemsControlFromItemContainer(item);
+ if (itemsControl != null)
+ {
+ int index = itemsControl.ItemContainerGenerator.IndexFromContainer(item);
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
+
diff --git a/Converters/NullToBoolConverter.cs b/Converters/NullToBoolConverter.cs
index 60b2ca4..283c95f 100644
--- a/Converters/NullToBoolConverter.cs
+++ b/Converters/NullToBoolConverter.cs
@@ -1,35 +1,19 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Converters
-{
- using System;
- using System.Globalization;
- using System.Windows.Data;
-
- public class NullToBoolConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return value != null;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
+namespace ThreadPilot.Converters
+{
+ using System;
+ using System.Globalization;
+ using System.Windows.Data;
+
+ public class NullToBoolConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value != null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Helpers/AffinityHelper.cs b/Helpers/AffinityHelper.cs
index 3ce3ffd..8024f4a 100644
--- a/Helpers/AffinityHelper.cs
+++ b/Helpers/AffinityHelper.cs
@@ -1,41 +1,25 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Helpers
-{
- using System.Collections.Generic;
- using System.Linq;
- using System.Windows.Controls;
-
- public static class AffinityHelper
- {
- public static long CalculateAffinityMask(IEnumerable cpuCheckboxes)
- {
- return cpuCheckboxes
- .Where(cb => cb.IsChecked == true)
- .Sum(cb => (long)cb.Tag);
- }
-
- public static void UpdateCheckboxesFromMask(IEnumerable cpuCheckboxes, long affinityMask)
- {
- foreach (var checkbox in cpuCheckboxes)
- {
- var cpuBit = (long)checkbox.Tag;
- checkbox.IsChecked = (affinityMask & cpuBit) != 0;
- }
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Windows.Controls;
+
+ public static class AffinityHelper
+ {
+ public static long CalculateAffinityMask(IEnumerable cpuCheckboxes)
+ {
+ return cpuCheckboxes
+ .Where(cb => cb.IsChecked == true)
+ .Sum(cb => (long)cb.Tag);
+ }
+
+ public static void UpdateCheckboxesFromMask(IEnumerable cpuCheckboxes, long affinityMask)
+ {
+ foreach (var checkbox in cpuCheckboxes)
+ {
+ var cpuBit = (long)checkbox.Tag;
+ checkbox.IsChecked = (affinityMask & cpuBit) != 0;
+ }
+ }
+ }
+}
diff --git a/Helpers/Converters.cs b/Helpers/Converters.cs
index f9d72de..d633a12 100644
--- a/Helpers/Converters.cs
+++ b/Helpers/Converters.cs
@@ -1,122 +1,106 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Helpers
-{
- using System;
- using System.Globalization;
- using System.Windows;
- using System.Windows.Data;
-
- public class BytesToMbConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is long bytes)
- {
- return Math.Round((double)bytes / (1024 * 1024), 1);
- }
- return 0;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-
- public class AffinityMaskConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is long mask)
- {
- var selectedIndices = new System.Collections.Generic.List();
- // Dynamic core count based on system, limited to 64 (long is 64-bit)
- int maxCores = Math.Min(64, Environment.ProcessorCount);
-
- for (int i = 0; i < maxCores; i++)
- {
- if ((mask & (1L << i)) != 0)
- {
- selectedIndices.Add(i);
- }
- }
-
- if (selectedIndices.Count == 0)
- {
- return "None";
- }
-
- // Build the display string
- var indicesStr = string.Join(", ", selectedIndices);
- int selectedCount = selectedIndices.Count;
-
- // Detect if this is likely physical cores only (every other logical processor)
- // This heuristic checks if selected indices are evenly spaced by 2 (e.g., 0,2,4,6,8...)
- bool isProbablyPhysicalCoresOnly = false;
- if (selectedCount > 1 && selectedCount <= maxCores / 2)
- {
- isProbablyPhysicalCoresOnly = true;
- for (int i = 1; i < selectedIndices.Count; i++)
- {
- if (selectedIndices[i] - selectedIndices[i - 1] != 2)
- {
- isProbablyPhysicalCoresOnly = false;
- break;
- }
- }
- }
-
- // Choose terminology based on what's selected
- string label;
- if (selectedCount == maxCores)
- {
- label = $"All threads (0-{maxCores - 1})";
- }
- else if (isProbablyPhysicalCoresOnly && selectedIndices[0] == 0)
- {
- label = $"Physical cores ({indicesStr}) - {selectedCount} cores";
- }
- else
- {
- label = $"Threads ({indicesStr}) - {selectedCount} threads";
- }
-
- return label;
- }
- return "Unknown";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-
- public class BoolToFontWeightConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return (bool)value ? FontWeights.Bold : FontWeights.Normal;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+
+ public class BytesToMbConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long bytes)
+ {
+ return Math.Round((double)bytes / (1024 * 1024), 1);
+ }
+ return 0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class AffinityMaskConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long mask)
+ {
+ var selectedIndices = new System.Collections.Generic.List();
+ // Dynamic core count based on system, limited to 64 (long is 64-bit)
+ int maxCores = Math.Min(64, Environment.ProcessorCount);
+
+ for (int i = 0; i < maxCores; i++)
+ {
+ if ((mask & (1L << i)) != 0)
+ {
+ selectedIndices.Add(i);
+ }
+ }
+
+ if (selectedIndices.Count == 0)
+ {
+ return "None";
+ }
+
+ // Build the display string
+ var indicesStr = string.Join(", ", selectedIndices);
+ int selectedCount = selectedIndices.Count;
+
+ // Detect if this is likely physical cores only (every other logical processor)
+ // This heuristic checks if selected indices are evenly spaced by 2 (e.g., 0,2,4,6,8...)
+ bool isProbablyPhysicalCoresOnly = false;
+ if (selectedCount > 1 && selectedCount <= maxCores / 2)
+ {
+ isProbablyPhysicalCoresOnly = true;
+ for (int i = 1; i < selectedIndices.Count; i++)
+ {
+ if (selectedIndices[i] - selectedIndices[i - 1] != 2)
+ {
+ isProbablyPhysicalCoresOnly = false;
+ break;
+ }
+ }
+ }
+
+ // Choose terminology based on what's selected
+ string label;
+ if (selectedCount == maxCores)
+ {
+ label = $"All threads (0-{maxCores - 1})";
+ }
+ else if (isProbablyPhysicalCoresOnly && selectedIndices[0] == 0)
+ {
+ label = $"Physical cores ({indicesStr}) - {selectedCount} cores";
+ }
+ else
+ {
+ label = $"Threads ({indicesStr}) - {selectedCount} threads";
+ }
+
+ return label;
+ }
+ return "Unknown";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class BoolToFontWeightConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return (bool)value ? FontWeights.Bold : FontWeights.Normal;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Helpers/DwmHelper.cs b/Helpers/DwmHelper.cs
index 2552b79..acc7480 100644
--- a/Helpers/DwmHelper.cs
+++ b/Helpers/DwmHelper.cs
@@ -1,56 +1,34 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Helpers
-{
- using System;
- using System.Runtime.InteropServices;
- using System.Windows;
- using System.Windows.Interop;
-
- ///
- /// Desktop Window Manager helper methods.
- ///
- public static class DwmHelper
- {
- private const int DwmUseImmersiveDarkMode = 20;
- private const int DwmUseImmersiveDarkModeLegacy = 19;
-
- [DllImport("dwmapi.dll")]
- private static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);
-
- ///
- /// Applies dark/light title-bar styling through DWM attributes.
- ///
- public static void ApplyWindowCaptionTheme(Window window, bool useDarkTheme)
- {
- ArgumentNullException.ThrowIfNull(window);
-
- var windowHandle = new WindowInteropHelper(window).Handle;
- if (windowHandle == IntPtr.Zero)
- {
- return;
- }
-
- var darkMode = useDarkTheme ? 1 : 0;
- var result = DwmSetWindowAttribute(windowHandle, DwmUseImmersiveDarkMode, ref darkMode, Marshal.SizeOf());
- if (result != 0)
- {
- _ = DwmSetWindowAttribute(windowHandle, DwmUseImmersiveDarkModeLegacy, ref darkMode, Marshal.SizeOf());
- }
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using System.Windows;
+ using System.Windows.Interop;
+
+ public static class DwmHelper
+ {
+ private const int DwmUseImmersiveDarkMode = 20;
+ private const int DwmUseImmersiveDarkModeLegacy = 19;
+
+ [DllImport("dwmapi.dll")]
+ private static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);
+
+ public static void ApplyWindowCaptionTheme(Window window, bool useDarkTheme)
+ {
+ ArgumentNullException.ThrowIfNull(window);
+
+ var windowHandle = new WindowInteropHelper(window).Handle;
+ if (windowHandle == IntPtr.Zero)
+ {
+ return;
+ }
+
+ var darkMode = useDarkTheme ? 1 : 0;
+ var result = DwmSetWindowAttribute(windowHandle, DwmUseImmersiveDarkMode, ref darkMode, Marshal.SizeOf());
+ if (result != 0)
+ {
+ _ = DwmSetWindowAttribute(windowHandle, DwmUseImmersiveDarkModeLegacy, ref darkMode, Marshal.SizeOf());
+ }
+ }
+ }
+}
diff --git a/Helpers/NavigationBehavior.cs b/Helpers/NavigationBehavior.cs
index 30a347a..525d786 100644
--- a/Helpers/NavigationBehavior.cs
+++ b/Helpers/NavigationBehavior.cs
@@ -1,101 +1,73 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Helpers
-{
- using System;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows;
- using ThreadPilot.ViewModels;
-
- ///
- /// Coordinates serialized navigation transitions and unsaved-changes policy.
- ///
- public sealed class NavigationBehavior : IDisposable
- {
- private readonly SemaphoreSlim navigationGuard = new(1, 1);
- private bool isHandlingNavigation;
-
- ///
- /// Tries entering the navigation critical section.
- ///
- public async Task TryEnterAsync()
- {
- if (this.isHandlingNavigation)
- {
- return false;
- }
-
- await this.navigationGuard.WaitAsync().ConfigureAwait(false);
- this.isHandlingNavigation = true;
- return true;
- }
-
- ///
- /// Leaves the navigation critical section.
- ///
- public void Exit()
- {
- this.isHandlingNavigation = false;
- this.navigationGuard.Release();
- }
-
- ///
- /// Applies unsaved-settings policy before navigating away from the settings section.
- ///
- public static async Task EnsureCanNavigateAsync(
- string targetTag,
- SettingsViewModel settingsViewModel,
- Func>? showUnsavedSettingsPromptAsync = null)
- {
- ArgumentNullException.ThrowIfNull(targetTag);
- ArgumentNullException.ThrowIfNull(settingsViewModel);
-
- if (!settingsViewModel.HasPendingChanges || string.Equals(targetTag, "Settings", StringComparison.Ordinal))
- {
- return true;
- }
-
- var result = showUnsavedSettingsPromptAsync != null
- ? await showUnsavedSettingsPromptAsync().ConfigureAwait(false)
- : MessageBox.Show(
- "You have unsaved changes in Settings.\n\nChoose an action:\n- Yes: Save changes\n- No: Discard changes\n- Cancel: Stay on current tab",
- "Unsaved Settings",
- MessageBoxButton.YesNoCancel,
- MessageBoxImage.Warning);
-
- return result switch
- {
- MessageBoxResult.Cancel => false,
- MessageBoxResult.Yes => await settingsViewModel.SaveIfDirtyAsync().ConfigureAwait(false),
- MessageBoxResult.No => await DiscardPendingChangesAsync(settingsViewModel).ConfigureAwait(false),
- _ => true,
- };
- }
-
- public void Dispose()
- {
- this.navigationGuard.Dispose();
- }
-
- private static async Task DiscardPendingChangesAsync(SettingsViewModel settingsViewModel)
- {
- await settingsViewModel.DiscardPendingChangesAsync().ConfigureAwait(false);
- return true;
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using ThreadPilot.ViewModels;
+
+ public sealed class NavigationBehavior : IDisposable
+ {
+ private readonly SemaphoreSlim navigationGuard = new(1, 1);
+ private bool isHandlingNavigation;
+
+ public async Task TryEnterAsync()
+ {
+ if (this.isHandlingNavigation)
+ {
+ return false;
+ }
+
+ await this.navigationGuard.WaitAsync().ConfigureAwait(false);
+ this.isHandlingNavigation = true;
+ return true;
+ }
+
+ public void Exit()
+ {
+ this.isHandlingNavigation = false;
+ this.navigationGuard.Release();
+ }
+
+ public static async Task EnsureCanNavigateAsync(
+ string targetTag,
+ SettingsViewModel settingsViewModel,
+ Func>? showUnsavedSettingsPromptAsync = null)
+ {
+ ArgumentNullException.ThrowIfNull(targetTag);
+ ArgumentNullException.ThrowIfNull(settingsViewModel);
+
+ if (!settingsViewModel.HasPendingChanges || string.Equals(targetTag, "Settings", StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ var result = showUnsavedSettingsPromptAsync != null
+ ? await showUnsavedSettingsPromptAsync().ConfigureAwait(false)
+ : MessageBox.Show(
+ "You have unsaved changes in Settings.\n\nChoose an action:\n- Yes: Save changes\n- No: Discard changes\n- Cancel: Stay on current tab",
+ "Unsaved Settings",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Warning);
+
+ return result switch
+ {
+ MessageBoxResult.Cancel => false,
+ MessageBoxResult.Yes => await settingsViewModel.SaveIfDirtyAsync().ConfigureAwait(false),
+ MessageBoxResult.No => await DiscardPendingChangesAsync(settingsViewModel).ConfigureAwait(false),
+ _ => true,
+ };
+ }
+
+ public void Dispose()
+ {
+ this.navigationGuard.Dispose();
+ }
+
+ private static async Task DiscardPendingChangesAsync(SettingsViewModel settingsViewModel)
+ {
+ await settingsViewModel.DiscardPendingChangesAsync().ConfigureAwait(false);
+ return true;
+ }
+ }
+}
diff --git a/Helpers/ServiceProviderExtensions.cs b/Helpers/ServiceProviderExtensions.cs
index 102c923..4c17c04 100644
--- a/Helpers/ServiceProviderExtensions.cs
+++ b/Helpers/ServiceProviderExtensions.cs
@@ -1,32 +1,16 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Helpers
-{
- using System;
- using Microsoft.Extensions.DependencyInjection;
-
- public static class ServiceProviderExtensions
- {
- public static IServiceProvider Services => ((App)App.Current).ServiceProvider;
-
- public static T? GetService()
- where T : class
- {
- return Services.GetService(typeof(T)) as T;
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class ServiceProviderExtensions
+ {
+ public static IServiceProvider Services => ((App)App.Current).ServiceProvider;
+
+ public static T? GetService()
+ where T : class
+ {
+ return Services.GetService(typeof(T)) as T;
+ }
+ }
+}
diff --git a/Helpers/StartupMinimizedSuggestionPolicy.cs b/Helpers/StartupMinimizedSuggestionPolicy.cs
index 8036392..6364070 100644
--- a/Helpers/StartupMinimizedSuggestionPolicy.cs
+++ b/Helpers/StartupMinimizedSuggestionPolicy.cs
@@ -1,17 +1,17 @@
-namespace ThreadPilot.Helpers
-{
- using System;
- using ThreadPilot.Models;
-
- public static class StartupMinimizedSuggestionPolicy
- {
- public static bool ShouldShow(ApplicationSettingsModel settings, StartupWindowBehavior behavior)
- {
- ArgumentNullException.ThrowIfNull(settings);
-
- return behavior.ShouldShowWindow
- && !settings.StartMinimized
- && !settings.HasSeenStartupMinimizedSuggestion;
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using ThreadPilot.Models;
+
+ public static class StartupMinimizedSuggestionPolicy
+ {
+ public static bool ShouldShow(ApplicationSettingsModel settings, StartupWindowBehavior behavior)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+
+ return behavior.ShouldShowWindow
+ && !settings.StartMinimized
+ && !settings.HasSeenStartupMinimizedSuggestion;
+ }
+ }
+}
diff --git a/Helpers/StartupWindowBehavior.cs b/Helpers/StartupWindowBehavior.cs
index 642b66a..1021880 100644
--- a/Helpers/StartupWindowBehavior.cs
+++ b/Helpers/StartupWindowBehavior.cs
@@ -1,46 +1,46 @@
-namespace ThreadPilot.Helpers
-{
- using System.Windows;
-
- public readonly record struct StartupWindowBehavior(
- bool ShouldShowWindow,
- bool ShowInTaskbar,
- Visibility Visibility,
- WindowState WindowState,
- bool HideAfterShow,
- bool ActivateAfterShow)
- {
- public static StartupWindowBehavior Resolve(bool isAutostart, bool startMinimized)
- {
- if (isAutostart && startMinimized)
- {
- return new StartupWindowBehavior(
- ShouldShowWindow: false,
- ShowInTaskbar: false,
- Visibility: Visibility.Hidden,
- WindowState: WindowState.Minimized,
- HideAfterShow: false,
- ActivateAfterShow: false);
- }
-
- if (startMinimized)
- {
- return new StartupWindowBehavior(
- ShouldShowWindow: false,
- ShowInTaskbar: false,
- Visibility: Visibility.Hidden,
- WindowState: WindowState.Minimized,
- HideAfterShow: false,
- ActivateAfterShow: false);
- }
-
- return new StartupWindowBehavior(
- ShouldShowWindow: true,
- ShowInTaskbar: true,
- Visibility: Visibility.Visible,
- WindowState: WindowState.Normal,
- HideAfterShow: false,
- ActivateAfterShow: true);
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System.Windows;
+
+ public readonly record struct StartupWindowBehavior(
+ bool ShouldShowWindow,
+ bool ShowInTaskbar,
+ Visibility Visibility,
+ WindowState WindowState,
+ bool HideAfterShow,
+ bool ActivateAfterShow)
+ {
+ public static StartupWindowBehavior Resolve(bool isAutostart, bool startMinimized)
+ {
+ if (isAutostart && startMinimized)
+ {
+ return new StartupWindowBehavior(
+ ShouldShowWindow: false,
+ ShowInTaskbar: false,
+ Visibility: Visibility.Hidden,
+ WindowState: WindowState.Minimized,
+ HideAfterShow: false,
+ ActivateAfterShow: false);
+ }
+
+ if (startMinimized)
+ {
+ return new StartupWindowBehavior(
+ ShouldShowWindow: false,
+ ShowInTaskbar: false,
+ Visibility: Visibility.Hidden,
+ WindowState: WindowState.Minimized,
+ HideAfterShow: false,
+ ActivateAfterShow: false);
+ }
+
+ return new StartupWindowBehavior(
+ ShouldShowWindow: true,
+ ShowInTaskbar: true,
+ Visibility: Visibility.Visible,
+ WindowState: WindowState.Normal,
+ HideAfterShow: false,
+ ActivateAfterShow: true);
+ }
+ }
+}
diff --git a/Helpers/WindowPlacementHelper.cs b/Helpers/WindowPlacementHelper.cs
index 175ea49..335ed0e 100644
--- a/Helpers/WindowPlacementHelper.cs
+++ b/Helpers/WindowPlacementHelper.cs
@@ -1,295 +1,295 @@
-namespace ThreadPilot.Helpers
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Windows;
- using System.Windows.Forms;
- using System.Windows.Media;
- using DrawingRectangle = System.Drawing.Rectangle;
- using WpfPoint = System.Windows.Point;
-
- public readonly record struct WindowBounds(double Left, double Top, double Width, double Height)
- {
- public double Right => this.Left + this.Width;
-
- public double Bottom => this.Top + this.Height;
-
- public double Area => Math.Max(0, this.Width) * Math.Max(0, this.Height);
- }
-
- public readonly record struct MonitorWorkingArea(double Left, double Top, double Width, double Height, bool IsPrimary)
- {
- public double Right => this.Left + this.Width;
-
- public double Bottom => this.Top + this.Height;
-
- public double Area => Math.Max(0, this.Width) * Math.Max(0, this.Height);
- }
-
- public readonly record struct WindowPlacementCorrection(WindowBounds Bounds, bool WasCorrected);
-
- public static class WindowPlacementHelper
- {
- private const double DefaultWindowWidth = 1280;
- private const double DefaultWindowHeight = 864;
- private const double MinimumVisibleAreaRatio = 0.25;
- private const double Epsilon = 0.001;
-
- public static WindowPlacementCorrection CorrectWindowBounds(
- WindowBounds currentBounds,
- IReadOnlyCollection workingAreas)
- {
- var validWorkingAreas = workingAreas
- .Where(IsValidWorkingArea)
- .ToArray();
-
- if (validWorkingAreas.Length == 0)
- {
- var fallbackBounds = new WindowBounds(
- IsFinite(currentBounds.Left) ? currentBounds.Left : 0,
- IsFinite(currentBounds.Top) ? currentBounds.Top : 0,
- ResolveDimension(currentBounds.Width, DefaultWindowWidth),
- ResolveDimension(currentBounds.Height, DefaultWindowHeight));
-
- return new WindowPlacementCorrection(fallbackBounds, !BoundsAreEquivalent(currentBounds, fallbackBounds));
- }
-
- var targetArea = SelectTargetWorkingArea(currentBounds, validWorkingAreas);
- var correctedWidth = Math.Min(ResolveDimension(currentBounds.Width, DefaultWindowWidth), targetArea.Width);
- var correctedHeight = Math.Min(ResolveDimension(currentBounds.Height, DefaultWindowHeight), targetArea.Height);
- var sanitizedBounds = new WindowBounds(
- currentBounds.Left,
- currentBounds.Top,
- correctedWidth,
- correctedHeight);
-
- var hasSufficientIntersection = HasSufficientIntersection(sanitizedBounds, targetArea);
- double correctedLeft;
- double correctedTop;
-
- if (!hasSufficientIntersection || !IsFinite(sanitizedBounds.Left) || !IsFinite(sanitizedBounds.Top))
- {
- correctedLeft = targetArea.Left + ((targetArea.Width - correctedWidth) / 2);
- correctedTop = targetArea.Top + ((targetArea.Height - correctedHeight) / 2);
- }
- else
- {
- correctedLeft = Clamp(sanitizedBounds.Left, targetArea.Left, targetArea.Right - correctedWidth);
- correctedTop = Clamp(sanitizedBounds.Top, targetArea.Top, targetArea.Bottom - correctedHeight);
- }
-
- var correctedBounds = new WindowBounds(
- Math.Round(correctedLeft),
- Math.Round(correctedTop),
- Math.Round(correctedWidth),
- Math.Round(correctedHeight));
-
- return new WindowPlacementCorrection(correctedBounds, !BoundsAreEquivalent(currentBounds, correctedBounds));
- }
-
- public static bool TryCorrectWindowPlacement(Window window)
- {
- ArgumentNullException.ThrowIfNull(window);
-
- try
- {
- if (window.WindowState == WindowState.Maximized)
- {
- return false;
- }
-
- var workingAreas = GetWorkingAreasInWindowDips(window);
- var currentBounds = new WindowBounds(
- window.Left,
- window.Top,
- ResolveWindowDimension(window.ActualWidth, window.Width, DefaultWindowWidth),
- ResolveWindowDimension(window.ActualHeight, window.Height, DefaultWindowHeight));
-
- var correction = CorrectWindowBounds(currentBounds, workingAreas);
- if (!correction.WasCorrected)
- {
- return false;
- }
-
- window.Width = correction.Bounds.Width;
- window.Height = correction.Bounds.Height;
- window.Left = correction.Bounds.Left;
- window.Top = correction.Bounds.Top;
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private static MonitorWorkingArea[] GetWorkingAreasInWindowDips(Window window)
- {
- var transformFromDevice = GetTransformFromDevice(window);
- var screens = Screen.AllScreens;
- if (screens.Length == 0)
- {
- return new[]
- {
- new MonitorWorkingArea(
- SystemParameters.WorkArea.Left,
- SystemParameters.WorkArea.Top,
- SystemParameters.WorkArea.Width,
- SystemParameters.WorkArea.Height,
- true),
- };
- }
-
- return screens
- .Select(screen => ConvertWorkingAreaToDips(screen.WorkingArea, screen.Primary, transformFromDevice))
- .Where(IsValidWorkingArea)
- .ToArray();
- }
-
- private static Matrix GetTransformFromDevice(Window window)
- {
- try
- {
- var source = PresentationSource.FromVisual(window);
- return source?.CompositionTarget?.TransformFromDevice ?? Matrix.Identity;
- }
- catch
- {
- return Matrix.Identity;
- }
- }
-
- private static MonitorWorkingArea ConvertWorkingAreaToDips(
- DrawingRectangle workingArea,
- bool isPrimary,
- Matrix transformFromDevice)
- {
- var topLeft = transformFromDevice.Transform(new WpfPoint(workingArea.Left, workingArea.Top));
- var bottomRight = transformFromDevice.Transform(new WpfPoint(workingArea.Right, workingArea.Bottom));
-
- return new MonitorWorkingArea(
- topLeft.X,
- topLeft.Y,
- bottomRight.X - topLeft.X,
- bottomRight.Y - topLeft.Y,
- isPrimary);
- }
-
- private static MonitorWorkingArea SelectTargetWorkingArea(
- WindowBounds currentBounds,
- IReadOnlyList workingAreas)
- {
- var bestIntersection = workingAreas
- .Select(area => new
- {
- Area = area,
- IntersectionArea = GetIntersectionArea(currentBounds, area),
- })
- .OrderByDescending(candidate => candidate.IntersectionArea)
- .ThenByDescending(candidate => candidate.Area.IsPrimary)
- .First();
-
- if (bestIntersection.IntersectionArea > 0 && HasSufficientIntersection(currentBounds, bestIntersection.Area))
- {
- return bestIntersection.Area;
- }
-
- if (!IsFinite(currentBounds.Left) || !IsFinite(currentBounds.Top))
- {
- return workingAreas.FirstOrDefault(area => area.IsPrimary, workingAreas[0]);
- }
-
- var centerX = currentBounds.Left + (ResolveDimension(currentBounds.Width, DefaultWindowWidth) / 2);
- var centerY = currentBounds.Top + (ResolveDimension(currentBounds.Height, DefaultWindowHeight) / 2);
-
- return workingAreas
- .OrderBy(area => GetSquaredDistanceToArea(centerX, centerY, area))
- .ThenByDescending(area => area.IsPrimary)
- .First();
- }
-
- private static double GetSquaredDistanceToArea(double x, double y, MonitorWorkingArea area)
- {
- var nearestX = Clamp(x, area.Left, area.Right);
- var nearestY = Clamp(y, area.Top, area.Bottom);
- var deltaX = x - nearestX;
- var deltaY = y - nearestY;
-
- return (deltaX * deltaX) + (deltaY * deltaY);
- }
-
- private static bool HasSufficientIntersection(WindowBounds bounds, MonitorWorkingArea area)
- {
- var intersectionArea = GetIntersectionArea(bounds, area);
- var boundedWindowArea = Math.Max(1, Math.Min(bounds.Width, area.Width) * Math.Min(bounds.Height, area.Height));
-
- return intersectionArea / boundedWindowArea >= MinimumVisibleAreaRatio;
- }
-
- private static double GetIntersectionArea(WindowBounds bounds, MonitorWorkingArea area)
- {
- if (!IsFinite(bounds.Left) || !IsFinite(bounds.Top) || !IsFinite(bounds.Width) || !IsFinite(bounds.Height))
- {
- return 0;
- }
-
- var left = Math.Max(bounds.Left, area.Left);
- var top = Math.Max(bounds.Top, area.Top);
- var right = Math.Min(bounds.Right, area.Right);
- var bottom = Math.Min(bounds.Bottom, area.Bottom);
- var width = Math.Max(0, right - left);
- var height = Math.Max(0, bottom - top);
-
- return width * height;
- }
-
- private static bool IsValidWorkingArea(MonitorWorkingArea workingArea)
- {
- return IsFinite(workingArea.Left)
- && IsFinite(workingArea.Top)
- && IsFinite(workingArea.Width)
- && IsFinite(workingArea.Height)
- && workingArea.Width > 0
- && workingArea.Height > 0;
- }
-
- private static double ResolveWindowDimension(double actualValue, double configuredValue, double fallback)
- {
- if (IsFinite(actualValue) && actualValue > 0)
- {
- return actualValue;
- }
-
- return ResolveDimension(configuredValue, fallback);
- }
-
- private static double ResolveDimension(double value, double fallback)
- {
- return IsFinite(value) && value > 0 ? value : fallback;
- }
-
- private static double Clamp(double value, double minimum, double maximum)
- {
- if (maximum < minimum)
- {
- return minimum;
- }
-
- return Math.Min(Math.Max(value, minimum), maximum);
- }
-
- private static bool IsFinite(double value)
- {
- return !double.IsNaN(value) && !double.IsInfinity(value);
- }
-
- private static bool BoundsAreEquivalent(WindowBounds left, WindowBounds right)
- {
- return Math.Abs(left.Left - right.Left) < Epsilon
- && Math.Abs(left.Top - right.Top) < Epsilon
- && Math.Abs(left.Width - right.Width) < Epsilon
- && Math.Abs(left.Height - right.Height) < Epsilon;
- }
- }
-}
+namespace ThreadPilot.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Windows;
+ using System.Windows.Forms;
+ using System.Windows.Media;
+ using DrawingRectangle = System.Drawing.Rectangle;
+ using WpfPoint = System.Windows.Point;
+
+ public readonly record struct WindowBounds(double Left, double Top, double Width, double Height)
+ {
+ public double Right => this.Left + this.Width;
+
+ public double Bottom => this.Top + this.Height;
+
+ public double Area => Math.Max(0, this.Width) * Math.Max(0, this.Height);
+ }
+
+ public readonly record struct MonitorWorkingArea(double Left, double Top, double Width, double Height, bool IsPrimary)
+ {
+ public double Right => this.Left + this.Width;
+
+ public double Bottom => this.Top + this.Height;
+
+ public double Area => Math.Max(0, this.Width) * Math.Max(0, this.Height);
+ }
+
+ public readonly record struct WindowPlacementCorrection(WindowBounds Bounds, bool WasCorrected);
+
+ public static class WindowPlacementHelper
+ {
+ private const double DefaultWindowWidth = 1280;
+ private const double DefaultWindowHeight = 864;
+ private const double MinimumVisibleAreaRatio = 0.25;
+ private const double Epsilon = 0.001;
+
+ public static WindowPlacementCorrection CorrectWindowBounds(
+ WindowBounds currentBounds,
+ IReadOnlyCollection workingAreas)
+ {
+ var validWorkingAreas = workingAreas
+ .Where(IsValidWorkingArea)
+ .ToArray();
+
+ if (validWorkingAreas.Length == 0)
+ {
+ var fallbackBounds = new WindowBounds(
+ IsFinite(currentBounds.Left) ? currentBounds.Left : 0,
+ IsFinite(currentBounds.Top) ? currentBounds.Top : 0,
+ ResolveDimension(currentBounds.Width, DefaultWindowWidth),
+ ResolveDimension(currentBounds.Height, DefaultWindowHeight));
+
+ return new WindowPlacementCorrection(fallbackBounds, !BoundsAreEquivalent(currentBounds, fallbackBounds));
+ }
+
+ var targetArea = SelectTargetWorkingArea(currentBounds, validWorkingAreas);
+ var correctedWidth = Math.Min(ResolveDimension(currentBounds.Width, DefaultWindowWidth), targetArea.Width);
+ var correctedHeight = Math.Min(ResolveDimension(currentBounds.Height, DefaultWindowHeight), targetArea.Height);
+ var sanitizedBounds = new WindowBounds(
+ currentBounds.Left,
+ currentBounds.Top,
+ correctedWidth,
+ correctedHeight);
+
+ var hasSufficientIntersection = HasSufficientIntersection(sanitizedBounds, targetArea);
+ double correctedLeft;
+ double correctedTop;
+
+ if (!hasSufficientIntersection || !IsFinite(sanitizedBounds.Left) || !IsFinite(sanitizedBounds.Top))
+ {
+ correctedLeft = targetArea.Left + ((targetArea.Width - correctedWidth) / 2);
+ correctedTop = targetArea.Top + ((targetArea.Height - correctedHeight) / 2);
+ }
+ else
+ {
+ correctedLeft = Clamp(sanitizedBounds.Left, targetArea.Left, targetArea.Right - correctedWidth);
+ correctedTop = Clamp(sanitizedBounds.Top, targetArea.Top, targetArea.Bottom - correctedHeight);
+ }
+
+ var correctedBounds = new WindowBounds(
+ Math.Round(correctedLeft),
+ Math.Round(correctedTop),
+ Math.Round(correctedWidth),
+ Math.Round(correctedHeight));
+
+ return new WindowPlacementCorrection(correctedBounds, !BoundsAreEquivalent(currentBounds, correctedBounds));
+ }
+
+ public static bool TryCorrectWindowPlacement(Window window)
+ {
+ ArgumentNullException.ThrowIfNull(window);
+
+ try
+ {
+ if (window.WindowState == WindowState.Maximized)
+ {
+ return false;
+ }
+
+ var workingAreas = GetWorkingAreasInWindowDips(window);
+ var currentBounds = new WindowBounds(
+ window.Left,
+ window.Top,
+ ResolveWindowDimension(window.ActualWidth, window.Width, DefaultWindowWidth),
+ ResolveWindowDimension(window.ActualHeight, window.Height, DefaultWindowHeight));
+
+ var correction = CorrectWindowBounds(currentBounds, workingAreas);
+ if (!correction.WasCorrected)
+ {
+ return false;
+ }
+
+ window.Width = correction.Bounds.Width;
+ window.Height = correction.Bounds.Height;
+ window.Left = correction.Bounds.Left;
+ window.Top = correction.Bounds.Top;
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static MonitorWorkingArea[] GetWorkingAreasInWindowDips(Window window)
+ {
+ var transformFromDevice = GetTransformFromDevice(window);
+ var screens = Screen.AllScreens;
+ if (screens.Length == 0)
+ {
+ return new[]
+ {
+ new MonitorWorkingArea(
+ SystemParameters.WorkArea.Left,
+ SystemParameters.WorkArea.Top,
+ SystemParameters.WorkArea.Width,
+ SystemParameters.WorkArea.Height,
+ true),
+ };
+ }
+
+ return screens
+ .Select(screen => ConvertWorkingAreaToDips(screen.WorkingArea, screen.Primary, transformFromDevice))
+ .Where(IsValidWorkingArea)
+ .ToArray();
+ }
+
+ private static Matrix GetTransformFromDevice(Window window)
+ {
+ try
+ {
+ var source = PresentationSource.FromVisual(window);
+ return source?.CompositionTarget?.TransformFromDevice ?? Matrix.Identity;
+ }
+ catch
+ {
+ return Matrix.Identity;
+ }
+ }
+
+ private static MonitorWorkingArea ConvertWorkingAreaToDips(
+ DrawingRectangle workingArea,
+ bool isPrimary,
+ Matrix transformFromDevice)
+ {
+ var topLeft = transformFromDevice.Transform(new WpfPoint(workingArea.Left, workingArea.Top));
+ var bottomRight = transformFromDevice.Transform(new WpfPoint(workingArea.Right, workingArea.Bottom));
+
+ return new MonitorWorkingArea(
+ topLeft.X,
+ topLeft.Y,
+ bottomRight.X - topLeft.X,
+ bottomRight.Y - topLeft.Y,
+ isPrimary);
+ }
+
+ private static MonitorWorkingArea SelectTargetWorkingArea(
+ WindowBounds currentBounds,
+ IReadOnlyList workingAreas)
+ {
+ var bestIntersection = workingAreas
+ .Select(area => new
+ {
+ Area = area,
+ IntersectionArea = GetIntersectionArea(currentBounds, area),
+ })
+ .OrderByDescending(candidate => candidate.IntersectionArea)
+ .ThenByDescending(candidate => candidate.Area.IsPrimary)
+ .First();
+
+ if (bestIntersection.IntersectionArea > 0 && HasSufficientIntersection(currentBounds, bestIntersection.Area))
+ {
+ return bestIntersection.Area;
+ }
+
+ if (!IsFinite(currentBounds.Left) || !IsFinite(currentBounds.Top))
+ {
+ return workingAreas.FirstOrDefault(area => area.IsPrimary, workingAreas[0]);
+ }
+
+ var centerX = currentBounds.Left + (ResolveDimension(currentBounds.Width, DefaultWindowWidth) / 2);
+ var centerY = currentBounds.Top + (ResolveDimension(currentBounds.Height, DefaultWindowHeight) / 2);
+
+ return workingAreas
+ .OrderBy(area => GetSquaredDistanceToArea(centerX, centerY, area))
+ .ThenByDescending(area => area.IsPrimary)
+ .First();
+ }
+
+ private static double GetSquaredDistanceToArea(double x, double y, MonitorWorkingArea area)
+ {
+ var nearestX = Clamp(x, area.Left, area.Right);
+ var nearestY = Clamp(y, area.Top, area.Bottom);
+ var deltaX = x - nearestX;
+ var deltaY = y - nearestY;
+
+ return (deltaX * deltaX) + (deltaY * deltaY);
+ }
+
+ private static bool HasSufficientIntersection(WindowBounds bounds, MonitorWorkingArea area)
+ {
+ var intersectionArea = GetIntersectionArea(bounds, area);
+ var boundedWindowArea = Math.Max(1, Math.Min(bounds.Width, area.Width) * Math.Min(bounds.Height, area.Height));
+
+ return intersectionArea / boundedWindowArea >= MinimumVisibleAreaRatio;
+ }
+
+ private static double GetIntersectionArea(WindowBounds bounds, MonitorWorkingArea area)
+ {
+ if (!IsFinite(bounds.Left) || !IsFinite(bounds.Top) || !IsFinite(bounds.Width) || !IsFinite(bounds.Height))
+ {
+ return 0;
+ }
+
+ var left = Math.Max(bounds.Left, area.Left);
+ var top = Math.Max(bounds.Top, area.Top);
+ var right = Math.Min(bounds.Right, area.Right);
+ var bottom = Math.Min(bounds.Bottom, area.Bottom);
+ var width = Math.Max(0, right - left);
+ var height = Math.Max(0, bottom - top);
+
+ return width * height;
+ }
+
+ private static bool IsValidWorkingArea(MonitorWorkingArea workingArea)
+ {
+ return IsFinite(workingArea.Left)
+ && IsFinite(workingArea.Top)
+ && IsFinite(workingArea.Width)
+ && IsFinite(workingArea.Height)
+ && workingArea.Width > 0
+ && workingArea.Height > 0;
+ }
+
+ private static double ResolveWindowDimension(double actualValue, double configuredValue, double fallback)
+ {
+ if (IsFinite(actualValue) && actualValue > 0)
+ {
+ return actualValue;
+ }
+
+ return ResolveDimension(configuredValue, fallback);
+ }
+
+ private static double ResolveDimension(double value, double fallback)
+ {
+ return IsFinite(value) && value > 0 ? value : fallback;
+ }
+
+ private static double Clamp(double value, double minimum, double maximum)
+ {
+ if (maximum < minimum)
+ {
+ return minimum;
+ }
+
+ return Math.Min(Math.Max(value, minimum), maximum);
+ }
+
+ private static bool IsFinite(double value)
+ {
+ return !double.IsNaN(value) && !double.IsInfinity(value);
+ }
+
+ private static bool BoundsAreEquivalent(WindowBounds left, WindowBounds right)
+ {
+ return Math.Abs(left.Left - right.Left) < Epsilon
+ && Math.Abs(left.Top - right.Top) < Epsilon
+ && Math.Abs(left.Width - right.Width) < Epsilon
+ && Math.Abs(left.Height - right.Height) < Epsilon;
+ }
+ }
+}
diff --git a/MainWindow.Behaviors.partial.cs b/MainWindow.Behaviors.partial.cs
index 1c857ee..0ad177e 100644
--- a/MainWindow.Behaviors.partial.cs
+++ b/MainWindow.Behaviors.partial.cs
@@ -1,2045 +1,2025 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot
-{
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Timers;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Input;
- using System.Windows.Media.Animation;
- using System.Windows.Media.Effects;
- using System.Windows.Media.Imaging;
- using Microsoft.Extensions.DependencyInjection;
- using ThreadPilot.Helpers;
- using ThreadPilot.Models;
- using ThreadPilot.Services;
- using ThreadPilot.ViewModels;
- using ThreadPilot.Views;
-
- public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
- {
- private void SetDataContexts()
- {
- // Set DataContext for the main window
- this.DataContext = this.mainWindowViewModel;
-
- // Set DataContext for the power plans view
- this.PowerPlanViewControl.DataContext = this.powerPlanViewModel;
-
- // Set DataContext for the association view
- this.AssociationView.DataContext = this.associationViewModel;
-
- // Set DataContext for the log viewer view
- this.LogViewerViewControl.DataContext = this.logViewerViewModel;
-
- // Set DataContext for the system tweaks view
- this.SystemTweaksView.DataContext = this.systemTweaksViewModel;
-
- // Set DataContext for the settings view
- this.SettingsView.DataContext = this.settingsViewModel;
- }
-
- private void InitializeLoadingOverlay()
- {
- try
- {
- var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
-
- // Ensure overlay is visible while initialization runs
- if (loadingOverlay != null)
- {
- loadingOverlay.Visibility = this.isSilentStartupMode ? Visibility.Collapsed : Visibility.Visible;
- loadingOverlay.Opacity = this.isSilentStartupMode ? 0 : 1;
- }
-
- if (!this.isSilentStartupMode)
- {
- this.ApplyUIContentBlur(15);
- }
-
- // Start spinner animation if available
- var spinnerAnimation = this.FindResource("SpinnerAnimation") as Storyboard;
- if (!this.isSilentStartupMode)
- {
- spinnerAnimation?.Begin();
- }
-
- // Set a timeout guard for initialization
- this.initializationTimeoutTimer = new System.Timers.Timer(15000)
- {
- AutoReset = false,
- };
- this.initializationTimeoutTimer.Elapsed += this.OnInitializationTimeout;
- this.initializationTimeoutTimer.Start();
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to initialize loading overlay: {ex.Message}");
- }
- }
-
- private async Task InitializeApplicationAsync()
- {
- try
- {
- this.LogDebug("=== Starting InitializeApplicationAsync ===");
-
- await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Loading view models...", "Loading process, power plan and rules data."));
- this.LogDebug("About to call LoadViewModelsAsync...");
- await this.LoadViewModelsAsync();
- this.LogDebug("LoadViewModelsAsync completed successfully");
- this.CompleteInitializationTask("ViewModels");
-
- this.LogDebug("About to initialize MainWindowViewModel...");
- await this.mainWindowViewModel.InitializeAsync();
- this.LogDebug("MainWindowViewModel initialized successfully");
- this.CompleteInitializationTask("MainWindowViewModel");
-
- await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Initializing services...", "Starting monitoring, tray and notification services."));
- this.LogDebug("About to call InitializeServicesAsync...");
- await this.InitializeServicesAsync();
- this.LogDebug("InitializeServicesAsync completed successfully");
- this.CompleteInitializationTask("Services");
- this.QueueStartupUpdateCheck();
-
- await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Finalizing startup...", "Applying final UI state and startup checks."));
- this.LogDebug("Finalizing startup...");
- await Task.Delay(500); // Brief delay to show final status
- this.CompleteInitializationTask("Finalization");
-
- // All initialization complete
- this.LogDebug("All initialization complete, hiding overlay...");
- await this.Dispatcher.InvokeAsync(() => this.HideLoadingOverlay());
- this.LogDebug("=== InitializeApplicationAsync completed successfully ===");
- }
- catch (Exception ex)
- {
- this.LogDebug($"=== ERROR in InitializeApplicationAsync: {ex} ===");
- await this.Dispatcher.InvokeAsync(() => this.ShowInitializationError(ex));
- }
- }
-
- private void QueueStartupUpdateCheck()
- {
- if (Interlocked.Exchange(ref this.startupUpdateCheckStarted, 1) != 0)
- {
- return;
- }
-
- TaskSafety.FireAndForget(this.CheckForUpdatesAtStartupAsync(), ex =>
- {
- this.LogDebug($"Startup update check failed: {ex.Message}");
- });
- }
-
- private async Task CheckForUpdatesAtStartupAsync()
- {
- try
- {
- this.LogDebug("Startup update check started");
- var updateService = this.serviceProvider.GetRequiredService();
- var result = await updateService.CheckForUpdatesAsync(new UpdateCheckRequest(UpdateCheckTrigger.Startup));
-
- if (result.Status == UpdateCheckStatus.Skipped)
- {
- this.LogDebug($"Startup update check skipped: {result.Message}");
- return;
- }
-
- if (!result.IsUpdateAvailable || result.Release == null)
- {
- this.LogDebug($"Startup update check complete: {result.Message}");
- return;
- }
-
- await this.notificationService.ShowNotificationAsync(
- "Update available",
- $"ThreadPilot {result.Release.Version} is available. Open Settings to download and install it.",
- NotificationType.Information);
- this.LogDebug($"Startup update check found update: installed {result.CurrentVersion}, latest {result.Release.Version}");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Startup update check ignored failure: {ex.Message}");
- }
- }
-
- private static Version GetCurrentApplicationVersion()
- {
- var rawVersion = typeof(App).Assembly
- .GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
- .OfType()
- .FirstOrDefault()?
- .InformationalVersion
- ?? typeof(App).Assembly.GetName().Version?.ToString()
- ?? "0.0.0";
-
- var sanitized = rawVersion.Trim();
- if (sanitized.StartsWith("v", StringComparison.OrdinalIgnoreCase))
- {
- sanitized = sanitized[1..];
- }
-
- sanitized = sanitized.Split('-', '+')[0];
-
- return Version.TryParse(sanitized, out var version)
- ? version
- : new Version(0, 0, 0);
- }
-
- private void UpdateLoadingStatus(string stage, string details = "")
- {
- if (this.mainWindowViewModel != null)
- {
- this.mainWindowViewModel.InitializationStage = stage;
- this.mainWindowViewModel.InitializationDetails = details;
- }
- }
-
- private void CompleteInitializationTask(string taskName)
- {
- lock (this.initializationLock)
- {
- this.initializationTasks.Add(taskName);
- System.Diagnostics.Debug.WriteLine($"Initialization task completed: {taskName}");
- }
- }
-
- private void HideLoadingOverlay()
- {
- try
- {
- System.Diagnostics.Debug.WriteLine("=== Starting HideLoadingOverlay ===");
- this.isInitializationComplete = true;
- this.initializationTimeoutTimer?.Stop();
- this.initializationTimeoutTimer?.Dispose();
-
- if (this.isSilentStartupMode)
- {
- var silentLoadingOverlay = this.FindName("LoadingOverlay") as Grid;
- if (silentLoadingOverlay != null)
- {
- silentLoadingOverlay.Visibility = Visibility.Collapsed;
- silentLoadingOverlay.Opacity = 0;
- }
-
- this.ClearUIContentBlur();
- this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
- return;
- }
-
- // Stop spinner animation
- var spinnerAnimation = this.FindResource("SpinnerAnimation") as Storyboard;
- spinnerAnimation?.Stop();
- System.Diagnostics.Debug.WriteLine("Spinner animation stopped");
-
- // Start fade-out animation
- var fadeOutAnimation = this.FindResource("FadeOutAnimation") as Storyboard;
- if (fadeOutAnimation != null)
- {
- System.Diagnostics.Debug.WriteLine("Starting fade-out animation");
- fadeOutAnimation.Completed += (s, e) =>
- {
- System.Diagnostics.Debug.WriteLine("Fade-out animation completed, hiding overlay");
- var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
- if (loadingOverlay != null)
- {
- loadingOverlay.Visibility = Visibility.Collapsed;
- System.Diagnostics.Debug.WriteLine("Loading overlay visibility set to Collapsed");
- }
-
- // Disable app content blur and restore style-driven behavior.
- this.ClearUIContentBlur();
- System.Diagnostics.Debug.WriteLine("=== Loading overlay hidden successfully ===");
-
- // Show elevation warning if needed
- this.TryShowElevationWarning();
- this.TryShowStartupMinimizedSuggestion();
- };
- fadeOutAnimation.Begin();
- }
- else
- {
- System.Diagnostics.Debug.WriteLine("WARNING: FadeOutAnimation not found, hiding overlay immediately");
- // Fallback: hide overlay immediately if animation fails
- var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
- if (loadingOverlay != null)
- {
- loadingOverlay.Visibility = Visibility.Collapsed;
- }
-
- this.ClearUIContentBlur();
- System.Diagnostics.Debug.WriteLine("=== Loading overlay hidden immediately (fallback) ===");
-
- // Show elevation warning if needed
- this.TryShowElevationWarning();
- this.TryShowStartupMinimizedSuggestion();
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"=== ERROR hiding loading overlay: {ex} ===");
- // Emergency fallback: hide overlay without animation
- try
- {
- var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
- if (loadingOverlay != null)
- {
- loadingOverlay.Visibility = Visibility.Collapsed;
- }
-
- this.ClearUIContentBlur();
- System.Diagnostics.Debug.WriteLine("Emergency fallback: overlay hidden without animation");
-
- // Show elevation warning if needed
- this.TryShowElevationWarning();
- this.TryShowStartupMinimizedSuggestion();
- }
- catch (Exception fallbackEx)
- {
- System.Diagnostics.Debug.WriteLine($"Emergency fallback also failed: {fallbackEx}");
- }
- }
- }
-
- private void ApplyUIContentBlur(double radius)
- {
- if (this.UIContent.Effect is not BlurEffect blur)
- {
- blur = new BlurEffect();
- this.UIContent.Effect = blur;
- }
-
- blur.KernelType = KernelType.Gaussian;
- blur.Radius = radius;
- }
-
- private void ClearUIContentBlur()
- {
- this.UIContent.Effect = null;
- }
-
- private void OnInitializationTimeout(object? sender, ElapsedEventArgs e)
- {
- this.Dispatcher.InvokeAsync(() =>
- {
- if (!this.isInitializationComplete)
- {
- this.ShowInitializationError(new TimeoutException("Application initialization timed out after 15 seconds"));
- }
- });
- }
-
- private void ShowInitializationError(Exception ex)
- {
- try
- {
- this.UpdateLoadingStatus("Initialization failed", ex.Message);
-
- var result = System.Windows.MessageBox.Show(
- $"ThreadPilot failed to initialize properly:\n\n{ex.Message}\n\nDebug log: {this.debugLogPath}\n\nWould you like to retry initialization or close the application?",
- "Initialization Error",
- MessageBoxButton.YesNo,
- MessageBoxImage.Error);
-
- if (result == MessageBoxResult.Yes)
- {
- // Retry initialization - marshal to UI thread to prevent cross-thread access exceptions
- this.isInitializationComplete = false;
- this.initializationTasks.Clear();
- this.UpdateLoadingStatus("Retrying initialization...", "Restarting startup sequence.");
- this.LogDebug("=== RETRYING INITIALIZATION ===");
- _ = this.Dispatcher.InvokeAsync(async () => await this.InitializeApplicationAsync());
- }
- else
- {
- // Close application
- this.LogDebug("User chose to close application");
- System.Windows.Application.Current.Shutdown();
- }
- }
- catch (Exception overlayEx)
- {
- this.LogDebug($"Error showing initialization error: {overlayEx.Message}");
- System.Windows.Application.Current.Shutdown();
- }
- }
-
- private async Task LoadViewModelsAsync()
- {
- try
- {
- this.LogDebug("=== Starting LoadViewModelsAsync ===");
-
- this.LogDebug("About to initialize ProcessViewModel (including CPU topology)...");
- try
- {
- // Use the full initialization method instead of just LoadProcesses
- var processTask = this.processViewModel.InitializeAsync();
- var processResult = await Task.WhenAny(processTask, Task.Delay(15000)); // 15 second timeout for full initialization
- if (processResult != processTask)
- {
- this.LogDebug("ProcessViewModel.InitializeAsync() timed out, trying fallback...");
- // Fallback: just load processes without full initialization
- await this.processViewModel.LoadProcesses();
- this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
- }
- else
- {
- await processTask; // Ensure we get any exceptions
- this.LogDebug($"ProcessViewModel initialized successfully (including CPU topology), process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
- }
- }
- catch (Exception processEx)
- {
- this.LogDebug($"ProcessViewModel initialization failed: {processEx.Message}, trying fallback...");
- // Fallback: just load processes without full initialization
- await this.processViewModel.LoadProcesses();
- this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed after exception, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
- }
-
- this.LogDebug("About to load PowerPlanViewModel...");
- var powerPlanTask = this.powerPlanViewModel.LoadPowerPlans();
- var powerPlanResult = await Task.WhenAny(powerPlanTask, Task.Delay(5000)); // 5 second timeout
- if (powerPlanResult != powerPlanTask)
- {
- throw new TimeoutException("PowerPlanViewModel.LoadPowerPlans() timed out after 5 seconds");
- }
- await powerPlanTask; // Ensure we get any exceptions
- this.LogDebug("PowerPlanViewModel loaded successfully");
-
- this.LogDebug("Skipping optional diagnostics initialization until the diagnostics page is opened.");
-
- this.LogDebug("About to load SystemTweaksViewModel...");
- var systemTweaksTask = this.systemTweaksViewModel.LoadCommand.ExecuteAsync(null);
- var systemTweaksResult = await Task.WhenAny(systemTweaksTask, Task.Delay(5000)); // 5 second timeout
- if (systemTweaksResult != systemTweaksTask)
- {
- throw new TimeoutException("SystemTweaksViewModel.LoadCommand.ExecuteAsync() timed out after 5 seconds");
- }
- await systemTweaksTask; // Ensure we get any exceptions
- this.LogDebug("SystemTweaksViewModel loaded successfully");
-
- // Initialize keyboard shortcuts after window is loaded
- this.Loaded += this.OnWindowLoaded;
- this.LogDebug("Keyboard shortcuts event handler attached");
-
- // The association view model loads its data automatically in its constructor
- this.LogDebug("=== LoadViewModelsAsync completed successfully ===");
- }
- catch (Exception ex)
- {
- this.LogDebug($"=== ERROR in LoadViewModelsAsync: {ex} ===");
- throw; // Re-throw to be handled by initialization error handler
- }
- }
-
- private async Task InitializeServicesAsync()
- {
- this.LogDebug("=== Starting InitializeServicesAsync ===");
-
- this.LogDebug("About to initialize settings...");
- await this.InitializeSettingsAsync();
- this.LogDebug("Settings initialized successfully");
-
- this.LogDebug("About to initialize system tray...");
- try
- {
- var systemTrayTask = this.InitializeSystemTrayAsync();
- var systemTrayResult = await Task.WhenAny(systemTrayTask, Task.Delay(5000)); // 5 second timeout
- if (systemTrayResult != systemTrayTask)
- {
- this.LogDebug("System tray initialization timed out, continuing with basic tray setup...");
- // Initialize basic system tray without context menu updates (Initialize() is idempotent)
- await this.InitializeBasicSystemTrayAsync();
- this.LogDebug("Basic system tray initialized (without context menu)");
- }
- else
- {
- await systemTrayTask; // Ensure we get any exceptions
- this.LogDebug("System tray initialized successfully");
- }
- }
- catch (Exception systemTrayEx)
- {
- this.LogDebug($"System tray initialization failed: {systemTrayEx.Message}, using basic tray...");
- // Fallback: basic system tray initialization
- try
- {
- await this.InitializeBasicSystemTrayAsync();
- this.LogDebug("Fallback system tray initialized");
- }
- catch (Exception fallbackEx)
- {
- this.LogDebug($"Even fallback system tray failed: {fallbackEx.Message}");
- }
- }
-
- this.LogDebug("About to initialize notifications...");
- this.InitializeNotifications();
- this.LogDebug("Notifications initialized successfully");
-
- this.LogDebug("About to initialize monitoring...");
- await this.InitializeMonitoringAsync();
- this.LogDebug("Monitoring initialized successfully");
-
- if (this.skipProcessMonitoringDuringStartup)
- {
- this.LogDebug("Skipping process monitoring manager startup (temporary bypass enabled)");
- }
- else
- {
- this.LogDebug("About to start process monitoring manager...");
- try
- {
- var monitoringTask = this.StartProcessMonitoringManagerAsync();
- var timeoutTask = Task.Delay(8000); // 8 second timeout
- var completedTask = await Task.WhenAny(monitoringTask, timeoutTask);
-
- if (completedTask == timeoutTask)
- {
- this.LogDebug("Process monitoring manager startup timed out after 8 seconds, continuing without monitoring...");
- }
- else
- {
- try
- {
- await monitoringTask; // Ensure we get any exceptions
- this.LogDebug("Process monitoring manager started successfully");
- }
- catch (Exception taskEx)
- {
- this.LogDebug($"Process monitoring manager task failed: {taskEx.Message}");
- }
- }
- }
- catch (Exception monitoringEx)
- {
- this.LogDebug($"Process monitoring manager startup failed: {monitoringEx.Message}, continuing without monitoring...");
- }
- }
-
- this.LogDebug("=== InitializeServicesAsync completed successfully ===");
- }
-
- private async Task InitializeSettingsAsync()
- {
- try
- {
- await this.settingsService.LoadSettingsAsync();
-
- // Apply initial settings
- var settings = this.settingsService.Settings;
- var useDarkTheme = settings.HasUserThemePreference
- ? settings.UseDarkTheme
- : this.themeService.GetSystemUsesDarkTheme();
-
- if (!settings.HasUserThemePreference && settings.UseDarkTheme != useDarkTheme)
- {
- settings.UseDarkTheme = useDarkTheme;
- await this.settingsService.UpdateSettingsAsync(settings);
- }
-
- this.themeService.ApplyTheme(useDarkTheme);
- this.mainWindowViewModel.IsDarkTheme = useDarkTheme;
- DwmHelper.ApplyWindowCaptionTheme(this, useDarkTheme);
-
- if (settings.StartMinimized)
- {
- this.WindowState = WindowState.Minimized;
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to load settings: {ex.Message}");
- }
- }
-
- private async Task InitializeSystemTrayAsync()
- {
- try
- {
- this.systemTrayService.Initialize();
- this.systemTrayService.Show();
-
- // Subscribe to tray events
- this.UnsubscribeSystemTrayEvents();
- this.systemTrayService.ShowMainWindowRequested += this.OnShowMainWindowRequested;
- this.systemTrayService.DashboardRequested += this.OnDashboardRequested;
- this.systemTrayService.ExitRequested += this.OnExitRequested;
- this.systemTrayService.MonitoringToggleRequested += this.OnMonitoringToggleRequested;
- this.systemTrayService.SettingsRequested += this.OnSettingsRequested;
- this.systemTrayService.PowerPlanChangeRequested += this.OnPowerPlanChangeRequested;
- this.systemTrayService.ProfileApplicationRequested += this.OnProfileApplicationRequested;
- this.systemTrayService.PerformanceDashboardRequested += this.OnPerformanceDashboardRequested;
-
- // Update settings and tooltip
- this.systemTrayService.UpdateSettings(this.settingsService.Settings);
- this.systemTrayService.ApplyTheme(this.themeService.IsDarkTheme);
- this.systemTrayService.UpdateTooltip("ThreadPilot - Process & Power Plan Manager");
-
- // Initialize system tray context menu with current data
- await this.UpdateSystemTrayContextMenuAsync();
-
- // Start periodic system tray updates
- this.StartSystemTrayUpdateTimer();
- }
- catch (Exception ex)
- {
- // Log error but don't fail startup
- System.Diagnostics.Debug.WriteLine($"Failed to initialize system tray: {ex.Message}");
- }
- }
-
- private async Task InitializeBasicSystemTrayAsync()
- {
- try
- {
- this.LogDebug("Initializing basic system tray (without full context menu)...");
-
- // Initialize basic tray icon (this is idempotent)
- this.systemTrayService.Initialize();
- this.systemTrayService.Show();
-
- // Subscribe to essential tray events only
- this.UnsubscribeSystemTrayEvents();
- this.systemTrayService.ShowMainWindowRequested += this.OnShowMainWindowRequested;
- this.systemTrayService.DashboardRequested += this.OnDashboardRequested;
- this.systemTrayService.ExitRequested += this.OnExitRequested;
-
- // Update basic settings and tooltip
- this.systemTrayService.UpdateSettings(this.settingsService.Settings);
- this.systemTrayService.ApplyTheme(this.themeService.IsDarkTheme);
- this.systemTrayService.UpdateTooltip("ThreadPilot - Process & Power Plan Manager (Basic Mode)");
-
- this.LogDebug("Basic system tray initialization completed");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to initialize basic system tray: {ex.Message}");
- throw;
- }
- }
-
- private void OnShowMainWindowRequested(object? sender, EventArgs e)
- {
- this.ShowWindowFromTray();
- }
-
- private void OnExitRequested(object? sender, EventArgs e)
- {
- TaskSafety.FireAndForget(this.OnExitRequestedAsync(), ex =>
- {
- this.LogDebug($"OnExitRequested failed: {ex.Message}");
- });
- }
-
- private async Task OnExitRequestedAsync()
- {
- await this.PerformGracefulShutdownAsync();
- }
-
- private void OnDashboardRequested(object? sender, EventArgs e)
- {
- this.ShowWindowFromTray("Process");
- }
-
- ///
- /// Performs graceful shutdown with cleanup of all applied optimizations
- /// Similar to CPU Set Setter's ExitAppGracefully.
- ///
- private async Task PerformGracefulShutdownAsync(bool validateUnsavedChanges = true)
- {
- if (this.isPerformingShutdown)
- {
- return;
- }
-
- if (validateUnsavedChanges && !await this.HandleUnsavedSettingsBeforeExitAsync())
- {
- return;
- }
-
- this.isPerformingShutdown = true;
-
- try
- {
- this.LogDebug("Starting graceful shutdown...");
- this.selfResourceManagementService.RestoreForegroundMode();
-
- // 1. Stop monitoring services
- try
- {
- this.LogDebug("Stopping process monitoring manager...");
- await this.processMonitorManagerService.StopAsync();
- this.LogDebug("Process monitoring manager stopped");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error stopping process monitoring: {ex.Message}");
- }
-
- // 2. Cleanup applied CPU masks (like CPU Set Setter's ClearAllProcessMasksNoSave)
- if (this.settingsService.Settings.ClearMasksOnClose)
- {
- try
- {
- this.LogDebug("Clearing all applied CPU masks...");
- var processService = this.serviceProvider.GetRequiredService();
- await processService.ClearAllAppliedMasksAsync();
- this.LogDebug("CPU masks cleared");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error clearing CPU masks: {ex.Message}");
- }
-
- // Also reset priorities
- try
- {
- this.LogDebug("Resetting all process priorities...");
- var processService = this.serviceProvider.GetRequiredService();
- await processService.ResetAllProcessPrioritiesAsync();
- this.LogDebug("Process priorities reset");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error resetting priorities: {ex.Message}");
- }
- }
-
- // 3. Restore default power plan if configured
- if (this.settingsService.Settings.RestoreDefaultPowerPlanOnExit)
- {
- try
- {
- var targetDefaultPowerPlanGuid = this.settingsService.Settings.DefaultPowerPlanId;
-
- try
- {
- await this.processPowerPlanAssociationService.LoadConfigurationAsync();
- var (associationDefaultPowerPlanGuid, _) = await this.processPowerPlanAssociationService.GetDefaultPowerPlanAsync();
- if (!string.IsNullOrWhiteSpace(associationDefaultPowerPlanGuid))
- {
- targetDefaultPowerPlanGuid = associationDefaultPowerPlanGuid;
- }
- }
- catch (Exception associationEx)
- {
- this.LogDebug($"Could not read default power plan from association config: {associationEx.Message}");
- }
-
- if (string.IsNullOrWhiteSpace(targetDefaultPowerPlanGuid))
- {
- this.LogDebug("No default power plan configured for restore on exit");
- }
- else
- {
- this.LogDebug("Restoring default power plan...");
- var powerPlanService = this.serviceProvider.GetRequiredService();
- await powerPlanService.SetActivePowerPlanByGuidAsync(targetDefaultPowerPlanGuid);
- this.LogDebug("Default power plan restored");
- }
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error restoring power plan: {ex.Message}");
- }
- }
-
- // 4. Save settings
- try
- {
- this.LogDebug("Saving settings...");
- await this.settingsService.SaveSettingsAsync();
- this.LogDebug("Settings saved");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error saving settings: {ex.Message}");
- }
-
- // 5. Dispose tray service
- try
- {
- this.LogDebug("Disposing system tray...");
- this.systemTrayService.Dispose();
- this.LogDebug("System tray disposed");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error disposing tray: {ex.Message}");
- }
-
- this.LogDebug("Graceful shutdown completed");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Error during graceful shutdown: {ex.Message}");
- }
- finally
- {
- // Ensure application exits
- System.Windows.Application.Current.Shutdown();
- }
- }
-
- private async Task HandleUnsavedSettingsBeforeExitAsync()
- {
- if (!this.settingsViewModel.HasPendingChanges)
- {
- return true;
- }
-
- var result = await this.ShowUnsavedSettingsDialogAsync(
- "You have unsaved changes in Settings. Save before exiting, discard the changes, or cancel to return to ThreadPilot.");
-
- if (result == MessageBoxResult.Cancel)
- {
- return false;
- }
-
- if (result == MessageBoxResult.Yes)
- {
- var saved = await this.settingsViewModel.SaveIfDirtyAsync();
- return saved;
- }
-
- await this.settingsViewModel.DiscardPendingChangesAsync();
- return true;
- }
-
- private async Task HandleWindowCloseAsync()
- {
- if (!await this.HandleUnsavedSettingsBeforeExitAsync())
- {
- return;
- }
-
- if (this.settingsService.Settings.CloseToTray)
- {
- this.WindowState = WindowState.Minimized;
- return;
- }
-
- await this.PerformGracefulShutdownAsync(validateUnsavedChanges: false);
- }
-
- private void OnMonitoringToggleRequested(object? sender, MonitoringToggleEventArgs e)
- {
- TaskSafety.FireAndForget(this.OnMonitoringToggleRequestedAsync(e), ex =>
- {
- this.LogDebug($"OnMonitoringToggleRequested failed: {ex.Message}");
- });
- }
-
- private async Task OnMonitoringToggleRequestedAsync(MonitoringToggleEventArgs e)
- {
- try
- {
- if (e.EnableMonitoring)
- {
- await this.processMonitorManagerService.StartAsync();
- await this.notificationService.ShowSuccessNotificationAsync(
- "Automation Monitoring Enabled",
- "Process rule automation and power plan management have been enabled.");
- }
- else
- {
- await this.processMonitorManagerService.StopAsync();
- await this.notificationService.ShowNotificationAsync(
- "Automation Monitoring Disabled",
- "Process rule automation and power plan management have been disabled.",
- Models.NotificationType.Warning);
- }
- }
- catch (Exception ex)
- {
- await this.notificationService.ShowErrorNotificationAsync(
- "Automation Monitoring Error",
- "Failed to toggle automation monitoring.",
- ex);
- }
- }
-
- private void OnSettingsRequested(object? sender, EventArgs e)
- {
- try
- {
- this.ShowWindowFromTray("Settings");
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to open settings: {ex.Message}");
- }
- }
-
- private void OnPowerPlanChangeRequested(object? sender, PowerPlanChangeRequestedEventArgs e)
- {
- TaskSafety.FireAndForget(this.OnPowerPlanChangeRequestedAsync(e), ex =>
- {
- this.LogDebug($"OnPowerPlanChangeRequested failed: {ex.Message}");
- });
- }
-
- private async Task OnPowerPlanChangeRequestedAsync(PowerPlanChangeRequestedEventArgs e)
- {
- try
- {
- var powerPlanService = this.serviceProvider.GetRequiredService();
- var success = await powerPlanService.SetActivePowerPlanByGuidAsync(e.PowerPlanGuid);
-
- if (success)
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot",
- $"Power plan changed to {e.PowerPlanName}", 2000);
- }
- else
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot Error",
- $"Failed to change power plan to {e.PowerPlanName}", 3000);
- }
- }
- catch (Exception ex)
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot Error",
- $"Error changing power plan: {ex.Message}", 3000);
- }
- }
-
- private void OnProfileApplicationRequested(object? sender, ProfileApplicationRequestedEventArgs e)
- {
- TaskSafety.FireAndForget(this.OnProfileApplicationRequestedAsync(e), ex =>
- {
- this.LogDebug($"OnProfileApplicationRequested failed: {ex.Message}");
- });
- }
-
- private async Task OnProfileApplicationRequestedAsync(ProfileApplicationRequestedEventArgs e)
- {
- try
- {
- var processService = this.serviceProvider.GetRequiredService();
- var selectedProcess = this.processViewModel.SelectedProcess;
-
- if (selectedProcess != null)
- {
- var success = await processService.LoadProcessProfile(e.ProfileName, selectedProcess);
-
- if (success)
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot",
- $"Profile '{e.ProfileName}' applied to {selectedProcess.Name}", 2000);
- }
- else
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot Error",
- $"Failed to apply profile '{e.ProfileName}'", 3000);
- }
- }
- else
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot",
- "No process selected for profile application", 2000);
- }
- }
- catch (Exception ex)
- {
- this.systemTrayService.ShowBalloonTip(
- "ThreadPilot Error",
- $"Error applying profile: {ex.Message}", 3000);
- }
- }
-
- private void OnPerformanceDashboardRequested(object? sender, EventArgs e)
- {
- try
- {
- this.ShowWindowFromTray("Performance");
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to open performance dashboard: {ex.Message}");
- }
- }
-
- private async Task InitializeKeyboardShortcutsAsync()
- {
- try
- {
- // Set window handle for global hotkey registration
- var windowInteropHelper = new System.Windows.Interop.WindowInteropHelper(this);
- var handle = windowInteropHelper.EnsureHandle();
-
- if (this.keyboardShortcutService is KeyboardShortcutService service)
- {
- service.SetWindowHandle(handle);
- }
-
- // Subscribe to shortcut activation events
- this.keyboardShortcutService.ShortcutActivated -= this.OnShortcutActivated;
- this.keyboardShortcutService.ShortcutActivated += this.OnShortcutActivated;
-
- // Load shortcuts from settings - with error handling
- try
- {
- await this.keyboardShortcutService.LoadShortcutsFromSettingsAsync();
- }
- catch (Exception settingsEx)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to load shortcuts from settings, using defaults: {settingsEx.Message}");
- // Continue with default shortcuts if settings loading fails
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to initialize keyboard shortcuts: {ex.Message}");
- // Don't let keyboard shortcut initialization failure prevent the app from starting
- }
- }
-
- private void OnShortcutActivated(object? sender, ShortcutActivatedEventArgs e)
- {
- try
- {
- System.Windows.Application.Current.Dispatcher.InvokeAsync(async () =>
- {
- await this.HandleShortcutActionAsync(e.ActionName);
- });
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error handling shortcut {e.ActionName}: {ex.Message}");
- }
- }
-
- private async Task HandleShortcutActionAsync(string actionName)
- {
- switch (actionName)
- {
- case ShortcutActions.ShowMainWindow:
- if (this.IsVisible && this.WindowState != WindowState.Minimized)
- {
- this.ShowInTaskbar = false;
- this.Hide();
- this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
- }
- else
- {
- this.ShowWindowFromTray();
- }
- break;
-
- case ShortcutActions.ToggleMonitoring:
- // Toggle monitoring - implementation can be added later
- await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "Toggle monitoring shortcut activated");
- break;
-
- case ShortcutActions.PowerPlanHighPerformance:
- // Switch to high performance power plan - implementation can be added later
- await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "High Performance power plan shortcut activated");
- break;
-
- case ShortcutActions.OpenTweaks:
- this.ShowWindowFromTray("Tweaks");
- break;
-
- case ShortcutActions.OpenSettings:
- this.ShowWindowFromTray("Settings");
- break;
-
- case ShortcutActions.RefreshProcessList:
- // Refresh process list - implementation can be added later
- await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "Refresh process list shortcut activated");
- break;
-
- case ShortcutActions.ExitApplication:
- this.Close();
- break;
- }
- }
-
- private async Task UpdateSystemTrayContextMenuAsync()
- {
- try
- {
- await this.systemTrayStatusUpdater.UpdateContextMenuAsync(this.systemTrayService);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to update system tray context menu: {ex.Message}");
- }
- }
-
- private void StartSystemTrayUpdateTimer()
- {
- try
- {
- this.systemTrayUpdateTimer?.Stop();
- this.systemTrayUpdateTimer?.Dispose();
- this.systemTrayUpdateTimer = null;
-
- if (!this.systemTrayStatusUpdater.ShouldRunPerformanceStatusUpdates)
- {
- return;
- }
-
- this.systemTrayUpdateFailureStreak = 0;
- this.systemTrayUpdateTimer = new System.Timers.Timer(SystemTrayUpdateBaseIntervalMs);
- this.systemTrayUpdateTimer.Elapsed += async (s, e) =>
- {
- if (this.isSystemTrayUpdatesSuspended)
- {
- return;
- }
-
- if (Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 1) == 1)
- {
- return;
- }
-
- try
- {
- var updateSucceeded = await this.UpdateSystemTrayStatusAsync();
- this.ApplySystemTrayUpdateBackoff(updateSucceeded);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error in system tray update timer: {ex.Message}");
- this.ApplySystemTrayUpdateBackoff(updateSucceeded: false);
- }
- finally
- {
- Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
- }
- };
- this.systemTrayUpdateTimer.AutoReset = true;
-
- if (!this.isSystemTrayUpdatesSuspended &&
- this.IsVisible &&
- this.WindowState != WindowState.Minimized)
- {
- this.systemTrayUpdateTimer.Start();
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to start system tray update timer: {ex.Message}");
- }
- }
-
- private void ApplySystemTrayUpdateBackoff(bool updateSucceeded)
- {
- if (this.systemTrayUpdateTimer == null)
- {
- return;
- }
-
- if (updateSucceeded)
- {
- this.systemTrayUpdateFailureStreak = 0;
- if (Math.Abs(this.systemTrayUpdateTimer.Interval - SystemTrayUpdateBaseIntervalMs) > 1)
- {
- this.systemTrayUpdateTimer.Interval = SystemTrayUpdateBaseIntervalMs;
- }
-
- return;
- }
-
- this.systemTrayUpdateFailureStreak = Math.Min(4, this.systemTrayUpdateFailureStreak + 1);
- var exponentialDelay = SystemTrayUpdateBaseIntervalMs * Math.Pow(2, this.systemTrayUpdateFailureStreak);
- var nextIntervalMs = Math.Min(SystemTrayUpdateMaxIntervalMs, exponentialDelay);
-
- if (Math.Abs(this.systemTrayUpdateTimer.Interval - nextIntervalMs) > 1)
- {
- this.systemTrayUpdateTimer.Interval = nextIntervalMs;
- }
- }
-
- private async Task UpdateSystemTrayStatusAsync()
- {
- try
- {
- return await this.systemTrayStatusUpdater.UpdateStatusAsync(
- this.systemTrayService,
- action => this.Dispatcher.InvokeAsync(action).Task);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to update system tray status: {ex.Message}");
- return false;
- }
- }
-
- private void InitializeNotifications()
- {
- try
- {
- // Subscribe to settings changes to update notification service
- this.settingsService.SettingsChanged += this.OnSettingsChanged;
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to initialize notifications: {ex.Message}");
- }
- }
-
- private async Task InitializeMonitoringAsync()
- {
- try
- {
- // Subscribe to monitoring status changes
- this.processMonitorService.MonitoringStatusChanged += this.OnMonitoringStatusChanged;
-
- // Update tray with initial monitoring status
- this.systemTrayService.UpdateMonitoringStatus(
- this.processMonitorService.IsMonitoring,
- this.processMonitorService.IsWmiAvailable);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to initialize monitoring: {ex.Message}");
- }
- }
-
- private async Task StartProcessMonitoringManagerAsync()
- {
- try
- {
- this.LogDebug("Subscribing to process monitor manager events...");
- // Subscribe to process monitor manager events
- this.processMonitorManagerService.ServiceStatusChanged += this.OnProcessMonitorManagerStatusChanged;
-
- this.LogDebug("Starting process monitoring manager service...");
- // Start the process monitoring manager service with internal timeout
- var startTask = this.processMonitorManagerService.StartAsync();
- var timeoutTask = Task.Delay(6000); // 6 second internal timeout
- var completedTask = await Task.WhenAny(startTask, timeoutTask);
-
- if (completedTask == timeoutTask)
- {
- this.LogDebug("ProcessMonitorManagerService.StartAsync() timed out internally");
- throw new TimeoutException("Process monitoring manager service startup timed out");
- }
-
- await startTask; // Get any exceptions
- this.LogDebug("Process monitoring manager service started, showing notification...");
-
- if (!this.isSilentStartupMode)
- {
- await this.notificationService.ShowSuccessNotificationAsync(
- "ThreadPilot Started",
- "Process monitoring and power plan management is now active");
- }
-
- this.LogDebug(this.isSilentStartupMode
- ? "Startup success notification skipped for silent startup"
- : "Success notification shown");
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to start process monitoring manager: {ex.Message}");
- try
- {
- await this.notificationService.ShowErrorNotificationAsync(
- "Startup Error",
- "Failed to start process monitoring manager",
- ex);
- }
- catch (Exception notificationEx)
- {
- this.LogDebug($"Failed to show error notification: {notificationEx.Message}");
- }
- throw; // Re-throw to be caught by outer handler
- }
- }
-
- private void OnSettingsChanged(object? sender, ApplicationSettingsChangedEventArgs e)
- {
- // Update tray service with new settings
- this.systemTrayService.UpdateSettings(e.NewSettings);
-
- var useDarkTheme = e.NewSettings.HasUserThemePreference
- ? e.NewSettings.UseDarkTheme
- : this.themeService.GetSystemUsesDarkTheme();
-
- this.themeService.ApplyTheme(useDarkTheme);
- this.mainWindowViewModel.IsDarkTheme = useDarkTheme;
- this.systemTrayService.ApplyTheme(useDarkTheme);
- DwmHelper.ApplyWindowCaptionTheme(this, useDarkTheme);
- this.ApplySelfResourcePolicy(this.lastAppliedRefreshState ?? this.GetForegroundActivityState(), e.NewSettings);
- }
-
- private void OnMonitoringStatusChanged(object? sender, MonitoringStatusEventArgs e)
- {
- // Update tray icon and status
- this.systemTrayService.UpdateMonitoringStatus(e.IsMonitoring, e.IsWmiAvailable);
-
- // Show notification if there's an error
- if (e.Error != null && this.settingsService.Settings.EnableErrorNotifications)
- {
- this.notificationService.ShowErrorNotificationAsync(
- "Automation Monitoring Error",
- e.StatusMessage ?? "An error occurred with automation monitoring.",
- e.Error);
- }
- }
-
- private void OnProcessMonitorManagerStatusChanged(object? sender, ServiceStatusEventArgs e)
- {
- // Update main window status
- this.mainWindowViewModel.UpdateProcessMonitoringStatus(e.IsRunning, e.Status);
-
- // Show notification for critical status changes
- if (!e.IsRunning && e.Error != null && this.settingsService.Settings.EnableErrorNotifications)
- {
- this.notificationService.ShowErrorNotificationAsync(
- "Automation Monitoring Error",
- e.Details ?? "Automation monitoring encountered an error.",
- e.Error);
- }
- }
-
- protected override void OnStateChanged(EventArgs e)
- {
- try
- {
- if (this.WindowState == WindowState.Minimized)
- {
- var activityState = AppActivityState.Minimized;
- if (this.settingsService.Settings.MinimizeToTray)
- {
- this.ShowInTaskbar = false;
- this.Hide();
- this.systemTrayService.Show();
- activityState = AppActivityState.TrayHidden;
- }
-
- this.ApplyAppRefreshPolicy(activityState);
- }
- else if (this.WindowState == WindowState.Normal || this.WindowState == WindowState.Maximized)
- {
- this.ShowInTaskbar = true;
- this.EnsureDashboardVisibleOnScreen();
-
- this.ApplyAppRefreshPolicy(this.GetForegroundActivityState());
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error handling window state change: {ex.Message}");
- }
-
- base.OnStateChanged(e);
- }
-
- private void SuspendHiddenModeRefreshes()
- {
- this.isSystemTrayUpdatesSuspended = true;
- this.systemTrayUpdateTimer?.Stop();
- Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
- this.powerPlanViewModel.PauseAutoRefresh();
- }
-
- private void ResumeForegroundRefreshes()
- {
- this.isSystemTrayUpdatesSuspended = false;
- this.systemTrayUpdateFailureStreak = 0;
- this.systemTrayUpdateTimer?.Stop();
-
- if (!this.systemTrayStatusUpdater.ShouldRunPerformanceStatusUpdates)
- {
- return;
- }
-
- if (this.systemTrayUpdateTimer != null)
- {
- this.systemTrayUpdateTimer.Interval = SystemTrayUpdateBaseIntervalMs;
- }
- this.systemTrayUpdateTimer?.Start();
-
- _ = this.Dispatcher.InvokeAsync(async () =>
- {
- try
- {
- var updateSucceeded = await this.UpdateSystemTrayStatusAsync();
- this.ApplySystemTrayUpdateBackoff(updateSucceeded);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to refresh tray status after resume: {ex.Message}");
- }
- });
- }
-
- private AppActivityState GetForegroundActivityState()
- {
- if (this.ProcessManagementTab.Visibility == Visibility.Visible)
- {
- return AppActivityState.ForegroundProcessView;
- }
-
- return this.PerformanceViewControl.Visibility == Visibility.Visible
- ? AppActivityState.ForegroundDiagnosticsView
- : AppActivityState.ForegroundOtherTab;
- }
-
- private void ApplyAppRefreshPolicy(AppActivityState state)
- {
- if (!AppRefreshPolicy.ShouldApplyTransition(this.lastAppliedRefreshState, state))
- {
- return;
- }
-
- this.lastAppliedRefreshState = state;
-
- var decision = AppRefreshPolicy.Evaluate(state);
- var isHiddenState = state is AppActivityState.Minimized or AppActivityState.TrayHidden;
- var isProcessViewActive = state == AppActivityState.ForegroundProcessView;
-
- if (isHiddenState)
- {
- this.isSystemTrayUpdatesSuspended = true;
- this.systemTrayUpdateTimer?.Stop();
- Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
- }
- else
- {
- this.ResumeForegroundRefreshes();
- }
-
- this.processViewModel.SetProcessViewActive(isProcessViewActive);
- this.processViewModel.ApplyRefreshDecision(decision);
-
- if (decision.PowerPlanUiRefreshEnabled)
- {
- this.powerPlanViewModel.ResumeAutoRefresh(refreshImmediately: state != AppActivityState.ForegroundOtherTab);
- }
- else
- {
- this.powerPlanViewModel.PauseAutoRefresh();
- }
-
- if (decision.PerformanceUiMonitoringEnabled)
- {
- _ = this.GetPerformanceViewModel().ActivateDiagnosticsAsync();
- }
- else if (this.performanceViewModel != null)
- {
- _ = this.performanceViewModel.SuspendBackgroundMonitoringAsync();
- }
-
- this.ApplySelfResourcePolicy(state);
- }
-
- private void ApplySelfResourcePolicy(AppActivityState state, ApplicationSettingsModel? settings = null)
- {
- var currentSettings = settings ?? this.settingsService.Settings;
- var isHiddenState = state is AppActivityState.Minimized or AppActivityState.TrayHidden;
-
- if (SelfResourcePolicy.ShouldApplyLowImpactMode(isHiddenState, currentSettings.EnableSelfLowImpactMode))
- {
- this.selfResourceManagementService.ApplyLowImpactMode(SelfResourcePolicy.ShouldLimitAffinity(
- isHiddenState,
- currentSettings.EnableSelfLowImpactMode,
- currentSettings.EnableSelfAffinityLimit));
- return;
- }
-
- this.selfResourceManagementService.RestoreForegroundMode();
- }
-
- protected override void OnSourceInitialized(EventArgs e)
- {
- base.OnSourceInitialized(e);
- try
- {
- DwmHelper.ApplyWindowCaptionTheme(this, this.themeService.IsDarkTheme);
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Failed to apply window caption theme: {ex.Message}");
- }
-
- this.EnsureDashboardVisibleOnScreen();
- }
-
- [System.Diagnostics.Conditional("DEBUG")]
- private void LogDebug(string message)
- {
- try
- {
- var timestampedMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [MainWindow] {message}";
- System.Diagnostics.Debug.WriteLine(timestampedMessage);
- File.AppendAllText(this.debugLogPath, timestampedMessage + Environment.NewLine);
- }
- catch
- {
- // Swallow logging failures to avoid impacting runtime behavior
- }
- }
-
- private void OnWindowLoaded(object? sender, RoutedEventArgs e)
- {
- TaskSafety.FireAndForget(this.OnWindowLoadedAsync(), ex =>
- {
- this.LogDebug($"OnWindowLoaded failed: {ex.Message}");
- });
- }
-
- private async Task OnWindowLoadedAsync()
- {
- this.Loaded -= this.OnWindowLoaded;
- this.EnsureDashboardVisibleOnScreen();
- await this.InitializeKeyboardShortcutsAsync();
- }
-
- private void OnOpenRulesRequested(object? sender, EventArgs e)
- {
- this.SelectMainTab("Rules");
- }
-
- private void ShowWindowFromTray(string? tabTag = null)
- {
- this.ShowInTaskbar = true;
- this.EnsureDashboardVisibleOnScreen();
-
- if (!this.IsVisible)
- {
- this.Show();
- }
- else
- {
- this.Visibility = Visibility.Visible;
- }
-
- if (this.WindowState == WindowState.Minimized)
- {
- this.WindowState = WindowState.Normal;
- }
-
- this.EnsureDashboardVisibleOnScreen();
- this.ShowInTaskbar = true;
-
- if (tabTag != null)
- {
- this.SelectMainTab(tabTag);
- }
-
- // Force foreground restoration when invoked from tray context menu.
- this.Topmost = true;
- this.Activate();
- this.Focus();
- this.Topmost = false;
- this.Activate();
- this.Focus();
-
- var processViewWillBeActive = tabTag == null
- ? this.ProcessManagementTab.Visibility == Visibility.Visible
- : string.Equals(tabTag, "Process", StringComparison.Ordinal);
-
- this.ApplyAppRefreshPolicy(processViewWillBeActive
- ? AppActivityState.ForegroundProcessView
- : AppActivityState.ForegroundOtherTab);
- }
-
- internal bool EnsureDashboardVisibleOnScreen()
- {
- return WindowPlacementHelper.TryCorrectWindowPlacement(this);
- }
-
- private void SelectMainTab(string tag)
- {
- if (string.IsNullOrEmpty(tag))
- {
- return;
- }
-
- if (string.Equals(tag, "Performance", StringComparison.Ordinal))
- {
- this.GetPerformanceViewModel();
- }
-
- this.ApplySectionVisibility(tag);
-
- if (string.Equals(tag, "Performance", StringComparison.Ordinal))
- {
- this.TryShowPerformanceIntro();
- }
- }
-
- private void TryShowPerformanceIntro()
- {
- if (this.isPerformanceIntroVisible || !this.isInitializationComplete)
- {
- return;
- }
-
- try
- {
- var settings = this.settingsService.Settings;
- if (settings.HasSeenPerformanceIntro)
- {
- return;
- }
-
- this.isPerformanceIntroVisible = true;
- this.PerformanceIntroOverlay.Visibility = Visibility.Visible;
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to show Performance intro overlay: {ex.Message}");
- }
- }
-
- private void TryShowStartupMinimizedSuggestion()
- {
- if (!this.showStartupMinimizedSuggestionOnReady
- || this.isSilentStartupMode
- || !this.isInitializationComplete
- || this.isElevationWarningVisible)
- {
- return;
- }
-
- try
- {
- if (!StartupMinimizedSuggestionPolicy.ShouldShow(
- this.settingsService.Settings,
- StartupWindowBehavior.Resolve(isAutostart: false, startMinimized: false)))
- {
- return;
- }
-
- this.StartupMinimizedSuggestionOverlay.Visibility = Visibility.Visible;
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to show startup minimized suggestion: {ex.Message}");
- }
- }
-
- private async Task PersistStartupMinimizedSuggestionSeenAsync()
- {
- try
- {
- var settings = this.settingsService.Settings;
- if (settings.HasSeenStartupMinimizedSuggestion)
- {
- return;
- }
-
- settings.HasSeenStartupMinimizedSuggestion = true;
- await this.settingsService.UpdateSettingsAsync(settings);
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to persist startup minimized suggestion state: {ex.Message}");
- }
- }
-
- private void HideStartupMinimizedSuggestion()
- {
- this.showStartupMinimizedSuggestionOnReady = false;
- this.StartupMinimizedSuggestionOverlay.Visibility = Visibility.Collapsed;
- }
-
- private async void StartupSuggestionOpenSettings_Click(object sender, RoutedEventArgs e)
- {
- await this.PersistStartupMinimizedSuggestionSeenAsync();
- this.HideStartupMinimizedSuggestion();
- this.SelectMainTab("Settings");
- }
-
- private async void StartupSuggestionDontShowAgain_Click(object sender, RoutedEventArgs e)
- {
- await this.PersistStartupMinimizedSuggestionSeenAsync();
- this.HideStartupMinimizedSuggestion();
- }
-
- private void HidePerformanceIntro()
- {
- this.isPerformanceIntroVisible = false;
- this.PerformanceIntroOverlay.Visibility = Visibility.Collapsed;
- }
-
- private Task ShowUnsavedSettingsDialogAsync(string message)
- {
- if (!this.Dispatcher.CheckAccess())
- {
- return this.Dispatcher.InvokeAsync(() => this.ShowUnsavedSettingsDialogAsync(message)).Task.Unwrap();
- }
-
- if (this.unsavedSettingsDialogCompletionSource != null)
- {
- return Task.FromResult(MessageBoxResult.Cancel);
- }
-
- this.UnsavedSettingsDialogMessage.Text = message;
- this.unsavedSettingsDialogCompletionSource = new TaskCompletionSource(
- TaskCreationOptions.RunContinuationsAsynchronously);
- this.UnsavedSettingsOverlay.Visibility = Visibility.Visible;
- return this.unsavedSettingsDialogCompletionSource.Task;
- }
-
- private void CompleteUnsavedSettingsDialog(MessageBoxResult result)
- {
- var completionSource = this.unsavedSettingsDialogCompletionSource;
- if (completionSource == null)
- {
- return;
- }
-
- this.unsavedSettingsDialogCompletionSource = null;
- this.UnsavedSettingsOverlay.Visibility = Visibility.Collapsed;
- completionSource.TrySetResult(result);
- }
-
- private void UnsavedSettingsSave_Click(object sender, RoutedEventArgs e)
- {
- this.CompleteUnsavedSettingsDialog(MessageBoxResult.Yes);
- }
-
- private void UnsavedSettingsDiscard_Click(object sender, RoutedEventArgs e)
- {
- this.CompleteUnsavedSettingsDialog(MessageBoxResult.No);
- }
-
- private void UnsavedSettingsCancel_Click(object sender, RoutedEventArgs e)
- {
- this.CompleteUnsavedSettingsDialog(MessageBoxResult.Cancel);
- }
-
- private async void PerformanceIntroContinue_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- var settings = this.settingsService.Settings;
- if (!settings.HasSeenPerformanceIntro)
- {
- settings.HasSeenPerformanceIntro = true;
- await this.settingsService.UpdateSettingsAsync(settings);
- }
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to persist Performance intro state: {ex.Message}");
- }
- finally
- {
- this.HidePerformanceIntro();
- }
- }
-
- // Elevation Warning Modal Management
- private bool isElevationWarningVisible = false;
- private double previousElevationAppContentOpacity = 1;
- private double previousElevationBackdropBlurRadius = 0;
-
- private void TryShowElevationWarning()
- {
- if (this.isElevationWarningVisible || !this.isInitializationComplete)
- {
- return;
- }
-
- try
- {
- var settings = this.settingsService.Settings;
-
- // Only show if user is not admin AND hasn't dismissed the warning yet
- if (this.elevationService?.IsRunningAsAdministrator() == true || settings.HasSeenElevationWarning)
- {
- return;
- }
-
- this.isElevationWarningVisible = true;
- var elevationOverlay = this.FindName("ElevationWarningOverlay") as Grid;
- if (elevationOverlay != null)
- {
- elevationOverlay.Visibility = Visibility.Visible;
- }
-
- // Apply blur and disable interaction
- this.previousElevationAppContentOpacity = this.UIContent.Opacity;
- this.UIContent.IsHitTestVisible = false;
- this.UIContent.Opacity = 0.74;
-
- var elevationBlur = this.FindName("ElevationWarningBlur") as BlurEffect;
- if (elevationBlur != null)
- {
- this.previousElevationBackdropBlurRadius = elevationBlur.Radius;
- elevationBlur.Radius = 16;
- }
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to show elevation warning overlay: {ex.Message}");
- }
- }
-
- private void HideElevationWarning()
- {
- this.isElevationWarningVisible = false;
- var elevationOverlay = this.FindName("ElevationWarningOverlay") as Grid;
- if (elevationOverlay != null)
- {
- elevationOverlay.Visibility = Visibility.Collapsed;
- }
-
- // Restore interaction and remove blur
- this.UIContent.IsHitTestVisible = true;
- this.UIContent.Opacity = this.previousElevationAppContentOpacity;
-
- var elevationBlur = this.FindName("ElevationWarningBlur") as BlurEffect;
- if (elevationBlur != null)
- {
- elevationBlur.Radius = this.previousElevationBackdropBlurRadius;
- }
-
- this.TryShowStartupMinimizedSuggestion();
- }
-
- private void ElevationWarningDismiss_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- var settings = this.settingsService.Settings;
- if (!settings.HasSeenElevationWarning)
- {
- settings.HasSeenElevationWarning = true;
- _ = this.settingsService.UpdateSettingsAsync(settings);
- }
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to persist elevation warning dismiss state: {ex.Message}");
- }
- finally
- {
- this.HideElevationWarning();
- }
- }
-
- private async void ElevationWarningRequestElevation_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- if (this.elevationService != null)
- {
- var success = await this.elevationService.RequestElevationIfNeeded();
- if (success)
- {
- System.Diagnostics.Debug.WriteLine("Elevation requested successfully from warning dialog");
- }
- }
- }
- catch (Exception ex)
- {
- this.LogDebug($"Failed to request elevation from warning dialog: {ex.Message}");
- }
- finally
- {
- // Hide the warning after attempting elevation (regardless of success)
- this.HideElevationWarning();
- }
- }
-
- private void ApplySectionVisibility(string tag)
- {
- this.ProcessManagementTab.Visibility = tag == "Process" ? Visibility.Visible : Visibility.Collapsed;
- this.CoreMasksTab.Visibility = tag == "Masks" ? Visibility.Visible : Visibility.Collapsed;
- this.PowerPlanViewControl.Visibility = tag == "Power" ? Visibility.Visible : Visibility.Collapsed;
- this.AssociationView.Visibility = tag == "Rules" ? Visibility.Visible : Visibility.Collapsed;
- this.PerformanceViewControl.Visibility = tag == "Performance" ? Visibility.Visible : Visibility.Collapsed;
- this.LogViewerViewControl.Visibility = tag == "Logs" ? Visibility.Visible : Visibility.Collapsed;
- this.SystemTweaksView.Visibility = tag == "Tweaks" ? Visibility.Visible : Visibility.Collapsed;
- this.SettingsView.Visibility = tag == "Settings" ? Visibility.Visible : Visibility.Collapsed;
-
- this.NavProcess.IsActive = tag == "Process";
- this.NavMasks.IsActive = tag == "Masks";
- this.NavPower.IsActive = tag == "Power";
- this.NavRules.IsActive = tag == "Rules";
- this.NavPerf.IsActive = tag == "Performance";
- this.NavLogs.IsActive = tag == "Logs";
- this.NavTweaks.IsActive = tag == "Tweaks";
- this.NavSettings.IsActive = tag == "Settings";
-
- if (!this.IsVisible)
- {
- this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
- return;
- }
-
- if (this.WindowState == WindowState.Minimized)
- {
- this.ApplyAppRefreshPolicy(AppActivityState.Minimized);
- return;
- }
-
- var activityState = tag switch
- {
- "Process" => AppActivityState.ForegroundProcessView,
- "Performance" => AppActivityState.ForegroundDiagnosticsView,
- _ => AppActivityState.ForegroundOtherTab,
- };
-
- this.ApplyAppRefreshPolicy(activityState);
- }
-
- private void NavMenuItem_Click(object sender, RoutedEventArgs e)
- {
- TaskSafety.FireAndForget(this.NavMenuItem_ClickAsync(sender, e), ex =>
- {
- this.LogDebug($"NavMenuItem_Click failed: {ex.Message}");
- });
- }
-
- private async Task NavMenuItem_ClickAsync(object sender, RoutedEventArgs e)
- {
- if (!await this.navigationBehavior.TryEnterAsync())
- {
- return;
- }
-
- try
- {
- var invokedItem = sender as Wpf.Ui.Controls.NavigationViewItem;
- if (invokedItem == null)
- {
- return;
- }
-
- var tag = invokedItem.Tag?.ToString();
- if (string.IsNullOrEmpty(tag))
- {
- return;
- }
-
- if (!this.IsLoaded)
- {
- return;
- }
-
- var canNavigate = await NavigationBehavior.EnsureCanNavigateAsync(
- tag,
- this.settingsViewModel,
- () => this.ShowUnsavedSettingsDialogAsync(
- "You have unsaved changes in Settings. Save before switching tabs, discard the changes, or cancel to stay on Settings."));
- if (!canNavigate)
- {
- return;
- }
-
- if (string.Equals(tag, "Performance", StringComparison.Ordinal))
- {
- this.GetPerformanceViewModel();
- }
-
- this.ApplySectionVisibility(tag);
-
- if (string.Equals(tag, "Performance", StringComparison.Ordinal))
- {
- this.TryShowPerformanceIntro();
- }
- }
- finally
- {
- this.navigationBehavior.Exit();
- }
- }
-
- protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
- {
- if (this.isPerformingShutdown)
- {
- base.OnClosing(e);
- return;
- }
-
- if (this.isPerformanceIntroVisible)
- {
- e.Cancel = true;
- System.Windows.MessageBox.Show(
- "Please complete the Performance introduction before closing the application.\n\nClick 'Continue to Performance' to proceed.",
- "Performance Introduction Required",
- MessageBoxButton.OK,
- MessageBoxImage.Information);
- return;
- }
-
- e.Cancel = true;
- _ = this.HandleWindowCloseAsync();
- }
-
- protected override void OnClosed(EventArgs e)
- {
- try
- {
- this.Loaded -= this.OnWindowLoaded;
- this.processViewModel.OpenRulesRequested -= this.OnOpenRulesRequested;
-
- this.settingsService.SettingsChanged -= this.OnSettingsChanged;
- this.processMonitorService.MonitoringStatusChanged -= this.OnMonitoringStatusChanged;
- this.processMonitorManagerService.ServiceStatusChanged -= this.OnProcessMonitorManagerStatusChanged;
- this.keyboardShortcutService.ShortcutActivated -= this.OnShortcutActivated;
-
- this.UnsubscribeSystemTrayEvents();
-
- this.systemTrayUpdateTimer?.Stop();
- this.systemTrayUpdateTimer?.Dispose();
-
- this.initializationTimeoutTimer?.Stop();
- this.initializationTimeoutTimer?.Dispose();
- this.performanceViewModel?.Dispose();
-
- this.selfResourceManagementService.RestoreForegroundMode();
- this.navigationBehavior.Dispose();
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error disposing timers: {ex.Message}");
- }
-
- base.OnClosed(e);
- }
-
- private void UnsubscribeSystemTrayEvents()
- {
- this.systemTrayService.ShowMainWindowRequested -= this.OnShowMainWindowRequested;
- this.systemTrayService.DashboardRequested -= this.OnDashboardRequested;
- this.systemTrayService.ExitRequested -= this.OnExitRequested;
- this.systemTrayService.MonitoringToggleRequested -= this.OnMonitoringToggleRequested;
- this.systemTrayService.SettingsRequested -= this.OnSettingsRequested;
- this.systemTrayService.PowerPlanChangeRequested -= this.OnPowerPlanChangeRequested;
- this.systemTrayService.ProfileApplicationRequested -= this.OnProfileApplicationRequested;
- this.systemTrayService.PerformanceDashboardRequested -= this.OnPerformanceDashboardRequested;
- }
- }
-}
+namespace ThreadPilot
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Timers;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Input;
+ using System.Windows.Media.Animation;
+ using System.Windows.Media.Effects;
+ using System.Windows.Media.Imaging;
+ using Microsoft.Extensions.DependencyInjection;
+ using ThreadPilot.Helpers;
+ using ThreadPilot.Models;
+ using ThreadPilot.Services;
+ using ThreadPilot.ViewModels;
+ using ThreadPilot.Views;
+
+ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
+ {
+ private void SetDataContexts()
+ {
+ // Set DataContext for the main window
+ this.DataContext = this.mainWindowViewModel;
+
+ // Set DataContext for the power plans view
+ this.PowerPlanViewControl.DataContext = this.powerPlanViewModel;
+
+ // Set DataContext for the association view
+ this.AssociationView.DataContext = this.associationViewModel;
+
+ // Set DataContext for the log viewer view
+ this.LogViewerViewControl.DataContext = this.logViewerViewModel;
+
+ // Set DataContext for the system tweaks view
+ this.SystemTweaksView.DataContext = this.systemTweaksViewModel;
+
+ // Set DataContext for the settings view
+ this.SettingsView.DataContext = this.settingsViewModel;
+ }
+
+ private void InitializeLoadingOverlay()
+ {
+ try
+ {
+ var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
+
+ // Ensure overlay is visible while initialization runs
+ if (loadingOverlay != null)
+ {
+ loadingOverlay.Visibility = this.isSilentStartupMode ? Visibility.Collapsed : Visibility.Visible;
+ loadingOverlay.Opacity = this.isSilentStartupMode ? 0 : 1;
+ }
+
+ if (!this.isSilentStartupMode)
+ {
+ this.ApplyUIContentBlur(15);
+ }
+
+ // Start spinner animation if available
+ var spinnerAnimation = this.FindResource("SpinnerAnimation") as Storyboard;
+ if (!this.isSilentStartupMode)
+ {
+ spinnerAnimation?.Begin();
+ }
+
+ // Set a timeout guard for initialization
+ this.initializationTimeoutTimer = new System.Timers.Timer(15000)
+ {
+ AutoReset = false,
+ };
+ this.initializationTimeoutTimer.Elapsed += this.OnInitializationTimeout;
+ this.initializationTimeoutTimer.Start();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to initialize loading overlay: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeApplicationAsync()
+ {
+ try
+ {
+ this.LogDebug("=== Starting InitializeApplicationAsync ===");
+
+ await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Loading view models...", "Loading process, power plan and rules data."));
+ this.LogDebug("About to call LoadViewModelsAsync...");
+ await this.LoadViewModelsAsync();
+ this.LogDebug("LoadViewModelsAsync completed successfully");
+ this.CompleteInitializationTask("ViewModels");
+
+ this.LogDebug("About to initialize MainWindowViewModel...");
+ await this.mainWindowViewModel.InitializeAsync();
+ this.LogDebug("MainWindowViewModel initialized successfully");
+ this.CompleteInitializationTask("MainWindowViewModel");
+
+ await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Initializing services...", "Starting monitoring, tray and notification services."));
+ this.LogDebug("About to call InitializeServicesAsync...");
+ await this.InitializeServicesAsync();
+ this.LogDebug("InitializeServicesAsync completed successfully");
+ this.CompleteInitializationTask("Services");
+ this.QueueStartupUpdateCheck();
+
+ await this.Dispatcher.InvokeAsync(() => this.UpdateLoadingStatus("Finalizing startup...", "Applying final UI state and startup checks."));
+ this.LogDebug("Finalizing startup...");
+ await Task.Delay(500); // Brief delay to show final status
+ this.CompleteInitializationTask("Finalization");
+
+ // All initialization complete
+ this.LogDebug("All initialization complete, hiding overlay...");
+ await this.Dispatcher.InvokeAsync(() => this.HideLoadingOverlay());
+ this.LogDebug("=== InitializeApplicationAsync completed successfully ===");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"=== ERROR in InitializeApplicationAsync: {ex} ===");
+ await this.Dispatcher.InvokeAsync(() => this.ShowInitializationError(ex));
+ }
+ }
+
+ private void QueueStartupUpdateCheck()
+ {
+ if (Interlocked.Exchange(ref this.startupUpdateCheckStarted, 1) != 0)
+ {
+ return;
+ }
+
+ TaskSafety.FireAndForget(this.CheckForUpdatesAtStartupAsync(), ex =>
+ {
+ this.LogDebug($"Startup update check failed: {ex.Message}");
+ });
+ }
+
+ private async Task CheckForUpdatesAtStartupAsync()
+ {
+ try
+ {
+ this.LogDebug("Startup update check started");
+ var updateService = this.serviceProvider.GetRequiredService();
+ var result = await updateService.CheckForUpdatesAsync(new UpdateCheckRequest(UpdateCheckTrigger.Startup));
+
+ if (result.Status == UpdateCheckStatus.Skipped)
+ {
+ this.LogDebug($"Startup update check skipped: {result.Message}");
+ return;
+ }
+
+ if (!result.IsUpdateAvailable || result.Release == null)
+ {
+ this.LogDebug($"Startup update check complete: {result.Message}");
+ return;
+ }
+
+ await this.notificationService.ShowNotificationAsync(
+ "Update available",
+ $"ThreadPilot {result.Release.Version} is available. Open Settings to download and install it.",
+ NotificationType.Information);
+ this.LogDebug($"Startup update check found update: installed {result.CurrentVersion}, latest {result.Release.Version}");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Startup update check ignored failure: {ex.Message}");
+ }
+ }
+
+ private static Version GetCurrentApplicationVersion()
+ {
+ var rawVersion = typeof(App).Assembly
+ .GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
+ .OfType()
+ .FirstOrDefault()?
+ .InformationalVersion
+ ?? typeof(App).Assembly.GetName().Version?.ToString()
+ ?? "0.0.0";
+
+ var sanitized = rawVersion.Trim();
+ if (sanitized.StartsWith("v", StringComparison.OrdinalIgnoreCase))
+ {
+ sanitized = sanitized[1..];
+ }
+
+ sanitized = sanitized.Split('-', '+')[0];
+
+ return Version.TryParse(sanitized, out var version)
+ ? version
+ : new Version(0, 0, 0);
+ }
+
+ private void UpdateLoadingStatus(string stage, string details = "")
+ {
+ if (this.mainWindowViewModel != null)
+ {
+ this.mainWindowViewModel.InitializationStage = stage;
+ this.mainWindowViewModel.InitializationDetails = details;
+ }
+ }
+
+ private void CompleteInitializationTask(string taskName)
+ {
+ lock (this.initializationLock)
+ {
+ this.initializationTasks.Add(taskName);
+ System.Diagnostics.Debug.WriteLine($"Initialization task completed: {taskName}");
+ }
+ }
+
+ private void HideLoadingOverlay()
+ {
+ try
+ {
+ System.Diagnostics.Debug.WriteLine("=== Starting HideLoadingOverlay ===");
+ this.isInitializationComplete = true;
+ this.initializationTimeoutTimer?.Stop();
+ this.initializationTimeoutTimer?.Dispose();
+
+ if (this.isSilentStartupMode)
+ {
+ var silentLoadingOverlay = this.FindName("LoadingOverlay") as Grid;
+ if (silentLoadingOverlay != null)
+ {
+ silentLoadingOverlay.Visibility = Visibility.Collapsed;
+ silentLoadingOverlay.Opacity = 0;
+ }
+
+ this.ClearUIContentBlur();
+ this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
+ return;
+ }
+
+ // Stop spinner animation
+ var spinnerAnimation = this.FindResource("SpinnerAnimation") as Storyboard;
+ spinnerAnimation?.Stop();
+ System.Diagnostics.Debug.WriteLine("Spinner animation stopped");
+
+ // Start fade-out animation
+ var fadeOutAnimation = this.FindResource("FadeOutAnimation") as Storyboard;
+ if (fadeOutAnimation != null)
+ {
+ System.Diagnostics.Debug.WriteLine("Starting fade-out animation");
+ fadeOutAnimation.Completed += (s, e) =>
+ {
+ System.Diagnostics.Debug.WriteLine("Fade-out animation completed, hiding overlay");
+ var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
+ if (loadingOverlay != null)
+ {
+ loadingOverlay.Visibility = Visibility.Collapsed;
+ System.Diagnostics.Debug.WriteLine("Loading overlay visibility set to Collapsed");
+ }
+
+ // Disable app content blur and restore style-driven behavior.
+ this.ClearUIContentBlur();
+ System.Diagnostics.Debug.WriteLine("=== Loading overlay hidden successfully ===");
+
+ // Show elevation warning if needed
+ this.TryShowElevationWarning();
+ this.TryShowStartupMinimizedSuggestion();
+ };
+ fadeOutAnimation.Begin();
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine("WARNING: FadeOutAnimation not found, hiding overlay immediately");
+ // Fallback: hide overlay immediately if animation fails
+ var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
+ if (loadingOverlay != null)
+ {
+ loadingOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ this.ClearUIContentBlur();
+ System.Diagnostics.Debug.WriteLine("=== Loading overlay hidden immediately (fallback) ===");
+
+ // Show elevation warning if needed
+ this.TryShowElevationWarning();
+ this.TryShowStartupMinimizedSuggestion();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"=== ERROR hiding loading overlay: {ex} ===");
+ // Emergency fallback: hide overlay without animation
+ try
+ {
+ var loadingOverlay = this.FindName("LoadingOverlay") as Grid;
+ if (loadingOverlay != null)
+ {
+ loadingOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ this.ClearUIContentBlur();
+ System.Diagnostics.Debug.WriteLine("Emergency fallback: overlay hidden without animation");
+
+ // Show elevation warning if needed
+ this.TryShowElevationWarning();
+ this.TryShowStartupMinimizedSuggestion();
+ }
+ catch (Exception fallbackEx)
+ {
+ System.Diagnostics.Debug.WriteLine($"Emergency fallback also failed: {fallbackEx}");
+ }
+ }
+ }
+
+ private void ApplyUIContentBlur(double radius)
+ {
+ if (this.UIContent.Effect is not BlurEffect blur)
+ {
+ blur = new BlurEffect();
+ this.UIContent.Effect = blur;
+ }
+
+ blur.KernelType = KernelType.Gaussian;
+ blur.Radius = radius;
+ }
+
+ private void ClearUIContentBlur()
+ {
+ this.UIContent.Effect = null;
+ }
+
+ private void OnInitializationTimeout(object? sender, ElapsedEventArgs e)
+ {
+ this.Dispatcher.InvokeAsync(() =>
+ {
+ if (!this.isInitializationComplete)
+ {
+ this.ShowInitializationError(new TimeoutException("Application initialization timed out after 15 seconds"));
+ }
+ });
+ }
+
+ private void ShowInitializationError(Exception ex)
+ {
+ try
+ {
+ this.UpdateLoadingStatus("Initialization failed", ex.Message);
+
+ var result = System.Windows.MessageBox.Show(
+ $"ThreadPilot failed to initialize properly:\n\n{ex.Message}\n\nDebug log: {this.debugLogPath}\n\nWould you like to retry initialization or close the application?",
+ "Initialization Error",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Error);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ // Retry initialization - marshal to UI thread to prevent cross-thread access exceptions
+ this.isInitializationComplete = false;
+ this.initializationTasks.Clear();
+ this.UpdateLoadingStatus("Retrying initialization...", "Restarting startup sequence.");
+ this.LogDebug("=== RETRYING INITIALIZATION ===");
+ _ = this.Dispatcher.InvokeAsync(async () => await this.InitializeApplicationAsync());
+ }
+ else
+ {
+ // Close application
+ this.LogDebug("User chose to close application");
+ System.Windows.Application.Current.Shutdown();
+ }
+ }
+ catch (Exception overlayEx)
+ {
+ this.LogDebug($"Error showing initialization error: {overlayEx.Message}");
+ System.Windows.Application.Current.Shutdown();
+ }
+ }
+
+ private async Task LoadViewModelsAsync()
+ {
+ try
+ {
+ this.LogDebug("=== Starting LoadViewModelsAsync ===");
+
+ this.LogDebug("About to initialize ProcessViewModel (including CPU topology)...");
+ try
+ {
+ // Use the full initialization method instead of just LoadProcesses
+ var processTask = this.processViewModel.InitializeAsync();
+ var processResult = await Task.WhenAny(processTask, Task.Delay(15000)); // 15 second timeout for full initialization
+ if (processResult != processTask)
+ {
+ this.LogDebug("ProcessViewModel.InitializeAsync() timed out, trying fallback...");
+ // Fallback: just load processes without full initialization
+ await this.processViewModel.LoadProcesses();
+ this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
+ }
+ else
+ {
+ await processTask; // Ensure we get any exceptions
+ this.LogDebug($"ProcessViewModel initialized successfully (including CPU topology), process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
+ }
+ }
+ catch (Exception processEx)
+ {
+ this.LogDebug($"ProcessViewModel initialization failed: {processEx.Message}, trying fallback...");
+ // Fallback: just load processes without full initialization
+ await this.processViewModel.LoadProcesses();
+ this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed after exception, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
+ }
+
+ this.LogDebug("About to load PowerPlanViewModel...");
+ var powerPlanTask = this.powerPlanViewModel.LoadPowerPlans();
+ var powerPlanResult = await Task.WhenAny(powerPlanTask, Task.Delay(5000)); // 5 second timeout
+ if (powerPlanResult != powerPlanTask)
+ {
+ throw new TimeoutException("PowerPlanViewModel.LoadPowerPlans() timed out after 5 seconds");
+ }
+ await powerPlanTask; // Ensure we get any exceptions
+ this.LogDebug("PowerPlanViewModel loaded successfully");
+
+ this.LogDebug("Skipping optional diagnostics initialization until the diagnostics page is opened.");
+
+ this.LogDebug("About to load SystemTweaksViewModel...");
+ var systemTweaksTask = this.systemTweaksViewModel.LoadCommand.ExecuteAsync(null);
+ var systemTweaksResult = await Task.WhenAny(systemTweaksTask, Task.Delay(5000)); // 5 second timeout
+ if (systemTweaksResult != systemTweaksTask)
+ {
+ throw new TimeoutException("SystemTweaksViewModel.LoadCommand.ExecuteAsync() timed out after 5 seconds");
+ }
+ await systemTweaksTask; // Ensure we get any exceptions
+ this.LogDebug("SystemTweaksViewModel loaded successfully");
+
+ // Initialize keyboard shortcuts after window is loaded
+ this.Loaded += this.OnWindowLoaded;
+ this.LogDebug("Keyboard shortcuts event handler attached");
+
+ // The association view model loads its data automatically in its constructor
+ this.LogDebug("=== LoadViewModelsAsync completed successfully ===");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"=== ERROR in LoadViewModelsAsync: {ex} ===");
+ throw; // Re-throw to be handled by initialization error handler
+ }
+ }
+
+ private async Task InitializeServicesAsync()
+ {
+ this.LogDebug("=== Starting InitializeServicesAsync ===");
+
+ this.LogDebug("About to initialize settings...");
+ await this.InitializeSettingsAsync();
+ this.LogDebug("Settings initialized successfully");
+
+ this.LogDebug("About to initialize system tray...");
+ try
+ {
+ var systemTrayTask = this.InitializeSystemTrayAsync();
+ var systemTrayResult = await Task.WhenAny(systemTrayTask, Task.Delay(5000)); // 5 second timeout
+ if (systemTrayResult != systemTrayTask)
+ {
+ this.LogDebug("System tray initialization timed out, continuing with basic tray setup...");
+ // Initialize basic system tray without context menu updates (Initialize() is idempotent)
+ await this.InitializeBasicSystemTrayAsync();
+ this.LogDebug("Basic system tray initialized (without context menu)");
+ }
+ else
+ {
+ await systemTrayTask; // Ensure we get any exceptions
+ this.LogDebug("System tray initialized successfully");
+ }
+ }
+ catch (Exception systemTrayEx)
+ {
+ this.LogDebug($"System tray initialization failed: {systemTrayEx.Message}, using basic tray...");
+ // Fallback: basic system tray initialization
+ try
+ {
+ await this.InitializeBasicSystemTrayAsync();
+ this.LogDebug("Fallback system tray initialized");
+ }
+ catch (Exception fallbackEx)
+ {
+ this.LogDebug($"Even fallback system tray failed: {fallbackEx.Message}");
+ }
+ }
+
+ this.LogDebug("About to initialize notifications...");
+ this.InitializeNotifications();
+ this.LogDebug("Notifications initialized successfully");
+
+ this.LogDebug("About to initialize monitoring...");
+ await this.InitializeMonitoringAsync();
+ this.LogDebug("Monitoring initialized successfully");
+
+ if (this.skipProcessMonitoringDuringStartup)
+ {
+ this.LogDebug("Skipping process monitoring manager startup (temporary bypass enabled)");
+ }
+ else
+ {
+ this.LogDebug("About to start process monitoring manager...");
+ try
+ {
+ var monitoringTask = this.StartProcessMonitoringManagerAsync();
+ var timeoutTask = Task.Delay(8000); // 8 second timeout
+ var completedTask = await Task.WhenAny(monitoringTask, timeoutTask);
+
+ if (completedTask == timeoutTask)
+ {
+ this.LogDebug("Process monitoring manager startup timed out after 8 seconds, continuing without monitoring...");
+ }
+ else
+ {
+ try
+ {
+ await monitoringTask; // Ensure we get any exceptions
+ this.LogDebug("Process monitoring manager started successfully");
+ }
+ catch (Exception taskEx)
+ {
+ this.LogDebug($"Process monitoring manager task failed: {taskEx.Message}");
+ }
+ }
+ }
+ catch (Exception monitoringEx)
+ {
+ this.LogDebug($"Process monitoring manager startup failed: {monitoringEx.Message}, continuing without monitoring...");
+ }
+ }
+
+ this.LogDebug("=== InitializeServicesAsync completed successfully ===");
+ }
+
+ private async Task InitializeSettingsAsync()
+ {
+ try
+ {
+ await this.settingsService.LoadSettingsAsync();
+
+ // Apply initial settings
+ var settings = this.settingsService.Settings;
+ var useDarkTheme = settings.HasUserThemePreference
+ ? settings.UseDarkTheme
+ : this.themeService.GetSystemUsesDarkTheme();
+
+ if (!settings.HasUserThemePreference && settings.UseDarkTheme != useDarkTheme)
+ {
+ settings.UseDarkTheme = useDarkTheme;
+ await this.settingsService.UpdateSettingsAsync(settings);
+ }
+
+ this.themeService.ApplyTheme(useDarkTheme);
+ this.mainWindowViewModel.IsDarkTheme = useDarkTheme;
+ DwmHelper.ApplyWindowCaptionTheme(this, useDarkTheme);
+
+ if (settings.StartMinimized)
+ {
+ this.WindowState = WindowState.Minimized;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to load settings: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeSystemTrayAsync()
+ {
+ try
+ {
+ this.systemTrayService.Initialize();
+ this.systemTrayService.Show();
+
+ // Subscribe to tray events
+ this.UnsubscribeSystemTrayEvents();
+ this.systemTrayService.ShowMainWindowRequested += this.OnShowMainWindowRequested;
+ this.systemTrayService.DashboardRequested += this.OnDashboardRequested;
+ this.systemTrayService.ExitRequested += this.OnExitRequested;
+ this.systemTrayService.MonitoringToggleRequested += this.OnMonitoringToggleRequested;
+ this.systemTrayService.SettingsRequested += this.OnSettingsRequested;
+ this.systemTrayService.PowerPlanChangeRequested += this.OnPowerPlanChangeRequested;
+ this.systemTrayService.ProfileApplicationRequested += this.OnProfileApplicationRequested;
+ this.systemTrayService.PerformanceDashboardRequested += this.OnPerformanceDashboardRequested;
+
+ // Update settings and tooltip
+ this.systemTrayService.UpdateSettings(this.settingsService.Settings);
+ this.systemTrayService.ApplyTheme(this.themeService.IsDarkTheme);
+ this.systemTrayService.UpdateTooltip("ThreadPilot - Process & Power Plan Manager");
+
+ // Initialize system tray context menu with current data
+ await this.UpdateSystemTrayContextMenuAsync();
+
+ // Start periodic system tray updates
+ this.StartSystemTrayUpdateTimer();
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't fail startup
+ System.Diagnostics.Debug.WriteLine($"Failed to initialize system tray: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeBasicSystemTrayAsync()
+ {
+ try
+ {
+ this.LogDebug("Initializing basic system tray (without full context menu)...");
+
+ // Initialize basic tray icon (this is idempotent)
+ this.systemTrayService.Initialize();
+ this.systemTrayService.Show();
+
+ // Subscribe to essential tray events only
+ this.UnsubscribeSystemTrayEvents();
+ this.systemTrayService.ShowMainWindowRequested += this.OnShowMainWindowRequested;
+ this.systemTrayService.DashboardRequested += this.OnDashboardRequested;
+ this.systemTrayService.ExitRequested += this.OnExitRequested;
+
+ // Update basic settings and tooltip
+ this.systemTrayService.UpdateSettings(this.settingsService.Settings);
+ this.systemTrayService.ApplyTheme(this.themeService.IsDarkTheme);
+ this.systemTrayService.UpdateTooltip("ThreadPilot - Process & Power Plan Manager (Basic Mode)");
+
+ this.LogDebug("Basic system tray initialization completed");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to initialize basic system tray: {ex.Message}");
+ throw;
+ }
+ }
+
+ private void OnShowMainWindowRequested(object? sender, EventArgs e)
+ {
+ this.ShowWindowFromTray();
+ }
+
+ private void OnExitRequested(object? sender, EventArgs e)
+ {
+ TaskSafety.FireAndForget(this.OnExitRequestedAsync(), ex =>
+ {
+ this.LogDebug($"OnExitRequested failed: {ex.Message}");
+ });
+ }
+
+ private async Task OnExitRequestedAsync()
+ {
+ await this.PerformGracefulShutdownAsync();
+ }
+
+ private void OnDashboardRequested(object? sender, EventArgs e)
+ {
+ this.ShowWindowFromTray("Process");
+ }
+
+ private async Task PerformGracefulShutdownAsync(bool validateUnsavedChanges = true)
+ {
+ if (this.isPerformingShutdown)
+ {
+ return;
+ }
+
+ if (validateUnsavedChanges && !await this.HandleUnsavedSettingsBeforeExitAsync())
+ {
+ return;
+ }
+
+ this.isPerformingShutdown = true;
+
+ try
+ {
+ this.LogDebug("Starting graceful shutdown...");
+ this.selfResourceManagementService.RestoreForegroundMode();
+
+ // 1. Stop monitoring services
+ try
+ {
+ this.LogDebug("Stopping process monitoring manager...");
+ await this.processMonitorManagerService.StopAsync();
+ this.LogDebug("Process monitoring manager stopped");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error stopping process monitoring: {ex.Message}");
+ }
+
+ // 2. Cleanup applied CPU masks (like CPU Set Setter's ClearAllProcessMasksNoSave)
+ if (this.settingsService.Settings.ClearMasksOnClose)
+ {
+ try
+ {
+ this.LogDebug("Clearing all applied CPU masks...");
+ var processService = this.serviceProvider.GetRequiredService();
+ await processService.ClearAllAppliedMasksAsync();
+ this.LogDebug("CPU masks cleared");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error clearing CPU masks: {ex.Message}");
+ }
+
+ // Also reset priorities
+ try
+ {
+ this.LogDebug("Resetting all process priorities...");
+ var processService = this.serviceProvider.GetRequiredService();
+ await processService.ResetAllProcessPrioritiesAsync();
+ this.LogDebug("Process priorities reset");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error resetting priorities: {ex.Message}");
+ }
+ }
+
+ // 3. Restore default power plan if configured
+ if (this.settingsService.Settings.RestoreDefaultPowerPlanOnExit)
+ {
+ try
+ {
+ var targetDefaultPowerPlanGuid = this.settingsService.Settings.DefaultPowerPlanId;
+
+ try
+ {
+ await this.processPowerPlanAssociationService.LoadConfigurationAsync();
+ var (associationDefaultPowerPlanGuid, _) = await this.processPowerPlanAssociationService.GetDefaultPowerPlanAsync();
+ if (!string.IsNullOrWhiteSpace(associationDefaultPowerPlanGuid))
+ {
+ targetDefaultPowerPlanGuid = associationDefaultPowerPlanGuid;
+ }
+ }
+ catch (Exception associationEx)
+ {
+ this.LogDebug($"Could not read default power plan from association config: {associationEx.Message}");
+ }
+
+ if (string.IsNullOrWhiteSpace(targetDefaultPowerPlanGuid))
+ {
+ this.LogDebug("No default power plan configured for restore on exit");
+ }
+ else
+ {
+ this.LogDebug("Restoring default power plan...");
+ var powerPlanService = this.serviceProvider.GetRequiredService();
+ await powerPlanService.SetActivePowerPlanByGuidAsync(targetDefaultPowerPlanGuid);
+ this.LogDebug("Default power plan restored");
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error restoring power plan: {ex.Message}");
+ }
+ }
+
+ // 4. Save settings
+ try
+ {
+ this.LogDebug("Saving settings...");
+ await this.settingsService.SaveSettingsAsync();
+ this.LogDebug("Settings saved");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error saving settings: {ex.Message}");
+ }
+
+ // 5. Dispose tray service
+ try
+ {
+ this.LogDebug("Disposing system tray...");
+ this.systemTrayService.Dispose();
+ this.LogDebug("System tray disposed");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error disposing tray: {ex.Message}");
+ }
+
+ this.LogDebug("Graceful shutdown completed");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Error during graceful shutdown: {ex.Message}");
+ }
+ finally
+ {
+ // Ensure application exits
+ System.Windows.Application.Current.Shutdown();
+ }
+ }
+
+ private async Task HandleUnsavedSettingsBeforeExitAsync()
+ {
+ if (!this.settingsViewModel.HasPendingChanges)
+ {
+ return true;
+ }
+
+ var result = await this.ShowUnsavedSettingsDialogAsync(
+ "You have unsaved changes in Settings. Save before exiting, discard the changes, or cancel to return to ThreadPilot.");
+
+ if (result == MessageBoxResult.Cancel)
+ {
+ return false;
+ }
+
+ if (result == MessageBoxResult.Yes)
+ {
+ var saved = await this.settingsViewModel.SaveIfDirtyAsync();
+ return saved;
+ }
+
+ await this.settingsViewModel.DiscardPendingChangesAsync();
+ return true;
+ }
+
+ private async Task HandleWindowCloseAsync()
+ {
+ if (!await this.HandleUnsavedSettingsBeforeExitAsync())
+ {
+ return;
+ }
+
+ if (this.settingsService.Settings.CloseToTray)
+ {
+ this.WindowState = WindowState.Minimized;
+ return;
+ }
+
+ await this.PerformGracefulShutdownAsync(validateUnsavedChanges: false);
+ }
+
+ private void OnMonitoringToggleRequested(object? sender, MonitoringToggleEventArgs e)
+ {
+ TaskSafety.FireAndForget(this.OnMonitoringToggleRequestedAsync(e), ex =>
+ {
+ this.LogDebug($"OnMonitoringToggleRequested failed: {ex.Message}");
+ });
+ }
+
+ private async Task OnMonitoringToggleRequestedAsync(MonitoringToggleEventArgs e)
+ {
+ try
+ {
+ if (e.EnableMonitoring)
+ {
+ await this.processMonitorManagerService.StartAsync();
+ await this.notificationService.ShowSuccessNotificationAsync(
+ "Automation Monitoring Enabled",
+ "Process rule automation and power plan management have been enabled.");
+ }
+ else
+ {
+ await this.processMonitorManagerService.StopAsync();
+ await this.notificationService.ShowNotificationAsync(
+ "Automation Monitoring Disabled",
+ "Process rule automation and power plan management have been disabled.",
+ Models.NotificationType.Warning);
+ }
+ }
+ catch (Exception ex)
+ {
+ await this.notificationService.ShowErrorNotificationAsync(
+ "Automation Monitoring Error",
+ "Failed to toggle automation monitoring.",
+ ex);
+ }
+ }
+
+ private void OnSettingsRequested(object? sender, EventArgs e)
+ {
+ try
+ {
+ this.ShowWindowFromTray("Settings");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to open settings: {ex.Message}");
+ }
+ }
+
+ private void OnPowerPlanChangeRequested(object? sender, PowerPlanChangeRequestedEventArgs e)
+ {
+ TaskSafety.FireAndForget(this.OnPowerPlanChangeRequestedAsync(e), ex =>
+ {
+ this.LogDebug($"OnPowerPlanChangeRequested failed: {ex.Message}");
+ });
+ }
+
+ private async Task OnPowerPlanChangeRequestedAsync(PowerPlanChangeRequestedEventArgs e)
+ {
+ try
+ {
+ var powerPlanService = this.serviceProvider.GetRequiredService();
+ var success = await powerPlanService.SetActivePowerPlanByGuidAsync(e.PowerPlanGuid);
+
+ if (success)
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot",
+ $"Power plan changed to {e.PowerPlanName}", 2000);
+ }
+ else
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot Error",
+ $"Failed to change power plan to {e.PowerPlanName}", 3000);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot Error",
+ $"Error changing power plan: {ex.Message}", 3000);
+ }
+ }
+
+ private void OnProfileApplicationRequested(object? sender, ProfileApplicationRequestedEventArgs e)
+ {
+ TaskSafety.FireAndForget(this.OnProfileApplicationRequestedAsync(e), ex =>
+ {
+ this.LogDebug($"OnProfileApplicationRequested failed: {ex.Message}");
+ });
+ }
+
+ private async Task OnProfileApplicationRequestedAsync(ProfileApplicationRequestedEventArgs e)
+ {
+ try
+ {
+ var processService = this.serviceProvider.GetRequiredService();
+ var selectedProcess = this.processViewModel.SelectedProcess;
+
+ if (selectedProcess != null)
+ {
+ var success = await processService.LoadProcessProfile(e.ProfileName, selectedProcess);
+
+ if (success)
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot",
+ $"Profile '{e.ProfileName}' applied to {selectedProcess.Name}", 2000);
+ }
+ else
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot Error",
+ $"Failed to apply profile '{e.ProfileName}'", 3000);
+ }
+ }
+ else
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot",
+ "No process selected for profile application", 2000);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.systemTrayService.ShowBalloonTip(
+ "ThreadPilot Error",
+ $"Error applying profile: {ex.Message}", 3000);
+ }
+ }
+
+ private void OnPerformanceDashboardRequested(object? sender, EventArgs e)
+ {
+ try
+ {
+ this.ShowWindowFromTray("Performance");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to open performance dashboard: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeKeyboardShortcutsAsync()
+ {
+ try
+ {
+ // Set window handle for global hotkey registration
+ var windowInteropHelper = new System.Windows.Interop.WindowInteropHelper(this);
+ var handle = windowInteropHelper.EnsureHandle();
+
+ if (this.keyboardShortcutService is KeyboardShortcutService service)
+ {
+ service.SetWindowHandle(handle);
+ }
+
+ // Subscribe to shortcut activation events
+ this.keyboardShortcutService.ShortcutActivated -= this.OnShortcutActivated;
+ this.keyboardShortcutService.ShortcutActivated += this.OnShortcutActivated;
+
+ // Load shortcuts from settings - with error handling
+ try
+ {
+ await this.keyboardShortcutService.LoadShortcutsFromSettingsAsync();
+ }
+ catch (Exception settingsEx)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to load shortcuts from settings, using defaults: {settingsEx.Message}");
+ // Continue with default shortcuts if settings loading fails
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to initialize keyboard shortcuts: {ex.Message}");
+ // Don't let keyboard shortcut initialization failure prevent the app from starting
+ }
+ }
+
+ private void OnShortcutActivated(object? sender, ShortcutActivatedEventArgs e)
+ {
+ try
+ {
+ System.Windows.Application.Current.Dispatcher.InvokeAsync(async () =>
+ {
+ await this.HandleShortcutActionAsync(e.ActionName);
+ });
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error handling shortcut {e.ActionName}: {ex.Message}");
+ }
+ }
+
+ private async Task HandleShortcutActionAsync(string actionName)
+ {
+ switch (actionName)
+ {
+ case ShortcutActions.ShowMainWindow:
+ if (this.IsVisible && this.WindowState != WindowState.Minimized)
+ {
+ this.ShowInTaskbar = false;
+ this.Hide();
+ this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
+ }
+ else
+ {
+ this.ShowWindowFromTray();
+ }
+ break;
+
+ case ShortcutActions.ToggleMonitoring:
+ // Toggle monitoring - implementation can be added later
+ await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "Toggle monitoring shortcut activated");
+ break;
+
+ case ShortcutActions.PowerPlanHighPerformance:
+ // Switch to high performance power plan - implementation can be added later
+ await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "High Performance power plan shortcut activated");
+ break;
+
+ case ShortcutActions.OpenTweaks:
+ this.ShowWindowFromTray("Tweaks");
+ break;
+
+ case ShortcutActions.OpenSettings:
+ this.ShowWindowFromTray("Settings");
+ break;
+
+ case ShortcutActions.RefreshProcessList:
+ // Refresh process list - implementation can be added later
+ await this.notificationService.ShowNotificationAsync("Keyboard Shortcut", "Refresh process list shortcut activated");
+ break;
+
+ case ShortcutActions.ExitApplication:
+ this.Close();
+ break;
+ }
+ }
+
+ private async Task UpdateSystemTrayContextMenuAsync()
+ {
+ try
+ {
+ await this.systemTrayStatusUpdater.UpdateContextMenuAsync(this.systemTrayService);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to update system tray context menu: {ex.Message}");
+ }
+ }
+
+ private void StartSystemTrayUpdateTimer()
+ {
+ try
+ {
+ this.systemTrayUpdateTimer?.Stop();
+ this.systemTrayUpdateTimer?.Dispose();
+ this.systemTrayUpdateTimer = null;
+
+ if (!this.systemTrayStatusUpdater.ShouldRunPerformanceStatusUpdates)
+ {
+ return;
+ }
+
+ this.systemTrayUpdateFailureStreak = 0;
+ this.systemTrayUpdateTimer = new System.Timers.Timer(SystemTrayUpdateBaseIntervalMs);
+ this.systemTrayUpdateTimer.Elapsed += async (s, e) =>
+ {
+ if (this.isSystemTrayUpdatesSuspended)
+ {
+ return;
+ }
+
+ if (Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 1) == 1)
+ {
+ return;
+ }
+
+ try
+ {
+ var updateSucceeded = await this.UpdateSystemTrayStatusAsync();
+ this.ApplySystemTrayUpdateBackoff(updateSucceeded);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error in system tray update timer: {ex.Message}");
+ this.ApplySystemTrayUpdateBackoff(updateSucceeded: false);
+ }
+ finally
+ {
+ Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
+ }
+ };
+ this.systemTrayUpdateTimer.AutoReset = true;
+
+ if (!this.isSystemTrayUpdatesSuspended &&
+ this.IsVisible &&
+ this.WindowState != WindowState.Minimized)
+ {
+ this.systemTrayUpdateTimer.Start();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to start system tray update timer: {ex.Message}");
+ }
+ }
+
+ private void ApplySystemTrayUpdateBackoff(bool updateSucceeded)
+ {
+ if (this.systemTrayUpdateTimer == null)
+ {
+ return;
+ }
+
+ if (updateSucceeded)
+ {
+ this.systemTrayUpdateFailureStreak = 0;
+ if (Math.Abs(this.systemTrayUpdateTimer.Interval - SystemTrayUpdateBaseIntervalMs) > 1)
+ {
+ this.systemTrayUpdateTimer.Interval = SystemTrayUpdateBaseIntervalMs;
+ }
+
+ return;
+ }
+
+ this.systemTrayUpdateFailureStreak = Math.Min(4, this.systemTrayUpdateFailureStreak + 1);
+ var exponentialDelay = SystemTrayUpdateBaseIntervalMs * Math.Pow(2, this.systemTrayUpdateFailureStreak);
+ var nextIntervalMs = Math.Min(SystemTrayUpdateMaxIntervalMs, exponentialDelay);
+
+ if (Math.Abs(this.systemTrayUpdateTimer.Interval - nextIntervalMs) > 1)
+ {
+ this.systemTrayUpdateTimer.Interval = nextIntervalMs;
+ }
+ }
+
+ private async Task UpdateSystemTrayStatusAsync()
+ {
+ try
+ {
+ return await this.systemTrayStatusUpdater.UpdateStatusAsync(
+ this.systemTrayService,
+ action => this.Dispatcher.InvokeAsync(action).Task);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to update system tray status: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void InitializeNotifications()
+ {
+ try
+ {
+ // Subscribe to settings changes to update notification service
+ this.settingsService.SettingsChanged += this.OnSettingsChanged;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to initialize notifications: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeMonitoringAsync()
+ {
+ try
+ {
+ // Subscribe to monitoring status changes
+ this.processMonitorService.MonitoringStatusChanged += this.OnMonitoringStatusChanged;
+
+ // Update tray with initial monitoring status
+ this.systemTrayService.UpdateMonitoringStatus(
+ this.processMonitorService.IsMonitoring,
+ this.processMonitorService.IsWmiAvailable);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to initialize monitoring: {ex.Message}");
+ }
+ }
+
+ private async Task StartProcessMonitoringManagerAsync()
+ {
+ try
+ {
+ this.LogDebug("Subscribing to process monitor manager events...");
+ // Subscribe to process monitor manager events
+ this.processMonitorManagerService.ServiceStatusChanged += this.OnProcessMonitorManagerStatusChanged;
+
+ this.LogDebug("Starting process monitoring manager service...");
+ // Start the process monitoring manager service with internal timeout
+ var startTask = this.processMonitorManagerService.StartAsync();
+ var timeoutTask = Task.Delay(6000); // 6 second internal timeout
+ var completedTask = await Task.WhenAny(startTask, timeoutTask);
+
+ if (completedTask == timeoutTask)
+ {
+ this.LogDebug("ProcessMonitorManagerService.StartAsync() timed out internally");
+ throw new TimeoutException("Process monitoring manager service startup timed out");
+ }
+
+ await startTask; // Get any exceptions
+ this.LogDebug("Process monitoring manager service started, showing notification...");
+
+ if (!this.isSilentStartupMode)
+ {
+ await this.notificationService.ShowSuccessNotificationAsync(
+ "ThreadPilot Started",
+ "Process monitoring and power plan management is now active");
+ }
+
+ this.LogDebug(this.isSilentStartupMode
+ ? "Startup success notification skipped for silent startup"
+ : "Success notification shown");
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to start process monitoring manager: {ex.Message}");
+ try
+ {
+ await this.notificationService.ShowErrorNotificationAsync(
+ "Startup Error",
+ "Failed to start process monitoring manager",
+ ex);
+ }
+ catch (Exception notificationEx)
+ {
+ this.LogDebug($"Failed to show error notification: {notificationEx.Message}");
+ }
+ throw; // Re-throw to be caught by outer handler
+ }
+ }
+
+ private void OnSettingsChanged(object? sender, ApplicationSettingsChangedEventArgs e)
+ {
+ // Update tray service with new settings
+ this.systemTrayService.UpdateSettings(e.NewSettings);
+
+ var useDarkTheme = e.NewSettings.HasUserThemePreference
+ ? e.NewSettings.UseDarkTheme
+ : this.themeService.GetSystemUsesDarkTheme();
+
+ this.themeService.ApplyTheme(useDarkTheme);
+ this.mainWindowViewModel.IsDarkTheme = useDarkTheme;
+ this.systemTrayService.ApplyTheme(useDarkTheme);
+ DwmHelper.ApplyWindowCaptionTheme(this, useDarkTheme);
+ this.ApplySelfResourcePolicy(this.lastAppliedRefreshState ?? this.GetForegroundActivityState(), e.NewSettings);
+ }
+
+ private void OnMonitoringStatusChanged(object? sender, MonitoringStatusEventArgs e)
+ {
+ // Update tray icon and status
+ this.systemTrayService.UpdateMonitoringStatus(e.IsMonitoring, e.IsWmiAvailable);
+
+ // Show notification if there's an error
+ if (e.Error != null && this.settingsService.Settings.EnableErrorNotifications)
+ {
+ this.notificationService.ShowErrorNotificationAsync(
+ "Automation Monitoring Error",
+ e.StatusMessage ?? "An error occurred with automation monitoring.",
+ e.Error);
+ }
+ }
+
+ private void OnProcessMonitorManagerStatusChanged(object? sender, ServiceStatusEventArgs e)
+ {
+ // Update main window status
+ this.mainWindowViewModel.UpdateProcessMonitoringStatus(e.IsRunning, e.Status);
+
+ // Show notification for critical status changes
+ if (!e.IsRunning && e.Error != null && this.settingsService.Settings.EnableErrorNotifications)
+ {
+ this.notificationService.ShowErrorNotificationAsync(
+ "Automation Monitoring Error",
+ e.Details ?? "Automation monitoring encountered an error.",
+ e.Error);
+ }
+ }
+
+ protected override void OnStateChanged(EventArgs e)
+ {
+ try
+ {
+ if (this.WindowState == WindowState.Minimized)
+ {
+ var activityState = AppActivityState.Minimized;
+ if (this.settingsService.Settings.MinimizeToTray)
+ {
+ this.ShowInTaskbar = false;
+ this.Hide();
+ this.systemTrayService.Show();
+ activityState = AppActivityState.TrayHidden;
+ }
+
+ this.ApplyAppRefreshPolicy(activityState);
+ }
+ else if (this.WindowState == WindowState.Normal || this.WindowState == WindowState.Maximized)
+ {
+ this.ShowInTaskbar = true;
+ this.EnsureDashboardVisibleOnScreen();
+
+ this.ApplyAppRefreshPolicy(this.GetForegroundActivityState());
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error handling window state change: {ex.Message}");
+ }
+
+ base.OnStateChanged(e);
+ }
+
+ private void SuspendHiddenModeRefreshes()
+ {
+ this.isSystemTrayUpdatesSuspended = true;
+ this.systemTrayUpdateTimer?.Stop();
+ Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
+ this.powerPlanViewModel.PauseAutoRefresh();
+ }
+
+ private void ResumeForegroundRefreshes()
+ {
+ this.isSystemTrayUpdatesSuspended = false;
+ this.systemTrayUpdateFailureStreak = 0;
+ this.systemTrayUpdateTimer?.Stop();
+
+ if (!this.systemTrayStatusUpdater.ShouldRunPerformanceStatusUpdates)
+ {
+ return;
+ }
+
+ if (this.systemTrayUpdateTimer != null)
+ {
+ this.systemTrayUpdateTimer.Interval = SystemTrayUpdateBaseIntervalMs;
+ }
+ this.systemTrayUpdateTimer?.Start();
+
+ _ = this.Dispatcher.InvokeAsync(async () =>
+ {
+ try
+ {
+ var updateSucceeded = await this.UpdateSystemTrayStatusAsync();
+ this.ApplySystemTrayUpdateBackoff(updateSucceeded);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to refresh tray status after resume: {ex.Message}");
+ }
+ });
+ }
+
+ private AppActivityState GetForegroundActivityState()
+ {
+ if (this.ProcessManagementTab.Visibility == Visibility.Visible)
+ {
+ return AppActivityState.ForegroundProcessView;
+ }
+
+ return this.PerformanceViewControl.Visibility == Visibility.Visible
+ ? AppActivityState.ForegroundDiagnosticsView
+ : AppActivityState.ForegroundOtherTab;
+ }
+
+ private void ApplyAppRefreshPolicy(AppActivityState state)
+ {
+ if (!AppRefreshPolicy.ShouldApplyTransition(this.lastAppliedRefreshState, state))
+ {
+ return;
+ }
+
+ this.lastAppliedRefreshState = state;
+
+ var decision = AppRefreshPolicy.Evaluate(state);
+ var isHiddenState = state is AppActivityState.Minimized or AppActivityState.TrayHidden;
+ var isProcessViewActive = state == AppActivityState.ForegroundProcessView;
+
+ if (isHiddenState)
+ {
+ this.isSystemTrayUpdatesSuspended = true;
+ this.systemTrayUpdateTimer?.Stop();
+ Interlocked.Exchange(ref this.isSystemTrayUpdateInProgress, 0);
+ }
+ else
+ {
+ this.ResumeForegroundRefreshes();
+ }
+
+ this.processViewModel.SetProcessViewActive(isProcessViewActive);
+ this.processViewModel.ApplyRefreshDecision(decision);
+
+ if (decision.PowerPlanUiRefreshEnabled)
+ {
+ this.powerPlanViewModel.ResumeAutoRefresh(refreshImmediately: state != AppActivityState.ForegroundOtherTab);
+ }
+ else
+ {
+ this.powerPlanViewModel.PauseAutoRefresh();
+ }
+
+ if (decision.PerformanceUiMonitoringEnabled)
+ {
+ _ = this.GetPerformanceViewModel().ActivateDiagnosticsAsync();
+ }
+ else if (this.performanceViewModel != null)
+ {
+ _ = this.performanceViewModel.SuspendBackgroundMonitoringAsync();
+ }
+
+ this.ApplySelfResourcePolicy(state);
+ }
+
+ private void ApplySelfResourcePolicy(AppActivityState state, ApplicationSettingsModel? settings = null)
+ {
+ var currentSettings = settings ?? this.settingsService.Settings;
+ var isHiddenState = state is AppActivityState.Minimized or AppActivityState.TrayHidden;
+
+ if (SelfResourcePolicy.ShouldApplyLowImpactMode(isHiddenState, currentSettings.EnableSelfLowImpactMode))
+ {
+ this.selfResourceManagementService.ApplyLowImpactMode(SelfResourcePolicy.ShouldLimitAffinity(
+ isHiddenState,
+ currentSettings.EnableSelfLowImpactMode,
+ currentSettings.EnableSelfAffinityLimit));
+ return;
+ }
+
+ this.selfResourceManagementService.RestoreForegroundMode();
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+ try
+ {
+ DwmHelper.ApplyWindowCaptionTheme(this, this.themeService.IsDarkTheme);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to apply window caption theme: {ex.Message}");
+ }
+
+ this.EnsureDashboardVisibleOnScreen();
+ }
+
+ [System.Diagnostics.Conditional("DEBUG")]
+ private void LogDebug(string message)
+ {
+ try
+ {
+ var timestampedMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [MainWindow] {message}";
+ System.Diagnostics.Debug.WriteLine(timestampedMessage);
+ File.AppendAllText(this.debugLogPath, timestampedMessage + Environment.NewLine);
+ }
+ catch
+ {
+ // Swallow logging failures to avoid impacting runtime behavior
+ }
+ }
+
+ private void OnWindowLoaded(object? sender, RoutedEventArgs e)
+ {
+ TaskSafety.FireAndForget(this.OnWindowLoadedAsync(), ex =>
+ {
+ this.LogDebug($"OnWindowLoaded failed: {ex.Message}");
+ });
+ }
+
+ private async Task OnWindowLoadedAsync()
+ {
+ this.Loaded -= this.OnWindowLoaded;
+ this.EnsureDashboardVisibleOnScreen();
+ await this.InitializeKeyboardShortcutsAsync();
+ }
+
+ private void OnOpenRulesRequested(object? sender, EventArgs e)
+ {
+ this.SelectMainTab("Rules");
+ }
+
+ private void ShowWindowFromTray(string? tabTag = null)
+ {
+ this.ShowInTaskbar = true;
+ this.EnsureDashboardVisibleOnScreen();
+
+ if (!this.IsVisible)
+ {
+ this.Show();
+ }
+ else
+ {
+ this.Visibility = Visibility.Visible;
+ }
+
+ if (this.WindowState == WindowState.Minimized)
+ {
+ this.WindowState = WindowState.Normal;
+ }
+
+ this.EnsureDashboardVisibleOnScreen();
+ this.ShowInTaskbar = true;
+
+ if (tabTag != null)
+ {
+ this.SelectMainTab(tabTag);
+ }
+
+ // Force foreground restoration when invoked from tray context menu.
+ this.Topmost = true;
+ this.Activate();
+ this.Focus();
+ this.Topmost = false;
+ this.Activate();
+ this.Focus();
+
+ var processViewWillBeActive = tabTag == null
+ ? this.ProcessManagementTab.Visibility == Visibility.Visible
+ : string.Equals(tabTag, "Process", StringComparison.Ordinal);
+
+ this.ApplyAppRefreshPolicy(processViewWillBeActive
+ ? AppActivityState.ForegroundProcessView
+ : AppActivityState.ForegroundOtherTab);
+ }
+
+ internal bool EnsureDashboardVisibleOnScreen()
+ {
+ return WindowPlacementHelper.TryCorrectWindowPlacement(this);
+ }
+
+ private void SelectMainTab(string tag)
+ {
+ if (string.IsNullOrEmpty(tag))
+ {
+ return;
+ }
+
+ if (string.Equals(tag, "Performance", StringComparison.Ordinal))
+ {
+ this.GetPerformanceViewModel();
+ }
+
+ this.ApplySectionVisibility(tag);
+
+ if (string.Equals(tag, "Performance", StringComparison.Ordinal))
+ {
+ this.TryShowPerformanceIntro();
+ }
+ }
+
+ private void TryShowPerformanceIntro()
+ {
+ if (this.isPerformanceIntroVisible || !this.isInitializationComplete)
+ {
+ return;
+ }
+
+ try
+ {
+ var settings = this.settingsService.Settings;
+ if (settings.HasSeenPerformanceIntro)
+ {
+ return;
+ }
+
+ this.isPerformanceIntroVisible = true;
+ this.PerformanceIntroOverlay.Visibility = Visibility.Visible;
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to show Performance intro overlay: {ex.Message}");
+ }
+ }
+
+ private void TryShowStartupMinimizedSuggestion()
+ {
+ if (!this.showStartupMinimizedSuggestionOnReady
+ || this.isSilentStartupMode
+ || !this.isInitializationComplete
+ || this.isElevationWarningVisible)
+ {
+ return;
+ }
+
+ try
+ {
+ if (!StartupMinimizedSuggestionPolicy.ShouldShow(
+ this.settingsService.Settings,
+ StartupWindowBehavior.Resolve(isAutostart: false, startMinimized: false)))
+ {
+ return;
+ }
+
+ this.StartupMinimizedSuggestionOverlay.Visibility = Visibility.Visible;
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to show startup minimized suggestion: {ex.Message}");
+ }
+ }
+
+ private async Task PersistStartupMinimizedSuggestionSeenAsync()
+ {
+ try
+ {
+ var settings = this.settingsService.Settings;
+ if (settings.HasSeenStartupMinimizedSuggestion)
+ {
+ return;
+ }
+
+ settings.HasSeenStartupMinimizedSuggestion = true;
+ await this.settingsService.UpdateSettingsAsync(settings);
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to persist startup minimized suggestion state: {ex.Message}");
+ }
+ }
+
+ private void HideStartupMinimizedSuggestion()
+ {
+ this.showStartupMinimizedSuggestionOnReady = false;
+ this.StartupMinimizedSuggestionOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ private async void StartupSuggestionOpenSettings_Click(object sender, RoutedEventArgs e)
+ {
+ await this.PersistStartupMinimizedSuggestionSeenAsync();
+ this.HideStartupMinimizedSuggestion();
+ this.SelectMainTab("Settings");
+ }
+
+ private async void StartupSuggestionDontShowAgain_Click(object sender, RoutedEventArgs e)
+ {
+ await this.PersistStartupMinimizedSuggestionSeenAsync();
+ this.HideStartupMinimizedSuggestion();
+ }
+
+ private void HidePerformanceIntro()
+ {
+ this.isPerformanceIntroVisible = false;
+ this.PerformanceIntroOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ private Task ShowUnsavedSettingsDialogAsync(string message)
+ {
+ if (!this.Dispatcher.CheckAccess())
+ {
+ return this.Dispatcher.InvokeAsync(() => this.ShowUnsavedSettingsDialogAsync(message)).Task.Unwrap();
+ }
+
+ if (this.unsavedSettingsDialogCompletionSource != null)
+ {
+ return Task.FromResult(MessageBoxResult.Cancel);
+ }
+
+ this.UnsavedSettingsDialogMessage.Text = message;
+ this.unsavedSettingsDialogCompletionSource = new TaskCompletionSource(
+ TaskCreationOptions.RunContinuationsAsynchronously);
+ this.UnsavedSettingsOverlay.Visibility = Visibility.Visible;
+ return this.unsavedSettingsDialogCompletionSource.Task;
+ }
+
+ private void CompleteUnsavedSettingsDialog(MessageBoxResult result)
+ {
+ var completionSource = this.unsavedSettingsDialogCompletionSource;
+ if (completionSource == null)
+ {
+ return;
+ }
+
+ this.unsavedSettingsDialogCompletionSource = null;
+ this.UnsavedSettingsOverlay.Visibility = Visibility.Collapsed;
+ completionSource.TrySetResult(result);
+ }
+
+ private void UnsavedSettingsSave_Click(object sender, RoutedEventArgs e)
+ {
+ this.CompleteUnsavedSettingsDialog(MessageBoxResult.Yes);
+ }
+
+ private void UnsavedSettingsDiscard_Click(object sender, RoutedEventArgs e)
+ {
+ this.CompleteUnsavedSettingsDialog(MessageBoxResult.No);
+ }
+
+ private void UnsavedSettingsCancel_Click(object sender, RoutedEventArgs e)
+ {
+ this.CompleteUnsavedSettingsDialog(MessageBoxResult.Cancel);
+ }
+
+ private async void PerformanceIntroContinue_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var settings = this.settingsService.Settings;
+ if (!settings.HasSeenPerformanceIntro)
+ {
+ settings.HasSeenPerformanceIntro = true;
+ await this.settingsService.UpdateSettingsAsync(settings);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to persist Performance intro state: {ex.Message}");
+ }
+ finally
+ {
+ this.HidePerformanceIntro();
+ }
+ }
+
+ // Elevation Warning Modal Management
+ private bool isElevationWarningVisible = false;
+ private double previousElevationAppContentOpacity = 1;
+ private double previousElevationBackdropBlurRadius = 0;
+
+ private void TryShowElevationWarning()
+ {
+ if (this.isElevationWarningVisible || !this.isInitializationComplete)
+ {
+ return;
+ }
+
+ try
+ {
+ var settings = this.settingsService.Settings;
+
+ // Only show if user is not admin AND hasn't dismissed the warning yet
+ if (this.elevationService?.IsRunningAsAdministrator() == true || settings.HasSeenElevationWarning)
+ {
+ return;
+ }
+
+ this.isElevationWarningVisible = true;
+ var elevationOverlay = this.FindName("ElevationWarningOverlay") as Grid;
+ if (elevationOverlay != null)
+ {
+ elevationOverlay.Visibility = Visibility.Visible;
+ }
+
+ // Apply blur and disable interaction
+ this.previousElevationAppContentOpacity = this.UIContent.Opacity;
+ this.UIContent.IsHitTestVisible = false;
+ this.UIContent.Opacity = 0.74;
+
+ var elevationBlur = this.FindName("ElevationWarningBlur") as BlurEffect;
+ if (elevationBlur != null)
+ {
+ this.previousElevationBackdropBlurRadius = elevationBlur.Radius;
+ elevationBlur.Radius = 16;
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to show elevation warning overlay: {ex.Message}");
+ }
+ }
+
+ private void HideElevationWarning()
+ {
+ this.isElevationWarningVisible = false;
+ var elevationOverlay = this.FindName("ElevationWarningOverlay") as Grid;
+ if (elevationOverlay != null)
+ {
+ elevationOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ // Restore interaction and remove blur
+ this.UIContent.IsHitTestVisible = true;
+ this.UIContent.Opacity = this.previousElevationAppContentOpacity;
+
+ var elevationBlur = this.FindName("ElevationWarningBlur") as BlurEffect;
+ if (elevationBlur != null)
+ {
+ elevationBlur.Radius = this.previousElevationBackdropBlurRadius;
+ }
+
+ this.TryShowStartupMinimizedSuggestion();
+ }
+
+ private void ElevationWarningDismiss_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var settings = this.settingsService.Settings;
+ if (!settings.HasSeenElevationWarning)
+ {
+ settings.HasSeenElevationWarning = true;
+ _ = this.settingsService.UpdateSettingsAsync(settings);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to persist elevation warning dismiss state: {ex.Message}");
+ }
+ finally
+ {
+ this.HideElevationWarning();
+ }
+ }
+
+ private async void ElevationWarningRequestElevation_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (this.elevationService != null)
+ {
+ var success = await this.elevationService.RequestElevationIfNeeded();
+ if (success)
+ {
+ System.Diagnostics.Debug.WriteLine("Elevation requested successfully from warning dialog");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ this.LogDebug($"Failed to request elevation from warning dialog: {ex.Message}");
+ }
+ finally
+ {
+ // Hide the warning after attempting elevation (regardless of success)
+ this.HideElevationWarning();
+ }
+ }
+
+ private void ApplySectionVisibility(string tag)
+ {
+ this.ProcessManagementTab.Visibility = tag == "Process" ? Visibility.Visible : Visibility.Collapsed;
+ this.CoreMasksTab.Visibility = tag == "Masks" ? Visibility.Visible : Visibility.Collapsed;
+ this.PowerPlanViewControl.Visibility = tag == "Power" ? Visibility.Visible : Visibility.Collapsed;
+ this.AssociationView.Visibility = tag == "Rules" ? Visibility.Visible : Visibility.Collapsed;
+ this.PerformanceViewControl.Visibility = tag == "Performance" ? Visibility.Visible : Visibility.Collapsed;
+ this.LogViewerViewControl.Visibility = tag == "Logs" ? Visibility.Visible : Visibility.Collapsed;
+ this.SystemTweaksView.Visibility = tag == "Tweaks" ? Visibility.Visible : Visibility.Collapsed;
+ this.SettingsView.Visibility = tag == "Settings" ? Visibility.Visible : Visibility.Collapsed;
+
+ this.NavProcess.IsActive = tag == "Process";
+ this.NavMasks.IsActive = tag == "Masks";
+ this.NavPower.IsActive = tag == "Power";
+ this.NavRules.IsActive = tag == "Rules";
+ this.NavPerf.IsActive = tag == "Performance";
+ this.NavLogs.IsActive = tag == "Logs";
+ this.NavTweaks.IsActive = tag == "Tweaks";
+ this.NavSettings.IsActive = tag == "Settings";
+
+ if (!this.IsVisible)
+ {
+ this.ApplyAppRefreshPolicy(AppActivityState.TrayHidden);
+ return;
+ }
+
+ if (this.WindowState == WindowState.Minimized)
+ {
+ this.ApplyAppRefreshPolicy(AppActivityState.Minimized);
+ return;
+ }
+
+ var activityState = tag switch
+ {
+ "Process" => AppActivityState.ForegroundProcessView,
+ "Performance" => AppActivityState.ForegroundDiagnosticsView,
+ _ => AppActivityState.ForegroundOtherTab,
+ };
+
+ this.ApplyAppRefreshPolicy(activityState);
+ }
+
+ private void NavMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ TaskSafety.FireAndForget(this.NavMenuItem_ClickAsync(sender, e), ex =>
+ {
+ this.LogDebug($"NavMenuItem_Click failed: {ex.Message}");
+ });
+ }
+
+ private async Task NavMenuItem_ClickAsync(object sender, RoutedEventArgs e)
+ {
+ if (!await this.navigationBehavior.TryEnterAsync())
+ {
+ return;
+ }
+
+ try
+ {
+ var invokedItem = sender as Wpf.Ui.Controls.NavigationViewItem;
+ if (invokedItem == null)
+ {
+ return;
+ }
+
+ var tag = invokedItem.Tag?.ToString();
+ if (string.IsNullOrEmpty(tag))
+ {
+ return;
+ }
+
+ if (!this.IsLoaded)
+ {
+ return;
+ }
+
+ var canNavigate = await NavigationBehavior.EnsureCanNavigateAsync(
+ tag,
+ this.settingsViewModel,
+ () => this.ShowUnsavedSettingsDialogAsync(
+ "You have unsaved changes in Settings. Save before switching tabs, discard the changes, or cancel to stay on Settings."));
+ if (!canNavigate)
+ {
+ return;
+ }
+
+ if (string.Equals(tag, "Performance", StringComparison.Ordinal))
+ {
+ this.GetPerformanceViewModel();
+ }
+
+ this.ApplySectionVisibility(tag);
+
+ if (string.Equals(tag, "Performance", StringComparison.Ordinal))
+ {
+ this.TryShowPerformanceIntro();
+ }
+ }
+ finally
+ {
+ this.navigationBehavior.Exit();
+ }
+ }
+
+ protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
+ {
+ if (this.isPerformingShutdown)
+ {
+ base.OnClosing(e);
+ return;
+ }
+
+ if (this.isPerformanceIntroVisible)
+ {
+ e.Cancel = true;
+ System.Windows.MessageBox.Show(
+ "Please complete the Performance introduction before closing the application.\n\nClick 'Continue to Performance' to proceed.",
+ "Performance Introduction Required",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ return;
+ }
+
+ e.Cancel = true;
+ _ = this.HandleWindowCloseAsync();
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ try
+ {
+ this.Loaded -= this.OnWindowLoaded;
+ this.processViewModel.OpenRulesRequested -= this.OnOpenRulesRequested;
+
+ this.settingsService.SettingsChanged -= this.OnSettingsChanged;
+ this.processMonitorService.MonitoringStatusChanged -= this.OnMonitoringStatusChanged;
+ this.processMonitorManagerService.ServiceStatusChanged -= this.OnProcessMonitorManagerStatusChanged;
+ this.keyboardShortcutService.ShortcutActivated -= this.OnShortcutActivated;
+
+ this.UnsubscribeSystemTrayEvents();
+
+ this.systemTrayUpdateTimer?.Stop();
+ this.systemTrayUpdateTimer?.Dispose();
+
+ this.initializationTimeoutTimer?.Stop();
+ this.initializationTimeoutTimer?.Dispose();
+ this.performanceViewModel?.Dispose();
+
+ this.selfResourceManagementService.RestoreForegroundMode();
+ this.navigationBehavior.Dispose();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error disposing timers: {ex.Message}");
+ }
+
+ base.OnClosed(e);
+ }
+
+ private void UnsubscribeSystemTrayEvents()
+ {
+ this.systemTrayService.ShowMainWindowRequested -= this.OnShowMainWindowRequested;
+ this.systemTrayService.DashboardRequested -= this.OnDashboardRequested;
+ this.systemTrayService.ExitRequested -= this.OnExitRequested;
+ this.systemTrayService.MonitoringToggleRequested -= this.OnMonitoringToggleRequested;
+ this.systemTrayService.SettingsRequested -= this.OnSettingsRequested;
+ this.systemTrayService.PowerPlanChangeRequested -= this.OnPowerPlanChangeRequested;
+ this.systemTrayService.ProfileApplicationRequested -= this.OnProfileApplicationRequested;
+ this.systemTrayService.PerformanceDashboardRequested -= this.OnPerformanceDashboardRequested;
+ }
+ }
+}
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 22b9c91..eec3de9 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -1,216 +1,200 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot
-{
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Timers;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Input;
- using System.Windows.Media.Animation;
- using System.Windows.Media.Effects;
- using System.Windows.Media.Imaging;
- using Microsoft.Extensions.DependencyInjection;
- using ThreadPilot.Helpers;
- using ThreadPilot.Services;
- using ThreadPilot.ViewModels;
- using ThreadPilot.Views;
-
- public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
- {
- private const int SystemTrayUpdateBaseIntervalMs = 10000;
- private const int SystemTrayUpdateMaxIntervalMs = 60000;
-
- private readonly ProcessViewModel processViewModel;
- private readonly PowerPlanViewModel powerPlanViewModel;
- private readonly IDiagnosticsViewModelProvider diagnosticsViewModelProvider;
- private readonly ProcessPowerPlanAssociationViewModel associationViewModel;
- private readonly LogViewerViewModel logViewerViewModel;
- private readonly ISystemTrayService systemTrayService;
- private readonly ISystemTrayStatusUpdater systemTrayStatusUpdater;
- private readonly IApplicationSettingsService settingsService;
- private readonly INotificationService notificationService;
- private readonly IProcessMonitorService processMonitorService;
- private readonly IProcessMonitorManagerService processMonitorManagerService;
- private readonly IProcessPowerPlanAssociationService processPowerPlanAssociationService;
- private readonly SettingsViewModel settingsViewModel;
- private readonly MainWindowViewModel mainWindowViewModel;
- private readonly SystemTweaksViewModel systemTweaksViewModel;
- private readonly ISelfResourceManagementService selfResourceManagementService;
- private readonly IKeyboardShortcutService keyboardShortcutService;
- private readonly IServiceProvider serviceProvider;
- private readonly IThemeService themeService;
- private System.Timers.Timer? systemTrayUpdateTimer;
- private PerformanceViewModel? performanceViewModel;
- private bool isSystemTrayUpdatesSuspended;
- private int isSystemTrayUpdateInProgress;
- private int systemTrayUpdateFailureStreak;
- private int startupUpdateCheckStarted;
- private AppActivityState? lastAppliedRefreshState;
- private readonly IElevationService elevationService;
- private readonly ISecurityService securityService;
-
- // Loading overlay management
- private bool isInitializationComplete = false;
- private readonly List initializationTasks = new();
- private readonly object initializationLock = new();
- private System.Timers.Timer? initializationTimeoutTimer;
- private readonly string debugLogPath = Path.Combine(Path.GetTempPath(), "ThreadPilot_Debug.log");
-
- // Flag to skip process monitoring during startup if it causes issues
- private readonly bool skipProcessMonitoringDuringStartup = false;
- private bool isPerformingShutdown = false;
- private readonly NavigationBehavior navigationBehavior = new();
- private bool isPerformanceIntroVisible = false;
- private double previousAppContentOpacity = 1;
- private TaskCompletionSource? unsavedSettingsDialogCompletionSource;
- private bool isSilentStartupMode;
- private bool showStartupMinimizedSuggestionOnReady;
-
- public MainWindow(
- ProcessViewModel processViewModel,
- PowerPlanViewModel powerPlanViewModel,
- IDiagnosticsViewModelProvider diagnosticsViewModelProvider,
- ProcessPowerPlanAssociationViewModel associationViewModel,
- LogViewerViewModel logViewerViewModel,
- ISystemTrayService systemTrayService,
- ISystemTrayStatusUpdater systemTrayStatusUpdater,
- IApplicationSettingsService settingsService,
- INotificationService notificationService,
- IProcessMonitorService processMonitorService,
- IProcessMonitorManagerService processMonitorManagerService,
- IProcessPowerPlanAssociationService processPowerPlanAssociationService,
- SettingsViewModel settingsViewModel,
- MainWindowViewModel mainWindowViewModel,
- SystemTweaksViewModel systemTweaksViewModel,
- ISelfResourceManagementService selfResourceManagementService,
- IKeyboardShortcutService keyboardShortcutService,
- IThemeService themeService,
- IServiceProvider serviceProvider,
- IElevationService elevationService,
- ISecurityService securityService)
- {
- try
- {
- System.Diagnostics.Debug.WriteLine("MainWindow constructor starting...");
-
- this.InitializeComponent();
- System.Diagnostics.Debug.WriteLine("InitializeComponent completed");
- this.ConfigureDiagnosticsNavigation();
-
- // Initialize loading overlay
- this.InitializeLoadingOverlay();
- this.LogDebug("Loading overlay initialized");
- this.LogDebug($"Debug log file: {this.debugLogPath}");
-
- this.processViewModel = processViewModel;
- this.powerPlanViewModel = powerPlanViewModel;
- this.diagnosticsViewModelProvider = diagnosticsViewModelProvider;
- this.associationViewModel = associationViewModel;
- this.logViewerViewModel = logViewerViewModel;
- this.systemTrayService = systemTrayService;
- this.systemTrayStatusUpdater = systemTrayStatusUpdater;
- this.settingsService = settingsService;
- this.notificationService = notificationService;
- this.processMonitorService = processMonitorService;
- this.processMonitorManagerService = processMonitorManagerService;
- this.processPowerPlanAssociationService = processPowerPlanAssociationService;
- this.settingsViewModel = settingsViewModel;
- this.mainWindowViewModel = mainWindowViewModel;
- this.systemTweaksViewModel = systemTweaksViewModel;
- this.selfResourceManagementService = selfResourceManagementService;
- this.keyboardShortcutService = keyboardShortcutService;
- this.themeService = themeService;
- this.serviceProvider = serviceProvider;
- this.elevationService = elevationService;
- this.securityService = securityService;
-
- this.processViewModel.OpenRulesRequested += this.OnOpenRulesRequested;
-
- System.Diagnostics.Debug.WriteLine("Dependencies assigned");
-
- this.SetDataContexts();
- System.Diagnostics.Debug.WriteLine("DataContexts set");
-
- this.UpdateLoadingStatus("Starting ThreadPilot...", "Preparing startup sequence.");
-
- // Start async initialization - marshal to UI thread to prevent cross-thread access exceptions
- _ = this.Dispatcher.InvokeAsync(async () => await this.InitializeApplicationAsync());
- System.Diagnostics.Debug.WriteLine("Async initialization started");
- System.Diagnostics.Debug.WriteLine("MainWindow constructor completed successfully");
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error in MainWindow constructor: {ex}");
- System.Windows.MessageBox.Show(
- $"Error initializing MainWindow:\n\n{ex.Message}\n\nStack Trace:\n{ex.StackTrace}",
- "MainWindow Initialization Error", MessageBoxButton.OK, MessageBoxImage.Error);
- throw;
- }
- }
-
- public void ConfigureStartupMode(bool isSilentStartupMode, bool showStartupMinimizedSuggestionOnReady)
- {
- this.isSilentStartupMode = isSilentStartupMode;
- this.showStartupMinimizedSuggestionOnReady = showStartupMinimizedSuggestionOnReady;
-
- if (!isSilentStartupMode)
- {
- return;
- }
-
- this.showStartupMinimizedSuggestionOnReady = false;
- this.LoadingOverlay.Visibility = Visibility.Collapsed;
- this.ClearUIContentBlur();
-
- if (this.FindResource("SpinnerAnimation") is Storyboard spinnerAnimation)
- {
- spinnerAnimation.Stop();
- }
-
- this.isSystemTrayUpdatesSuspended = true;
- this.lastAppliedRefreshState = AppActivityState.TrayHidden;
- this.processViewModel.SetProcessViewActive(false);
- this.processViewModel.ApplyRefreshDecision(AppRefreshPolicy.Evaluate(AppActivityState.TrayHidden));
- this.powerPlanViewModel.PauseAutoRefresh();
- }
-
- private void ConfigureDiagnosticsNavigation()
- {
- this.NavPerf.Visibility = AppNavigationOptions.ShowAdvancedDiagnostics
- ? Visibility.Visible
- : Visibility.Collapsed;
- }
-
- private PerformanceViewModel GetPerformanceViewModel()
- {
- if (this.performanceViewModel != null)
- {
- return this.performanceViewModel;
- }
-
- this.performanceViewModel = this.diagnosticsViewModelProvider.GetOrCreate();
- this.PerformanceViewControl.DataContext = this.performanceViewModel;
- return this.performanceViewModel;
- }
- }
-}
+namespace ThreadPilot
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Timers;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Input;
+ using System.Windows.Media.Animation;
+ using System.Windows.Media.Effects;
+ using System.Windows.Media.Imaging;
+ using Microsoft.Extensions.DependencyInjection;
+ using ThreadPilot.Helpers;
+ using ThreadPilot.Services;
+ using ThreadPilot.ViewModels;
+ using ThreadPilot.Views;
+
+ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
+ {
+ private const int SystemTrayUpdateBaseIntervalMs = 10000;
+ private const int SystemTrayUpdateMaxIntervalMs = 60000;
+
+ private readonly ProcessViewModel processViewModel;
+ private readonly PowerPlanViewModel powerPlanViewModel;
+ private readonly Lazy performanceViewModelFactory;
+ private readonly ProcessPowerPlanAssociationViewModel associationViewModel;
+ private readonly LogViewerViewModel logViewerViewModel;
+ private readonly ISystemTrayService systemTrayService;
+ private readonly ISystemTrayStatusUpdater systemTrayStatusUpdater;
+ private readonly IApplicationSettingsService settingsService;
+ private readonly INotificationService notificationService;
+ private readonly IProcessMonitorService processMonitorService;
+ private readonly IProcessMonitorManagerService processMonitorManagerService;
+ private readonly IProcessPowerPlanAssociationService processPowerPlanAssociationService;
+ private readonly SettingsViewModel settingsViewModel;
+ private readonly MainWindowViewModel mainWindowViewModel;
+ private readonly SystemTweaksViewModel systemTweaksViewModel;
+ private readonly ISelfResourceManagementService selfResourceManagementService;
+ private readonly IKeyboardShortcutService keyboardShortcutService;
+ private readonly IServiceProvider serviceProvider;
+ private readonly IThemeService themeService;
+ private System.Timers.Timer? systemTrayUpdateTimer;
+ private PerformanceViewModel? performanceViewModel;
+ private bool isSystemTrayUpdatesSuspended;
+ private int isSystemTrayUpdateInProgress;
+ private int systemTrayUpdateFailureStreak;
+ private int startupUpdateCheckStarted;
+ private AppActivityState? lastAppliedRefreshState;
+ private readonly IElevationService elevationService;
+ private readonly ISecurityService securityService;
+
+ // Loading overlay management
+ private bool isInitializationComplete = false;
+ private readonly List initializationTasks = new();
+ private readonly object initializationLock = new();
+ private System.Timers.Timer? initializationTimeoutTimer;
+ private readonly string debugLogPath = Path.Combine(Path.GetTempPath(), "ThreadPilot_Debug.log");
+
+ // Flag to skip process monitoring during startup if it causes issues
+ private readonly bool skipProcessMonitoringDuringStartup = false;
+ private bool isPerformingShutdown = false;
+ private readonly NavigationBehavior navigationBehavior = new();
+ private bool isPerformanceIntroVisible = false;
+ private double previousAppContentOpacity = 1;
+ private TaskCompletionSource? unsavedSettingsDialogCompletionSource;
+ private bool isSilentStartupMode;
+ private bool showStartupMinimizedSuggestionOnReady;
+
+ public MainWindow(
+ ProcessViewModel processViewModel,
+ PowerPlanViewModel powerPlanViewModel,
+ Lazy performanceViewModelFactory,
+ ProcessPowerPlanAssociationViewModel associationViewModel,
+ LogViewerViewModel logViewerViewModel,
+ ISystemTrayService systemTrayService,
+ ISystemTrayStatusUpdater systemTrayStatusUpdater,
+ IApplicationSettingsService settingsService,
+ INotificationService notificationService,
+ IProcessMonitorService processMonitorService,
+ IProcessMonitorManagerService processMonitorManagerService,
+ IProcessPowerPlanAssociationService processPowerPlanAssociationService,
+ SettingsViewModel settingsViewModel,
+ MainWindowViewModel mainWindowViewModel,
+ SystemTweaksViewModel systemTweaksViewModel,
+ ISelfResourceManagementService selfResourceManagementService,
+ IKeyboardShortcutService keyboardShortcutService,
+ IThemeService themeService,
+ IServiceProvider serviceProvider,
+ IElevationService elevationService,
+ ISecurityService securityService)
+ {
+ try
+ {
+ System.Diagnostics.Debug.WriteLine("MainWindow constructor starting...");
+
+ this.InitializeComponent();
+ System.Diagnostics.Debug.WriteLine("InitializeComponent completed");
+ this.ConfigureDiagnosticsNavigation();
+
+ // Initialize loading overlay
+ this.InitializeLoadingOverlay();
+ this.LogDebug("Loading overlay initialized");
+ this.LogDebug($"Debug log file: {this.debugLogPath}");
+
+ this.processViewModel = processViewModel;
+ this.powerPlanViewModel = powerPlanViewModel;
+ this.performanceViewModelFactory = performanceViewModelFactory;
+ this.associationViewModel = associationViewModel;
+ this.logViewerViewModel = logViewerViewModel;
+ this.systemTrayService = systemTrayService;
+ this.systemTrayStatusUpdater = systemTrayStatusUpdater;
+ this.settingsService = settingsService;
+ this.notificationService = notificationService;
+ this.processMonitorService = processMonitorService;
+ this.processMonitorManagerService = processMonitorManagerService;
+ this.processPowerPlanAssociationService = processPowerPlanAssociationService;
+ this.settingsViewModel = settingsViewModel;
+ this.mainWindowViewModel = mainWindowViewModel;
+ this.systemTweaksViewModel = systemTweaksViewModel;
+ this.selfResourceManagementService = selfResourceManagementService;
+ this.keyboardShortcutService = keyboardShortcutService;
+ this.themeService = themeService;
+ this.serviceProvider = serviceProvider;
+ this.elevationService = elevationService;
+ this.securityService = securityService;
+
+ this.processViewModel.OpenRulesRequested += this.OnOpenRulesRequested;
+
+ System.Diagnostics.Debug.WriteLine("Dependencies assigned");
+
+ this.SetDataContexts();
+ System.Diagnostics.Debug.WriteLine("DataContexts set");
+
+ this.UpdateLoadingStatus("Starting ThreadPilot...", "Preparing startup sequence.");
+
+ // Start async initialization - marshal to UI thread to prevent cross-thread access exceptions
+ _ = this.Dispatcher.InvokeAsync(async () => await this.InitializeApplicationAsync());
+ System.Diagnostics.Debug.WriteLine("Async initialization started");
+ System.Diagnostics.Debug.WriteLine("MainWindow constructor completed successfully");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error in MainWindow constructor: {ex}");
+ System.Windows.MessageBox.Show(
+ $"Error initializing MainWindow:\n\n{ex.Message}\n\nStack Trace:\n{ex.StackTrace}",
+ "MainWindow Initialization Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ throw;
+ }
+ }
+
+ public void ConfigureStartupMode(bool isSilentStartupMode, bool showStartupMinimizedSuggestionOnReady)
+ {
+ this.isSilentStartupMode = isSilentStartupMode;
+ this.showStartupMinimizedSuggestionOnReady = showStartupMinimizedSuggestionOnReady;
+
+ if (!isSilentStartupMode)
+ {
+ return;
+ }
+
+ this.showStartupMinimizedSuggestionOnReady = false;
+ this.LoadingOverlay.Visibility = Visibility.Collapsed;
+ this.ClearUIContentBlur();
+
+ if (this.FindResource("SpinnerAnimation") is Storyboard spinnerAnimation)
+ {
+ spinnerAnimation.Stop();
+ }
+
+ this.isSystemTrayUpdatesSuspended = true;
+ this.lastAppliedRefreshState = AppActivityState.TrayHidden;
+ this.processViewModel.SetProcessViewActive(false);
+ this.processViewModel.ApplyRefreshDecision(AppRefreshPolicy.Evaluate(AppActivityState.TrayHidden));
+ this.powerPlanViewModel.PauseAutoRefresh();
+ }
+
+ private void ConfigureDiagnosticsNavigation()
+ {
+ this.NavPerf.Visibility = AppNavigationOptions.ShowAdvancedDiagnostics
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ }
+
+ private PerformanceViewModel GetPerformanceViewModel()
+ {
+ if (this.performanceViewModel != null)
+ {
+ return this.performanceViewModel;
+ }
+
+ this.performanceViewModel = this.performanceViewModelFactory.Value;
+ this.PerformanceViewControl.DataContext = this.performanceViewModel;
+ return this.performanceViewModel;
+ }
+ }
+}
diff --git a/Models/ApplicationSettingsModel.cs b/Models/ApplicationSettingsModel.cs
index 3219d8a..c3fb5d0 100644
--- a/Models/ApplicationSettingsModel.cs
+++ b/Models/ApplicationSettingsModel.cs
@@ -1,400 +1,359 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Models
-{
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Text.Json;
- using CommunityToolkit.Mvvm.ComponentModel;
- using ThreadPilot.Models.Core;
- using ThreadPilot.Services;
-
- ///
- /// Model for application settings including notifications and tray preferences.
- ///
- public partial class ApplicationSettingsModel : ObservableObject, IModel
- {
- private static readonly JsonSerializerOptions UserSettingsComparisonJsonOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- };
-
- [ObservableProperty]
- private string id = "ApplicationSettings"; // Singleton settings
-
- [ObservableProperty]
- private DateTime createdAt = DateTime.UtcNow;
-
- [ObservableProperty]
- private DateTime updatedAt = DateTime.UtcNow;
-
- [ObservableProperty]
- private bool enableNotifications = true;
-
- [ObservableProperty]
- private NotificationLevelProfile notificationLevel = NotificationLevelProfile.All;
-
- [ObservableProperty]
- private bool enableBalloonNotifications = true;
-
- [ObservableProperty]
- private bool enableToastNotifications = true;
-
- [ObservableProperty]
- private bool enablePowerPlanChangeNotifications = true;
-
- [ObservableProperty]
- private bool enableProcessMonitoringNotifications = true;
-
- [ObservableProperty]
- private bool enableErrorNotifications = true;
-
- [ObservableProperty]
- private bool enableSuccessNotifications = true;
-
- [ObservableProperty]
- private bool minimizeToTray = true;
-
- [ObservableProperty]
- private bool closeToTray = true; // Default true: close to tray like CPU Set Setter
-
- [ObservableProperty]
- private bool startMinimized = false;
-
- [ObservableProperty]
- private bool showTrayIcon = true;
-
- [ObservableProperty]
- private bool enableQuickApplyFromTray = true;
-
- [ObservableProperty]
- private bool enableMonitoringControlFromTray = true;
-
- [ObservableProperty]
- private int notificationDisplayDurationMs = 3000;
-
- [ObservableProperty]
- private int balloonNotificationTimeoutMs = 5000;
-
- [ObservableProperty]
- private NotificationPosition notificationPosition = NotificationPosition.BottomRight;
-
- [ObservableProperty]
- private NotificationSound notificationSound = NotificationSound.Default;
-
- [ObservableProperty]
- private bool enableNotificationSound = false;
-
- [ObservableProperty]
- private string customTrayIconPath = string.Empty;
-
- [ObservableProperty]
- private bool useCustomTrayIcon = false;
-
- [ObservableProperty]
- private TrayIconStyle trayIconStyle = TrayIconStyle.Default;
-
- [ObservableProperty]
- private bool showDetailedTooltips = true;
-
- [ObservableProperty]
- private bool enableContextMenuAnimations = true;
-
- [ObservableProperty]
- private bool autoHideNotifications = true;
-
- [ObservableProperty]
- private bool enableNotificationHistory = true;
-
- [ObservableProperty]
- private int maxNotificationHistoryItems = 50;
-
- // Autostart Settings
- [ObservableProperty]
- private bool autostartWithWindows = true;
-
- // Power Plan Settings
- [ObservableProperty]
- private string defaultPowerPlanId = string.Empty;
-
- [ObservableProperty]
- private string defaultPowerPlanName = "Balanced";
-
- [ObservableProperty]
- private bool restoreDefaultPowerPlanOnExit = true;
-
- ///
- /// When true, all applied CPU masks are cleared when exiting the application
- /// (processes return to using all cores).
- ///
- [ObservableProperty]
- private bool clearMasksOnClose = true;
-
- [ObservableProperty]
- private bool useDarkTheme = false;
-
- [ObservableProperty]
- private bool hasUserThemePreference = false;
-
- [ObservableProperty]
- private string language = LocalizationService.DefaultLanguage;
-
- [ObservableProperty]
- private bool enableAutomaticUpdateChecks = true;
-
- [ObservableProperty]
- private DateTimeOffset? lastUpdateCheckUtc = null;
-
- [ObservableProperty]
- private int updateCheckIntervalDays = 7;
-
- [ObservableProperty]
- private bool includePrereleaseUpdates = false;
-
- // Monitoring Settings
- [ObservableProperty]
- private int pollingIntervalMs = 5000;
-
- [ObservableProperty]
- private int fallbackPollingIntervalMs = 10000;
-
- [ObservableProperty]
- private bool enableWmiMonitoring = true;
-
- [ObservableProperty]
- private bool enableFallbackPolling = true;
-
- [ObservableProperty]
- private bool applyPersistentRulesOnProcessStart = true;
-
- // Advanced Settings
- [ObservableProperty]
- private bool enableDebugLogging = false;
-
- [ObservableProperty]
- private bool enablePerformanceCounters = false;
-
- [ObservableProperty]
- private bool hasSeenPerformanceIntro = false;
-
- [ObservableProperty]
- private bool hasSeenElevationWarning = false;
-
- [ObservableProperty]
- private bool hasSeenStartupMinimizedSuggestion = false;
-
- [ObservableProperty]
- private bool enableSelfLowImpactMode = true;
-
- [ObservableProperty]
- private bool enableSelfAffinityLimit = false;
-
- [ObservableProperty]
- private int maxLogFileSizeMb = 10;
-
- [ObservableProperty]
- private int logRetentionDays = 7;
-
- ///
- /// Keyboard shortcuts configuration.
- ///
- [ObservableProperty]
- private List keyboardShortcuts = new();
-
- ///
- /// Copies settings from another instance.
- ///
- public void CopyFrom(ApplicationSettingsModel other)
- {
- if (other == null)
- {
- return;
- }
-
- this.EnableNotifications = other.EnableNotifications;
- this.NotificationLevel = other.NotificationLevel;
- this.EnableBalloonNotifications = other.EnableBalloonNotifications;
- this.EnableToastNotifications = other.EnableToastNotifications;
- this.EnablePowerPlanChangeNotifications = other.EnablePowerPlanChangeNotifications;
- this.EnableProcessMonitoringNotifications = other.EnableProcessMonitoringNotifications;
- this.EnableErrorNotifications = other.EnableErrorNotifications;
- this.EnableSuccessNotifications = other.EnableSuccessNotifications;
- this.MinimizeToTray = other.MinimizeToTray;
- this.CloseToTray = other.CloseToTray;
- this.StartMinimized = other.StartMinimized;
- this.ShowTrayIcon = other.ShowTrayIcon;
- this.EnableQuickApplyFromTray = other.EnableQuickApplyFromTray;
- this.EnableMonitoringControlFromTray = other.EnableMonitoringControlFromTray;
- this.NotificationDisplayDurationMs = other.NotificationDisplayDurationMs;
- this.BalloonNotificationTimeoutMs = other.BalloonNotificationTimeoutMs;
- this.NotificationPosition = other.NotificationPosition;
- this.NotificationSound = other.NotificationSound;
- this.EnableNotificationSound = other.EnableNotificationSound;
- this.CustomTrayIconPath = other.CustomTrayIconPath;
- this.UseCustomTrayIcon = other.UseCustomTrayIcon;
- this.TrayIconStyle = other.TrayIconStyle;
- this.ShowDetailedTooltips = other.ShowDetailedTooltips;
- this.EnableContextMenuAnimations = other.EnableContextMenuAnimations;
- this.AutoHideNotifications = other.AutoHideNotifications;
- this.EnableNotificationHistory = other.EnableNotificationHistory;
- this.MaxNotificationHistoryItems = other.MaxNotificationHistoryItems;
-
- // Autostart Settings
- this.AutostartWithWindows = other.AutostartWithWindows;
-
- // Power Plan Settings
- this.DefaultPowerPlanId = other.DefaultPowerPlanId;
- this.DefaultPowerPlanName = other.DefaultPowerPlanName;
- this.RestoreDefaultPowerPlanOnExit = other.RestoreDefaultPowerPlanOnExit;
- this.ClearMasksOnClose = other.ClearMasksOnClose;
- this.UseDarkTheme = other.UseDarkTheme;
- this.HasUserThemePreference = other.HasUserThemePreference;
- this.Language = LocalizationService.NormalizeLanguage(other.Language);
- this.EnableAutomaticUpdateChecks = other.EnableAutomaticUpdateChecks;
- this.LastUpdateCheckUtc = other.LastUpdateCheckUtc;
- this.UpdateCheckIntervalDays = other.UpdateCheckIntervalDays;
- this.IncludePrereleaseUpdates = other.IncludePrereleaseUpdates;
-
- // Monitoring Settings
- this.PollingIntervalMs = other.PollingIntervalMs;
- this.FallbackPollingIntervalMs = other.FallbackPollingIntervalMs;
- this.EnableWmiMonitoring = other.EnableWmiMonitoring;
- this.EnableFallbackPolling = other.EnableFallbackPolling;
- this.ApplyPersistentRulesOnProcessStart = other.ApplyPersistentRulesOnProcessStart;
-
- // Advanced Settings
- this.EnableDebugLogging = other.EnableDebugLogging;
- this.EnablePerformanceCounters = other.EnablePerformanceCounters;
- this.HasSeenPerformanceIntro = other.HasSeenPerformanceIntro;
- this.HasSeenElevationWarning = other.HasSeenElevationWarning;
- this.HasSeenStartupMinimizedSuggestion = other.HasSeenStartupMinimizedSuggestion;
- this.EnableSelfLowImpactMode = other.EnableSelfLowImpactMode;
- this.EnableSelfAffinityLimit = other.EnableSelfAffinityLimit;
- this.MaxLogFileSizeMb = other.MaxLogFileSizeMb;
- this.LogRetentionDays = other.LogRetentionDays;
-
- // Keyboard Shortcuts
- this.KeyboardShortcuts = other.KeyboardShortcuts != null
- ? new List(other.KeyboardShortcuts)
- : new List();
- }
-
- // IModel implementation - properties are auto-generated by ObservableProperty
- public ValidationResult Validate()
- {
- var errors = new List();
-
- if (this.NotificationDisplayDurationMs < 1000 || this.NotificationDisplayDurationMs > 30000)
- {
- errors.Add("Notification display duration must be between 1 and 30 seconds");
- }
-
- if (this.PollingIntervalMs < 1000 || this.PollingIntervalMs > 60000)
- {
- errors.Add("Process polling interval must be between 1 and 60 seconds");
- }
-
- if (this.FallbackPollingIntervalMs < 1000 || this.FallbackPollingIntervalMs > 60000)
- {
- errors.Add("Fallback polling interval must be between 1 and 60 seconds");
- }
-
- if (this.UpdateCheckIntervalDays < 1 || this.UpdateCheckIntervalDays > 365)
- {
- errors.Add("Update check interval must be between 1 and 365 days");
- }
-
- return errors.Count == 0 ? ValidationResult.Success() : ValidationResult.Failure(errors.ToArray());
- }
-
- public IModel Clone()
- {
- var clone = new ApplicationSettingsModel();
- clone.CopyFrom(this);
- clone.Id = this.Id;
- clone.CreatedAt = this.CreatedAt;
- clone.UpdatedAt = this.UpdatedAt;
- return clone;
- }
-
- public bool HasSameUserSettingsAs(ApplicationSettingsModel? other)
- {
- if (other == null)
- {
- return false;
- }
-
- var currentSnapshot = (ApplicationSettingsModel)this.Clone();
- var otherSnapshot = (ApplicationSettingsModel)other.Clone();
-
- currentSnapshot.Id = otherSnapshot.Id;
- currentSnapshot.CreatedAt = otherSnapshot.CreatedAt;
- currentSnapshot.UpdatedAt = otherSnapshot.UpdatedAt;
-
- var currentJson = JsonSerializer.Serialize(currentSnapshot, UserSettingsComparisonJsonOptions);
- var otherJson = JsonSerializer.Serialize(otherSnapshot, UserSettingsComparisonJsonOptions);
- return string.Equals(currentJson, otherJson, StringComparison.Ordinal);
- }
- }
-
- ///
- /// Notification level profile options.
- ///
- public enum NotificationLevelProfile
- {
- All,
- WarningsAndErrorsOnly,
- Silent,
- }
-
- ///
- /// Notification position options.
- ///
- public enum NotificationPosition
- {
- TopLeft,
- TopRight,
- BottomLeft,
- BottomRight,
- Center,
- }
-
- ///
- /// Notification sound options.
- ///
- public enum NotificationSound
- {
- None,
- Default,
- Information,
- Warning,
- Error,
- Custom,
- }
-
- ///
- /// Tray icon style options.
- ///
- public enum TrayIconStyle
- {
- Default,
- Monochrome,
- Colored,
- Custom,
- }
-}
+namespace ThreadPilot.Models
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Text.Json;
+ using CommunityToolkit.Mvvm.ComponentModel;
+ using ThreadPilot.Models.Core;
+ using ThreadPilot.Services;
+
+ public partial class ApplicationSettingsModel : ObservableObject, IModel
+ {
+ private static readonly JsonSerializerOptions UserSettingsComparisonJsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ [ObservableProperty]
+ private string id = "ApplicationSettings"; // Singleton settings
+
+ [ObservableProperty]
+ private DateTime createdAt = DateTime.UtcNow;
+
+ [ObservableProperty]
+ private DateTime updatedAt = DateTime.UtcNow;
+
+ [ObservableProperty]
+ private bool enableNotifications = true;
+
+ [ObservableProperty]
+ private NotificationLevelProfile notificationLevel = NotificationLevelProfile.All;
+
+ [ObservableProperty]
+ private bool enableBalloonNotifications = true;
+
+ [ObservableProperty]
+ private bool enableToastNotifications = true;
+
+ [ObservableProperty]
+ private bool enablePowerPlanChangeNotifications = true;
+
+ [ObservableProperty]
+ private bool enableProcessMonitoringNotifications = true;
+
+ [ObservableProperty]
+ private bool enableErrorNotifications = true;
+
+ [ObservableProperty]
+ private bool enableSuccessNotifications = true;
+
+ [ObservableProperty]
+ private bool minimizeToTray = true;
+
+ [ObservableProperty]
+ private bool closeToTray = true; // Default true: close to tray like CPU Set Setter
+
+ [ObservableProperty]
+ private bool startMinimized = false;
+
+ [ObservableProperty]
+ private bool showTrayIcon = true;
+
+ [ObservableProperty]
+ private bool enableQuickApplyFromTray = true;
+
+ [ObservableProperty]
+ private bool enableMonitoringControlFromTray = true;
+
+ [ObservableProperty]
+ private int notificationDisplayDurationMs = 3000;
+
+ [ObservableProperty]
+ private int balloonNotificationTimeoutMs = 5000;
+
+ [ObservableProperty]
+ private NotificationPosition notificationPosition = NotificationPosition.BottomRight;
+
+ [ObservableProperty]
+ private NotificationSound notificationSound = NotificationSound.Default;
+
+ [ObservableProperty]
+ private bool enableNotificationSound = false;
+
+ [ObservableProperty]
+ private string customTrayIconPath = string.Empty;
+
+ [ObservableProperty]
+ private bool useCustomTrayIcon = false;
+
+ [ObservableProperty]
+ private TrayIconStyle trayIconStyle = TrayIconStyle.Default;
+
+ [ObservableProperty]
+ private bool showDetailedTooltips = true;
+
+ [ObservableProperty]
+ private bool enableContextMenuAnimations = true;
+
+ [ObservableProperty]
+ private bool autoHideNotifications = true;
+
+ [ObservableProperty]
+ private bool enableNotificationHistory = true;
+
+ [ObservableProperty]
+ private int maxNotificationHistoryItems = 50;
+
+ // Autostart Settings
+ [ObservableProperty]
+ private bool autostartWithWindows = true;
+
+ // Power Plan Settings
+ [ObservableProperty]
+ private string defaultPowerPlanId = string.Empty;
+
+ [ObservableProperty]
+ private string defaultPowerPlanName = "Balanced";
+
+ [ObservableProperty]
+ private bool restoreDefaultPowerPlanOnExit = true;
+
+ [ObservableProperty]
+ private bool clearMasksOnClose = true;
+
+ [ObservableProperty]
+ private bool useDarkTheme = false;
+
+ [ObservableProperty]
+ private bool hasUserThemePreference = false;
+
+ [ObservableProperty]
+ private string language = LocalizationService.DefaultLanguage;
+
+ [ObservableProperty]
+ private bool enableAutomaticUpdateChecks = true;
+
+ [ObservableProperty]
+ private DateTimeOffset? lastUpdateCheckUtc = null;
+
+ [ObservableProperty]
+ private int updateCheckIntervalDays = 7;
+
+ [ObservableProperty]
+ private bool includePrereleaseUpdates = false;
+
+ // Monitoring Settings
+ [ObservableProperty]
+ private int pollingIntervalMs = 5000;
+
+ [ObservableProperty]
+ private int fallbackPollingIntervalMs = 10000;
+
+ [ObservableProperty]
+ private bool enableWmiMonitoring = true;
+
+ [ObservableProperty]
+ private bool enableFallbackPolling = true;
+
+ [ObservableProperty]
+ private bool applyPersistentRulesOnProcessStart = true;
+
+ // Advanced Settings
+ [ObservableProperty]
+ private bool enableDebugLogging = false;
+
+ [ObservableProperty]
+ private bool enablePerformanceCounters = false;
+
+ [ObservableProperty]
+ private bool hasSeenPerformanceIntro = false;
+
+ [ObservableProperty]
+ private bool hasSeenElevationWarning = false;
+
+ [ObservableProperty]
+ private bool hasSeenStartupMinimizedSuggestion = false;
+
+ [ObservableProperty]
+ private bool enableSelfLowImpactMode = true;
+
+ [ObservableProperty]
+ private bool enableSelfAffinityLimit = false;
+
+ [ObservableProperty]
+ private int maxLogFileSizeMb = 10;
+
+ [ObservableProperty]
+ private int logRetentionDays = 7;
+
+ [ObservableProperty]
+ private List keyboardShortcuts = new();
+
+ public void CopyFrom(ApplicationSettingsModel other)
+ {
+ if (other == null)
+ {
+ return;
+ }
+
+ this.EnableNotifications = other.EnableNotifications;
+ this.NotificationLevel = other.NotificationLevel;
+ this.EnableBalloonNotifications = other.EnableBalloonNotifications;
+ this.EnableToastNotifications = other.EnableToastNotifications;
+ this.EnablePowerPlanChangeNotifications = other.EnablePowerPlanChangeNotifications;
+ this.EnableProcessMonitoringNotifications = other.EnableProcessMonitoringNotifications;
+ this.EnableErrorNotifications = other.EnableErrorNotifications;
+ this.EnableSuccessNotifications = other.EnableSuccessNotifications;
+ this.MinimizeToTray = other.MinimizeToTray;
+ this.CloseToTray = other.CloseToTray;
+ this.StartMinimized = other.StartMinimized;
+ this.ShowTrayIcon = other.ShowTrayIcon;
+ this.EnableQuickApplyFromTray = other.EnableQuickApplyFromTray;
+ this.EnableMonitoringControlFromTray = other.EnableMonitoringControlFromTray;
+ this.NotificationDisplayDurationMs = other.NotificationDisplayDurationMs;
+ this.BalloonNotificationTimeoutMs = other.BalloonNotificationTimeoutMs;
+ this.NotificationPosition = other.NotificationPosition;
+ this.NotificationSound = other.NotificationSound;
+ this.EnableNotificationSound = other.EnableNotificationSound;
+ this.CustomTrayIconPath = other.CustomTrayIconPath;
+ this.UseCustomTrayIcon = other.UseCustomTrayIcon;
+ this.TrayIconStyle = other.TrayIconStyle;
+ this.ShowDetailedTooltips = other.ShowDetailedTooltips;
+ this.EnableContextMenuAnimations = other.EnableContextMenuAnimations;
+ this.AutoHideNotifications = other.AutoHideNotifications;
+ this.EnableNotificationHistory = other.EnableNotificationHistory;
+ this.MaxNotificationHistoryItems = other.MaxNotificationHistoryItems;
+
+ // Autostart Settings
+ this.AutostartWithWindows = other.AutostartWithWindows;
+
+ // Power Plan Settings
+ this.DefaultPowerPlanId = other.DefaultPowerPlanId;
+ this.DefaultPowerPlanName = other.DefaultPowerPlanName;
+ this.RestoreDefaultPowerPlanOnExit = other.RestoreDefaultPowerPlanOnExit;
+ this.ClearMasksOnClose = other.ClearMasksOnClose;
+ this.UseDarkTheme = other.UseDarkTheme;
+ this.HasUserThemePreference = other.HasUserThemePreference;
+ this.Language = LocalizationService.NormalizeLanguage(other.Language);
+ this.EnableAutomaticUpdateChecks = other.EnableAutomaticUpdateChecks;
+ this.LastUpdateCheckUtc = other.LastUpdateCheckUtc;
+ this.UpdateCheckIntervalDays = other.UpdateCheckIntervalDays;
+ this.IncludePrereleaseUpdates = other.IncludePrereleaseUpdates;
+
+ // Monitoring Settings
+ this.PollingIntervalMs = other.PollingIntervalMs;
+ this.FallbackPollingIntervalMs = other.FallbackPollingIntervalMs;
+ this.EnableWmiMonitoring = other.EnableWmiMonitoring;
+ this.EnableFallbackPolling = other.EnableFallbackPolling;
+ this.ApplyPersistentRulesOnProcessStart = other.ApplyPersistentRulesOnProcessStart;
+
+ // Advanced Settings
+ this.EnableDebugLogging = other.EnableDebugLogging;
+ this.EnablePerformanceCounters = other.EnablePerformanceCounters;
+ this.HasSeenPerformanceIntro = other.HasSeenPerformanceIntro;
+ this.HasSeenElevationWarning = other.HasSeenElevationWarning;
+ this.HasSeenStartupMinimizedSuggestion = other.HasSeenStartupMinimizedSuggestion;
+ this.EnableSelfLowImpactMode = other.EnableSelfLowImpactMode;
+ this.EnableSelfAffinityLimit = other.EnableSelfAffinityLimit;
+ this.MaxLogFileSizeMb = other.MaxLogFileSizeMb;
+ this.LogRetentionDays = other.LogRetentionDays;
+
+ // Keyboard Shortcuts
+ this.KeyboardShortcuts = other.KeyboardShortcuts != null
+ ? new List(other.KeyboardShortcuts)
+ : new List();
+ }
+
+ // IModel implementation - properties are auto-generated by ObservableProperty
+ public ValidationResult Validate()
+ {
+ var errors = new List();
+
+ if (this.NotificationDisplayDurationMs < 1000 || this.NotificationDisplayDurationMs > 30000)
+ {
+ errors.Add("Notification display duration must be between 1 and 30 seconds");
+ }
+
+ if (this.PollingIntervalMs < 1000 || this.PollingIntervalMs > 60000)
+ {
+ errors.Add("Process polling interval must be between 1 and 60 seconds");
+ }
+
+ if (this.FallbackPollingIntervalMs < 1000 || this.FallbackPollingIntervalMs > 60000)
+ {
+ errors.Add("Fallback polling interval must be between 1 and 60 seconds");
+ }
+
+ if (this.UpdateCheckIntervalDays < 1 || this.UpdateCheckIntervalDays > 365)
+ {
+ errors.Add("Update check interval must be between 1 and 365 days");
+ }
+
+ return errors.Count == 0 ? ValidationResult.Success() : ValidationResult.Failure(errors.ToArray());
+ }
+
+ public IModel Clone()
+ {
+ var clone = new ApplicationSettingsModel();
+ clone.CopyFrom(this);
+ clone.Id = this.Id;
+ clone.CreatedAt = this.CreatedAt;
+ clone.UpdatedAt = this.UpdatedAt;
+ return clone;
+ }
+
+ public bool HasSameUserSettingsAs(ApplicationSettingsModel? other)
+ {
+ if (other == null)
+ {
+ return false;
+ }
+
+ var currentSnapshot = (ApplicationSettingsModel)this.Clone();
+ var otherSnapshot = (ApplicationSettingsModel)other.Clone();
+
+ currentSnapshot.Id = otherSnapshot.Id;
+ currentSnapshot.CreatedAt = otherSnapshot.CreatedAt;
+ currentSnapshot.UpdatedAt = otherSnapshot.UpdatedAt;
+
+ var currentJson = JsonSerializer.Serialize(currentSnapshot, UserSettingsComparisonJsonOptions);
+ var otherJson = JsonSerializer.Serialize(otherSnapshot, UserSettingsComparisonJsonOptions);
+ return string.Equals(currentJson, otherJson, StringComparison.Ordinal);
+ }
+ }
+
+ public enum NotificationLevelProfile
+ {
+ All,
+ WarningsAndErrorsOnly,
+ Silent,
+ }
+
+ public enum NotificationPosition
+ {
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight,
+ Center,
+ }
+
+ public enum NotificationSound
+ {
+ None,
+ Default,
+ Information,
+ Warning,
+ Error,
+ Custom,
+ }
+
+ public enum TrayIconStyle
+ {
+ Default,
+ Monochrome,
+ Colored,
+ Custom,
+ }
+}
diff --git a/Models/ConditionalProcessProfile.cs b/Models/ConditionalProcessProfile.cs
index da5c406..21e2ead 100644
--- a/Models/ConditionalProcessProfile.cs
+++ b/Models/ConditionalProcessProfile.cs
@@ -1,325 +1,273 @@
-/*
- * ThreadPilot - Advanced Windows Process and Power Plan Manager
- * Copyright (C) 2025 Prime Build
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, version 3 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-namespace ThreadPilot.Models
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using CommunityToolkit.Mvvm.ComponentModel;
-
- ///
- /// Condition types for profile triggers.
- ///
- public enum ProfileConditionType
- {
- SystemLoad,
- TimeOfDay,
- PowerState,
- ProcessCount,
- MemoryUsage,
- CpuTemperature,
- BatteryLevel,
- NetworkActivity,
- UserIdle,
- Custom,
- }
-
- ///
- /// Comparison operators for conditions.
- ///
- public enum ComparisonOperator
- {
- Equals,
- NotEquals,
- GreaterThan,
- LessThan,
- GreaterThanOrEqual,
- LessThanOrEqual,
- Contains,
- NotContains,
- Between,
- NotBetween,
- }
-
- ///
- /// Logical operators for combining conditions.
- ///
- public enum LogicalOperator
- {
- And,
- Or,
- Not,
- }
-
- ///
- /// System state information for condition evaluation.
- ///
- public class SystemState
- {
- public double CpuUsage { get; set; }
-
- public double MemoryUsage { get; set; }
-
- public int ProcessCount { get; set; }
-
- public DateTime CurrentTime { get; set; } = DateTime.Now;
-
- public bool IsOnBattery { get; set; }
-
- public int BatteryLevel { get; set; }
-
- public double CpuTemperature { get; set; }
-
- public bool IsUserIdle { get; set; }
-
- public TimeSpan UserIdleTime { get; set; }
-
- public double NetworkActivity { get; set; }
-
- public Dictionary CustomProperties { get; set; } = new();
- }
-
- ///
- /// Individual condition for profile evaluation.
- ///
- public partial class ProfileCondition : ObservableObject
- {
- [ObservableProperty]
- private string name = string.Empty;
-
- [ObservableProperty]
- private ProfileConditionType conditionType;
-
- [ObservableProperty]
- private ComparisonOperator comparisonOperator;
-
- [ObservableProperty]
- private object? value;
-
- [ObservableProperty]
- private object? secondaryValue; // For Between/NotBetween operations
-
- [ObservableProperty]
- private bool isEnabled = true;
-
- [ObservableProperty]
- private string description = string.Empty;
-
- ///
- /// Evaluate this condition against the current system state.
- ///
- public bool Evaluate(ProcessModel process, SystemState systemState)
- {
- if (!this.IsEnabled)
- {
- return true; // Disabled conditions are considered true
- }
-
- try
- {
- var actualValue = this.GetActualValue(process, systemState);
- return CompareValues(actualValue, this.Value, this.SecondaryValue, this.ComparisonOperator);
- }
- catch (Exception)
- {
- return false; // Failed conditions are considered false
- }
- }
-
- private object? GetActualValue(ProcessModel process, SystemState systemState)
- {
- return this.ConditionType switch
- {
- ProfileConditionType.SystemLoad => systemState.CpuUsage,
- ProfileConditionType.TimeOfDay => systemState.CurrentTime.TimeOfDay.TotalHours,
- ProfileConditionType.PowerState => systemState.IsOnBattery,
- ProfileConditionType.ProcessCount => systemState.ProcessCount,
- ProfileConditionType.MemoryUsage => systemState.MemoryUsage,
- ProfileConditionType.CpuTemperature => systemState.CpuTemperature,
- ProfileConditionType.BatteryLevel => systemState.BatteryLevel,
- ProfileConditionType.NetworkActivity => systemState.NetworkActivity,
- ProfileConditionType.UserIdle => systemState.IsUserIdle,
- ProfileConditionType.Custom => systemState.CustomProperties.GetValueOrDefault(this.Name),
- _ => null,
- };
- }
-
- private static bool CompareValues(object? actual, object? expected, object? secondary, ComparisonOperator op)
- {
- if (actual == null || expected == null)
- {
- return false;
- }
-
- return op switch
- {
- ComparisonOperator.Equals => actual.Equals(expected),
- ComparisonOperator.NotEquals => !actual.Equals(expected),
- ComparisonOperator.GreaterThan => Comparer