import GObject from 'gi://GObject'; import St from 'gi://St'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import Clutter from 'gi://Clutter'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; // Try common ports - worktree (10001) first, then default (10000) const DEFAULT_PORTS = [10001, 10000]; const DEBOUNCE_DELAY = 2200; // Wait 2.2s after workspace switch (dmcore polls every 2s) let DESKMETER_API_URL = null; const TaskIndicator = GObject.registerClass( class TaskIndicator extends PanelMenu.Button { _init() { super._init(0.0, 'Deskmeter Task Indicator', false); // Create label for task display this._label = new St.Label({ text: 'detecting...', y_align: Clutter.ActorAlign.CENTER, style_class: 'deskmeter-task-label' }); this.add_child(this._label); this._debounceTimeout = null; this._workspaceManager = global.workspace_manager; this._apiUrl = null; // Connect to workspace switch signal this._workspaceSwitchedId = this._workspaceManager.connect( 'workspace-switched', this._onWorkspaceSwitched.bind(this) ); // Detect API port, then start updates this._detectApiPort(); } _detectApiPort() { // Try each port in sequence this._tryNextPort(0); } _tryNextPort(index) { if (index >= DEFAULT_PORTS.length) { // No ports responded, use default and let it show "offline" this._apiUrl = `http://localhost:${DEFAULT_PORTS[DEFAULT_PORTS.length - 1]}/api/current_task`; this._scheduleUpdate(); return; } const port = DEFAULT_PORTS[index]; const url = `http://localhost:${port}/api/current_task`; try { let file = Gio.File.new_for_uri(url); file.load_contents_async(null, (source, result) => { try { let [success, contents] = source.load_contents_finish(result); if (success) { // Port responded, use it this._apiUrl = url; this._scheduleUpdate(); return; } } catch (e) { // This port failed, try next this._tryNextPort(index + 1); } }); } catch (e) { // This port failed, try next this._tryNextPort(index + 1); } } _onWorkspaceSwitched() { // Debounce updates - dmcore takes ~2 seconds to detect and update // We wait a bit to ensure the task has been updated in MongoDB this._scheduleUpdate(); } _scheduleUpdate() { // Clear any pending update if (this._debounceTimeout) { GLib.source_remove(this._debounceTimeout); } // Schedule new update this._debounceTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DEBOUNCE_DELAY, () => { this._updateTask(); this._debounceTimeout = null; return GLib.SOURCE_REMOVE; }); } _updateTask() { if (!this._apiUrl) { this._label.set_text('detecting...'); return; } try { // Create HTTP request let file = Gio.File.new_for_uri(this._apiUrl); file.load_contents_async(null, (source, result) => { try { let [success, contents] = source.load_contents_finish(result); if (success) { let decoder = new TextDecoder('utf-8'); let data = JSON.parse(decoder.decode(contents)); // Update label with task path let displayText = data.task_path || 'no task'; // Optionally truncate long paths if (displayText.length > 40) { let parts = displayText.split('/'); if (parts.length > 2) { displayText = '.../' + parts.slice(-2).join('/'); } else { displayText = displayText.substring(0, 37) + '...'; } } this._label.set_text(displayText); } } catch (e) { this._label.set_text('error'); logError(e, 'Failed to parse deskmeter response'); } }); } catch (e) { this._label.set_text('offline'); logError(e, 'Failed to fetch deskmeter task'); } } destroy() { try { if (this._debounceTimeout) { GLib.source_remove(this._debounceTimeout); this._debounceTimeout = null; } if (this._workspaceSwitchedId) { this._workspaceManager.disconnect(this._workspaceSwitchedId); this._workspaceSwitchedId = null; } super.destroy(); } catch (e) { // Log error but don't crash GNOME Shell logError(e, 'Failed to destroy TaskIndicator'); } } }); export default class Extension { constructor() { this._indicator = null; } enable() { try { this._indicator = new TaskIndicator(); // Add to panel - position after workspace indicator // Panel boxes: left, center, right // We'll add it to the left panel, after other items Main.panel.addToStatusArea('deskmeter-task-indicator', this._indicator, 1, 'left'); } catch (e) { // Log error but don't crash GNOME Shell logError(e, 'Failed to enable Deskmeter extension'); // Clean up if partially initialized if (this._indicator) { try { this._indicator.destroy(); } catch (destroyError) { logError(destroyError, 'Failed to cleanup indicator'); } this._indicator = null; } } } disable() { try { if (this._indicator) { this._indicator.destroy(); this._indicator = null; } } catch (e) { // Log error but don't crash GNOME Shell logError(e, 'Failed to disable Deskmeter extension'); this._indicator = null; } } }