on master help sign in
feat: use dots to separate providers only in menu bar

Within a provider, windows are separated by spaces. Between providers, segments are separated by middle dots. Before: โšก42 ยท ๐Ÿ•62% ยท ๐Ÿ“…8% ยท ๐Ÿค–12% ยท ๐Ÿ“†3% After: โšก42 ยท ๐Ÿ•62% ๐Ÿ“…8% ยท ๐Ÿค–12% ๐Ÿ“†3%

commita4745cdf6dcfa0d780730fa90b27b25ab334059d authoriko <mail@iko.soy> dateFri 17 Jul 2026, 13:54 (3 months ago) parenta489805
2 files changed+36โˆ’20
UsageMenubar/ViewModel.swift+29โˆ’13 UsageMenubarTests/StatusBarTextTests.swift+7โˆ’7
UsageMenubar/ViewModel.swift+29โˆ’13
@@ -237,11 +237,12 @@ final class ViewModel: ObservableObject {
237237 balanceFormatter.string(from: NSNumber(value: value)) ?? "\(value)"
238238 }
239239
240โˆ’ /// The text to show in the menu bar: `โšก{balance}` (Hyper), `๐Ÿ•{percent}%` (Claude
241โˆ’ /// 5-hour), `๐Ÿ“…{percent}%` (Claude 7-day), `๐Ÿค–{percent}%` (z.ai 5-hour), and
242โˆ’ /// `๐Ÿ“†{percent}%` (z.ai weekly) โ€” each omitted when absent or at 0%, and entirely
243โˆ’ /// absent when the service is not configured. An unconfigured service produces no
244โˆ’ /// placeholder in the menu bar.
240+ /// The text to show in the menu bar. Each configured provider contributes a
241+ /// segment; segments are separated by ` ยท ` (middle dot). Within a segment,
242+ /// windows are separated by a space. An unconfigured service produces no
243+ /// segment โ€” no placeholder, no icon.
244+ ///
245+ /// Example: `โšก42 ยท ๐Ÿ•62% ๐Ÿ“…8% ยท ๐Ÿค–12% ๐Ÿ“†3%`
245246 ///
246247 /// Takes its values as parameters rather than reading the properties: `@Published`
247248 /// publishes in `willSet`, so a Combine subscriber that called back into the view
@@ -255,28 +256,43 @@ final class ViewModel: ObservableObject {
255256 zaiFiveHourPercent: Int?,
256257 zaiWeeklyPercent: Int?
257258 ) -> String {
258โˆ’ var parts: [String] = []
259+ var segments: [String] = []
260+
261+ // Hyper
259262 if hyperConfigured, let balance = balance {
260โˆ’ parts.append("โšก\(formatBalance(balance))")
263+ segments.append("โšก\(formatBalance(balance))")
261264 }
265+
266+ // Claude โ€” windows joined by space within the segment
267+ var claudeWindows: [String] = []
262268 if let fiveHour = claudeFiveHourPercent, fiveHour > 0 {
263โˆ’ parts.append("๐Ÿ•\(fiveHour)%")
269+ claudeWindows.append("๐Ÿ•\(fiveHour)%")
264270 }
265271 if let sevenDay = claudeSevenDayPercent, sevenDay > 0 {
266โˆ’ parts.append("๐Ÿ“…\(sevenDay)%")
272+ claudeWindows.append("๐Ÿ“…\(sevenDay)%")
273+ }
274+ if !claudeWindows.isEmpty {
275+ segments.append(claudeWindows.joined(separator: " "))
267276 }
277+
278+ // z.ai โ€” windows joined by space within the segment
279+ var zaiWindows: [String] = []
268280 if let zaiFiveHour = zaiFiveHourPercent, zaiFiveHour > 0 {
269โˆ’ parts.append("๐Ÿค–\(zaiFiveHour)%")
281+ zaiWindows.append("๐Ÿค–\(zaiFiveHour)%")
270282 }
271283 if let zaiWeekly = zaiWeeklyPercent, zaiWeekly > 0 {
272โˆ’ parts.append("๐Ÿ“†\(zaiWeekly)%")
284+ zaiWindows.append("๐Ÿ“†\(zaiWeekly)%")
285+ }
286+ if !zaiWindows.isEmpty {
287+ segments.append(zaiWindows.joined(separator: " "))
273288 }
274โˆ’ if parts.isEmpty {
289+
290+ if segments.isEmpty {
275291 // No service is configured or has anything to show. While loading,
276292 // show an ellipsis; otherwise the title is empty.
277293 return isLoading ? "โ€ฆ" : ""
278294 }
279โˆ’ return parts.joined(separator: " ยท ")
295+ return segments.joined(separator: " ยท ")
280296 }
281297
282298 /// The text to show in the menu bar for the current state.
UsageMenubarTests/StatusBarTextTests.swift+7โˆ’7
@@ -17,7 +17,7 @@ final class StatusBarTextTests: XCTestCase {
1717 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
1818 zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
1919 )
20โˆ’ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ยท ๐Ÿ“…8%")
20+ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ๐Ÿ“…8%")
2121 }
2222
2323 func testHyperOnly() {
@@ -37,7 +37,7 @@ final class StatusBarTextTests: XCTestCase {
3737 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
3838 zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
3939 )
40โˆ’ XCTAssertEqual(title, "๐Ÿ•62% ยท ๐Ÿ“…8%")
40+ XCTAssertEqual(title, "๐Ÿ•62% ๐Ÿ“…8%")
4141 }
4242
4343 func testLoadingWithNothingToShow() {
@@ -80,7 +80,7 @@ final class StatusBarTextTests: XCTestCase {
8080 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
8181 zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
8282 )
83โˆ’ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ยท ๐Ÿ“…8%")
83+ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ๐Ÿ“…8%")
8484 }
8585
8686 func testZeroPercentIsShownRatherThanOmitted() {
@@ -154,7 +154,7 @@ final class StatusBarTextTests: XCTestCase {
154154 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
155155 zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
156156 )
157โˆ’ XCTAssertEqual(title, "๐Ÿ•62% ยท ๐Ÿ“…8%")
157+ XCTAssertEqual(title, "๐Ÿ•62% ๐Ÿ“…8%")
158158 }
159159
160160 // MARK: - z.ai in Title
@@ -186,7 +186,7 @@ final class StatusBarTextTests: XCTestCase {
186186 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
187187 zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
188188 )
189โˆ’ XCTAssertEqual(title, "๐Ÿ•62% ยท ๐Ÿ“…8% ยท ๐Ÿค–12%")
189+ XCTAssertEqual(title, "๐Ÿ•62% ๐Ÿ“…8% ยท ๐Ÿค–12%")
190190 }
191191
192192 func testAllThreeServices() {
@@ -196,7 +196,7 @@ final class StatusBarTextTests: XCTestCase {
196196 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
197197 zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
198198 )
199โˆ’ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ยท ๐Ÿ“…8% ยท ๐Ÿค–12% ยท ๐Ÿ“†3%")
199+ XCTAssertEqual(title, "โšก42 ยท ๐Ÿ•62% ๐Ÿ“…8% ยท ๐Ÿค–12% ๐Ÿ“†3%")
200200 }
201201
202202 func testZaiZeroIsOmitted() {
@@ -238,7 +238,7 @@ final class StatusBarTextTests: XCTestCase {
238238 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
239239 zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
240240 )
241โˆ’ XCTAssertEqual(title, "๐Ÿค–12% ยท ๐Ÿ“†3%")
241+ XCTAssertEqual(title, "๐Ÿค–12% ๐Ÿ“†3%")
242242 }
243243
244244 func testZaiWeeklyZeroIsOmitted() {