const Applet = imports.ui.applet; const PopupMenu = imports.ui.popupMenu; const Mainloop = imports.mainloop; const GLib = imports.gi.GLib; const Soup = imports.gi.Soup; const Lang = imports.lang; const Settings = imports.ui.settings; // Constants const CREDENTIALS_PATH = GLib.get_home_dir() + '/.claude/.credentials.json'; const API_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage'; function ClaudeStatusApplet(metadata, orientation, panel_height, instance_id) { this._init(metadata, orientation, panel_height, instance_id); } ClaudeStatusApplet.prototype = { __proto__: Applet.TextIconApplet.prototype, _init: function(metadata, orientation, panel_height, instance_id) { Applet.TextIconApplet.prototype._init.call(this, orientation, panel_height, instance_id); this.metadata = metadata; this.orientation = orientation; // Initialize settings this.settings = new Settings.AppletSettings(this, metadata.uuid, instance_id); this.settings.bind("refresh-interval", "refreshInterval", this._onSettingsChanged); this.settings.bind("color-low", "colorLow", this._onSettingsChanged); this.settings.bind("color-medium", "colorMedium", this._onSettingsChanged); this.settings.bind("color-high", "colorHigh", this._onSettingsChanged); this.settings.bind("show-label-prefix", "showLabelPrefix", this._onSettingsChanged); // Initialize HTTP session if (Soup.Session.prototype.hasOwnProperty('new')) { this.httpSession = new Soup.Session(); } else { this.httpSession = Soup.Session.new(); } // Set up UI this.set_applet_icon_symbolic_name("dialog-information"); this.set_applet_tooltip("Claude Status - Click for details"); // Initialize menu this.menuManager = new PopupMenu.PopupMenuManager(this); this.menu = new Applet.AppletPopupMenu(this, orientation); this.menuManager.addMenu(this.menu); // Menu items (will be populated later) this.sessionItem = null; this.weeklyItem = null; this.errorItem = null; this._buildMenu(); // Initial data fetch this.accessToken = null; this.updateTimeoutId = null; this._loadCredentials(); if (this.accessToken) { this._fetchUsageData(); } // Set up periodic updates this._updateLoop(); }, _buildMenu: function() { // Session usage item this.sessionItem = new PopupMenu.PopupMenuItem("Session: Loading...", { reactive: false }); this.menu.addMenuItem(this.sessionItem); // Weekly usage item this.weeklyItem = new PopupMenu.PopupMenuItem("Weekly: Loading...", { reactive: false }); this.menu.addMenuItem(this.weeklyItem); // Error message item (hidden by default) this.errorItem = new PopupMenu.PopupMenuItem("", { reactive: false }); this.menu.addMenuItem(this.errorItem); this.errorItem.actor.hide(); // Separator this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); // Refresh button let refreshItem = new PopupMenu.PopupMenuItem("Refresh"); refreshItem.connect('activate', Lang.bind(this, this._fetchUsageData)); this.menu.addMenuItem(refreshItem); }, _loadCredentials: function() { try { let [success, contents] = GLib.file_get_contents(CREDENTIALS_PATH); if (!success) { this._showError("Could not read credentials file"); return; } // Convert ByteArray to string if needed let credentialsText; if (contents instanceof Uint8Array) { credentialsText = new TextDecoder().decode(contents); } else { credentialsText = contents.toString(); } let credentials = JSON.parse(credentialsText); if (credentials && credentials.claudeAiOauth && credentials.claudeAiOauth.accessToken) { this.accessToken = credentials.claudeAiOauth.accessToken; } else { this._showError("No access token found in credentials"); } } catch (e) { this._showError("Failed to load credentials: " + e.message); } }, _fetchUsageData: function() { if (!this.accessToken) { this._loadCredentials(); if (!this.accessToken) { return; } } try { let message = Soup.Message.new('GET', API_ENDPOINT); // Set headers let headers = message.get_request_headers(); headers.append('Authorization', 'Bearer ' + this.accessToken); headers.append('anthropic-beta', 'oauth-2025-04-20'); headers.append('Content-Type', 'application/json'); // Send async request this.httpSession.send_and_read_async( message, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(session, result) { try { let bytes = session.send_and_read_finish(result); let decoder = new TextDecoder('utf-8'); let responseData = decoder.decode(bytes.get_data()); let statusCode = message.get_status(); if (statusCode === 200) { let data = JSON.parse(responseData); this._updateDisplay(data); this._hideError(); } else if (statusCode === 401) { this._showError("Authentication failed. Check credentials."); } else { this._showError("API error: " + statusCode); } } catch (e) { this._showError("Failed to parse response: " + e.message); } }) ); } catch (e) { this._showError("Request failed: " + e.message); } }, _updateDisplay: function(data) { try { // Extract five_hour (session) data let sessionData = data ? data.five_hour : null; let weeklyData = data ? data.seven_day : null; // Update panel text with session percentage if (sessionData && sessionData.utilization !== undefined) { let percentage = Math.round(sessionData.utilization); let label = percentage + "%"; if (this.showLabelPrefix) { label = "Claude: " + label; } this.set_applet_label(label); // Update session menu item let resetTime = this._formatResetTime(sessionData.resets_at); this.sessionItem.label.set_text("Session: " + percentage + "% (resets " + resetTime + ")"); // Update color based on usage using settings let color; if (percentage >= 80) { color = this.colorHigh; } else if (percentage >= 50) { color = this.colorMedium; } else { color = this.colorLow; } this.actor.style = 'color: ' + color + ';'; } else { this.set_applet_label("?%"); this.sessionItem.label.set_text("Session: No data"); } // Update weekly menu item if (weeklyData && weeklyData.utilization !== undefined) { let percentage = Math.round(weeklyData.utilization); let resetTime = this._formatResetTime(weeklyData.resets_at); this.weeklyItem.label.set_text("Weekly: " + percentage + "% (resets " + resetTime + ")"); } else { this.weeklyItem.label.set_text("Weekly: No data"); } } catch (e) { this._showError("Failed to update display: " + e.message); } }, _formatResetTime: function(resetAt) { if (!resetAt) { return "unknown"; } try { // Parse ISO 8601 timestamp let resetDate = new Date(resetAt); let now = new Date(); let diffMs = resetDate - now; if (diffMs < 0) { return "soon"; } let diffSeconds = Math.floor(diffMs / 1000); let diffMinutes = Math.floor(diffSeconds / 60); let diffHours = Math.floor(diffMinutes / 60); let diffDays = Math.floor(diffHours / 24); if (diffDays > 0) { let hours = diffHours % 24; return "in " + diffDays + "d " + hours + "h"; } else if (diffHours > 0) { let minutes = diffMinutes % 60; return "in " + diffHours + "h " + minutes + "m"; } else if (diffMinutes > 0) { return "in " + diffMinutes + "m"; } else { return "in <1m"; } } catch (e) { return "unknown"; } }, _showError: function(message) { this.set_applet_label("ERR"); this.actor.style = 'color: #ff6b6b;'; this.errorItem.label.set_text("Error: " + message); this.errorItem.actor.show(); }, _hideError: function() { this.errorItem.actor.hide(); }, _updateLoop: function() { // Cancel existing timeout if any if (this.updateTimeoutId) { Mainloop.source_remove(this.updateTimeoutId); this.updateTimeoutId = null; } this._fetchUsageData(); // Convert minutes to seconds let intervalSeconds = this.refreshInterval * 60; this.updateTimeoutId = Mainloop.timeout_add_seconds(intervalSeconds, Lang.bind(this, this._updateLoop)); }, _onSettingsChanged: function() { // Restart update loop with new interval this._updateLoop(); }, on_applet_clicked: function(event) { this.menu.toggle(); }, on_applet_removed_from_panel: function() { // Cleanup timeout if (this.updateTimeoutId) { Mainloop.source_remove(this.updateTimeoutId); this.updateTimeoutId = null; } } }; function main(metadata, orientation, panel_height, instance_id) { return new ClaudeStatusApplet(metadata, orientation, panel_height, instance_id); }