on abde53a help sign in
CLAUDE.md+2−2
since splitsnapshots
unifiedsplit
@@ -20,8 +20,8 @@ click-to-copy on the headline, and threshold-crossing notifications.
20- `KeychainHelper.swift` — Keychain wrapper (KeychainStore + KeychainHelper + ZaiKeychainHelper)
20- `KeychainHelper.swift` — Keychain wrapper (KeychainStore + KeychainHelper + ZaiKeychainHelper)
21- `VersionFormatter.swift` — Version string formatting
21- `VersionFormatter.swift` — Version string formatting
22
22
23−The status bar title shows `⚡{balance}` (Hyper), `⁕{percent}%` (Claude 5-hour),
23+The status bar title shows `⚡{balance}` (Hyper), `🕐{percent}%` (Claude 5-hour),
24−`⁕{percent}%` (Claude 7-day), `Z{percent}%` (z.ai 5-hour), and `Z{percent}%`
24+`📅{percent}%` (Claude 7-day), `🤖{percent}%` (z.ai 5-hour), and `📆{percent}%`
25(z.ai weekly) — each omitted when absent or at 0%, and entirely absent when the
25(z.ai weekly) — each omitted when absent or at 0%, and entirely absent when the
26service is not configured (no API key or no credentials). An unconfigured service
26service is not configured (no API key or no credentials). An unconfigured service
27produces no placeholder in the menu bar. The three fetches are independent —
27produces no placeholder in the menu bar. The three fetches are independent —
README.md+6−6
@@ -9,8 +9,8 @@ A macOS menu bar application that displays usage limits and credit balances for
9## Features
9## Features
10
10
11- ⚡ **Hyper credits** — balance from `hyper.charm.land`, with sparkline trend, click-to-copy, and low-balance notifications
11- ⚡ **Hyper credits** — balance from `hyper.charm.land`, with sparkline trend, click-to-copy, and low-balance notifications
12−- ⁕ **Claude Code limits** — Pro/Max 5-hour and 7-day usage windows, with countdown to next reset. No setup: if you've signed in with the Claude Code CLI, it just appears
12+- 🤖 **Claude Code limits** — Pro/Max 5-hour and 7-day usage windows, with countdown to next reset. No setup: if you've signed in with the Claude Code CLI, it just appears
13−- Z **z.ai Coding Plan** — 5-hour and weekly token quota percentages, with reset countdowns. Enter your API key in settings
13+- 🤖 **z.ai Coding Plan** — 5-hour and weekly token quota percentages, with reset countdowns. Enter your API key in settings
14- 🔄 **Auto-refresh** — configurable interval (1m / 5m / 15m / 30m), plus refresh on wake from sleep
14- 🔄 **Auto-refresh** — configurable interval (1m / 5m / 15m / 30m), plus refresh on wake from sleep
15- 🖱️ **Right-click menu** — quick access to Refresh and Quit
15- 🖱️ **Right-click menu** — quick access to Refresh and Quit
16- 🔒 **Keychain storage** — API keys stored securely in macOS Keychain
16- 🔒 **Keychain storage** — API keys stored securely in macOS Keychain
@@ -24,10 +24,10 @@ Each service shows an emoji-prefixed segment when configured and above 0%:
24| Service | Emoji | Example |
24| Service | Emoji | Example |
25|---------|-------|---------|
25|---------|-------|---------|
26| Hyper balance | ⚡ | `⚡42` |
26| Hyper balance | ⚡ | `⚡42` |
27−| Claude 5-hour | ⁕ | `⁕62%` |
27+| Claude 5-hour | 🕐 | `🕐62%` |
28−| Claude 7-day | ⁕ | `⁕8%` |
28+| Claude 7-day | 📅 | `📅8%` |
29−| z.ai 5-hour | Z | `Z12%` |
29+| z.ai 5-hour | 🤖 | `🤖12%` |
30−| z.ai weekly | Z | `Z3%` |
30+| z.ai weekly | 📆 | `📆3%` |
31
31
32Unconfigured services don't appear at all — no placeholder, no icon.
32Unconfigured services don't appear at all — no placeholder, no icon.
33
33
UsageMenubar/Info.plist+1−1
@@ -7,7 +7,7 @@
7 <key>CFBundleDisplayName</key>
7 <key>CFBundleDisplayName</key>
8 <string>Usage Menubar</string>
8 <string>Usage Menubar</string>
9 <key>CFBundleIdentifier</key>
9 <key>CFBundleIdentifier</key>
10− <string>soy.iko.usage-menubar</string>
10+ <string>com.ilyakooo0.usage-menubar</string>
11 <key>CFBundleVersion</key>
11 <key>CFBundleVersion</key>
12 <string>1</string>
12 <string>1</string>
13 <key>CFBundleShortVersionString</key>
13 <key>CFBundleShortVersionString</key>
UsageMenubar/KeychainHelper.swift+2−2
@@ -82,7 +82,7 @@ struct KeychainStore {
82
82
83/// Simple wrapper around the macOS Keychain for storing the Hyper API key.
83/// Simple wrapper around the macOS Keychain for storing the Hyper API key.
84enum KeychainHelper {
84enum KeychainHelper {
85− private static let service = "soy.iko.usage-menubar"
85+ private static let service = "com.ilyakooo0.usage-menubar"
86 private static let account = "hyper-api-key"
86 private static let account = "hyper-api-key"
87
87
88 private static let store = KeychainStore(service: service, account: account)
88 private static let store = KeychainStore(service: service, account: account)
@@ -112,7 +112,7 @@ enum KeychainHelper {
112/// Uses the same keychain service as the Hyper key but a different account, so the
112/// Uses the same keychain service as the Hyper key but a different account, so the
113/// two keys coexist without clobbering each other.
113/// two keys coexist without clobbering each other.
114enum ZaiKeychainHelper {
114enum ZaiKeychainHelper {
115− private static let service = "soy.iko.usage-menubar"
115+ private static let service = "com.ilyakooo0.usage-menubar"
116 private static let account = "zai-api-key"
116 private static let account = "zai-api-key"
117
117
118 private static let store = KeychainStore(service: service, account: account)
118 private static let store = KeychainStore(service: service, account: account)
UsageMenubar/MenuView.swift+7−58
@@ -259,7 +259,7 @@ struct MenuView: View {
259 VStack(spacing: 8) {
259 VStack(spacing: 8) {
260 HStack(alignment: .firstTextBaseline, spacing: 8) {
260 HStack(alignment: .firstTextBaseline, spacing: 8) {
261 headlineNumber(
261 headlineNumber(
262− text: "⁕ \\(percent)%",
262+ text: "🅲 \(percent)%",
263 color: Self.usageColor(percent),
263 color: Self.usageColor(percent),
264 trend: viewModel.claudeTrend,
264 trend: viewModel.claudeTrend,
265 copiedConfirmation: viewModel.copiedClaudeConfirmation,
265 copiedConfirmation: viewModel.copiedClaudeConfirmation,
@@ -379,7 +379,7 @@ struct MenuView: View {
379 VStack(spacing: 8) {
379 VStack(spacing: 8) {
380 HStack(alignment: .firstTextBaseline, spacing: 8) {
380 HStack(alignment: .firstTextBaseline, spacing: 8) {
381 headlineNumber(
381 headlineNumber(
382− text: "Z \\(fiveHour)%",
382+ text: "🅉 \(fiveHour)%",
383 color: Self.usageColor(fiveHour),
383 color: Self.usageColor(fiveHour),
384 trend: viewModel.zaiTrend,
384 trend: viewModel.zaiTrend,
385 copiedConfirmation: viewModel.copiedZaiConfirmation,
385 copiedConfirmation: viewModel.copiedZaiConfirmation,
@@ -407,26 +407,11 @@ struct MenuView: View {
407 }
407 }
408 }
408 }
409
409
410− // Peak hours notice.
411− zaiPeakHours
412−
413− // Sparkline for the z.ai 5-hour % history, with an orange tint
410+ // Sparkline for the z.ai 5-hour % history.
414− // when currently in peak hours.
411+ sparkline(
415− if viewModel.zaiSparklineValues.count >= 2 {
416− Sparkline(
417− values: viewModel.zaiSparklineValues,
412+ values: viewModel.zaiSparklineValues,
418− color: Self.usageColor(zaiFiveHourPercent ?? 0)
413+ color: Self.usageColor(zaiFiveHourPercent ?? 0)
419− )
414+ )
420− .frame(height: 26)
421− .padding(.top, 4)
422− .transition(.opacity)
423− .background(
424− ViewModel.zaiInPeakHours
425− ? RoundedRectangle(cornerRadius: 4, style: .continuous)
426− .fill(Color.orange.opacity(0.08))
427− : nil
428− )
429− }
430 }
415 }
431 }
416 }
432
417
@@ -454,42 +439,6 @@ struct MenuView: View {
454 }
439 }
455 }
440 }
456
441
457− /// z.ai peak hours: 14:00–18:00 CST (UTC+8), i.e. 06:00–10:00 UTC.
458− /// Limits are most likely to bind during this window. Displayed in the
459− /// user's local timezone so they know when to expect slowdowns.
460− private var zaiPeakHours: some View {
461− let inPeak = ViewModel.zaiInPeakHours
462−
463− return HStack(spacing: 6) {
464− Circle()
465− .fill(inPeak ? Color.orange : Color.secondary.opacity(0.4))
466− .frame(width: 6, height: 6)
467−
468− Text(inPeak ? "Peak hours now" : "Peak \(Self.zaiPeakHoursLocal)")
469− .font(Self.footnoteFont)
470− .foregroundColor(inPeak ? .orange : .secondary)
471− }
472− }
473−
474− /// z.ai peak hours converted to the user's local timezone, e.g. "09:00–13:00".
475− private static var zaiPeakHoursLocal: String {
476− let formatter = DateFormatter()
477− formatter.timeZone = TimeZone.current
478− formatter.dateFormat = "HH:mm"
479−
480− var utc = DateComponents()
481− utc.timeZone = TimeZone(identifier: "UTC")
482− utc.hour = 6
483− var calendar = Calendar(identifier: .gregorian)
484− calendar.timeZone = TimeZone(identifier: "UTC")!
485− let start = calendar.date(from: utc)!
486−
487− utc.hour = 10
488− let end = calendar.date(from: utc)!
489−
490− return "\(formatter.string(from: start))–\(formatter.string(from: end))"
491− }
492−
493 /// Formats the time until a z.ai window resets, e.g. "3h 20m".
442 /// Formats the time until a z.ai window resets, e.g. "3h 20m".
494 private static func zaiResetsIn(from date: Date) -> String {
443 private static func zaiResetsIn(from date: Date) -> String {
495 let interval = date.timeIntervalSince(Date())
444 let interval = date.timeIntervalSince(Date())
UsageMenubar/UsageMenubarApp.swift+1−2
@@ -89,8 +89,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
89 claudeFiveHourPercent: fiveHour,
89 claudeFiveHourPercent: fiveHour,
90 claudeSevenDayPercent: sevenDay,
90 claudeSevenDayPercent: sevenDay,
91 zaiFiveHourPercent: zaiFiveHour,
91 zaiFiveHourPercent: zaiFiveHour,
92− zaiWeeklyPercent: zaiWeekly,
92+ zaiWeeklyPercent: zaiWeekly
93− zaiInPeakHours: ViewModel.zaiInPeakHours
94 )
93 )
95 }
94 }
96 .removeDuplicates()
95 .removeDuplicates()
UsageMenubar/ViewModel.swift+10−23
@@ -7,7 +7,7 @@ import UserNotifications
7/// Posted when the user changes the refresh interval, so the app delegate can
7/// Posted when the user changes the refresh interval, so the app delegate can
8/// restart its timer with the new period.
8/// restart its timer with the new period.
9extension Notification.Name {
9extension Notification.Name {
10− static let refreshIntervalDidChange = Notification.Name("soy.iko.usage-menubar.refreshIntervalDidChange")
10+ static let refreshIntervalDidChange = Notification.Name("com.ilyakooo0.usage-menubar.refreshIntervalDidChange")
11}
11}
12
12
13/// The direction a metric has moved over the most recent history points.
13/// The direction a metric has moved over the most recent history points.
@@ -241,9 +241,8 @@ final class ViewModel: ObservableObject {
241 /// segment prefixed with a provider emoji; segments are separated by ` · `
241 /// segment prefixed with a provider emoji; segments are separated by ` · `
242 /// (middle dot). Within a segment, windows are listed as bare percentages
242 /// (middle dot). Within a segment, windows are listed as bare percentages
243 /// separated by spaces. An unconfigured service produces no segment.
243 /// separated by spaces. An unconfigured service produces no segment.
244− /// When z.ai is in peak hours, a 🔶 marker is appended to the z.ai segment.
245 ///
244 ///
246− /// Example: `⚡42 · ⁕62% 8% · Z12% 3%🔶`
245+ /// Example: `⚡42 · 🅲62% 8% · 🅉12% 3%`
247 ///
246 ///
248 /// Takes its values as parameters rather than reading the properties: `@Published`
247 /// Takes its values as parameters rather than reading the properties: `@Published`
249 /// publishes in `willSet`, so a Combine subscriber that called back into the view
248 /// publishes in `willSet`, so a Combine subscriber that called back into the view
@@ -255,8 +254,7 @@ final class ViewModel: ObservableObject {
255 claudeFiveHourPercent: Int?,
254 claudeFiveHourPercent: Int?,
256 claudeSevenDayPercent: Int?,
255 claudeSevenDayPercent: Int?,
257 zaiFiveHourPercent: Int?,
256 zaiFiveHourPercent: Int?,
258− zaiWeeklyPercent: Int?,
257+ zaiWeeklyPercent: Int?
259− zaiInPeakHours: Bool
260 ) -> String {
258 ) -> String {
261 var segments: [String] = []
259 var segments: [String] = []
262
260
@@ -267,30 +265,26 @@ final class ViewModel: ObservableObject {
267
265
268 // Claude — provider emoji + bare percentages
266 // Claude — provider emoji + bare percentages
269 var claudeWindows: [String] = []
267 var claudeWindows: [String] = []
270− if let fiveHour = claudeFiveHourPercent {
268+ if let fiveHour = claudeFiveHourPercent, fiveHour > 0 {
271 claudeWindows.append("\(fiveHour)%")
269 claudeWindows.append("\(fiveHour)%")
272 }
270 }
273− if let sevenDay = claudeSevenDayPercent {
271+ if let sevenDay = claudeSevenDayPercent, sevenDay > 0 {
274 claudeWindows.append("\(sevenDay)%")
272 claudeWindows.append("\(sevenDay)%")
275 }
273 }
276 if !claudeWindows.isEmpty {
274 if !claudeWindows.isEmpty {
277− segments.append("⁕" + claudeWindows.joined(separator: " "))
275+ segments.append("🅲" + claudeWindows.joined(separator: " "))
278 }
276 }
279
277
280 // z.ai — provider emoji + bare percentages
278 // z.ai — provider emoji + bare percentages
281 var zaiWindows: [String] = []
279 var zaiWindows: [String] = []
282− if let zaiFiveHour = zaiFiveHourPercent {
280+ if let zaiFiveHour = zaiFiveHourPercent, zaiFiveHour > 0 {
283 zaiWindows.append("\(zaiFiveHour)%")
281 zaiWindows.append("\(zaiFiveHour)%")
284 }
282 }
285− if let zaiWeekly = zaiWeeklyPercent {
283+ if let zaiWeekly = zaiWeeklyPercent, zaiWeekly > 0 {
286 zaiWindows.append("\(zaiWeekly)%")
284 zaiWindows.append("\(zaiWeekly)%")
287 }
285 }
288 if !zaiWindows.isEmpty {
286 if !zaiWindows.isEmpty {
289− var segment = "Z" + zaiWindows.joined(separator: " ")
287+ segments.append("🅉" + zaiWindows.joined(separator: " "))
290− if zaiInPeakHours {
291− segment += "🔶"
292− }
293− segments.append(segment)
294 }
288 }
295
289
296 if segments.isEmpty {
290 if segments.isEmpty {
@@ -310,17 +304,10 @@ final class ViewModel: ObservableObject {
310 claudeFiveHourPercent: claudeFiveHourPercent,
304 claudeFiveHourPercent: claudeFiveHourPercent,
311 claudeSevenDayPercent: claudeSevenDayPercent,
305 claudeSevenDayPercent: claudeSevenDayPercent,
312 zaiFiveHourPercent: zaiFiveHourPercent,
306 zaiFiveHourPercent: zaiFiveHourPercent,
313− zaiWeeklyPercent: zaiWeeklyPercent,
307+ zaiWeeklyPercent: zaiWeeklyPercent
314− zaiInPeakHours: Self.zaiInPeakHours
315 )
308 )
316 }
309 }
317
310
318− /// Whether z.ai is currently in peak hours (06:00–10:00 UTC).
319− static var zaiInPeakHours: Bool {
320− let hour = Calendar(identifier: .gregorian).component(.hour, from: Date())
321− return hour >= 6 && hour < 10
322− }
323−
324 /// The formatted balance string for display in the popover.
311 /// The formatted balance string for display in the popover.
325 var formattedBalance: String {
312 var formattedBalance: String {
326 guard let balance = balance else { return "?" }
313 guard let balance = balance else { return "?" }
UsageMenubarTests/StatusBarTextTests.swift+46−106
@@ -15,10 +15,9 @@ final class StatusBarTextTests: XCTestCase {
15 balance: 42, isLoading: false,
15 balance: 42, isLoading: false,
16 hyperConfigured: true,
16 hyperConfigured: true,
17 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
17 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
18− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
18+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
19− zaiInPeakHours: false
20 )
19 )
21− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
20+ XCTAssertEqual(title, "⚡42 · \u{1F172}62% 8%")
22 }
21 }
23
22
24 func testHyperOnly() {
23 func testHyperOnly() {
@@ -26,8 +25,7 @@ final class StatusBarTextTests: XCTestCase {
26 balance: 42, isLoading: false,
25 balance: 42, isLoading: false,
27 hyperConfigured: true,
26 hyperConfigured: true,
28 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
27 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
29− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
28+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
30− zaiInPeakHours: false
31 )
29 )
32 XCTAssertEqual(title, "⚡42")
30 XCTAssertEqual(title, "⚡42")
33 }
31 }
@@ -37,10 +35,9 @@ final class StatusBarTextTests: XCTestCase {
37 balance: nil, isLoading: false,
35 balance: nil, isLoading: false,
38 hyperConfigured: false,
36 hyperConfigured: false,
39 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
37 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
40− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
38+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
41− zaiInPeakHours: false
42 )
39 )
43− XCTAssertEqual(title, "\u{2055}62% 8%")
40+ XCTAssertEqual(title, "\u{1F172}62% 8%")
44 }
41 }
45
42
46 func testLoadingWithNothingToShow() {
43 func testLoadingWithNothingToShow() {
@@ -48,8 +45,7 @@ final class StatusBarTextTests: XCTestCase {
48 balance: nil, isLoading: true,
45 balance: nil, isLoading: true,
49 hyperConfigured: false,
46 hyperConfigured: false,
50 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
47 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
51− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
48+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
52− zaiInPeakHours: false
53 )
49 )
54 XCTAssertEqual(title, "…")
50 XCTAssertEqual(title, "…")
55 }
51 }
@@ -59,8 +55,7 @@ final class StatusBarTextTests: XCTestCase {
59 balance: nil, isLoading: false,
55 balance: nil, isLoading: false,
60 hyperConfigured: false,
56 hyperConfigured: false,
61 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
57 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
62− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
58+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
63− zaiInPeakHours: false
64 )
59 )
65 XCTAssertEqual(title, "")
60 XCTAssertEqual(title, "")
66 }
61 }
@@ -73,10 +68,9 @@ final class StatusBarTextTests: XCTestCase {
73 balance: nil, isLoading: true,
68 balance: nil, isLoading: true,
74 hyperConfigured: false,
69 hyperConfigured: false,
75 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
70 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
76− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
71+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
77− zaiInPeakHours: false
78 )
72 )
79− XCTAssertEqual(title, "\u{2055}62%")
73+ XCTAssertEqual(title, "\u{1F172}62%")
80 }
74 }
81
75
82 func testLoadingKeepsAStaleBalanceOnScreen() {
76 func testLoadingKeepsAStaleBalanceOnScreen() {
@@ -84,21 +78,19 @@ final class StatusBarTextTests: XCTestCase {
84 balance: 42, isLoading: true,
78 balance: 42, isLoading: true,
85 hyperConfigured: true,
79 hyperConfigured: true,
86 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
80 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
87− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
81+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
88− zaiInPeakHours: false
89 )
82 )
90− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
83+ XCTAssertEqual(title, "⚡42 · \u{1F172}62% 8%")
91 }
84 }
92
85
93− func testZeroPercentIsShown() {
86+ func testZeroPercentIsShownRatherThanOmitted() {
94 let title = ViewModel.statusBarText(
87 let title = ViewModel.statusBarText(
95 balance: 42, isLoading: false,
88 balance: 42, isLoading: false,
96 hyperConfigured: true,
89 hyperConfigured: true,
97 claudeFiveHourPercent: 0, claudeSevenDayPercent: 8,
90 claudeFiveHourPercent: 0, claudeSevenDayPercent: 8,
98− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
91+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
99− zaiInPeakHours: false
100 )
92 )
101− XCTAssertEqual(title, "⚡42 · \u{2055}0% 8%")
93+ XCTAssertEqual(title, "⚡42 · \u{1F172}8%")
102 }
94 }
103
95
104 func testBothClaudeWindowsZero() {
96 func testBothClaudeWindowsZero() {
@@ -106,10 +98,9 @@ final class StatusBarTextTests: XCTestCase {
106 balance: 42, isLoading: false,
98 balance: 42, isLoading: false,
107 hyperConfigured: true,
99 hyperConfigured: true,
108 claudeFiveHourPercent: 0, claudeSevenDayPercent: 0,
100 claudeFiveHourPercent: 0, claudeSevenDayPercent: 0,
109− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
101+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
110− zaiInPeakHours: false
111 )
102 )
112− XCTAssertEqual(title, "⚡42 · \u{2055}0% 0%")
103+ XCTAssertEqual(title, "⚡42")
113 }
104 }
114
105
115 func testFiveHourOnly() {
106 func testFiveHourOnly() {
@@ -117,10 +108,9 @@ final class StatusBarTextTests: XCTestCase {
117 balance: nil, isLoading: false,
108 balance: nil, isLoading: false,
118 hyperConfigured: false,
109 hyperConfigured: false,
119 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
110 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
120− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
111+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
121− zaiInPeakHours: false
122 )
112 )
123− XCTAssertEqual(title, "\u{2055}62%")
113+ XCTAssertEqual(title, "\u{1F172}62%")
124 }
114 }
125
115
126 func testSevenDayOnly() {
116 func testSevenDayOnly() {
@@ -128,10 +118,9 @@ final class StatusBarTextTests: XCTestCase {
128 balance: nil, isLoading: false,
118 balance: nil, isLoading: false,
129 hyperConfigured: false,
119 hyperConfigured: false,
130 claudeFiveHourPercent: nil, claudeSevenDayPercent: 8,
120 claudeFiveHourPercent: nil, claudeSevenDayPercent: 8,
131− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
121+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
132− zaiInPeakHours: false
133 )
122 )
134− XCTAssertEqual(title, "\u{2055}8%")
123+ XCTAssertEqual(title, "\u{1F172}8%")
135 }
124 }
136
125
137 // MARK: - Hyper not configured
126 // MARK: - Hyper not configured
@@ -143,8 +132,7 @@ final class StatusBarTextTests: XCTestCase {
143 balance: 42, isLoading: false,
132 balance: 42, isLoading: false,
144 hyperConfigured: false,
133 hyperConfigured: false,
145 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
134 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
146− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
135+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
147− zaiInPeakHours: false
148 )
136 )
149 XCTAssertEqual(title, "")
137 XCTAssertEqual(title, "")
150 }
138 }
@@ -154,8 +142,7 @@ final class StatusBarTextTests: XCTestCase {
154 balance: nil, isLoading: true,
142 balance: nil, isLoading: true,
155 hyperConfigured: false,
143 hyperConfigured: false,
156 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
144 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
157− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
145+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
158− zaiInPeakHours: false
159 )
146 )
160 XCTAssertEqual(title, "…")
147 XCTAssertEqual(title, "…")
161 }
148 }
@@ -165,10 +152,9 @@ final class StatusBarTextTests: XCTestCase {
165 balance: nil, isLoading: false,
152 balance: nil, isLoading: false,
166 hyperConfigured: false,
153 hyperConfigured: false,
167 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
154 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
168− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
155+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
169− zaiInPeakHours: false
170 )
156 )
171− XCTAssertEqual(title, "\u{2055}62% 8%")
157+ XCTAssertEqual(title, "\u{1F172}62% 8%")
172 }
158 }
173
159
174 // MARK: - z.ai in Title
160 // MARK: - z.ai in Title
@@ -178,10 +164,9 @@ final class StatusBarTextTests: XCTestCase {
178 balance: nil, isLoading: false,
164 balance: nil, isLoading: false,
179 hyperConfigured: false,
165 hyperConfigured: false,
180 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
166 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
181− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
167+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
182− zaiInPeakHours: false
183 )
168 )
184− XCTAssertEqual(title, "\u{005A}12%")
169+ XCTAssertEqual(title, "\u{1F169}12%")
185 }
170 }
186
171
187 func testZaiWithHyper() {
172 func testZaiWithHyper() {
@@ -189,10 +174,9 @@ final class StatusBarTextTests: XCTestCase {
189 balance: 42, isLoading: false,
174 balance: 42, isLoading: false,
190 hyperConfigured: true,
175 hyperConfigured: true,
191 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
176 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
192− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
177+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
193− zaiInPeakHours: false
194 )
178 )
195− XCTAssertEqual(title, "⚡42 · \u{005A}12%")
179+ XCTAssertEqual(title, "⚡42 · \u{1F169}12%")
196 }
180 }
197
181
198 func testZaiWithClaude() {
182 func testZaiWithClaude() {
@@ -200,10 +184,9 @@ final class StatusBarTextTests: XCTestCase {
200 balance: nil, isLoading: false,
184 balance: nil, isLoading: false,
201 hyperConfigured: false,
185 hyperConfigured: false,
202 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
186 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
203− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
187+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
204− zaiInPeakHours: false
205 )
188 )
206− XCTAssertEqual(title, "\u{2055}62% 8% · \u{005A}12%")
189+ XCTAssertEqual(title, "\u{1F172}62% 8% · \u{1F169}12%")
207 }
190 }
208
191
209 func testAllThreeServices() {
192 func testAllThreeServices() {
@@ -211,21 +194,19 @@ final class StatusBarTextTests: XCTestCase {
211 balance: 42, isLoading: false,
194 balance: 42, isLoading: false,
212 hyperConfigured: true,
195 hyperConfigured: true,
213 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
196 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
214− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
197+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
215− zaiInPeakHours: false
216 )
198 )
217− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8% · \u{005A}12% 3%")
199+ XCTAssertEqual(title, "⚡42 · \u{1F172}62% 8% · \u{1F169}12% 3%")
218 }
200 }
219
201
220− func testZaiZeroIsShown() {
202+ func testZaiZeroIsOmitted() {
221 let title = ViewModel.statusBarText(
203 let title = ViewModel.statusBarText(
222 balance: 42, isLoading: false,
204 balance: 42, isLoading: false,
223 hyperConfigured: true,
205 hyperConfigured: true,
224 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
206 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
225− zaiFiveHourPercent: 0, zaiWeeklyPercent: 0,
207+ zaiFiveHourPercent: 0, zaiWeeklyPercent: 0
226− zaiInPeakHours: false
227 )
208 )
228− XCTAssertEqual(title, "⚡42 · \u{005A}0% 0%")
209+ XCTAssertEqual(title, "⚡42")
229 }
210 }
230
211
231 func testLoadingKeepsZaiPercentOnScreen() {
212 func testLoadingKeepsZaiPercentOnScreen() {
@@ -233,10 +214,9 @@ final class StatusBarTextTests: XCTestCase {
233 balance: nil, isLoading: true,
214 balance: nil, isLoading: true,
234 hyperConfigured: false,
215 hyperConfigured: false,
235 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
216 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
236− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
217+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
237− zaiInPeakHours: false
238 )
218 )
239− XCTAssertEqual(title, "\u{005A}12%")
219+ XCTAssertEqual(title, "\u{1F169}12%")
240 }
220 }
241
221
242 // MARK: - z.ai Weekly
222 // MARK: - z.ai Weekly
@@ -246,10 +226,9 @@ final class StatusBarTextTests: XCTestCase {
246 balance: nil, isLoading: false,
226 balance: nil, isLoading: false,
247 hyperConfigured: false,
227 hyperConfigured: false,
248 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
228 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
249− zaiFiveHourPercent: nil, zaiWeeklyPercent: 3,
229+ zaiFiveHourPercent: nil, zaiWeeklyPercent: 3
250− zaiInPeakHours: false
251 )
230 )
252− XCTAssertEqual(title, "\u{005A}3%")
231+ XCTAssertEqual(title, "\u{1F169}3%")
253 }
232 }
254
233
255 func testZaiWeeklyWithFiveHour() {
234 func testZaiWeeklyWithFiveHour() {
@@ -257,21 +236,19 @@ final class StatusBarTextTests: XCTestCase {
257 balance: nil, isLoading: false,
236 balance: nil, isLoading: false,
258 hyperConfigured: false,
237 hyperConfigured: false,
259 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
238 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
260− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
239+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
261− zaiInPeakHours: false
262 )
240 )
263− XCTAssertEqual(title, "\u{005A}12% 3%")
241+ XCTAssertEqual(title, "\u{1F169}12% 3%")
264 }
242 }
265
243
266− func testZaiWeeklyZeroIsShown() {
244+ func testZaiWeeklyZeroIsOmitted() {
267 let title = ViewModel.statusBarText(
245 let title = ViewModel.statusBarText(
268 balance: nil, isLoading: false,
246 balance: nil, isLoading: false,
269 hyperConfigured: false,
247 hyperConfigured: false,
270 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
248 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
271− zaiFiveHourPercent: 12, zaiWeeklyPercent: 0,
249+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 0
272− zaiInPeakHours: false
273 )
250 )
274− XCTAssertEqual(title, "\u{005A}12% 0%")
251+ XCTAssertEqual(title, "\u{1F169}12%")
275 }
252 }
276
253
277 func testLoadingKeepsZaiWeeklyOnScreen() {
254 func testLoadingKeepsZaiWeeklyOnScreen() {
@@ -279,46 +256,9 @@ final class StatusBarTextTests: XCTestCase {
279 balance: nil, isLoading: true,
256 balance: nil, isLoading: true,
280 hyperConfigured: false,
257 hyperConfigured: false,
281 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
258 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
282− zaiFiveHourPercent: nil, zaiWeeklyPercent: 3,
259+ zaiFiveHourPercent: nil, zaiWeeklyPercent: 3
283− zaiInPeakHours: false
284− )
285− XCTAssertEqual(title, "\u{005A}3%")
286− }
287−
288− // MARK: - z.ai Peak Hours
289−
290− func testPeakHoursAddsDiamondToZaiSegment() {
291− let title = ViewModel.statusBarText(
292− balance: 42, isLoading: false,
293− hyperConfigured: true,
294− claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
295− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
296− zaiInPeakHours: true
297− )
298− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8% · \u{005A}12% 3%\u{1F536}")
299− }
300−
301− func testPeakHoursWithZaiOnly() {
302− let title = ViewModel.statusBarText(
303− balance: nil, isLoading: false,
304− hyperConfigured: false,
305− claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
306− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
307− zaiInPeakHours: true
308− )
309− XCTAssertEqual(title, "\u{005A}12%\u{1F536}")
310− }
311−
312− func testPeakHoursIgnoredWhenZaiNotConfigured() {
313− let title = ViewModel.statusBarText(
314− balance: 42, isLoading: false,
315− hyperConfigured: true,
316− claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
317− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
318− zaiInPeakHours: true
319 )
260 )
320− // No z.ai segment, so peak marker has nothing to attach to.
321− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
261+ XCTAssertEqual(title, "\u{1F169}3%")
322 }
262 }
323
263
324 // MARK: - Claude Window Percentages
264 // MARK: - Claude Window Percentages
project.yml+4−4
@@ -6,7 +6,7 @@ name: UsageMenubar
6options:
6options:
7 # Reverse-DNS prefix xcodegen uses when it needs to synthesize a bundle id.
7 # Reverse-DNS prefix xcodegen uses when it needs to synthesize a bundle id.
8 # The app target pins its own id explicitly below, so this is just a default.
8 # The app target pins its own id explicitly below, so this is just a default.
9− bundleIdPrefix: soy.iko
9+ bundleIdPrefix: com.ilyakooo0
10 # Oldest macOS we support. SMAppService (launch at login) requires 13.0+;
10 # Oldest macOS we support. SMAppService (launch at login) requires 13.0+;
11 # we target 14.0 (Sonoma). Keep in sync with MACOSX_DEPLOYMENT_TARGET below.
11 # we target 14.0 (Sonoma). Keep in sync with MACOSX_DEPLOYMENT_TARGET below.
12 deploymentTarget:
12 deploymentTarget:
@@ -47,7 +47,7 @@ targets:
47 properties:
47 properties:
48 CFBundleName: UsageMenubar
48 CFBundleName: UsageMenubar
49 CFBundleDisplayName: Usage Menubar
49 CFBundleDisplayName: Usage Menubar
50− CFBundleIdentifier: soy.iko.usage-menubar
50+ CFBundleIdentifier: com.ilyakooo0.usage-menubar
51 # Overwritten by CI — see the note on MARKETING_VERSION above.
51 # Overwritten by CI — see the note on MARKETING_VERSION above.
52 CFBundleVersion: "1"
52 CFBundleVersion: "1"
53 CFBundleShortVersionString: "1.0.0"
53 CFBundleShortVersionString: "1.0.0"
@@ -57,7 +57,7 @@ targets:
57 NSHighResolutionCapable: true
57 NSHighResolutionCapable: true
58 settings:
58 settings:
59 base:
59 base:
60− PRODUCT_BUNDLE_IDENTIFIER: soy.iko.usage-menubar
60+ PRODUCT_BUNDLE_IDENTIFIER: com.ilyakooo0.usage-menubar
61 PRODUCT_NAME: UsageMenubar
61 PRODUCT_NAME: UsageMenubar
62 INFOPLIST_FILE: UsageMenubar/Info.plist
62 INFOPLIST_FILE: UsageMenubar/Info.plist
63 # Grants the outgoing-network-connection entitlement needed to reach
63 # Grants the outgoing-network-connection entitlement needed to reach
@@ -84,7 +84,7 @@ targets:
84 - target: UsageMenubar
84 - target: UsageMenubar
85 settings:
85 settings:
86 base:
86 base:
87− PRODUCT_BUNDLE_IDENTIFIER: soy.iko.usage-menubar.tests
87+ PRODUCT_BUNDLE_IDENTIFIER: com.ilyakooo0.usage-menubar.tests
88 PRODUCT_NAME: UsageMenubarTests
88 PRODUCT_NAME: UsageMenubarTests
89 BUNDLE_LOADER: "$(TEST_HOST)"
89 BUNDLE_LOADER: "$(TEST_HOST)"
90 TEST_HOST: "$(BUILT_PRODUCTS_DIR)/UsageMenubar.app/Contents/MacOS/UsageMenubar"
90 TEST_HOST: "$(BUILT_PRODUCTS_DIR)/UsageMenubar.app/Contents/MacOS/UsageMenubar"