Files

236 lines
6.6 KiB
JavaScript

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;
}
}
}