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
10 changes: 1 addition & 9 deletions Sources/CodexRunway/StatusBarContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final class StatusBarContentView: NSView {
super.draw(dirtyRect)
switch state.configuration.style {
case .text:
drawText()
break // Percentage text is rendered by the native status bar button.
case .countdown:
drawCountdown()
case .battery:
Expand Down Expand Up @@ -137,14 +137,6 @@ final class StatusBarContentView: NSView {

// MARK: - Countdown / meters / rings

private func drawText() {
let captions = layout.textCaptions
let frames = layout.columnFrames(widths: layout.textColumnWidths, gap: 4, in: bounds)
for (caption, frame) in zip(captions, frames) {
drawCentered(caption, font: layout.textFont, rect: frame, color: .labelColor)
}
}

private func drawCountdown() {
guard renderPlan.meters.count > 1 else {
drawCentered(state.content.text, font: layout.countdownFont, rect: bounds, color: .labelColor)
Expand Down
21 changes: 18 additions & 3 deletions Sources/CodexRunway/StatusControllerStatusBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,29 @@ extension StatusController {
text: model.selectedStatusText,
meters: model.selectedQuotaMeters,
displayMinute: Int(Date().timeIntervalSince1970 / 60)))
let didChange = statusBarView.update(state)
guard didChange else { return }
statusItem.length = statusBarView.preferredWidth
guard Self.updateStatusBarContent(state, statusItem: statusItem, contentView: statusBarView) else { return }
let quotaDetails = model.selectedQuotaMeters
.map { "\($0.title): \($0.remainingPercent)%" }
.joined(separator: " · ")
statusItem.button?.toolTip = quotaDetails.isEmpty
? "CodexRunway · \(model.selectedStatusText)"
: "CodexRunway · \(model.selectedStatusText)\n\(quotaDetails)"
}

@discardableResult
static func updateStatusBarContent(
_ state: StatusBarContentState,
statusItem: NSStatusItem,
contentView: StatusBarContentView) -> Bool
{
guard contentView.update(state) else { return false }
let layout = StatusBarContentLayout(state: state)
let usesNativeText = state.configuration.style == .text
contentView.isHidden = usesNativeText
// Let AppKit render the title's color and inactive-menu-bar appearance.
statusItem.button?.font = layout.textFont
statusItem.button?.title = usesNativeText ? layout.textCaptions.joined(separator: " ") : ""
statusItem.length = usesNativeText ? NSStatusItem.variableLength : contentView.preferredWidth
return true
}
}
46 changes: 43 additions & 3 deletions Tests/CodexRunwayTests/StatusBarRenderPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ struct StatusBarRenderPlanTests {
#expect(layout.preferredWidth == columnWidth)
}

@Test("all status bar styles render weekly-only and model-specific layouts")
@Test("custom-drawn status bar styles render weekly-only and model-specific layouts")
@MainActor
func allStylesRenderVariableQuotaCounts() throws {
func customStylesRenderVariableQuotaCounts() throws {
let weekly = meter(title: "每周", usedPercent: 11, windowMinutes: 10_080)
let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300)
let modelSpecific = meter(
Expand All @@ -198,7 +198,7 @@ struct StatusBarRenderPlanTests {
windowMinutes: 10_080,
source: .modelSpecific)

for style in StatusBarDisplayStyle.allCases {
for style in StatusBarDisplayStyle.allCases where style != .text {
let stylePreferences = preferences(style: style)
let compact = try render(preferences: stylePreferences, meters: [weekly])
let expanded = try render(
Expand All @@ -213,6 +213,46 @@ struct StatusBarRenderPlanTests {
}
}

@Test("native percentage titles update and switch back to custom-drawn styles")
@MainActor
func nativePercentageTitlesFollowState() throws {
_ = NSApplication.shared
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
defer { NSStatusBar.system.removeStatusItem(statusItem) }
let button = try #require(statusItem.button)
let view = StatusBarContentView(frame: .zero)
button.addSubview(view)
let weekly = meter(title: "每周", usedPercent: 0, windowMinutes: 10_080)
let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300)
let modelSpecific = meter(
title: "模型配额",
usedPercent: 11,
windowMinutes: 10_080,
source: .modelSpecific)

for (meters, title) in [([weekly], "100%"), ([fiveHour, weekly, modelSpecific], "80% 100% 89%"), ([], "--")] {
let textState = StatusBarContentState(
configuration: .init(preferences: preferences(style: .text), language: .simplifiedChinese),
content: .init(text: "6天", meters: meters, displayMinute: 0))
for style in StatusBarDisplayStyle.allCases where style != .text {
#expect(StatusController.updateStatusBarContent(textState, statusItem: statusItem, contentView: view))
#expect(button.title == title)
#expect(button.font == NSFont.systemFont(ofSize: 14, weight: .semibold))
#expect(view.isHidden)
#expect(statusItem.length == NSStatusItem.variableLength)
#expect(!StatusController.updateStatusBarContent(textState, statusItem: statusItem, contentView: view))

let customState = StatusBarContentState(
configuration: .init(preferences: preferences(style: style), language: .simplifiedChinese),
content: textState.content)
#expect(StatusController.updateStatusBarContent(customState, statusItem: statusItem, contentView: view))
#expect(button.title.isEmpty)
#expect(!view.isHidden)
#expect(statusItem.length == view.preferredWidth)
}
}
}

@Test("text style displays the same percentages as quota rings")
func textStyleUsesRingPercentages() {
let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300)
Expand Down