merged worktrees, task gkt window and gnome extension

This commit is contained in:
buenosairesam
2025-12-23 19:06:43 -03:00
parent f684da5288
commit ac475b9a5a
15 changed files with 430 additions and 1052 deletions

View File

@@ -0,0 +1,204 @@
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;
}
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "Deskmeter Task Indicator",
"description": "Displays current deskmeter task in GNOME panel",
"uuid": "deskmeter-indicator@local",
"shell-version": [
"40",
"41",
"42",
"43",
"44",
"45",
"46",
"47",
"48",
"49"
],
"url": "",
"version": 1
}

View File

@@ -0,0 +1,5 @@
.deskmeter-task-label {
font-weight: normal;
padding: 0 8px;
color: #ffffff;
}

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Install Deskmeter GNOME Task Indicator
EXTENSION_DIR="$HOME/.local/share/gnome-shell/extensions"
EXTENSION_NAME="deskmeter-indicator@local"
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)/$EXTENSION_NAME"
echo "Installing Deskmeter Task Indicator..."
# Create extensions directory if it doesn't exist
mkdir -p "$EXTENSION_DIR"
# Copy extension files
cp -r "$SOURCE_DIR" "$EXTENSION_DIR/"
echo "Extension copied to $EXTENSION_DIR/$EXTENSION_NAME"
echo ""
echo "Next steps:"
echo "1. Restart GNOME Shell:"
echo " - On X11: Press Alt+F2, type 'r', press Enter"
echo " - On Wayland: Log out and log back in"
echo ""
echo "2. Enable the extension:"
echo " gnome-extensions enable $EXTENSION_NAME"
echo ""
echo "Make sure dmweb is running on http://localhost:10000"

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Quick update script for GNOME extension development
EXTENSION_DIR="$HOME/.local/share/gnome-shell/extensions"
EXTENSION_NAME="deskmeter-indicator@local"
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)/$EXTENSION_NAME"
echo "Updating Deskmeter Task Indicator..."
# Copy extension files
cp -r "$SOURCE_DIR" "$EXTENSION_DIR/"
echo "Files copied."
echo ""
echo "Next: Restart GNOME Shell"
echo " X11: Alt+F2, type 'r', press Enter"
echo " Wayland: Log out and back in"
echo ""
echo "Check logs: journalctl -f -o cat /usr/bin/gnome-shell"

167
dmapp/dmos/task_window.py Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Deskmeter Task Window - Regular Mode
Shows current task in an always-on-top window visible on all workspaces
"""
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib
import urllib.request
import json
import sys
import os
import subprocess
# Try ports in order: env var, command line arg, or try common ports
DEFAULT_PORTS = [10001, 10000] # worktree first, then default
UPDATE_INTERVAL = 500 # milliseconds - fast updates to catch workspace changes
WORKSPACE_CHECK_INTERVAL = 200 # milliseconds - check for workspace changes
# Global API URL (will be set after port detection)
DESKMETER_API_URL = None
class TaskWindow(Gtk.ApplicationWindow):
def __init__(self, app, api_url):
super().__init__(application=app, title="Deskmeter Task")
self.api_url = api_url
# Set window properties
self.set_default_size(400, 60)
# Remove window decorations (no title bar, close button, etc.)
self.set_decorated(False)
# Create label for task display
self.label = Gtk.Label(label="Loading...")
self.label.set_markup('<span font_desc="14">Loading...</span>')
self.label.set_margin_top(20)
self.label.set_margin_bottom(20)
self.label.set_margin_start(20)
self.label.set_margin_end(20)
# Set label to allow selection (useful for copying)
self.label.set_selectable(True)
# Create box container
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.append(self.label)
self.set_child(box)
# Track current workspace for change detection
self.current_workspace = self._get_current_workspace()
self.last_task = None
# Start periodic updates
GLib.timeout_add(UPDATE_INTERVAL, self.update_task)
# Monitor workspace changes more frequently
GLib.timeout_add(WORKSPACE_CHECK_INTERVAL, self.check_workspace_change)
# Initial update
self.update_task()
def _get_current_workspace(self):
"""Get current workspace number using wmctrl"""
try:
result = subprocess.run(['wmctrl', '-d'], capture_output=True, text=True, timeout=1)
for line in result.stdout.splitlines():
if '*' in line: # Current workspace marked with *
return line.split()[0]
except:
pass
return None
def check_workspace_change(self):
"""Check if workspace changed and trigger immediate update"""
new_workspace = self._get_current_workspace()
if new_workspace != self.current_workspace and new_workspace is not None:
self.current_workspace = new_workspace
# Workspace changed, update immediately
GLib.timeout_add(2200, self.update_task) # Wait 2.2s for dmcore to update
return True
def update_task(self):
"""Fetch current task from API and update label"""
try:
with urllib.request.urlopen(self.api_url, timeout=2) as response:
data = json.loads(response.read().decode('utf-8'))
task_path = data.get('task_path', 'no task')
# Only update label if task changed (reduces flicker)
if task_path != self.last_task:
self.last_task = task_path
# Update label with markup for better visibility
self.label.set_markup(f'<span font_desc="14" weight="bold">{task_path}</span>')
except urllib.error.URLError as e:
self.label.set_markup('<span font_desc="14" foreground="red">offline - dmweb not running</span>')
self.last_task = None
except json.JSONDecodeError as e:
self.label.set_markup('<span font_desc="14" foreground="orange">error - invalid API response</span>')
self.last_task = None
except Exception as e:
self.label.set_markup(f'<span font_desc="14" foreground="red">error: {str(e)}</span>')
self.last_task = None
# Return True to keep the timer running
return True
class TaskApp(Gtk.Application):
def __init__(self, api_url):
super().__init__(application_id='local.deskmeter.task-window')
self.api_url = api_url
def do_activate(self):
window = TaskWindow(self, self.api_url)
window.present()
def detect_api_port():
"""Try to find which port the dmweb API is running on"""
# Check environment variable first
env_port = os.environ.get('DESKMETER_PORT')
if env_port:
url = f'http://localhost:{env_port}/api/current_task'
print(f"Using port from DESKMETER_PORT env var: {env_port}")
return url
# Try common ports
for port in DEFAULT_PORTS:
url = f'http://localhost:{port}/api/current_task'
try:
with urllib.request.urlopen(url, timeout=1) as response:
# If we get here, the port is responding
print(f"Found dmweb API on port {port}")
return url
except:
continue
# Fallback to default
print(f"Could not detect dmweb API, using default port {DEFAULT_PORTS[-1]}")
return f'http://localhost:{DEFAULT_PORTS[-1]}/api/current_task'
def main():
# Check for port argument
if len(sys.argv) > 1 and sys.argv[1].isdigit():
port = sys.argv[1]
api_url = f'http://localhost:{port}/api/current_task'
print(f"Using port from command line: {port}")
else:
api_url = detect_api_port()
print(f"API URL: {api_url}")
app = TaskApp(api_url)
return app.run([])
if __name__ == '__main__':
main()