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) const PERIODIC_REFRESH_SECONDS = 10; // Periodic refresh as backup to catch stale data 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._periodicTimeout = 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(); } _startPeriodicRefresh() { // Periodic refresh as backup to catch stale data this._periodicTimeout = GLib.timeout_add_seconds( GLib.PRIORITY_DEFAULT, PERIODIC_REFRESH_SECONDS, () => { this._updateTask(); return GLib.SOURCE_CONTINUE; }, ); } _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(); this._startPeriodicRefresh(); 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(); this._startPeriodicRefresh(); 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._periodicTimeout) { GLib.source_remove(this._periodicTimeout); this._periodicTimeout = 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; } } }