diff --git a/CHANGELOG.md b/CHANGELOG.md
index 943ab17..86a523d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# 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.
+- Licensed the project under the MIT License.
+
## 1.0.0 - 2026-07-22
- Added compact two-row `5H` and `WEEK` taskbar gauges.
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/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..4f05a60 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.
@@ -21,9 +21,12 @@
+
+
+
-
+
@@ -33,45 +36,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 +88,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 +96,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,47 +151,45 @@ 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
-
-`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.
+## Privacy Contract
-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.
+Codex Usage Widget treats rate-limit metadata as display telemetry, not user content.
----
+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
+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.
-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.
+## Reproduction Prompt
-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 concise English build prompt is available in [PROMPT.md](PROMPT.md).
---
-## Reproduction Prompt
+## License
-The concise English build prompt is available in [PROMPT.md](PROMPT.md).
+Released under the [MIT License](LICENSE). You may use, modify, and distribute this project subject to the license terms.
---
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;