From f8bee3953448e654f95871d8503895be8cbb4201 Mon Sep 17 00:00:00 2001 From: DOndY <120488163+Everyseok@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:32:03 +0900 Subject: [PATCH 1/2] Add signed-in Codex usage sync --- CHANGELOG.md | 7 + PROMPT.md | 6 +- README.md | 83 ++-- build.ps1 | 1 + docs/assets/readme/taskbar-preview.svg | 6 +- docs/assets/readme/widget-hero.svg | 19 +- install.ps1 | 2 +- src/CodexUsageWidget/CodexUsageWidget.csproj | 2 + src/CodexUsageWidget/Program.cs | 2 +- .../Properties/AssemblyInfo.cs | 6 +- .../Services/CodexAccountUsageReader.cs | 369 ++++++++++++++++++ src/CodexUsageWidget/UI/UsageWidgetForm.cs | 47 ++- 12 files changed, 484 insertions(+), 66 deletions(-) create mode 100644 src/CodexUsageWidget/Services/CodexAccountUsageReader.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 943ab17..914430b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0 - 2026-07-22 + +- Added live account usage through the existing signed-in Codex app-server session. +- Kept local session-file snapshots as an offline and low-latency fallback. +- Replaced permanent `N/A` text with a neutral em dash for unreported windows. +- Updated the README and reproduction prompt for the authenticated reader. + ## 1.0.0 - 2026-07-22 - Added compact two-row `5H` and `WEEK` taskbar gauges. diff --git a/PROMPT.md b/PROMPT.md index ffac57d..a138c6f 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -6,8 +6,10 @@ Build a Codex usage widget that sits on the Windows 11 taskbar, immediately to t - Show the remaining 5-hour and weekly limits as two stacked gauges labeled `5H` and `WEEK`. - Use a compact, transparent Fluent design approximately 232 px wide. - Use Arial Bold and the Windows 11 blue accent color. -- Read actual `rate_limits` values from local Codex session files and refresh within 0.5 seconds of a file change. -- Show `N/A` for limits that are not reported, and show reset times in a tooltip. +- Reuse the existing signed-in Codex session through `account/rateLimits/read`; never copy or store OAuth credentials. +- Keep local Codex `rate_limits` session events as an offline fallback and refresh within 0.5 seconds of a file change. +- Keep both rows visible, use an em dash for an unreported window, and replace it automatically when Codex provides a real percentage. +- Show reset times and the latest refresh time in a tooltip. - Hide automatically over full-screen apps and return when the taskbar is visible. - Support left-click refresh, a right-click menu, and single-instance execution. - Include an installer that starts the widget automatically at Windows sign-in. diff --git a/README.md b/README.md index 5eacfa9..5b0a041 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@

- A compact Windows taskbar gauge for the Codex limits that matter.
- See the current 5-hour and weekly allowance without reopening Codex.

- No API key. No auth-file access. No cloud service. No usage estimation. + A compact Windows taskbar gauge for Codex usage.
+ Keep the 5-hour and weekly windows visible without reopening Codex.

+ No separate API key. No auth-file access. No usage estimation.

@@ -23,7 +23,7 @@ Windows 10 and 11 Taskbar widget - Local real-time updates + Signed-in live updates No authentication-file reads

@@ -33,45 +33,46 @@ | Usual usage check | Codex Usage Widget | |---|---| -| Reopen Codex and expand the usage panel | Keeps both limits visible on the taskbar | -| Poll a remote endpoint or store an API key | Reads the rate-limit snapshot Codex already wrote locally | -| Estimate usage from tokens or elapsed time | Displays the reported `used_percent` value directly | -| Refresh on a fixed timer only | Reacts to session-file changes in about 500 ms | -| Looks like a separate desktop overlay | Uses a transparent 232 px Fluent taskbar surface | +| Reopen Codex and expand the usage panel | Keeps both rows visible on the taskbar | +| Store a separate OAuth token or API key | Reuses the existing signed-in Codex session | +| Estimate usage from tokens or elapsed time | Displays the account's reported percentage directly | +| Assume every plan exposes the same limits | Clearly marks windows the account does not provide | +| Use a separate desktop overlay | Fits inside a transparent 232 px Fluent taskbar surface | -The widget is intentionally narrow in scope. It watches Codex `token_count` events, extracts the latest `rate_limits` snapshot, converts usage into remaining percentage, and renders only two taskbar rows: `5H` and `WEEK`. +The primary reader asks the locally installed Codex app server for `account/rateLimits/read`. Codex performs the authenticated request, so the widget never opens, copies, or stores the underlying OAuth credential. Recent local session snapshots remain available as a fast fallback. --- ## Core Properties -- **Reported usage, not guessed usage**: values come from Codex rate-limit snapshots. -- **Two-window glance**: 5-hour and weekly limits stay visible in one compact surface. -- **Event-driven refresh**: a filesystem watcher reacts to JSONL changes with a 500 ms debounce. -- **Taskbar-aware placement**: the widget anchors immediately left of the Windows notification area. +- **Two-row glance**: `5H` and `WEEK` stay in fixed positions for muscle memory. +- **Truthful state**: an unavailable window shows an em dash instead of a fabricated percentage. +- **Signed-in refresh**: the existing Codex login is reused every 15 seconds and on manual refresh. +- **Fast local fallback**: a filesystem watcher reacts to JSONL changes with a 500 ms debounce. +- **Taskbar-aware placement**: the widget anchors immediately left of the notification area. - **Full-screen friendly**: it hides over full-screen apps and returns with the taskbar. -- **Local privacy boundary**: it does not read `auth.json`, make network requests, or require credentials. +- **Credential boundary**: it never reads `auth.json` or stores an OAuth token, API key, or password. --- -## Local Usage Runtime +## Runtime Architecture | Layer | Value | |---|---| | App type | C# WinForms taskbar widget | | Runtime | .NET Framework 4.8 | -| Session path | `%USERPROFILE%\.codex\sessions\**\*.jsonl` | -| Event type | `event_msg` → `token_count` | -| Usage payload | `rate_limits.primary` / `rate_limits.secondary` | -| Live update | `FileSystemWatcher` + 500 ms debounce | -| Fallback update | 15-second refresh timer | +| Primary source | Codex `account/rateLimits/read` | +| Authentication | Existing local Codex sign-in | +| Local fallback | `%USERPROFILE%\.codex\sessions\**\*.jsonl` | +| Fallback event | `event_msg` -> `token_count` -> `rate_limits` | +| Refresh | 15-second account read + click-to-refresh | +| Local reaction | `FileSystemWatcher` + 500 ms debounce | | Taskbar anchor | `Shell_TrayWnd` / `TrayNotifyWnd` | | Rendering | Per-pixel alpha layered window | -| Privacy boundary | No auth files, clipboard, screenshots, or network calls | ### Usage Model -Codex reports how much of a rate-limit window has been used. The widget converts it into the value shown on the taskbar: +Codex reports how much of a rate-limit window has been used. The widget converts that value into the remaining percentage shown on the taskbar: ```text remaining_percent = clamp(100 - used_percent, 0, 100) @@ -84,7 +85,7 @@ Rate-limit windows are classified by duration: WEEK = nearest reported window to 10,080 minutes, with a 6-day minimum ``` -If a reset timestamp has passed before a new snapshot arrives, the local display returns to `100%`. A limit omitted from the latest snapshot is shown as `N/A` rather than replaced with an estimate. +If the signed-in account returns `null` for a window, its fixed row shows `—`. The widget never derives one limit from the other. If Codex later reports that window, the real percentage appears automatically. --- @@ -92,12 +93,12 @@ If a reset timestamp has passed before a new snapshot arrives, the local display | Condition | Display | Accent | |---|---|---| -| Limit not reported | `N/A` | Neutral gray | +| Window not reported by the account | `—` | Neutral gray | | More than 35% remaining | Percentage gauge | Windows blue | | 16-35% remaining | Percentage gauge | Amber | | 0-15% remaining | Percentage gauge | Red | -Hovering the widget shows the exact remaining value, reset time, and timestamp of the latest Codex snapshot. Left-click refreshes immediately; right-click opens the native Refresh/Exit menu. +Hovering the widget shows the exact remaining value, reset time, and timestamp. Left-click refreshes immediately; right-click opens the native Refresh/Exit menu. --- @@ -147,41 +148,33 @@ The repository workflow builds on `windows-latest` and uploads `CodexUsageWidget |---|---| | Windows | Windows 10 or Windows 11 | | Runtime | .NET Framework 4.8 | -| Codex data | Desktop or CLI sessions under `%USERPROFILE%\.codex\sessions` | -| Network | Required only for clone, Actions, and release download | +| Codex | Signed-in Codex CLI recommended; Desktop/CLI session data is the fallback | +| Network | Required for live account refresh; local fallback remains available offline | | Installed location | `%LOCALAPPDATA%\CodexUsageWidget` | -| Taskbar footprint | 232 × 42 px at 100% scaling | +| Taskbar footprint | 232 x 42 px at 100% scaling | --- ## Troubleshooting -| Symptom | Fix | +| Symptom | Explanation / Fix | |---|---| -| `5H` shows `N/A` | The latest Codex snapshot may not report a separate 5-hour window for this plan. | -| Both rows show `N/A` | Use Codex once, then confirm that `.codex\sessions` contains recent JSONL files. | -| Values look stale | Left-click the widget; the 15-second fallback refresh will also re-read the latest snapshot. | +| `5H` shows `—` | The signed-in account does not currently report a separate 5-hour window. This is expected for some plans. | +| Both rows show `—` | Confirm `codex --version` works and Codex is signed in, or use Codex once to create local session data. | +| Values look stale | Left-click the widget; the 15-second account refresh also updates it automatically. | | Widget is not visible | Exit the full-screen app or confirm that the Windows taskbar is visible. | | Widget starts twice | Re-run `install.ps1`; the single-instance mutex keeps only one runtime active. | | Build fails | Confirm that .NET Framework 4.8 and its C# compiler are available. | --- -## Distribution Notes +## Privacy Contract -`build.ps1` produces a single WinForms executable with no third-party runtime packages. `install.ps1` copies it to the local app-data directory, replaces older widget Startup shortcuts that target the same executable, and launches the installed copy. +Codex Usage Widget treats rate-limit metadata as display telemetry, not user content. -The `v1.0.0` GitHub release contains the same executable verified by the local build flow. The repository remains source-first: generated files under `dist/` are ignored by Git and published through releases or Actions artifacts instead. +The primary reader starts the local Codex app server and requests `account/rateLimits/read`. Codex owns the authenticated network request; the widget never opens or copies the credential. The fallback reader scans recent session-file tails in memory, but parses only lines that match `event_msg` -> `token_count` and contain `rate_limits`. Prompt and response payloads are not interpreted, retained, or transmitted. ---- - -## Runtime Contract - -Codex Usage Widget treats local rate-limit metadata as display telemetry, not user content. - -The runtime scans the tail of recent session files in memory, but parses only lines that match `event_msg` → `token_count` and contain `rate_limits`. All other event types are ignored; prompt and response payloads are not interpreted, retained, or transmitted. The widget does not access authentication files, clipboard data, screenshots, focused-app content, URLs, or browser history, and it makes no network requests. - -The current parser depends on Codex's local JSONL session shape rather than a public usage API. A future Codex format change may require an update to `UsageReader`. +The widget does not access the clipboard, screenshots, focused-app content, URLs, or browser history. If the Codex app-server method is unavailable, it keeps using the last reported value and the local fallback. --- diff --git a/build.ps1 b/build.ps1 index a04c841..0ec73b0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -30,6 +30,7 @@ New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null /reference:System.dll ` /reference:System.Core.dll ` /reference:System.Drawing.dll ` + /reference:System.Web.Extensions.dll ` /reference:System.Windows.Forms.dll ` $sources diff --git a/docs/assets/readme/taskbar-preview.svg b/docs/assets/readme/taskbar-preview.svg index f3415de..9ffc089 100644 --- a/docs/assets/readme/taskbar-preview.svg +++ b/docs/assets/readme/taskbar-preview.svg @@ -33,12 +33,12 @@ 5H - N/A + WEEK - - 95% + + 83% diff --git a/docs/assets/readme/widget-hero.svg b/docs/assets/readme/widget-hero.svg index 5088e9d..d799a8b 100644 --- a/docs/assets/readme/widget-hero.svg +++ b/docs/assets/readme/widget-hero.svg @@ -38,18 +38,18 @@ - LOCAL LIVE + SIGNED IN Your limits. Always in sight. Two windows. One compact taskbar surface. - Reported locally. Refreshed as Codex writes. + Reported by Codex. Never estimated. 232 PX - 500 MS + 15 SEC @@ -64,16 +64,17 @@ 5H - N/A + + 72% WEEK - - - 95% + + + 83% - UPDATED FROM LOCAL RATE_LIMITS - 0.5s + CODEX ACCOUNT RATE LIMITS + LIVE diff --git a/install.ps1 b/install.ps1 index 6c8dcc7..e8e9b77 100644 --- a/install.ps1 +++ b/install.ps1 @@ -44,7 +44,7 @@ Get-ChildItem -LiteralPath $startupDirectory -File -Filter '*.lnk' | ForEach-Obj $shortcut = $shell.CreateShortcut($shortcutPath) $shortcut.TargetPath = $target $shortcut.WorkingDirectory = $installDirectory -$shortcut.Description = 'Codex 5-hour and weekly usage widget' +$shortcut.Description = 'Codex usage widget' $shortcut.Save() Start-Process -FilePath $target -WindowStyle Hidden diff --git a/src/CodexUsageWidget/CodexUsageWidget.csproj b/src/CodexUsageWidget/CodexUsageWidget.csproj index 39bc4d5..709860e 100644 --- a/src/CodexUsageWidget/CodexUsageWidget.csproj +++ b/src/CodexUsageWidget/CodexUsageWidget.csproj @@ -24,6 +24,7 @@ + @@ -32,6 +33,7 @@ + diff --git a/src/CodexUsageWidget/Program.cs b/src/CodexUsageWidget/Program.cs index 368c128..b731626 100644 --- a/src/CodexUsageWidget/Program.cs +++ b/src/CodexUsageWidget/Program.cs @@ -37,7 +37,7 @@ private static int Main(string[] arguments) private static int DumpLatestUsage() { - UsageSnapshot snapshot = UsageReader.ReadLatest(); + UsageSnapshot snapshot = CodexAccountUsageReader.ReadLatest() ?? UsageReader.ReadLatest(); if (snapshot == null) { Console.WriteLine("NO_DATA"); diff --git a/src/CodexUsageWidget/Properties/AssemblyInfo.cs b/src/CodexUsageWidget/Properties/AssemblyInfo.cs index ec3fd77..4f648c9 100644 --- a/src/CodexUsageWidget/Properties/AssemblyInfo.cs +++ b/src/CodexUsageWidget/Properties/AssemblyInfo.cs @@ -5,8 +5,8 @@ [assembly: AssemblyDescription("Compact Windows taskbar widget for Codex rate limits")] [assembly: AssemblyCompany("Everyseok")] [assembly: AssemblyProduct("Codex Usage Widget")] -[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyCopyright("Copyright (c) 2026")] [assembly: ComVisible(false)] [assembly: Guid("86bb3b93-c26d-4a73-bfc4-79efcb4bd2ea")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/src/CodexUsageWidget/Services/CodexAccountUsageReader.cs b/src/CodexUsageWidget/Services/CodexAccountUsageReader.cs new file mode 100644 index 0000000..17b26a2 --- /dev/null +++ b/src/CodexUsageWidget/Services/CodexAccountUsageReader.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using CodexUsageWidget.Models; + +namespace CodexUsageWidget.Services +{ + internal static class CodexAccountUsageReader + { + private const int ResponseTimeoutMilliseconds = 8000; + private const int InitializeRequestId = 1; + private const int RateLimitsRequestId = 2; + + private static readonly JavaScriptSerializer Serializer = new JavaScriptSerializer(); + + internal static UsageSnapshot ReadLatest() + { + Process process = null; + + try + { + process = StartAppServer(); + if (process == null) + return null; + + WriteMessage( + process, + "{\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"codex-usage-widget\",\"version\":\"1.1.0\"},\"capabilities\":{\"experimentalApi\":true}}}"); + + string initializeResponse = ReadResponse( + process.StandardOutput, + InitializeRequestId, + ResponseTimeoutMilliseconds); + if (initializeResponse == null) + return null; + + WriteMessage(process, "{\"method\":\"initialized\"}"); + WriteMessage( + process, + "{\"id\":2,\"method\":\"account/rateLimits/read\",\"params\":null}"); + + string rateLimitsResponse = ReadResponse( + process.StandardOutput, + RateLimitsRequestId, + ResponseTimeoutMilliseconds); + + return ParseRateLimitsResponse(rateLimitsResponse); + } + catch + { + return null; + } + finally + { + StopAppServer(process); + } + } + + private static Process StartAppServer() + { + string codexLauncher = FindCodexLauncher(); + if (codexLauncher == null) + return null; + + string commandProcessor = Environment.GetEnvironmentVariable("ComSpec"); + if (String.IsNullOrWhiteSpace(commandProcessor)) + commandProcessor = "cmd.exe"; + + string extension = Path.GetExtension(codexLauncher); + bool isCommandScript = + String.Equals(extension, ".cmd", StringComparison.OrdinalIgnoreCase) || + String.Equals(extension, ".bat", StringComparison.OrdinalIgnoreCase); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = isCommandScript ? commandProcessor : codexLauncher, + Arguments = isCommandScript + ? "/d /s /c \"\"" + codexLauncher + "\" app-server --stdio\"" + : "app-server --stdio", + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + Process process = new Process { StartInfo = startInfo }; + process.ErrorDataReceived += delegate { }; + + if (!process.Start()) + { + process.Dispose(); + return null; + } + + process.BeginErrorReadLine(); + return process; + } + + private static string FindCodexLauncher() + { + string pathValue = Environment.GetEnvironmentVariable("PATH"); + if (String.IsNullOrWhiteSpace(pathValue)) + return null; + + string[] extensions = { ".exe", ".cmd", ".bat" }; + foreach (string pathEntry in pathValue.Split(Path.PathSeparator)) + { + string directory = pathEntry.Trim().Trim('"'); + if (String.IsNullOrWhiteSpace(directory)) + continue; + + foreach (string extension in extensions) + { + try + { + string candidate = Path.Combine(directory, "codex" + extension); + if (File.Exists(candidate)) + return Path.GetFullPath(candidate); + } + catch + { + // Ignore malformed or inaccessible PATH entries. + } + } + } + + return null; + } + + private static void WriteMessage(Process process, string message) + { + process.StandardInput.WriteLine(message); + process.StandardInput.Flush(); + } + + private static string ReadResponse(StreamReader reader, int requestId, int timeoutMilliseconds) + { + DateTime deadline = DateTime.UtcNow.AddMilliseconds(timeoutMilliseconds); + + while (DateTime.UtcNow < deadline) + { + int remaining = Math.Max(1, (int)(deadline - DateTime.UtcNow).TotalMilliseconds); + Task readTask = Task.Factory.StartNew(delegate + { + return reader.ReadLine(); + }); + if (!readTask.Wait(remaining)) + return null; + + string line = readTask.Result; + if (line == null) + return null; + + int responseId; + if (TryGetResponseId(line, out responseId) && responseId == requestId) + return line; + } + + return null; + } + + private static bool TryGetResponseId(string json, out int requestId) + { + requestId = 0; + + try + { + IDictionary root = Serializer.DeserializeObject(json) as IDictionary; + object value; + return root != null && + root.TryGetValue("id", out value) && + TryConvertInt32(value, out requestId); + } + catch + { + return false; + } + } + + private static UsageSnapshot ParseRateLimitsResponse(string json) + { + if (String.IsNullOrWhiteSpace(json)) + return null; + + try + { + IDictionary root = Serializer.DeserializeObject(json) as IDictionary; + IDictionary result = GetObject(root, "result"); + IDictionary rateLimits = GetObject(result, "rateLimits"); + if (rateLimits == null) + return null; + + List windows = new List(); + AddWindow(windows, GetObject(rateLimits, "primary")); + AddWindow(windows, GetObject(rateLimits, "secondary")); + + return new UsageSnapshot + { + TimestampUtc = DateTime.UtcNow, + PlanType = GetString(rateLimits, "planType"), + FiveHour = FindWindow(windows, 240, 360, 300), + Weekly = FindWindow(windows, 6 * 24 * 60, Int32.MaxValue, 7 * 24 * 60) + }; + } + catch + { + return null; + } + } + + private static IDictionary GetObject( + IDictionary parent, + string key) + { + if (parent == null) + return null; + + object value; + return parent.TryGetValue(key, out value) + ? value as IDictionary + : null; + } + + private static string GetString(IDictionary parent, string key) + { + if (parent == null) + return null; + + object value; + return parent.TryGetValue(key, out value) && value != null + ? Convert.ToString(value, CultureInfo.InvariantCulture) + : null; + } + + private static void AddWindow( + ICollection windows, + IDictionary value) + { + if (value == null) + return; + + object usedValue; + object minutesValue; + double usedPercent; + int windowMinutes; + + if (!value.TryGetValue("usedPercent", out usedValue) || + !value.TryGetValue("windowDurationMins", out minutesValue) || + !TryConvertDouble(usedValue, out usedPercent) || + !TryConvertInt32(minutesValue, out windowMinutes)) + { + return; + } + + long resetsAt = 0; + object resetValue; + if (value.TryGetValue("resetsAt", out resetValue)) + TryConvertInt64(resetValue, out resetsAt); + + windows.Add(new LimitWindow + { + UsedPercent = usedPercent, + WindowMinutes = windowMinutes, + ResetsAt = resetsAt + }); + } + + private static LimitWindow FindWindow( + IEnumerable windows, + int minimumMinutes, + int maximumMinutes, + int preferredMinutes) + { + LimitWindow closest = null; + int closestDistance = Int32.MaxValue; + + foreach (LimitWindow window in windows) + { + if (window.WindowMinutes < minimumMinutes || window.WindowMinutes > maximumMinutes) + continue; + + int distance = Math.Abs(window.WindowMinutes - preferredMinutes); + if (distance < closestDistance) + { + closest = window; + closestDistance = distance; + } + } + + return closest; + } + + private static bool TryConvertDouble(object value, out double result) + { + result = 0; + try + { + result = Convert.ToDouble(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + return false; + } + } + + private static bool TryConvertInt32(object value, out int result) + { + result = 0; + try + { + result = Convert.ToInt32(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + return false; + } + } + + private static bool TryConvertInt64(object value, out long result) + { + result = 0; + try + { + result = Convert.ToInt64(value, CultureInfo.InvariantCulture); + return true; + } + catch + { + return false; + } + } + + private static void StopAppServer(Process process) + { + if (process == null) + return; + + try + { + process.StandardInput.Close(); + if (!process.WaitForExit(1000)) + process.Kill(); + } + catch + { + try + { + process.Kill(); + } + catch + { + // Process already exited. + } + } + finally + { + process.Dispose(); + } + } + } +} diff --git a/src/CodexUsageWidget/UI/UsageWidgetForm.cs b/src/CodexUsageWidget/UI/UsageWidgetForm.cs index 3127493..d9128fb 100644 --- a/src/CodexUsageWidget/UI/UsageWidgetForm.cs +++ b/src/CodexUsageWidget/UI/UsageWidgetForm.cs @@ -4,6 +4,7 @@ using System.Drawing.Imaging; using System.Globalization; using System.Text; +using System.Threading; using System.Windows.Forms; using CodexUsageWidget.Interop; using CodexUsageWidget.Models; @@ -28,6 +29,7 @@ internal sealed class UsageWidgetForm : Form private readonly UsageMonitor usageMonitor; private UsageSnapshot snapshot; + private int accountRefreshInProgress; internal UsageWidgetForm() { @@ -165,11 +167,52 @@ protected override void OnPaint(PaintEventArgs eventArgs) private void RefreshUsage() { UsageSnapshot latest = UsageReader.ReadLatest(); - if (latest != null) + if (latest != null && (snapshot == null || latest.TimestampUtc >= snapshot.TimestampUtc)) snapshot = latest; UpdateToolTip(); RenderLayered(); + RefreshAccountUsage(); + } + + private void RefreshAccountUsage() + { + if (Interlocked.CompareExchange(ref accountRefreshInProgress, 1, 0) != 0) + return; + + ThreadPool.QueueUserWorkItem(delegate + { + UsageSnapshot accountSnapshot = CodexAccountUsageReader.ReadLatest(); + + if (IsDisposed || !IsHandleCreated) + { + Interlocked.Exchange(ref accountRefreshInProgress, 0); + return; + } + + try + { + BeginInvoke((MethodInvoker)delegate + { + try + { + if (accountSnapshot != null) + snapshot = accountSnapshot; + + UpdateToolTip(); + RenderLayered(); + } + finally + { + Interlocked.Exchange(ref accountRefreshInProgress, 0); + } + }); + } + catch + { + Interlocked.Exchange(ref accountRefreshInProgress, 0); + } + }); } private void UpdateToolTip() @@ -373,7 +416,7 @@ private void DrawLimitRow( using (SolidBrush trackBrush = new SolidBrush(Color.FromArgb(72, 255, 255, 255))) graphics.FillPath(trackBrush, trackPath); - string value = "N/A"; + string value = "\u2014"; if (window != null) { double remaining = window.RemainingPercent; From f783204bad7bf015a47d5eb50e5b21f25d6aad08 Mon Sep 17 00:00:00 2001 From: DOndY <120488163+Everyseok@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:34:00 +0900 Subject: [PATCH 2/2] Add MIT license --- CHANGELOG.md | 1 + LICENSE | 21 +++++++++++++++++++++ README.md | 9 +++++++++ 3 files changed, 31 insertions(+) create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md index 914430b..86a523d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Kept local session-file snapshots as an offline and low-latency fallback. - Replaced permanent `N/A` text with a neutral em dash for unreported windows. - Updated the README and reproduction prompt for the authenticated reader. +- Licensed the project under the MIT License. ## 1.0.0 - 2026-07-22 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0102a93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jun Seok Kim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5b0a041..4f05a60 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ Release v1.0.0 + + MIT License + Windows 10 and 11 Taskbar widget Signed-in live updates @@ -184,6 +187,12 @@ The concise English build prompt is available in [PROMPT.md](PROMPT.md). --- +## License + +Released under the [MIT License](LICENSE). You may use, modify, and distribute this project subject to the license terms. + +--- + ## Author **Jun Seok Kim**