Skip to content

Commit 3ac1d26

Browse files
akashagarwal7claude
andcommitted
Add account email display, switch account, and dev run script
Show the logged-in email from `claude auth status` in the menu dropdown, refresh it on each usage poll, and add a "Switch Account..." button that runs `claude auth login`. Also add run.sh for quick build-and-launch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e9c31b2 commit 3ac1d26

4 files changed

Lines changed: 93 additions & 2 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ A macOS menu bar app that displays your [Claude Code](https://docs.anthropic.com
1313
## Features
1414

1515
- **Real-time usage tracking** - Monitor your Claude Code session and weekly usage limits
16+
- **Account display** - Shows the currently logged-in email address in the menu
17+
- **Switch accounts** - Quickly switch Claude accounts via `claude auth login` from the menu (make sure the right browser profile is focused if you use 2 profiles that are signed into 2 different claude accounts)
1618
- **Multiple display modes**:
1719
- Text: Shows percentages directly (e.g., `CC: 45% (32% weekly)`)
1820
- Pie Charts: Visual representation with two pie charts
@@ -60,16 +62,18 @@ brew upgrade clive
6062

6163
Once running, Clive appears in your menu bar showing your Claude Code usage. Click the icon to see:
6264

65+
- **Logged-in email** - The email address of the current Claude account
6366
- **Session usage** - Current session percentage and reset time
6467
- **Weekly usage** - Current week's percentage
68+
- **Switch Account** - Run `claude auth login` to switch to a different account
6569

6670
Access Settings (⌘,) to configure:
6771
- Display mode (Text, Pie Charts, or Bar Charts)
6872
- Refresh interval (1-30 minutes)
6973

7074
## How It Works
7175

72-
Clive periodically runs `claude /usage` to fetch your current usage statistics and displays them in the menu bar. The app parses the output to extract session and weekly usage percentages.
76+
Clive periodically runs `claude /usage` to fetch your current usage statistics and displays them in the menu bar. The app parses the output to extract session and weekly usage percentages. It also runs `claude auth status` to display the currently logged-in account email.
7377
No hacking of session tokens etc required :).
7478

7579
## License

clive/CliveApp.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3030
onError: { [weak self] error in
3131
self?.currentError = error
3232
self?.updateMenuBarForError(error)
33+
},
34+
onEmailUpdate: { [weak self] email in
35+
self?.updateEmailMenuItem(email)
3336
}
3437
)
3538
usageManager?.startPolling()
@@ -61,6 +64,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6164
private func setupMenu() {
6265
let menu = NSMenu()
6366

67+
let emailItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
68+
emailItem.isEnabled = false
69+
emailItem.tag = 102
70+
emailItem.isHidden = true
71+
menu.addItem(emailItem)
72+
6473
let errorItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
6574
errorItem.isEnabled = false
6675
errorItem.tag = 99
@@ -79,6 +88,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7988

8089
menu.addItem(NSMenuItem.separator())
8190
menu.addItem(NSMenuItem(title: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r"))
91+
menu.addItem(NSMenuItem(title: "Switch Account...", action: #selector(switchAccount), keyEquivalent: ""))
8292
menu.addItem(NSMenuItem.separator())
8393
menu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ","))
8494
menu.addItem(NSMenuItem.separator())
@@ -108,6 +118,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
108118
}
109119
}
110120

121+
private func updateEmailMenuItem(_ email: String?) {
122+
guard let menu = statusItem?.menu, let emailItem = menu.item(withTag: 102) else { return }
123+
if let email = email {
124+
emailItem.title = email
125+
emailItem.isHidden = false
126+
} else {
127+
emailItem.isHidden = true
128+
}
129+
}
130+
111131
private func updateMenuBar(with usage: UsageInfo?) {
112132
DispatchQueue.main.async { [weak self] in
113133
guard let self = self, let button = self.statusItem?.button else { return }
@@ -243,6 +263,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
243263
NSApp.activate(ignoringOtherApps: true)
244264
}
245265

266+
@objc func switchAccount() {
267+
let claudePath = SettingsManager.shared.claudePath
268+
guard FileManager.default.isExecutableFile(atPath: claudePath) else { return }
269+
270+
let process = Process()
271+
process.executableURL = URL(fileURLWithPath: claudePath)
272+
process.arguments = ["auth", "login"]
273+
274+
process.terminationHandler = { [weak self] _ in
275+
DispatchQueue.main.async {
276+
self?.usageManager?.refreshNow()
277+
}
278+
}
279+
280+
do {
281+
try process.run()
282+
} catch {
283+
// Failed to launch
284+
}
285+
}
286+
246287
@objc func refreshNow() {
247288
usageManager?.refreshNow()
248289
}

clive/UsageManager.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class UsageManager {
3333
private var refreshTimer: Timer?
3434
private let onUpdate: (UsageInfo?) -> Void
3535
private let onError: (UsageError?) -> Void
36+
private let onEmailUpdate: (String?) -> Void
3637
private var childPid: pid_t = 0
3738
private var masterFd: Int32 = -1
3839
private var timeoutTimer: Timer?
@@ -41,9 +42,10 @@ class UsageManager {
4142

4243
private let timeout: TimeInterval = 30
4344

44-
init(onUpdate: @escaping (UsageInfo?) -> Void, onError: @escaping (UsageError?) -> Void) {
45+
init(onUpdate: @escaping (UsageInfo?) -> Void, onError: @escaping (UsageError?) -> Void, onEmailUpdate: @escaping (String?) -> Void = { _ in }) {
4546
self.onUpdate = onUpdate
4647
self.onError = onError
48+
self.onEmailUpdate = onEmailUpdate
4749

4850
// Listen for refresh interval changes
4951
SettingsManager.shared.$refreshInterval.sink { [weak self] _ in
@@ -63,6 +65,7 @@ class UsageManager {
6365

6466
func startPolling() {
6567
refreshNow()
68+
fetchEmail()
6669
startTimer()
6770
}
6871

@@ -76,6 +79,7 @@ class UsageManager {
7679

7780
func refreshNow() {
7881
guard !isRefreshing else { return }
82+
fetchEmail()
7983
performRefresh()
8084
}
8185

@@ -93,6 +97,36 @@ class UsageManager {
9397
}
9498
}
9599

100+
func fetchEmail() {
101+
let claudePath = SettingsManager.shared.claudePath
102+
guard checkExecutableExists() else { return }
103+
104+
DispatchQueue.global(qos: .utility).async { [weak self] in
105+
let process = Process()
106+
process.executableURL = URL(fileURLWithPath: claudePath)
107+
process.arguments = ["auth", "status"]
108+
109+
let pipe = Pipe()
110+
process.standardOutput = pipe
111+
process.standardError = FileHandle.nullDevice
112+
113+
do {
114+
try process.run()
115+
process.waitUntilExit()
116+
117+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
118+
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
119+
let email = json["email"] as? String {
120+
DispatchQueue.main.async {
121+
self?.onEmailUpdate(email)
122+
}
123+
}
124+
} catch {
125+
// Silently fail - email display is non-critical
126+
}
127+
}
128+
}
129+
96130
private func performRefresh() {
97131
isRefreshing = true
98132

run.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
set -e
3+
4+
xcodebuild -scheme clive -configuration Debug build \
5+
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO \
6+
2>&1 | tail -3
7+
8+
pkill -x Clive 2>/dev/null || true
9+
sleep 1
10+
11+
open "$(xcodebuild -scheme clive -configuration Debug -showBuildSettings 2>/dev/null \
12+
| grep -m1 'BUILT_PRODUCTS_DIR' | awk '{print $3}')/Clive.app"

0 commit comments

Comments
 (0)