Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 with five preset accent colors selected from Settings
- 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
Expand All @@ -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.

Expand All @@ -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, open Settings, check for updates, or exit.
The Settings window opens from the gear button on the widget too. Theme, accent color,
widget layout, displayed limit, and Start with Windows changes apply as soon as they are
selected.

## Activity dots

Expand All @@ -74,7 +78,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**.
Expand All @@ -95,15 +99,18 @@ 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
- `accent-palette.txt`: selected preset accent color
- `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
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.
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
data folder contains any stable copy, saved preferences, and diagnostic logs.
Expand Down
10 changes: 5 additions & 5 deletions docs/ACTIVITY_DOTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 11 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ 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. `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 theme plus the selected
accent 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
activity-hook setup plus immediate theme, accent, widget-layout, displayed-limit, and Windows
startup preferences. Focused user controls render compact, detailed, and repeated limit-row
content.

## Dependency direction

Expand Down Expand Up @@ -79,4 +84,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.
12 changes: 11 additions & 1 deletion src/CodexUsageWidget/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -66,6 +67,12 @@ protected override void OnStartup(StartupEventArgs e)
var activityHookSetupService = new CodexActivityHookSetupService(
new CodexHookConfigurationManager(),
appServerSession);
_themeController = new AppThemeController(
this,
new ThemePreferenceMonitor(
new ThemePreferenceStore(),
new WindowsThemeMonitor()),
new AccentPaletteStore());

var window = new MainWindow(
usageMonitor,
Expand All @@ -76,7 +83,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();
Expand Down Expand Up @@ -126,6 +134,8 @@ public void Dispose()
_disposed = true;
_exceptionHandler?.Dispose();
_exceptionHandler = null;
_themeController?.Dispose();
_themeController = null;
_singleInstanceGuard?.Dispose();
_singleInstanceGuard = null;
GC.SuppressFinalize(this);
Expand Down
4 changes: 4 additions & 0 deletions src/CodexUsageWidget/Infrastructure/AppPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ public static class AppPaths
LocalDataDirectory,
"displayed-limit.txt");

public static string ThemePreferenceFile => Path.Combine(LocalDataDirectory, "theme.txt");

public static string AccentPaletteFile => Path.Combine(LocalDataDirectory, "accent-palette.txt");

public static string LogDirectory => Path.Combine(LocalDataDirectory, "logs");
}
10 changes: 10 additions & 0 deletions src/CodexUsageWidget/Infrastructure/Settings/AccentPalette.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CodexUsageWidget.Infrastructure.Settings;

public enum AccentPalette
{
Blue,
Violet,
Teal,
Emerald,
Pink
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.IO;

namespace CodexUsageWidget.Infrastructure.Settings;

public sealed class AccentPaletteStore
{
private readonly string _path;

public AccentPaletteStore(string? path = null)
{
_path = path ?? AppPaths.AccentPaletteFile;
}

public AccentPalette Load()
{
try
{
return File.ReadAllText(_path).Trim().ToLowerInvariant() switch
{
"violet" => AccentPalette.Violet,
"teal" => AccentPalette.Teal,
"emerald" => AccentPalette.Emerald,
"pink" => AccentPalette.Pink,
_ => AccentPalette.Blue
};
}
catch (IOException)
{
return AccentPalette.Blue;
}
catch (UnauthorizedAccessException)
{
return AccentPalette.Blue;
}
}

public void Save(AccentPalette palette)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
File.WriteAllText(_path, palette.ToString().ToLowerInvariant());
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace CodexUsageWidget.Infrastructure.Settings;

public enum EffectiveTheme
{
Light,
Dark
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CodexUsageWidget.Infrastructure.Settings;

public enum ThemePreference
{
System,
Light,
Dark
}
Original file line number Diff line number Diff line change
@@ -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<EffectiveTheme>? EffectiveThemeChanged;

public event Action<EffectiveTheme>? SystemThemeChanged;

public ThemePreference Preference { get; private set; }

public EffectiveTheme EffectiveTheme { get; private set; }

public EffectiveTheme SystemTheme => _windowsThemeMonitor.UsesLightSystemTheme
? 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.UsesLightAppTheme);

private void SetEffectiveTheme(EffectiveTheme theme)
{
EffectiveTheme = theme;
EffectiveThemeChanged?.Invoke(theme);
}

public void Dispose()
{
if (_disposed)
{
return;
}

_disposed = true;
_windowsThemeMonitor.ThemeChanged -= WindowsThemeMonitorOnThemeChanged;
_windowsThemeMonitor.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CodexUsageWidget.Infrastructure.Settings;

public static class ThemePreferenceResolver
{
public static EffectiveTheme Resolve(
ThemePreference preference,
bool appsUseLightTheme) => preference switch
{
ThemePreference.Light => EffectiveTheme.Light,
ThemePreference.Dark => EffectiveTheme.Dark,
_ => appsUseLightTheme ? EffectiveTheme.Light : EffectiveTheme.Dark
};
}
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
}
Loading