Skip to content
Merged
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,19 @@ That quits `CodexToolbar` if it is running, removes its launch-at-login registra

## Screenshots

Captured from v0.1.17 using fixed sample data in light and dark mode. The website uses the same projection screenshots.
Captured from v0.1.18 with fixed sample values. The default examples show a Weekly Codex window; the optional Spark example adds separately labeled 5h and Weekly windows. Available windows depend on what Codex reports for your account. The website uses the same Weekly projection screenshots.

| Theme | Normal | Warning | Critical | Projection |
| --- | --- | --- | --- | --- |
| Light | ![Normal light mode status popover](screenshots/readme-normal-light-popover.png) | ![Warning light mode status popover](screenshots/readme-warning-light-popover.png) | ![Critical light mode status popover](screenshots/readme-critical-light-popover.png) | ![Projection light mode status popover](screenshots/readme-projection-light-popover.png) |
| Dark | ![Normal dark mode status popover](screenshots/readme-normal-dark-popover.png) | ![Warning dark mode status popover](screenshots/readme-warning-dark-popover.png) | ![Critical dark mode status popover](screenshots/readme-critical-dark-popover.png) | ![Projection dark mode status popover](screenshots/readme-projection-dark-popover.png) |

With Spark usage enabled:

| Light | Dark |
| --- | --- |
| ![Weekly Codex usage with separate Spark 5h and Weekly limits in light mode](screenshots/readme-spark-light-popover.png) | ![Weekly Codex usage with separate Spark 5h and Weekly limits in dark mode](screenshots/readme-spark-dark-popover.png) |

## Notes

- The app launches its own `codex app-server` subprocess. You do not need to keep an interactive Codex CLI session open.
Expand Down
50 changes: 19 additions & 31 deletions Sources/CodexToolbar/App/ScreenshotScenario.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,38 +78,33 @@ struct ScreenshotScenario: Equatable, Sendable {
))!
}

// Screenshot fixtures mirror the observed window structure: Codex has a
// weekly primary window; Spark has separate 5h and weekly windows.
private static func snapshot(
primaryUsed: Int,
primaryReset: Date,
secondaryUsed: Int,
secondaryReset: Date,
secondaryDurationMinutes: Int = 10080
used: Int,
reset: Date,
durationMinutes: Int = 10080
) -> CodexRateLimitsSnapshot {
CodexRateLimitsSnapshot(
credits: nil,
limitId: "codex",
limitName: "Codex",
planType: .pro,
primary: CodexRateLimitWindow(
resetsAt: Int64(primaryReset.timeIntervalSince1970),
usedPercent: primaryUsed,
windowDurationMins: 300
resetsAt: Int64(reset.timeIntervalSince1970),
usedPercent: used,
windowDurationMins: durationMinutes
),
secondary: CodexRateLimitWindow(
resetsAt: Int64(secondaryReset.timeIntervalSince1970),
usedPercent: secondaryUsed,
windowDurationMins: secondaryDurationMinutes
)
secondary: nil
)
}

static let normal: ScreenshotScenario = {
let now = date(year: 2026, month: 3, day: 8, hour: 13, minute: 3)
let primaryReset = date(year: 2026, month: 3, day: 8, hour: 13, minute: 54)
let secondaryReset = date(year: 2026, month: 3, day: 11, hour: 0, minute: 0)
return ScreenshotScenario(
name: "normal",
snapshot: snapshot(primaryUsed: 19, primaryReset: primaryReset, secondaryUsed: 10, secondaryReset: secondaryReset),
snapshot: snapshot(used: 19, reset: secondaryReset),
rateLimitsByLimitId: nil,
now: now,
lastUpdated: now,
Expand All @@ -121,11 +116,10 @@ struct ScreenshotScenario: Equatable, Sendable {

static let warning: ScreenshotScenario = {
let now = date(year: 2026, month: 3, day: 8, hour: 14, minute: 11)
let primaryReset = date(year: 2026, month: 3, day: 8, hour: 14, minute: 46)
let secondaryReset = date(year: 2026, month: 3, day: 11, hour: 0, minute: 0)
return ScreenshotScenario(
name: "warning",
snapshot: snapshot(primaryUsed: 74, primaryReset: primaryReset, secondaryUsed: 41, secondaryReset: secondaryReset),
snapshot: snapshot(used: 74, reset: secondaryReset),
rateLimitsByLimitId: nil,
now: now,
lastUpdated: now,
Expand All @@ -137,11 +131,10 @@ struct ScreenshotScenario: Equatable, Sendable {

static let critical: ScreenshotScenario = {
let now = date(year: 2026, month: 3, day: 8, hour: 16, minute: 22)
let primaryReset = date(year: 2026, month: 3, day: 8, hour: 16, minute: 31)
let secondaryReset = date(year: 2026, month: 3, day: 11, hour: 0, minute: 0)
return ScreenshotScenario(
name: "critical",
snapshot: snapshot(primaryUsed: 94, primaryReset: primaryReset, secondaryUsed: 84, secondaryReset: secondaryReset),
snapshot: snapshot(used: 94, reset: secondaryReset),
rateLimitsByLimitId: nil,
now: now,
lastUpdated: now,
Expand All @@ -153,11 +146,10 @@ struct ScreenshotScenario: Equatable, Sendable {

static let exhausted: ScreenshotScenario = {
let now = date(year: 2026, month: 3, day: 8, hour: 18, minute: 5)
let primaryReset = date(year: 2026, month: 3, day: 8, hour: 18, minute: 5)
let secondaryReset = date(year: 2026, month: 3, day: 11, hour: 0, minute: 0)
return ScreenshotScenario(
name: "exhausted",
snapshot: snapshot(primaryUsed: 100, primaryReset: primaryReset, secondaryUsed: 91, secondaryReset: secondaryReset),
snapshot: snapshot(used: 100, reset: secondaryReset),
rateLimitsByLimitId: nil,
now: now,
lastUpdated: now,
Expand All @@ -169,16 +161,13 @@ struct ScreenshotScenario: Equatable, Sendable {

static let multiweek: ScreenshotScenario = {
let now = date(year: 2026, month: 3, day: 10, hour: 22, minute: 46)
let primaryReset = date(year: 2026, month: 3, day: 10, hour: 23, minute: 7)
let secondaryReset = date(year: 2026, month: 3, day: 17, hour: 0, minute: 0)
return ScreenshotScenario(
name: "multiweek",
snapshot: snapshot(
primaryUsed: 15,
primaryReset: primaryReset,
secondaryUsed: 99,
secondaryReset: secondaryReset,
secondaryDurationMinutes: 10081
used: 99,
reset: secondaryReset,
durationMinutes: 10081
),
rateLimitsByLimitId: nil,
now: now,
Expand All @@ -191,11 +180,10 @@ struct ScreenshotScenario: Equatable, Sendable {

static let projection: ScreenshotScenario = {
let now = date(year: 2026, month: 5, day: 13, hour: 12, minute: 0)
let primaryReset = date(year: 2026, month: 5, day: 13, hour: 16, minute: 0)
let secondaryReset = date(year: 2026, month: 5, day: 18, hour: 12, minute: 0)
return ScreenshotScenario(
name: "projection",
snapshot: snapshot(primaryUsed: 29, primaryReset: primaryReset, secondaryUsed: 25, secondaryReset: secondaryReset),
snapshot: snapshot(used: 45, reset: secondaryReset),
rateLimitsByLimitId: nil,
now: now,
lastUpdated: now,
Expand All @@ -212,7 +200,7 @@ struct ScreenshotScenario: Equatable, Sendable {

return ScreenshotScenario(
name: "spark",
snapshot: snapshot(primaryUsed: 40, primaryReset: primaryReset, secondaryUsed: 50, secondaryReset: secondaryReset),
snapshot: snapshot(used: 50, reset: secondaryReset),
rateLimitsByLimitId: [
"codex_bengalfox": CodexRateLimitsSnapshot(
credits: nil,
Expand All @@ -227,7 +215,7 @@ struct ScreenshotScenario: Equatable, Sendable {
secondary: CodexRateLimitWindow(
resetsAt: Int64(secondaryReset.timeIntervalSince1970) + 12_000,
usedPercent: 15,
windowDurationMins: 20_160
windowDurationMins: 10_080
)
)
],
Expand Down
41 changes: 21 additions & 20 deletions Tests/CodexToolbarTests/ScreenshotScenarioTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import XCTest

@MainActor
final class ScreenshotScenarioTests: XCTestCase {
func testPublishedDefaultScenariosContainOnlyWeeklyCodexWindow() {
for scenario in [ScreenshotScenario.normal, .warning, .critical, .projection] {
XCTAssertEqual(scenario.snapshot.primary?.windowDurationMins, 10080, scenario.name)
XCTAssertNil(scenario.snapshot.secondary, scenario.name)
XCTAssertNil(scenario.rateLimitsByLimitId, scenario.name)
}
}

func testWarningScenarioMapsToExpectedCardStates() throws {
let scenario = try XCTUnwrap(ScreenshotScenario.named("warning"))

Expand All @@ -15,12 +23,12 @@ final class ScreenshotScenarioTests: XCTestCase {
timeZone: scenario.timeZone
)

XCTAssertEqual(cards.count, 2)
XCTAssertEqual(cards[0].title, "5h")
XCTAssertEqual(cards.count, 1)
XCTAssertEqual(cards[0].title, "Weekly")
XCTAssertEqual(cards[0].progressState, .warning)
XCTAssertEqual(cards[0].usageText, "74% used · 26% remaining")
XCTAssertEqual(cards[0].combinedResetText, "Resets in 35m (2:46 PM)")
XCTAssertEqual(cards[1].progressState, .normal)
XCTAssertNil(scenario.snapshot.secondary)
XCTAssertEqual(scenario.snapshot.primary?.windowDurationMins, 10080)
}

func testScreenshotModeBuildsFixtureStoreWithoutLiveFetching() async {
Expand All @@ -36,7 +44,7 @@ final class ScreenshotScenarioTests: XCTestCase {
XCTAssertEqual(client.loadSnapshotCallCount, 0)
XCTAssertEqual(store.state, .ready)
XCTAssertEqual(store.cards.first?.usageText, "19% used · 81% remaining")
XCTAssertEqual(store.statusBarText, "5h: Open")
XCTAssertEqual(store.statusBarText, "Week: Open")
XCTAssertEqual(
store.statusItemPresentation,
.bar(.init(remainingPercent: 81, progressState: .normal))
Expand Down Expand Up @@ -66,7 +74,7 @@ final class ScreenshotScenarioTests: XCTestCase {
XCTAssertEqual(store.lastUpdated, ScreenshotScenario.multiweek.lastUpdated)
}

func testProjectionScenarioBuildsFiveHourAndWeeklyResetRiskChartData() throws {
func testProjectionScenarioBuildsWeeklyResetRiskChartData() throws {
let scenario = try XCTUnwrap(ScreenshotScenario.named("projection"))
let cards = RateLimitStore.makeCards(
from: scenario.snapshot,
Expand All @@ -75,17 +83,10 @@ final class ScreenshotScenarioTests: XCTestCase {
locale: scenario.locale,
timeZone: scenario.timeZone
)
let fiveHourCard = try XCTUnwrap(cards.first { $0.title == "5h" })
let fiveHourProjection = try XCTUnwrap(fiveHourCard.projection)
let weeklyCard = try XCTUnwrap(cards.first { $0.title == "Weekly" })
let weeklyProjection = try XCTUnwrap(weeklyCard.projection)

XCTAssertEqual(fiveHourProjection.state, .critical)
XCTAssertEqual(fiveHourProjection.summaryText, "Projected empty before reset")
XCTAssertEqual(fiveHourProjection.detailText, "Empty 2:26 PM at pace")
XCTAssertEqual(weeklyProjection.state, .resetFirst)
XCTAssertEqual(weeklyProjection.summaryText, "Reset comes first")
XCTAssertEqual(weeklyProjection.detailText, "On pace to last through reset")
XCTAssertEqual(cards.map(\.title), ["Weekly"])
let weeklyProjection = try XCTUnwrap(cards.first?.projection)
XCTAssertEqual(weeklyProjection.state, .warning)
XCTAssertEqual(weeklyProjection.summaryText, "Projected empty before reset")
}

func testSparkScenarioBuildsFamilySectionsWithCodexFirst() throws {
Expand All @@ -99,13 +100,13 @@ final class ScreenshotScenarioTests: XCTestCase {
)
let sections = RateLimitStore.makeCardSections(from: cards)

XCTAssertEqual(cards.count, 4)
XCTAssertEqual(cards.count, 3)
XCTAssertEqual(cards.first?.displayTitle, "GPT-5.3-Codex-Spark · 5h")
XCTAssertEqual(sections.map(\.familyId), ["codex", "codex_bengalfox"])
XCTAssertEqual(sections.map(\.title), [nil, "GPT-5.3-Codex-Spark limit"])
XCTAssertEqual(sections.map(\.showsTitle), [false, true])
XCTAssertEqual(sections[0].cards.map(\.title), ["5h", "Weekly"])
XCTAssertEqual(sections[1].cards.map(\.title), ["5h", "2 Week"])
XCTAssertEqual(sections[0].cards.map(\.title), ["Weekly"])
XCTAssertEqual(sections[1].cards.map(\.title), ["5h", "Weekly"])
}

func testSparkScenarioBuildsStoreFromLaunchWithoutLiveFetching() async {
Expand Down
Binary file modified screenshots/critical-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/critical-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/normal-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/normal-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/projection-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/projection-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-critical-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-critical-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-normal-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-normal-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-projection-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-projection-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/readme-spark-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/readme-spark-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-warning-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/readme-warning-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/spark-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/spark-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/warning-dark-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/warning-light-popover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion scripts/generate_screenshots.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SCREENSHOTS_DIR="$ROOT_DIR/screenshots"
APP_EXECUTABLE="$ROOT_DIR/dist/CodexToolbar.app/Contents/MacOS/CodexToolbar"
README_SCENARIOS=(normal warning critical projection)
README_SCENARIOS=(normal warning critical projection spark)

"$ROOT_DIR/scripts/build_app.sh" >/dev/null

Expand Down Expand Up @@ -35,6 +35,7 @@ run_capture() {
local appearance="$2"
local capture_status_item="${3:-false}"

CODEX_TOOLBAR_SCREENSHOT_VISIBLE_SUPPLEMENTAL_FAMILIES="codex_bengalfox" \
CODEX_TOOLBAR_SCREENSHOT_SCENARIO="$scenario" \
CODEX_TOOLBAR_SCREENSHOT_APPEARANCE="$appearance" \
CODEX_TOOLBAR_SCREENSHOT_OUTPUT_DIR="$SCREENSHOTS_DIR" \
Expand Down
Binary file modified site/assets/codextoolbar-tahoe-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/codextoolbar-tahoe-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ <h1>Keep Codex rate limits visible.</h1>
>
<img
src="/assets/codextoolbar-tahoe-light.png"
alt="Codex Toolbar showing 5h and Weekly rate-limit projection charts in the macOS popover."
alt="Codex Toolbar showing a Weekly Codex rate-limit projection in the macOS popover, using sample data."
width="704"
height="854"
height="520"
>
</picture>
</figure>
Expand Down