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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project follows semantic versioning for tagged releases.

## Unreleased

### Added

- Gave each macOS menu profile a colour and symbol. New profiles get a
stable colour from their name, with `default` in graphite, and show their
initial; **Appearance…** in a profile's actions chooses one of eight
colours and twelve symbols, applied at once. The palette leaves out red,
orange and yellow, which mean low quota.
- Added **Quota in menu bar**, an opt-in gear-menu toggle that shows the
lowest remaining Codex quota beside the menu-bar icon and keeps usage
current with the same five-minute background read as alerts.

### Changed

- Redesigned the project website as a dark ASCII page drawn live with WebGL:
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/Ducksss/codex-profiles/raw/refs/heads/main/macos/CodexProfilesMenu/Previews/menu-dark.png">
<img src="https://github.com/Ducksss/codex-profiles/raw/refs/heads/main/macos/CodexProfilesMenu/Previews/menu-light.png" width="360" alt="Codex Profiles menu with default, personal and work profiles, each showing its remaining Codex quota beside a ring, and project shortcuts nested under their profiles.">
<img src="https://github.com/Ducksss/codex-profiles/raw/refs/heads/main/macos/CodexProfilesMenu/Previews/menu-light.png" width="360" alt="Codex Profiles menu with default, personal and work profiles, each in its own colour and symbol and showing its remaining Codex quota beside a ring, and project shortcuts nested under their profiles.">
</picture>
<br />
<sub>Actual AppKit view with sample quota values. Light and dark appearance follow macOS.</sub>
Expand Down Expand Up @@ -127,9 +127,13 @@ profile's ⋯ menu lists every window. Hover for details or press **⌘R** to
refresh.

The menu bar icon gains a badge when a profile runs low. Turn on
**Low-quota alerts** in the gear menu for a notification when a profile
crosses into low or critical. Alerts are off by default, and the app never
switches profiles for you.
**Quota in menu bar** in the gear menu to see the lowest remaining
percentage beside the icon, or **Low-quota alerts** for a notification when a
profile crosses into low or critical. Both are off by default, and the app
never switches profiles for you.

Give each profile its own colour and symbol with **Appearance…** in its ⋯
menu, so work and personal are easy to tell apart.

For an unavailable reading, use **Sign in to Codex CLI…** in that profile's
actions menu, then refresh. Quota follows the profile's **CLI sign-in**.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ <h3>work</h3>
</div>
<div class="view" id="view-menu" role="tabpanel" aria-labelledby="tab-menu">
<div class="shot-frame">
<img src="menu-dark.png" width="720" height="680" loading="lazy" decoding="async" alt="Codex Profiles menu bar app in dark appearance: default, personal and work profiles, each with its remaining Codex quota as a figure and a ring, personal low in orange and work critical in red with their reset times, and project workspaces nested under their profiles.">
<img src="menu-dark.png" width="720" height="680" loading="lazy" decoding="async" alt="Codex Profiles menu bar app in dark appearance: default, personal and work profiles, each in its own colour with its remaining Codex quota as a figure and a ring, personal low in orange and work critical in red with their reset times, and project workspaces nested under their profiles.">
<span class="pin" data-k="1" aria-hidden="true" style="left: 41.7%; top: 19.1%">01</span>
<span class="pin" data-k="2" aria-hidden="true" style="left: 87.5%; top: 38.5%">02</span>
<span class="pin" data-k="3" aria-hidden="true" style="left: 88.9%; top: 74.4%">03</span>
Expand Down
5 changes: 4 additions & 1 deletion docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ Finder launches without a separately installed Codex CLI on PATH.
Profile rows show the tightest Codex CLI window's remaining percentage,
reported duration and a ring; a window at 25% or less is highlighted and its
reset countdown replaces the row description. The profile's actions menu lists
every window. Hover for reset times. Usage is read
every window. Hover for reset times. Each profile has a colour and symbol,
automatic until chosen with Appearance… in its actions. An opt-in, default-off
Quota in menu bar toggle shows the lowest remaining percentage beside the
menu-bar icon and reads usage about every five minutes while on. Usage is read
through the official app-server method account/rateLimits/read, cached in
memory for 60 seconds and refreshed with Command-R; a refresh keeps the
previous reading visible. Missing usage does not disable launches. The
Expand Down
Binary file modified docs/menu-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 50 additions & 13 deletions macos/CodexProfilesMenu/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ Workspaces whose profile is missing follow the groups. A hairline divides
groups; rows within a group have no rules, because the nesting already shows
what belongs together. Nesting and search replace a profile filter.

The 360-point popover uses semantic macOS colours, system type, monochrome
SF Symbols and the material supplied by NSPopover. The content view has no
The 360-point popover uses semantic macOS colours, system type, SF Symbols
and the material supplied by NSPopover. Colour is reserved for two jobs:
profile identity and quota emphasis. The content view has no
additional effect layer. One type scale sets profile names in 13-point
semibold, workspace names in 13-point regular and details in 11-point
secondary text; weight, not size, carries the hierarchy. A profile row is 46
points high: a 26-point monochrome monogram tile, the name above a detail
line, and its quota. A workspace row is a single 30-point line starting at
points high: a 26-point tile in the profile's colour holding its initial or
chosen symbol in white, the name above a detail line, and its quota. A workspace row is a single 30-point line starting at
its profile's title column: a pin or folder symbol, the project name and its
path on one baseline. A search result listed without its profile's row starts
under the profile tiles and names its profile instead. A workspace that
Expand Down Expand Up @@ -50,8 +51,26 @@ reset times, freshness and the Codex CLI sign-in scope. Countdowns stay in
English to match the surrounding text, while clock times follow the user's
locale and 12/24-hour setting. Workspace rows do not repeat account quota.
Only explicitly adding a workspace asks for a folder. App-level setup,
sign-in, the Return destination, Open at Login, Low-quota alerts, About and
Quit live in the gear menu.
sign-in, the Return destination, Open at Login, Low-quota alerts, Quota in
menu bar, About and Quit live in the gear menu.

Each profile has a colour and a symbol. Until the person chooses, `default`
is graphite and every other profile takes one of seven colours from an FNV-1a
hash of its name, so the colour stays the same on every launch and does not
shift when profiles are added; the symbol is the profile's initial. The
palette of blue, indigo, purple, pink, teal, green, brown and graphite leaves
out red, orange and yellow so they keep meaning low quota, and white symbols
keep 4.5:1 contrast on every colour. Appearance… in a profile's actions
replaces the list with an editor, in place rather than in a second popover so
dismissal and keyboard focus stay with the menu: a 40-point preview with the
profile's name and its CODEX_HOME path from the documented mapping, eight
round colour swatches, and twelve symbols, the initial and eleven SF Symbols.
A choice applies at once and updates the editor in place. Reset restores the
automatic appearance; Done, Return or Escape returns to the list. The chosen
swatch gains a ring and every choice reports its state to accessibility
clients, so a choice never depends on colour alone. A search result listed
without its profile's row shows a small square of the profile's colour beside
its name. The status item stays a monochrome template image.

Apple's [menu-bar guidance](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#Menu-bar-extras)
prefers a menu unless the functionality calls for richer controls. Search,
Expand Down Expand Up @@ -101,7 +120,8 @@ removal and relocation update affected rows before reloading, so a failed
refresh cannot restore an old launch identity. Superseded or removed rows
cannot launch. The CLI remains the only writer of profile and binding state;
the GUI persists only its pins,
recents, destination preference and Low-quota alerts choice in UserDefaults. Profile-only launches start
recents, destination preference, profile colours and symbols, and its
Low-quota alerts and menu-bar figure choices in UserDefaults. Profile-only launches start
in the home directory, avoiding accidental project-binding inheritance. Explicit
workspace launches pass their folder to the CLI and preserve its guard rules.

Expand Down Expand Up @@ -143,6 +163,16 @@ at the earliest reset, waiting at most a day so an implausible reset time
cannot overflow it. Remaining quota only falls within a window, so a
reading stays a true upper bound until then.

Quota in menu bar, an opt-in gear-menu toggle that is off by default, adds
the lowest remaining share across initialized profiles beside the icon in
monospaced menu-bar text, such as 7%. Unlike the badge it also covers healthy
windows, and the tooltip and accessibility label then name that profile and
window. The lowest figure can only rise when its own window resets, so the
same one-shot timer re-evaluates it then. While the figure is on, usage is
read about every five minutes through the same refreshUsage path as alerts,
so it stays current; it needs no notification permission. Background reads
stop only when both alerts and the figure are off.

Low-quota alerts is an opt-in gear-menu toggle, off by default so the app
never shows an unrequested permission prompt. Authorization is requested only
when the toggle is turned on; denial keeps it off and explains System
Expand All @@ -165,10 +195,15 @@ a threshold cannot repeat. A later critical notification replaces
the low one for the same window. The body names the reset time and mentions
another profile only when it has at least 50% of the same window left, no
low window of its own and a reading under 15 minutes old. Clicking a
notification opens the popover. While alerts are on, a five-minute loop with
timer tolerance calls the same refreshUsage path, so the cache, two readers,
deadlines and owned-process cleanup are unchanged; turning alerts off or
Quit stops it, and no reader starts without initialized profiles. The app
notification opens the popover. While alerts or the menu-bar figure are on,
a five-minute loop with timer tolerance calls the same refreshUsage path, so
the cache, two readers, deadlines and owned-process cleanup are unchanged;
turning both off or Quit stops it, and no reader starts without initialized
profiles. Each pass lists profiles before reading usage, so profiles created
or removed outside the app join or leave the checks while the menu stays
closed. After the first profile load these listings are quiet, leaving the
status message the person may be reading alone; until then, as after a login
launch, a pass loads profiles as opening the menu would. The app
never switches profiles or accounts automatically: alerts show the numbers
and the person chooses. The decision logic is plain Foundation behind a
notifier protocol, so tests never touch UNUserNotificationCenter.
Expand All @@ -182,7 +217,8 @@ values. These are layout previews;
they do not demonstrate live translucency, system permission prompts or
upstream app behaviour. Previews render at 2x whatever display draws them. The
AppKit interaction runner checks profile-first setup, nesting and alignment,
selection, search, dismissal, Command-Return, the Return destination and
automatic and chosen profile appearance, the appearance editor and its
persistence, the menu-bar figure toggle, selection, search, dismissal, Command-Return, the Return destination and
launch arguments against an isolated executable. It switches the same open view light → dark → light, checks
system-colour and accessibility notifications, and verifies opaque selection
policies and accessibility state. These checks do not change global OS
Expand All @@ -195,7 +231,8 @@ updates, refresh dimming, quota colours in both appearances, the usage lines
in profile actions, rows being changed, right-click actions, menu enablement after AppKit
validation, one-off destinations and Open at Login through a fake login item. An isolated real CLI integration verifies profile
selection and strict workspace-guard handling with a fake app-server.
Alert checks cover indicator state and its bounded timer, crossing and
Alert checks cover indicator state and its bounded timer, the lowest
figure and its tooltip, background reads that follow the figure, crossing and
de-duplication across jitter, early resets and missing reset times, the
settled baseline after enabling, headroom choice, preference persistence,
permission denial, undecided and unanswered prompts, background reads that
Expand Down
7 changes: 6 additions & 1 deletion macos/CodexProfilesMenu/Previews/RenderPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
struct RenderPreview {
static func main() {
guard CommandLine.arguments.count >= 2 else {
FileHandle.standardError.write(Data("Usage: RenderPreview <output.png> [--empty] [--dark]\n".utf8))
FileHandle.standardError.write(Data("Usage: RenderPreview <output.png> [--empty] [--dark] [--no-results] [--appearance]\n".utf8))
exit(2)
}

Expand Down Expand Up @@ -40,10 +40,15 @@ struct RenderPreview {
"default": sample(18, 36), "personal": sample(43, 78), "work": sample(93, 59),
])
if let first = bindings.first { store.togglePin(first) }
// Chosen colours and symbols, as in the design; new profiles get automatic ones.
store.setAppearance(ProfileAppearance(colour: .graphite, symbol: .person), for: "default")
store.setAppearance(ProfileAppearance(colour: .green, symbol: .house), for: "personal")
store.setAppearance(ProfileAppearance(colour: .blue, symbol: .briefcase), for: "work")
if CommandLine.arguments.contains("--no-results") { store.query = "does-not-exist" }

let controller = WorkspaceMenuViewController(store: store, defaults: defaults)
let previewView = controller.view
if CommandLine.arguments.contains("--appearance") { controller.editAppearance(of: "work") }
let size = controller.preferredContentSize
let window = NSWindow(contentRect: NSRect(origin: .zero, size: size), styleMask: .borderless, backing: .buffered, defer: false)
window.appearance = NSAppearance(named: CommandLine.arguments.contains("--dark") ? .darkAqua : .aqua)
Expand Down
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 macos/CodexProfilesMenu/Previews/menu-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 macos/CodexProfilesMenu/Previews/menu-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

/// The icon is a template image either way; a low quota adds a badge shape,
/// and the tooltip and accessibility label name each profile and window.
/// When chosen, the lowest remaining share follows the icon in menu-bar text.
private func updateStatusItem(_ indicator: QuotaIndicator) {
guard let button = statusItem?.button else { return }
guard let item = statusItem, let button = item.button else { return }
let now = Date()
let label = indicator.accessibilityLabel(at: now)
let figure = alerts.showsFigure ? indicator.figure : nil
let label = indicator.accessibilityLabel(at: now, showingFigure: figure != nil)
button.image = StatusItemIcon.image(for: indicator.level, accessibilityDescription: label)
button.toolTip = indicator.toolTip(at: now)
button.title = figure ?? ""
button.font = .monospacedDigitSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
button.imagePosition = figure == nil ? .imageOnly : .imageLeading
item.length = figure == nil ? NSStatusItem.squareLength : NSStatusItem.variableLength
button.toolTip = indicator.toolTip(at: now, showingFigure: figure != nil)
button.setAccessibilityLabel(label)
}

Expand Down
Loading
Loading