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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ of `codex.cmd` or `codex.exe`.
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
- Automatic Windows-language selection with English fallback, plus English and Simplified
Chinese overrides in 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 Down Expand Up @@ -66,9 +68,9 @@ subscription percentage.

Select the `−` button to move the widget to the taskbar. Right-click the taskbar label or
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.
The Settings window opens from the gear button on the widget too. Language, theme, accent
color, widget layout, displayed limit, and Start with Windows changes apply as soon as they
are selected.

## Activity dots

Expand Down Expand Up @@ -101,6 +103,7 @@ The application writes only under `%LOCALAPPDATA%\CodexUsageWidget`:
- `displayed-limit.txt`: selected summary limit
- `theme.txt`: system, light, or dark theme preference
- `accent-palette.txt`: selected preset accent color
- `language.txt`: system, English, or Simplified Chinese language 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
Expand Down
4 changes: 3 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ tests/CodexUsageWidget.Tests/ Unit tests for parsing, formatting and persistence
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
12. `AppLanguageController` resolves the saved system, English, or Simplified Chinese
preference, while standard .NET resources and a notifying WPF binding refresh existing UI.
13. `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.
Expand Down
20 changes: 17 additions & 3 deletions src/CodexUsageWidget/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using CodexUsageWidget.Infrastructure.Preview;
using CodexUsageWidget.Infrastructure.Settings;
using CodexUsageWidget.Infrastructure.Windows;
using CodexUsageWidget.Localization;
using CodexUsageWidget.Views;

namespace CodexUsageWidget;
Expand All @@ -18,8 +19,20 @@ public partial class App : System.Windows.Application, IDisposable
private FileLogger? _logger;
private GlobalExceptionHandler? _exceptionHandler;
private AppThemeController? _themeController;
private readonly AppLanguageController _languageController;
private bool _disposed;

public App()
: this(new AppLanguageController(new LanguagePreferenceStore()))
{
}

public App(AppLanguageController languageController)
{
ArgumentNullException.ThrowIfNull(languageController);
_languageController = languageController;
}

protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Expand Down Expand Up @@ -84,7 +97,8 @@ protected override void OnStartup(StartupEventArgs e)
new DisplayedLimitPreferenceStore(),
startupRegistrationService,
new TrayIconService(),
_themeController);
_themeController,
_languageController);
MainWindow = window;
activityMonitor.StartAsync().GetAwaiter().GetResult();
window.Show();
Expand All @@ -100,8 +114,8 @@ protected override void OnStartup(StartupEventArgs e)
activityMonitor?.DisposeAsync().AsTask().GetAwaiter().GetResult();
_logger.LogError("Application startup failed.", ex);
System.Windows.MessageBox.Show(
"Codex Usage Widget could not start. See the log under " + AppPaths.LogDirectory,
"Codex Usage Widget",
Strings.Format("App_StartupFailure", AppPaths.LogDirectory),
Strings.Get("App_Name"),
MessageBoxButton.OK,
MessageBoxImage.Error);
Shutdown(-1);
Expand Down
3 changes: 2 additions & 1 deletion src/CodexUsageWidget/Application/UsageMonitor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodexUsageWidget.Domain;
using CodexUsageWidget.Localization;

namespace CodexUsageWidget.Application;

Expand Down Expand Up @@ -67,7 +68,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default)
}
catch (OperationCanceledException) when (!_lifetime.IsCancellationRequested)
{
RefreshFailed?.Invoke("Codex did not respond in time.");
RefreshFailed?.Invoke(Strings.Get("Error_ResponseTimeout"));
}
catch (Exception ex)
{
Expand Down
21 changes: 15 additions & 6 deletions src/CodexUsageWidget/Application/UsageTextFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Globalization;
using CodexUsageWidget.Localization;

namespace CodexUsageWidget.Application;

public static class UsageTextFormatter
Expand All @@ -7,32 +10,38 @@ public static string ToFriendlyError(string message)
if (message.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
message.Contains("cannot find", StringComparison.OrdinalIgnoreCase))
{
return "Codex CLI was not found on PATH.";
return Strings.Get("Error_CliNotFound");
}

if (message.Contains("login", StringComparison.OrdinalIgnoreCase) ||
message.Contains("unauthorized", StringComparison.OrdinalIgnoreCase))
{
return "Run codex login, then refresh.";
return Strings.Get("Error_LoginRequired");
}

return message.Length > 100 ? message[..100] + "…" : message;
var detail = message.Length > 100 ? message[..100] + "…" : message;
return Strings.Format("Error_Unexpected", detail);
}

public static string FormatReset(DateTimeOffset reset, DateTimeOffset? now = null)
{
var remaining = reset - (now ?? DateTimeOffset.Now);
if (remaining <= TimeSpan.Zero)
{
return "now";
return Strings.Get("Usage_ResetsNow");
}

if (remaining < TimeSpan.FromHours(24))
{
return $"in {Math.Max(1, (int)Math.Ceiling(remaining.TotalHours))}h · {reset:HH:mm}";
return Strings.Format(
"Usage_ResetsInHours",
Math.Max(1, (int)Math.Ceiling(remaining.TotalHours)),
reset.ToString("HH:mm", CultureInfo.CurrentCulture));
}

return $"{reset:ddd HH:mm}";
return Strings.Format(
"Usage_ResetsAt",
reset.ToString("ddd HH:mm", CultureInfo.CurrentCulture));
}

public static string ColorForRemaining(double remainingPercent) => remainingPercent switch
Expand Down
2 changes: 2 additions & 0 deletions src/CodexUsageWidget/Infrastructure/AppPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static class AppPaths

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

public static string LanguagePreferenceFile => Path.Combine(LocalDataDirectory, "language.txt");

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

public static string LogDirectory => Path.Combine(LocalDataDirectory, "logs");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task<ActivityHookSetupStatus> GetStatusAsync(
{
return new ActivityHookSetupStatus(
ActivityHookSetupState.InstalledStatusUnavailable,
$"Codex could not report hook trust status: {ex.Message}");
ex.Message);
}
}

Expand Down Expand Up @@ -123,8 +123,6 @@ private static ActivityHookSetupStatus FromTrustEvaluation(
new ActivityHookSetupStatus(ActivityHookSetupState.Active),
CodexHookTrustEvaluation.Modified =>
new ActivityHookSetupStatus(ActivityHookSetupState.Modified),
_ => new ActivityHookSetupStatus(
ActivityHookSetupState.InstalledStatusUnavailable,
"The hook definitions are installed, but Codex did not report all expected hooks.")
_ => new ActivityHookSetupStatus(ActivityHookSetupState.InstalledStatusUnavailable)
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CodexUsageWidget.Infrastructure.Settings;

public enum LanguagePreference
{
System,
English,
SimplifiedChinese
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Globalization;

namespace CodexUsageWidget.Infrastructure.Settings;

public static class LanguagePreferenceResolver
{
public static LanguagePreference Resolve(
LanguagePreference preference,
CultureInfo uiCulture) => preference switch
{
LanguagePreference.English => LanguagePreference.English,
LanguagePreference.SimplifiedChinese => LanguagePreference.SimplifiedChinese,
_ => IsSimplifiedChinese(uiCulture)
? LanguagePreference.SimplifiedChinese
: LanguagePreference.English
};

private static bool IsSimplifiedChinese(CultureInfo culture)
{
for (var current = culture; !string.IsNullOrEmpty(current.Name); current = current.Parent)
{
if (string.Equals(current.Name, "zh-Hans", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.IO;

namespace CodexUsageWidget.Infrastructure.Settings;

public sealed class LanguagePreferenceStore
{
private readonly string _path;

public LanguagePreferenceStore(string? path = null)
{
_path = path ?? AppPaths.LanguagePreferenceFile;
}

public LanguagePreference Load()
{
try
{
return File.ReadAllText(_path).Trim().ToLowerInvariant() switch
{
"english" => LanguagePreference.English,
"simplified-chinese" => LanguagePreference.SimplifiedChinese,
_ => LanguagePreference.System
};
}
catch (IOException)
{
return LanguagePreference.System;
}
catch (UnauthorizedAccessException)
{
return LanguagePreference.System;
}
}

public void Save(LanguagePreference preference)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
File.WriteAllText(
_path,
preference switch
{
LanguagePreference.English => "english",
LanguagePreference.SimplifiedChinese => "simplified-chinese",
_ => "system"
});
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
Loading