merged worktrees, task gkt window and gnome extension
This commit is contained in:
BIN
dmapp/dmos/gnome-extension/deskmeter-indicator@local.zip
Normal file
BIN
dmapp/dmos/gnome-extension/deskmeter-indicator@local.zip
Normal file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.deskmeter-task-label {
|
||||
font-weight: normal;
|
||||
padding: 0 8px;
|
||||
color: #ffffff;
|
||||
}
|
||||
27
dmapp/dmos/gnome-extension/install.sh
Normal file
27
dmapp/dmos/gnome-extension/install.sh
Normal 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"
|
||||
20
dmapp/dmos/gnome-extension/update.sh
Normal file
20
dmapp/dmos/gnome-extension/update.sh
Normal 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
167
dmapp/dmos/task_window.py
Normal 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()
|
||||
Reference in New Issue
Block a user