From 0af7d3a96bd52b2b888460c5ad6935b6a121e910 Mon Sep 17 00:00:00 2001 From: ognjeeen Date: Thu, 27 Aug 2026 15:49:39 +0200 Subject: [PATCH 1/6] feat: add live settings preferences --- README.md | 17 +- docs/ARCHITECTURE.md | 9 +- src/CodexUsageWidget/App.xaml.cs | 11 +- .../Infrastructure/AppPaths.cs | 2 + .../Infrastructure/Settings/EffectiveTheme.cs | 7 + .../Settings/ThemePreference.cs | 8 + .../Settings/ThemePreferenceMonitor.cs | 71 +++ .../Settings/ThemePreferenceResolver.cs | 13 + .../Settings/ThemePreferenceStore.cs | 56 +++ .../Infrastructure/Windows/TrayIconService.cs | 123 +++-- .../Windows/WindowsThemeMonitor.cs | 42 ++ .../Views/ActivityHookChangeReviewWindow.xaml | 6 +- .../Views/ActivityHookSetupWindow.xaml | 16 +- .../Views/AppThemeController.cs | 166 +++++++ .../Views/Controls/CompactUsageView.xaml | 6 +- .../Views/Controls/DetailedUsageView.xaml | 22 +- .../Views/Controls/UsageLimitRow.xaml | 6 +- src/CodexUsageWidget/Views/MainWindow.xaml | 13 +- src/CodexUsageWidget/Views/MainWindow.xaml.cs | 105 ++++- .../Views/Resources/TaskbarMenuTheme.xaml | 16 +- .../Views/Resources/WidgetTheme.xaml | 134 +++--- .../Views/SettingsWindow.xaml | 442 ++++++++++++++++++ .../Views/SettingsWindow.xaml.cs | 139 ++++++ .../Views/TaskbarLabelWindow.xaml | 52 +-- .../Views/TaskbarLabelWindow.xaml.cs | 49 +- .../TaskbarSettingsMenuTests.cs | 104 ++++- .../ThemePreferenceResolverTests.cs | 21 + .../ThemePreferenceStoreTests.cs | 40 ++ 28 files changed, 1429 insertions(+), 267 deletions(-) create mode 100644 src/CodexUsageWidget/Infrastructure/Settings/EffectiveTheme.cs create mode 100644 src/CodexUsageWidget/Infrastructure/Settings/ThemePreference.cs create mode 100644 src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceMonitor.cs create mode 100644 src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceResolver.cs create mode 100644 src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceStore.cs create mode 100644 src/CodexUsageWidget/Infrastructure/Windows/WindowsThemeMonitor.cs create mode 100644 src/CodexUsageWidget/Views/AppThemeController.cs create mode 100644 src/CodexUsageWidget/Views/SettingsWindow.xaml create mode 100644 src/CodexUsageWidget/Views/SettingsWindow.xaml.cs create mode 100644 tests/CodexUsageWidget.Tests/ThemePreferenceResolverTests.cs create mode 100644 tests/CodexUsageWidget.Tests/ThemePreferenceStoreTests.cs diff --git a/README.md b/README.md index 446c132..389e5ac 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,10 @@ of `codex.cmd` or `codex.exe`. - Remaining percentage and reset time for every general Codex usage window - A selectable displayed limit shared by the widget headline, taskbar label, and tray icon -- Compact and detailed layouts, including credits, spend controls, earned resets, token - activity, and model-specific limits when Codex returns them +- Compact and detailed desktop-widget layouts selected from Settings or the quick chevron, + including credits, spend controls, earned resets, token activity, and model-specific limits + when Codex returns them +- System, light, and dark themes selected from a dedicated Settings window - A movable, always-on-top desktop widget and a compact label beside the notification area - Live task activity dots based on official local Codex lifecycle hooks - Automatic refresh every two minutes plus live rate-limit notifications @@ -51,7 +53,7 @@ visible below it. Compact mode focuses on limits; Details adds account and token **Taskbar label.** Shows the same selected percentage beside the Windows notification area. The tray icon and its tooltip follow that selection too. -Choose `5h limit`, `Weekly limit`, or `Most constrained` from the **Displayed limit** menu. +Choose `5h limit`, `Weekly limit`, or `Most constrained` in **Settings** under **Usage**. The 5-hour window is the default. Available windows depend on the Codex account, so the widget falls back to an available window when Codex does not return the selected one. @@ -63,8 +65,10 @@ subscription percentage. ![Codex Usage Widget taskbar label preview](docs/images/taskbar-label.png) Select the `−` button to move the widget to the taskbar. Right-click the taskbar label or -tray icon to refresh, change the display mode or displayed limit, configure activity dots, -toggle start with Windows, check for updates, or exit. +tray icon to refresh, change the display mode, configure activity dots, open Settings, +check for updates, or exit. The Settings window opens from the gear button on the widget too. +Theme, widget layout, displayed limit, and Start with Windows changes apply as soon as they +are selected. ## Activity dots @@ -95,6 +99,7 @@ The application writes only under `%LOCALAPPDATA%\CodexUsageWidget`: - `display-mode.txt`: desktop or taskbar display preference - `widget-density.txt`: compact or detailed widget preference - `displayed-limit.txt`: selected summary limit +- `theme.txt`: system, light, or dark theme preference - `logs\codex-usage-widget-YYYYMMDD.log`: diagnostic logs retained for 14 days The widget displays ChatGPT and Codex subscription limits. It does not display OpenAI API @@ -103,7 +108,7 @@ billing or API-key usage. ## Uninstall 1. If activity hooks are installed, open **Activity dots...** and select **Remove hooks**. -2. Turn off **Start with Windows** from the widget or tray menu. +2. Turn off **Start with Windows** in **Settings** under **General**. 3. Exit the widget. 4. Delete the extracted application folder and `%LOCALAPPDATA%\CodexUsageWidget`. The local data folder contains any stable copy, saved preferences, and diagnostic logs. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 25afc79..cd253d2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -39,8 +39,11 @@ tests/CodexUsageWidget.Tests/ Unit tests for parsing, formatting and persistence 9. `ActivityHookSetupWindow` presents setup status while a separate review dialog shows the exact proposed file content before installation or removal. 10. `UsageWidgetViewModel` maps snapshots to immutable presentation state. -11. `MainWindow` remains a window-lifecycle shell while focused user controls render - compact, detailed, and repeated limit-row content. +11. `AppThemeController` applies the saved system, light, or dark palette and observes + Windows theme changes without leaking registry access into view code. +12. `MainWindow` remains a window-lifecycle shell while the Settings window coordinates + immediate theme, widget-layout, displayed-limit, and Windows startup preferences and focused + user controls render compact, detailed, and repeated limit-row content. ## Dependency direction @@ -79,4 +82,6 @@ tests/CodexUsageWidget.Tests/ Unit tests for parsing, formatting and persistence - Keep reusable presentation state in `Views/ViewModels` and focused visual sections in `Views/Controls`; `MainWindow` should not absorb endpoint or rendering responsibilities. - Keep Win32 calls under `Infrastructure/Windows` and UI rendering under `Views`. +- Extend user preferences through the Settings window. Keep appearance application in + `AppThemeController` instead of placing theme decisions in individual views. - Avoid placing persistence, process management, or protocol parsing in code-behind. diff --git a/src/CodexUsageWidget/App.xaml.cs b/src/CodexUsageWidget/App.xaml.cs index 2fd0697..70a766b 100644 --- a/src/CodexUsageWidget/App.xaml.cs +++ b/src/CodexUsageWidget/App.xaml.cs @@ -17,6 +17,7 @@ public partial class App : System.Windows.Application, IDisposable private SingleInstanceGuard? _singleInstanceGuard; private FileLogger? _logger; private GlobalExceptionHandler? _exceptionHandler; + private AppThemeController? _themeController; private bool _disposed; protected override void OnStartup(StartupEventArgs e) @@ -66,6 +67,11 @@ protected override void OnStartup(StartupEventArgs e) var activityHookSetupService = new CodexActivityHookSetupService( new CodexHookConfigurationManager(), appServerSession); + _themeController = new AppThemeController( + this, + new ThemePreferenceMonitor( + new ThemePreferenceStore(), + new WindowsThemeMonitor())); var window = new MainWindow( usageMonitor, @@ -76,7 +82,8 @@ protected override void OnStartup(StartupEventArgs e) new WidgetDensityStore(), new DisplayedLimitPreferenceStore(), startupRegistrationService, - new TrayIconService()); + new TrayIconService(), + _themeController); MainWindow = window; activityMonitor.StartAsync().GetAwaiter().GetResult(); window.Show(); @@ -126,6 +133,8 @@ public void Dispose() _disposed = true; _exceptionHandler?.Dispose(); _exceptionHandler = null; + _themeController?.Dispose(); + _themeController = null; _singleInstanceGuard?.Dispose(); _singleInstanceGuard = null; GC.SuppressFinalize(this); diff --git a/src/CodexUsageWidget/Infrastructure/AppPaths.cs b/src/CodexUsageWidget/Infrastructure/AppPaths.cs index d7fc402..3cfa299 100644 --- a/src/CodexUsageWidget/Infrastructure/AppPaths.cs +++ b/src/CodexUsageWidget/Infrastructure/AppPaths.cs @@ -16,5 +16,7 @@ public static class AppPaths LocalDataDirectory, "displayed-limit.txt"); + public static string ThemePreferenceFile => Path.Combine(LocalDataDirectory, "theme.txt"); + public static string LogDirectory => Path.Combine(LocalDataDirectory, "logs"); } diff --git a/src/CodexUsageWidget/Infrastructure/Settings/EffectiveTheme.cs b/src/CodexUsageWidget/Infrastructure/Settings/EffectiveTheme.cs new file mode 100644 index 0000000..cea8960 --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Settings/EffectiveTheme.cs @@ -0,0 +1,7 @@ +namespace CodexUsageWidget.Infrastructure.Settings; + +public enum EffectiveTheme +{ + Light, + Dark +} diff --git a/src/CodexUsageWidget/Infrastructure/Settings/ThemePreference.cs b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreference.cs new file mode 100644 index 0000000..abde7a0 --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreference.cs @@ -0,0 +1,8 @@ +namespace CodexUsageWidget.Infrastructure.Settings; + +public enum ThemePreference +{ + System, + Light, + Dark +} diff --git a/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceMonitor.cs b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceMonitor.cs new file mode 100644 index 0000000..26b9fbd --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceMonitor.cs @@ -0,0 +1,71 @@ +using CodexUsageWidget.Infrastructure.Windows; + +namespace CodexUsageWidget.Infrastructure.Settings; + +public sealed class ThemePreferenceMonitor : IDisposable +{ + private readonly ThemePreferenceStore _store; + private readonly WindowsThemeMonitor _windowsThemeMonitor; + private bool _disposed; + + public ThemePreferenceMonitor( + ThemePreferenceStore store, + WindowsThemeMonitor windowsThemeMonitor) + { + _store = store; + _windowsThemeMonitor = windowsThemeMonitor; + Preference = store.Load(); + EffectiveTheme = ResolveEffectiveTheme(); + _windowsThemeMonitor.ThemeChanged += WindowsThemeMonitorOnThemeChanged; + } + + public event Action? EffectiveThemeChanged; + + public event Action? SystemThemeChanged; + + public ThemePreference Preference { get; private set; } + + public EffectiveTheme EffectiveTheme { get; private set; } + + public EffectiveTheme SystemTheme => _windowsThemeMonitor.UsesLightTheme + ? EffectiveTheme.Light + : EffectiveTheme.Dark; + + public void SetPreference(ThemePreference preference) + { + Preference = preference; + _store.Save(preference); + SetEffectiveTheme(ResolveEffectiveTheme()); + } + + private void WindowsThemeMonitorOnThemeChanged(object? sender, EventArgs e) + { + SystemThemeChanged?.Invoke(SystemTheme); + if (Preference == ThemePreference.System) + { + SetEffectiveTheme(ResolveEffectiveTheme()); + } + } + + private EffectiveTheme ResolveEffectiveTheme() => ThemePreferenceResolver.Resolve( + Preference, + _windowsThemeMonitor.UsesLightTheme); + + private void SetEffectiveTheme(EffectiveTheme theme) + { + EffectiveTheme = theme; + EffectiveThemeChanged?.Invoke(theme); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _windowsThemeMonitor.ThemeChanged -= WindowsThemeMonitorOnThemeChanged; + _windowsThemeMonitor.Dispose(); + } +} diff --git a/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceResolver.cs b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceResolver.cs new file mode 100644 index 0000000..ba5a2fd --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceResolver.cs @@ -0,0 +1,13 @@ +namespace CodexUsageWidget.Infrastructure.Settings; + +public static class ThemePreferenceResolver +{ + public static EffectiveTheme Resolve( + ThemePreference preference, + bool systemUsesLightTheme) => preference switch + { + ThemePreference.Light => EffectiveTheme.Light, + ThemePreference.Dark => EffectiveTheme.Dark, + _ => systemUsesLightTheme ? EffectiveTheme.Light : EffectiveTheme.Dark + }; +} diff --git a/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceStore.cs b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceStore.cs new file mode 100644 index 0000000..b14df62 --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Settings/ThemePreferenceStore.cs @@ -0,0 +1,56 @@ +using System.IO; + +namespace CodexUsageWidget.Infrastructure.Settings; + +public sealed class ThemePreferenceStore +{ + private readonly string _path; + + public ThemePreferenceStore(string? path = null) + { + _path = path ?? AppPaths.ThemePreferenceFile; + } + + public ThemePreference Load() + { + try + { + return File.ReadAllText(_path).Trim().ToLowerInvariant() switch + { + "light" => ThemePreference.Light, + "dark" => ThemePreference.Dark, + _ => ThemePreference.System + }; + } + catch (IOException) + { + return ThemePreference.System; + } + catch (UnauthorizedAccessException) + { + return ThemePreference.System; + } + } + + public void Save(ThemePreference preference) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(_path)!); + File.WriteAllText( + _path, + preference switch + { + ThemePreference.Light => "light", + ThemePreference.Dark => "dark", + _ => "system" + }); + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } +} diff --git a/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs b/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs index c10f5b7..0c46bc8 100644 --- a/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs +++ b/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs @@ -1,4 +1,3 @@ -using CodexUsageWidget.Application; using CodexUsageWidget.Infrastructure.Settings; using Forms = System.Windows.Forms; @@ -6,16 +5,10 @@ namespace CodexUsageWidget.Infrastructure.Windows; public sealed class TrayIconService : IDisposable { - private const string FiveHourUnavailableText = - "Codex did not return a global 5h limit for this account."; - private readonly Forms.NotifyIcon _notifyIcon; + private readonly Forms.ContextMenuStrip _menu; private readonly Forms.ToolStripMenuItem _desktopWidgetModeItem; private readonly Forms.ToolStripMenuItem _taskbarIndicatorModeItem; - private readonly Forms.ToolStripMenuItem _fiveHourLimitItem; - private readonly Forms.ToolStripMenuItem _weeklyLimitItem; - private readonly Forms.ToolStripMenuItem _mostConstrainedLimitItem; - private readonly Forms.ToolStripMenuItem _startWithWindowsItem; private System.Drawing.Icon _currentIcon; private bool _disposed; @@ -31,6 +24,10 @@ public TrayIconService() "Activity dots...", null, (_, _) => ActivityDotsSetupRequested?.Invoke(this, EventArgs.Empty)); + menu.Items.Add( + "Settings...", + null, + (_, _) => SettingsRequested?.Invoke(this, EventArgs.Empty)); menu.Items.Add(new Forms.ToolStripSeparator()); var displayModeMenu = new Forms.ToolStripMenuItem("Display mode"); @@ -46,31 +43,6 @@ public TrayIconService() displayModeMenu.DropDownItems.Add(_taskbarIndicatorModeItem); menu.Items.Add(displayModeMenu); - var displayedLimitMenu = new Forms.ToolStripMenuItem("Displayed limit"); - _fiveHourLimitItem = new Forms.ToolStripMenuItem("5h limit") - { - Enabled = false, - ToolTipText = FiveHourUnavailableText - }; - _fiveHourLimitItem.Click += (_, _) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.FiveHour); - _weeklyLimitItem = new Forms.ToolStripMenuItem("Weekly limit"); - _weeklyLimitItem.Click += (_, _) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.Weekly); - _mostConstrainedLimitItem = new Forms.ToolStripMenuItem("Most constrained"); - _mostConstrainedLimitItem.Click += (_, _) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.MostConstrained); - displayedLimitMenu.DropDownItems.Add(_fiveHourLimitItem); - displayedLimitMenu.DropDownItems.Add(_weeklyLimitItem); - displayedLimitMenu.DropDownItems.Add(_mostConstrainedLimitItem); - menu.Items.Add(displayedLimitMenu); - _startWithWindowsItem = new Forms.ToolStripMenuItem("Start with Windows") - { - CheckOnClick = true - }; - _startWithWindowsItem.Click += (_, _) => - StartupToggleRequested?.Invoke(this, EventArgs.Empty); - menu.Items.Add(_startWithWindowsItem); menu.Items.Add( "Check for updates...", null, @@ -79,6 +51,7 @@ public TrayIconService() menu.Items.Add("Exit", null, (_, _) => ExitRequested?.Invoke(this, EventArgs.Empty)); _currentIcon = UsageIconFactory.Create(null); + _menu = menu; _notifyIcon = new Forms.NotifyIcon { Icon = _currentIcon, @@ -87,6 +60,7 @@ public TrayIconService() ContextMenuStrip = menu }; _notifyIcon.MouseClick += NotifyIconOnMouseClick; + SetTheme(EffectiveTheme.Dark); } public event EventHandler? OpenRequested; @@ -95,14 +69,12 @@ public TrayIconService() public event EventHandler? ActivityDotsSetupRequested; + public event EventHandler? SettingsRequested; + public event EventHandler? DesktopModeRequested; public event EventHandler? TaskbarModeRequested; - public event Action? DisplayedLimitPreferenceChanged; - - public event EventHandler? StartupToggleRequested; - public event EventHandler? UpdateCheckRequested; public event EventHandler? ExitRequested; @@ -113,19 +85,37 @@ public void SetDisplayMode(WidgetDisplayMode mode) _taskbarIndicatorModeItem.Checked = mode == WidgetDisplayMode.TaskbarIndicator; } - public void SetStartupEnabled(bool enabled) => _startWithWindowsItem.Checked = enabled; - - public void SetDisplayedLimitPreference(DisplayedLimitPreference preference) + public void SetTheme(EffectiveTheme theme) { - _fiveHourLimitItem.Checked = preference == DisplayedLimitPreference.FiveHour; - _weeklyLimitItem.Checked = preference == DisplayedLimitPreference.Weekly; - _mostConstrainedLimitItem.Checked = preference == DisplayedLimitPreference.MostConstrained; + var light = theme == EffectiveTheme.Light; + var background = light + ? System.Drawing.Color.FromArgb(250, 250, 251) + : System.Drawing.Color.FromArgb(36, 36, 36); + var foreground = light + ? System.Drawing.Color.FromArgb(35, 35, 37) + : System.Drawing.Color.FromArgb(232, 232, 232); + + _menu.BackColor = background; + _menu.ForeColor = foreground; + ApplyMenuColors(_menu.Items, background, foreground); + _menu.Renderer = new Forms.ToolStripProfessionalRenderer( + new TrayMenuColorTable(light)); } - public void SetFiveHourLimitAvailability(bool available) + private static void ApplyMenuColors( + Forms.ToolStripItemCollection items, + System.Drawing.Color background, + System.Drawing.Color foreground) { - _fiveHourLimitItem.Enabled = available; - _fiveHourLimitItem.ToolTipText = available ? string.Empty : FiveHourUnavailableText; + foreach (Forms.ToolStripItem item in items) + { + item.BackColor = background; + item.ForeColor = foreground; + if (item is Forms.ToolStripMenuItem menuItem) + { + ApplyMenuColors(menuItem.DropDownItems, background, foreground); + } + } } public void UpdateUsage(double? remainingPercent) @@ -163,4 +153,45 @@ public void Dispose() _notifyIcon.Dispose(); _currentIcon.Dispose(); } + + private sealed class TrayMenuColorTable(bool light) : Forms.ProfessionalColorTable + { + private readonly System.Drawing.Color _background = light + ? System.Drawing.Color.FromArgb(250, 250, 251) + : System.Drawing.Color.FromArgb(36, 36, 36); + private readonly System.Drawing.Color _border = light + ? System.Drawing.Color.FromArgb(215, 217, 222) + : System.Drawing.Color.FromArgb(69, 69, 69); + private readonly System.Drawing.Color _hover = light + ? System.Drawing.Color.FromArgb(236, 238, 241) + : System.Drawing.Color.FromArgb(54, 54, 54); + + public override System.Drawing.Color ToolStripDropDownBackground => _background; + + public override System.Drawing.Color ImageMarginGradientBegin => _background; + + public override System.Drawing.Color ImageMarginGradientMiddle => _background; + + public override System.Drawing.Color ImageMarginGradientEnd => _background; + + public override System.Drawing.Color MenuBorder => _border; + + public override System.Drawing.Color MenuItemBorder => _hover; + + public override System.Drawing.Color MenuItemSelected => _hover; + + public override System.Drawing.Color MenuItemSelectedGradientBegin => _hover; + + public override System.Drawing.Color MenuItemSelectedGradientEnd => _hover; + + public override System.Drawing.Color MenuItemPressedGradientBegin => _hover; + + public override System.Drawing.Color MenuItemPressedGradientMiddle => _hover; + + public override System.Drawing.Color MenuItemPressedGradientEnd => _hover; + + public override System.Drawing.Color SeparatorDark => _border; + + public override System.Drawing.Color SeparatorLight => _background; + } } diff --git a/src/CodexUsageWidget/Infrastructure/Windows/WindowsThemeMonitor.cs b/src/CodexUsageWidget/Infrastructure/Windows/WindowsThemeMonitor.cs new file mode 100644 index 0000000..9d56b94 --- /dev/null +++ b/src/CodexUsageWidget/Infrastructure/Windows/WindowsThemeMonitor.cs @@ -0,0 +1,42 @@ +using Microsoft.Win32; + +namespace CodexUsageWidget.Infrastructure.Windows; + +public sealed class WindowsThemeMonitor : IDisposable +{ + private const string PersonalizeKey = + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightThemeValue = "AppsUseLightTheme"; + private readonly string _personalizeKey = PersonalizeKey; + private bool _disposed; + + public WindowsThemeMonitor() + { + SystemEvents.UserPreferenceChanged += SystemEventsOnUserPreferenceChanged; + } + + public event EventHandler? ThemeChanged; + + public bool UsesLightTheme + { + get + { + using var key = Registry.CurrentUser.OpenSubKey(_personalizeKey); + return key?.GetValue(AppsUseLightThemeValue) is not int value || value != 0; + } + } + + private void SystemEventsOnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) => + ThemeChanged?.Invoke(this, EventArgs.Empty); + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + SystemEvents.UserPreferenceChanged -= SystemEventsOnUserPreferenceChanged; + } +} diff --git a/src/CodexUsageWidget/Views/ActivityHookChangeReviewWindow.xaml b/src/CodexUsageWidget/Views/ActivityHookChangeReviewWindow.xaml index 61c3916..9359dc2 100644 --- a/src/CodexUsageWidget/Views/ActivityHookChangeReviewWindow.xaml +++ b/src/CodexUsageWidget/Views/ActivityHookChangeReviewWindow.xaml @@ -25,7 +25,7 @@ VerticalAlignment="Center" FontSize="19" FontWeight="SemiBold" - Foreground="{StaticResource TextPrimary}" /> + Foreground="{DynamicResource TextPrimary}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CodexUsageWidget/Views/SettingsWindow.xaml.cs b/src/CodexUsageWidget/Views/SettingsWindow.xaml.cs new file mode 100644 index 0000000..12fadab --- /dev/null +++ b/src/CodexUsageWidget/Views/SettingsWindow.xaml.cs @@ -0,0 +1,139 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using CodexUsageWidget.Application; +using CodexUsageWidget.Infrastructure.Settings; + +namespace CodexUsageWidget.Views; + +public partial class SettingsWindow : Window +{ + private bool _suppressChangeEvents; + + public SettingsWindow( + ThemePreference themePreference, + WidgetDensity widgetDensity, + DisplayedLimitPreference displayedLimitPreference, + bool fiveHourLimitAvailable, + bool startWithWindowsEnabled) + { + InitializeComponent(); + _suppressChangeEvents = true; + SetSelectedTheme(themePreference); + SetWidgetDensity(widgetDensity); + SetDisplayedLimitPreference(displayedLimitPreference); + SetFiveHourLimitAvailability(fiveHourLimitAvailable); + SetStartWithWindowsEnabled(startWithWindowsEnabled); + _suppressChangeEvents = false; + } + + public event Action? ThemePreferenceChanged; + + public event Action? WidgetDensityChanged; + + public event Action? DisplayedLimitPreferenceChanged; + + public event Action? StartWithWindowsChanged; + + public ThemePreference SelectedTheme => + LightThemeOption.IsChecked == true + ? ThemePreference.Light + : DarkThemeOption.IsChecked == true + ? ThemePreference.Dark + : ThemePreference.System; + + public DisplayedLimitPreference SelectedDisplayedLimit => + WeeklyLimitOption.IsChecked == true + ? DisplayedLimitPreference.Weekly + : MostConstrainedLimitOption.IsChecked == true + ? DisplayedLimitPreference.MostConstrained + : DisplayedLimitPreference.FiveHour; + + public WidgetDensity SelectedWidgetDensity => + DetailedLayoutOption.IsChecked == true + ? WidgetDensity.Detailed + : WidgetDensity.Compact; + + public bool StartWithWindowsEnabled => StartWithWindowsOption.IsChecked == true; + + public void SetDisplayedLimitPreference(DisplayedLimitPreference preference) + { + var previousSuppression = _suppressChangeEvents; + _suppressChangeEvents = true; + FiveHourLimitOption.IsChecked = preference == DisplayedLimitPreference.FiveHour; + WeeklyLimitOption.IsChecked = preference == DisplayedLimitPreference.Weekly; + MostConstrainedLimitOption.IsChecked = + preference == DisplayedLimitPreference.MostConstrained; + _suppressChangeEvents = previousSuppression; + } + + public void SetFiveHourLimitAvailability(bool available) + { + FiveHourLimitOption.IsEnabled = available; + ToolTipService.SetIsEnabled(FiveHourLimitOption, !available); + } + + public void SetWidgetDensity(WidgetDensity density) + { + var previousSuppression = _suppressChangeEvents; + _suppressChangeEvents = true; + CompactLayoutOption.IsChecked = density == WidgetDensity.Compact; + DetailedLayoutOption.IsChecked = density == WidgetDensity.Detailed; + _suppressChangeEvents = previousSuppression; + } + + public void SetStartWithWindowsEnabled(bool enabled) + { + var previousSuppression = _suppressChangeEvents; + _suppressChangeEvents = true; + StartWithWindowsOption.IsChecked = enabled; + _suppressChangeEvents = previousSuppression; + } + + private void SetSelectedTheme(ThemePreference preference) + { + SystemThemeOption.IsChecked = preference == ThemePreference.System; + LightThemeOption.IsChecked = preference == ThemePreference.Light; + DarkThemeOption.IsChecked = preference == ThemePreference.Dark; + } + + private void Header_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ButtonState == MouseButtonState.Pressed) + { + DragMove(); + } + } + + private void ThemeOption_OnChecked(object sender, RoutedEventArgs e) + { + if (!_suppressChangeEvents) + { + ThemePreferenceChanged?.Invoke(SelectedTheme); + } + } + + private void DisplayedLimitOption_OnChecked(object sender, RoutedEventArgs e) + { + if (!_suppressChangeEvents) + { + DisplayedLimitPreferenceChanged?.Invoke(SelectedDisplayedLimit); + } + } + + private void WidgetLayoutOption_OnChecked(object sender, RoutedEventArgs e) + { + if (!_suppressChangeEvents) + { + WidgetDensityChanged?.Invoke(SelectedWidgetDensity); + } + } + + private void StartWithWindowsOption_OnChanged(object sender, RoutedEventArgs e) + { + if (!_suppressChangeEvents) + { + StartWithWindowsChanged?.Invoke(StartWithWindowsEnabled); + } + } +} diff --git a/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml b/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml index 2112643..36e2910 100644 --- a/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml +++ b/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml @@ -19,6 +19,9 @@ + + + - + @@ -81,41 +84,11 @@ Style="{StaticResource TaskbarMenuIcon}" /> - + - - - - - - - - - - @@ -124,15 +97,6 @@ Style="{StaticResource TaskbarMenuIcon}" /> - - - - - + Foreground="{DynamicResource TaskbarTextSecondaryBrush}" /> diff --git a/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml.cs b/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml.cs index f16172f..9a03a7f 100644 --- a/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml.cs +++ b/src/CodexUsageWidget/Views/TaskbarLabelWindow.xaml.cs @@ -3,7 +3,7 @@ using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; -using CodexUsageWidget.Application; +using CodexUsageWidget.Infrastructure.Settings; using CodexUsageWidget.Infrastructure.Windows; namespace CodexUsageWidget.Views; @@ -65,14 +65,12 @@ public TaskbarLabelWindow() public event EventHandler? ActivityDotsSetupRequested; + public event EventHandler? SettingsRequested; + public event EventHandler? ActivityPreviewChanged; public event EventHandler? DesktopModeRequested; - public event Action? DisplayedLimitPreferenceChanged; - - public event EventHandler? StartupToggleRequested; - public event EventHandler? UpdateCheckRequested; public event EventHandler? ExitRequested; @@ -142,19 +140,21 @@ public void SetActivityState(bool isActive) ActivityDots.IsActive = isActive; } - public void SetStartupEnabled(bool enabled) => StartWithWindowsMenuItem.IsChecked = enabled; - - public void SetDisplayedLimitPreference(DisplayedLimitPreference preference) - { - FiveHourLimitMenuItem.IsChecked = preference == DisplayedLimitPreference.FiveHour; - WeeklyLimitMenuItem.IsChecked = preference == DisplayedLimitPreference.Weekly; - MostConstrainedLimitMenuItem.IsChecked = preference == DisplayedLimitPreference.MostConstrained; - } - - public void SetFiveHourLimitAvailability(bool available) + public void SetSystemTheme(EffectiveTheme theme) { - FiveHourLimitMenuItem.IsEnabled = available; - ToolTipService.SetIsEnabled(FiveHourLimitMenuItem, !available); + var light = theme == EffectiveTheme.Light; + Resources["TaskbarTextPrimaryBrush"] = new System.Windows.Media.SolidColorBrush( + light + ? System.Windows.Media.Color.FromRgb(32, 33, 36) + : System.Windows.Media.Color.FromRgb(242, 242, 242)); + Resources["TaskbarTextSecondaryBrush"] = new System.Windows.Media.SolidColorBrush( + light + ? System.Windows.Media.Color.FromRgb(72, 73, 78) + : System.Windows.Media.Color.FromRgb(212, 212, 212)); + Resources["TaskbarLabelHoverBrush"] = new System.Windows.Media.SolidColorBrush( + light + ? System.Windows.Media.Color.FromArgb(18, 0, 0, 0) + : System.Windows.Media.Color.FromArgb(24, 255, 255, 255)); } public void UpdateUsage( @@ -240,6 +240,9 @@ private void RefreshMenuItem_OnClick(object sender, RoutedEventArgs e) => private void ActivityDotsMenuItem_OnClick(object sender, RoutedEventArgs e) => ActivityDotsSetupRequested?.Invoke(this, EventArgs.Empty); + private void SettingsMenuItem_OnClick(object sender, RoutedEventArgs e) => + SettingsRequested?.Invoke(this, EventArgs.Empty); + private void ActivityPreviewMenuItem_OnClick(object sender, RoutedEventArgs e) => ActivityPreviewChanged?.Invoke(this, EventArgs.Empty); @@ -276,18 +279,6 @@ private void CloseTaskbarMenu() private void DesktopModeMenuItem_OnClick(object sender, RoutedEventArgs e) => DesktopModeRequested?.Invoke(this, EventArgs.Empty); - private void FiveHourLimitMenuItem_OnClick(object sender, RoutedEventArgs e) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.FiveHour); - - private void WeeklyLimitMenuItem_OnClick(object sender, RoutedEventArgs e) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.Weekly); - - private void MostConstrainedLimitMenuItem_OnClick(object sender, RoutedEventArgs e) => - DisplayedLimitPreferenceChanged?.Invoke(DisplayedLimitPreference.MostConstrained); - - private void StartWithWindowsMenuItem_OnClick(object sender, RoutedEventArgs e) => - StartupToggleRequested?.Invoke(this, EventArgs.Empty); - private void CheckForUpdatesMenuItem_OnClick(object sender, RoutedEventArgs e) => UpdateCheckRequested?.Invoke(this, EventArgs.Empty); diff --git a/tests/CodexUsageWidget.Tests/TaskbarSettingsMenuTests.cs b/tests/CodexUsageWidget.Tests/TaskbarSettingsMenuTests.cs index 6bfbe84..659389d 100644 --- a/tests/CodexUsageWidget.Tests/TaskbarSettingsMenuTests.cs +++ b/tests/CodexUsageWidget.Tests/TaskbarSettingsMenuTests.cs @@ -1,5 +1,7 @@ using System.Runtime.ExceptionServices; using System.Windows.Controls; +using CodexUsageWidget.Application; +using CodexUsageWidget.Infrastructure.Settings; using CodexUsageWidget.Views; namespace CodexUsageWidget.Tests; @@ -7,7 +9,7 @@ namespace CodexUsageWidget.Tests; public sealed class TaskbarSettingsMenuTests { [Fact] - public void SettingsMenuOffersUpdateCheck() + public void SettingsUiOffersExpectedActionsAndThemeChoices() { Exception? failure = null; var thread = new Thread(() => @@ -25,6 +27,106 @@ public void SettingsMenuOffersUpdateCheck() item.Header as string, "Check for updates...", StringComparison.Ordinal)); + Assert.Contains( + menu.Items.OfType(), + item => string.Equals( + item.Header as string, + "Settings...", + StringComparison.Ordinal)); + Assert.DoesNotContain( + menu.Items.OfType(), + item => string.Equals( + item.Header as string, + "Displayed limit", + StringComparison.Ordinal)); + Assert.DoesNotContain( + menu.Items.OfType(), + item => string.Equals( + item.Header as string, + "Start with Windows", + StringComparison.Ordinal)); + + var cases = new[] + { + (ThemePreference.System, "SystemThemeOption"), + (ThemePreference.Light, "LightThemeOption"), + (ThemePreference.Dark, "DarkThemeOption") + }; + foreach (var (preference, expectedOptionName) in cases) + { + var settings = new SettingsWindow( + themePreference: preference, + widgetDensity: WidgetDensity.Compact, + displayedLimitPreference: DisplayedLimitPreference.FiveHour, + fiveHourLimitAvailable: true, + startWithWindowsEnabled: false); + var option = Assert.IsType( + settings.FindName(expectedOptionName)); + Assert.True(option.IsChecked); + settings.Close(); + } + + var liveSettings = new SettingsWindow( + themePreference: ThemePreference.System, + widgetDensity: WidgetDensity.Compact, + displayedLimitPreference: DisplayedLimitPreference.FiveHour, + fiveHourLimitAvailable: true, + startWithWindowsEnabled: false); + ThemePreference? changedPreference = null; + liveSettings.ThemePreferenceChanged += preference => + changedPreference = preference; + + var lightOption = Assert.IsType( + liveSettings.FindName("LightThemeOption")); + lightOption.IsChecked = true; + + Assert.Equal(ThemePreference.Light, changedPreference); + liveSettings.Close(); + + var usageSettings = new SettingsWindow( + themePreference: ThemePreference.System, + widgetDensity: WidgetDensity.Compact, + displayedLimitPreference: DisplayedLimitPreference.Weekly, + fiveHourLimitAvailable: false, + startWithWindowsEnabled: true); + var weeklyOption = Assert.IsType( + usageSettings.FindName("WeeklyLimitOption")); + var fiveHourOption = Assert.IsType( + usageSettings.FindName("FiveHourLimitOption")); + Assert.True(weeklyOption.IsChecked); + Assert.False(fiveHourOption.IsEnabled); + + DisplayedLimitPreference? changedLimit = null; + usageSettings.DisplayedLimitPreferenceChanged += preference => + changedLimit = preference; + var mostConstrainedOption = Assert.IsType( + usageSettings.FindName("MostConstrainedLimitOption")); + mostConstrainedOption.IsChecked = true; + + Assert.Equal(DisplayedLimitPreference.MostConstrained, changedLimit); + + var startupOption = Assert.IsType( + usageSettings.FindName("StartWithWindowsOption")); + Assert.True(startupOption.IsChecked); + bool? changedStartupState = null; + usageSettings.StartWithWindowsChanged += enabled => + changedStartupState = enabled; + startupOption.IsChecked = false; + + Assert.False(changedStartupState); + + var compactLayoutOption = Assert.IsType( + usageSettings.FindName("CompactLayoutOption")); + Assert.True(compactLayoutOption.IsChecked); + WidgetDensity? changedDensity = null; + usageSettings.WidgetDensityChanged += density => + changedDensity = density; + var detailedLayoutOption = Assert.IsType( + usageSettings.FindName("DetailedLayoutOption")); + detailedLayoutOption.IsChecked = true; + + Assert.Equal(WidgetDensity.Detailed, changedDensity); + usageSettings.Close(); window.Close(); application.Shutdown(); diff --git a/tests/CodexUsageWidget.Tests/ThemePreferenceResolverTests.cs b/tests/CodexUsageWidget.Tests/ThemePreferenceResolverTests.cs new file mode 100644 index 0000000..caeb914 --- /dev/null +++ b/tests/CodexUsageWidget.Tests/ThemePreferenceResolverTests.cs @@ -0,0 +1,21 @@ +using CodexUsageWidget.Infrastructure.Settings; + +namespace CodexUsageWidget.Tests; + +public sealed class ThemePreferenceResolverTests +{ + [Theory] + [InlineData(ThemePreference.System, true, EffectiveTheme.Light)] + [InlineData(ThemePreference.System, false, EffectiveTheme.Dark)] + [InlineData(ThemePreference.Light, false, EffectiveTheme.Light)] + [InlineData(ThemePreference.Dark, true, EffectiveTheme.Dark)] + public void ResolveUsesPreferenceOrSystemTheme( + ThemePreference preference, + bool systemUsesLightTheme, + EffectiveTheme expected) + { + Assert.Equal( + expected, + ThemePreferenceResolver.Resolve(preference, systemUsesLightTheme)); + } +} diff --git a/tests/CodexUsageWidget.Tests/ThemePreferenceStoreTests.cs b/tests/CodexUsageWidget.Tests/ThemePreferenceStoreTests.cs new file mode 100644 index 0000000..add5fa6 --- /dev/null +++ b/tests/CodexUsageWidget.Tests/ThemePreferenceStoreTests.cs @@ -0,0 +1,40 @@ +using CodexUsageWidget.Infrastructure.Settings; + +namespace CodexUsageWidget.Tests; + +public sealed class ThemePreferenceStoreTests : IDisposable +{ + private readonly string _directory = Path.Combine( + Path.GetTempPath(), + "CodexUsageWidget.Tests", + Guid.NewGuid().ToString("N")); + + [Fact] + public void LoadUsesSystemWhenPreferenceDoesNotExist() + { + var store = new ThemePreferenceStore(Path.Combine(_directory, "theme.txt")); + + Assert.Equal(ThemePreference.System, store.Load()); + } + + [Theory] + [InlineData(ThemePreference.System)] + [InlineData(ThemePreference.Light)] + [InlineData(ThemePreference.Dark)] + public void SavePersistsPreference(ThemePreference preference) + { + var store = new ThemePreferenceStore(Path.Combine(_directory, "theme.txt")); + + store.Save(preference); + + Assert.Equal(preference, store.Load()); + } + + public void Dispose() + { + if (Directory.Exists(_directory)) + { + Directory.Delete(_directory, recursive: true); + } + } +} From 2fa1a89d2fed886bcddea259f63333d008c4791b Mon Sep 17 00:00:00 2001 From: ognjeeen Date: Thu, 27 Aug 2026 18:17:06 +0200 Subject: [PATCH 2/6] feat: move Codex activity into settings --- README.md | 12 +- docs/ACTIVITY_DOTS.md | 10 +- docs/ARCHITECTURE.md | 8 +- .../Infrastructure/Windows/TrayIconService.cs | 6 - .../Views/ActivityHookSetupWindow.xaml | 139 ------------------ .../ActivityHookSetupWindowController.cs | 57 ------- .../Controls/ActivityHookSetupControl.xaml | 101 +++++++++++++ .../ActivityHookSetupControl.xaml.cs} | 108 +++++--------- src/CodexUsageWidget/Views/MainWindow.xaml | 15 -- src/CodexUsageWidget/Views/MainWindow.xaml.cs | 39 +---- .../Views/SettingsWindow.xaml | 39 ++--- .../Views/SettingsWindow.xaml.cs | 8 +- .../Views/TaskbarLabelWindow.xaml | 7 - .../Views/TaskbarLabelWindow.xaml.cs | 5 - .../TaskbarSettingsMenuTests.cs | 44 +++++- 15 files changed, 226 insertions(+), 372 deletions(-) delete mode 100644 src/CodexUsageWidget/Views/ActivityHookSetupWindow.xaml delete mode 100644 src/CodexUsageWidget/Views/ActivityHookSetupWindowController.cs create mode 100644 src/CodexUsageWidget/Views/Controls/ActivityHookSetupControl.xaml rename src/CodexUsageWidget/Views/{ActivityHookSetupWindow.xaml.cs => Controls/ActivityHookSetupControl.xaml.cs} (67%) diff --git a/README.md b/README.md index 389e5ac..b816d73 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,9 @@ subscription percentage. ![Codex Usage Widget taskbar label preview](docs/images/taskbar-label.png) Select the `−` button to move the widget to the taskbar. Right-click the taskbar label or -tray icon to refresh, change the display mode, configure activity dots, open Settings, -check for updates, or exit. The Settings window opens from the gear button on the widget too. -Theme, widget layout, displayed limit, and Start with Windows changes apply as soon as they -are selected. +tray icon to refresh, change the display mode, open Settings, check for updates, or exit. +The Settings window opens from the gear button on the widget too. Theme, widget layout, +displayed limit, and Start with Windows changes apply as soon as they are selected. ## Activity dots @@ -78,7 +77,7 @@ read prompt text, responses, transcript paths, or model output. To enable them: -1. Open **Activity dots...** from the widget, taskbar label, or tray menu. +1. Open **Settings**, then find **Codex activity** under **Features**. 2. Select **Install hooks** and review the exact proposed `~/.codex/hooks.json` change. 3. Select **Copy /hooks and open Codex**, paste `/hooks`, and trust the three definitions. 4. Return to the widget and select **Check again**. @@ -107,7 +106,8 @@ billing or API-key usage. ## Uninstall -1. If activity hooks are installed, open **Activity dots...** and select **Remove hooks**. +1. If activity hooks are installed, open **Settings**, find **Codex activity** under + **Features**, and select **Remove hooks**. 2. Turn off **Start with Windows** in **Settings** under **General**. 3. Exit the widget. 4. Delete the extracted application folder and `%LOCALAPPDATA%\CodexUsageWidget`. The local diff --git a/docs/ACTIVITY_DOTS.md b/docs/ACTIVITY_DOTS.md index 6a7cb0b..8771b6e 100644 --- a/docs/ACTIVITY_DOTS.md +++ b/docs/ACTIVITY_DOTS.md @@ -19,20 +19,20 @@ paths, or model output. ## Set up in the widget -1. Select the three-dot activity button in the desktop widget, or choose - **Activity dots...** from the taskbar-label or tray menu. +1. Open **Settings** from the desktop widget, taskbar-label menu, or tray menu, then find + **Codex activity** under **Features**. 2. Select **Install hooks**. 3. Review the exact proposed `~/.codex/hooks.json` content and confirm the change. 4. Select **Copy /hooks and open Codex**. 5. Paste `/hooks` into Codex and trust the new `UserPromptSubmit`, `Stop`, and `SessionEnd` definitions. -6. Return to the setup window and select **Check again**. +6. Return to Settings and select **Check again**. Codex owns hook trust. New or changed definitions require approval in Codex. The widget only reads the reported trust state. -To remove the handlers, return to the setup window and select **Remove hooks**. The widget -preserves unrelated hooks and unknown configuration fields. +To remove the handlers, return to **Codex activity** under **Features** in Settings and +select **Remove hooks**. The widget preserves unrelated hooks and unknown configuration fields. ## Command-line setup and removal diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cd253d2..c8e2be6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -36,14 +36,14 @@ tests/CodexUsageWidget.Tests/ Unit tests for parsing, formatting and persistence transitions. 8. `CodexActivityHookSetupService` coordinates reviewable hook-file changes and reads trust state through `hooks/list`; `CodexHookTrustStatusParser` owns the protocol shape. -9. `ActivityHookSetupWindow` presents setup status while a separate review dialog shows - the exact proposed file content before installation or removal. +9. `ActivityHookSetupControl` presents setup status inside Settings while a separate review + dialog shows the exact proposed file content before installation or removal. 10. `UsageWidgetViewModel` maps snapshots to immutable presentation state. 11. `AppThemeController` applies the saved system, light, or dark palette and observes Windows theme changes without leaking registry access into view code. 12. `MainWindow` remains a window-lifecycle shell while the Settings window coordinates - immediate theme, widget-layout, displayed-limit, and Windows startup preferences and focused - user controls render compact, detailed, and repeated limit-row content. + activity-hook setup plus immediate theme, widget-layout, displayed-limit, and Windows startup + preferences. Focused user controls render compact, detailed, and repeated limit-row content. ## Dependency direction diff --git a/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs b/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs index 0c46bc8..4741a22 100644 --- a/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs +++ b/src/CodexUsageWidget/Infrastructure/Windows/TrayIconService.cs @@ -20,10 +20,6 @@ public TrayIconService() }; menu.Items.Add("Open", null, (_, _) => OpenRequested?.Invoke(this, EventArgs.Empty)); menu.Items.Add("Refresh", null, (_, _) => RefreshRequested?.Invoke(this, EventArgs.Empty)); - menu.Items.Add( - "Activity dots...", - null, - (_, _) => ActivityDotsSetupRequested?.Invoke(this, EventArgs.Empty)); menu.Items.Add( "Settings...", null, @@ -67,8 +63,6 @@ public TrayIconService() public event EventHandler? RefreshRequested; - public event EventHandler? ActivityDotsSetupRequested; - public event EventHandler? SettingsRequested; public event EventHandler? DesktopModeRequested; diff --git a/src/CodexUsageWidget/Views/ActivityHookSetupWindow.xaml b/src/CodexUsageWidget/Views/ActivityHookSetupWindow.xaml deleted file mode 100644 index 201c125..0000000 --- a/src/CodexUsageWidget/Views/ActivityHookSetupWindow.xaml +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -