more solid task updating, no need to stop the main loop to avoid race conditions
This commit is contained in:
@@ -4,11 +4,11 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import state
|
import state
|
||||||
import task
|
import task
|
||||||
from config import logger, switches
|
from config import logger, switches
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
desktops = ("Plan", "Think", "Work", "Other", "Away", "Work", "Work", "Work")
|
desktops = ("Plan", "Think", "Work", "Other", "Away", "Work", "Work", "Work")
|
||||||
unlabeled = "Away"
|
unlabeled = "Away"
|
||||||
@@ -39,22 +39,18 @@ def now():
|
|||||||
return datetime.datetime.now(ZoneInfo(cfg["timezone"]))
|
return datetime.datetime.now(ZoneInfo(cfg["timezone"]))
|
||||||
|
|
||||||
|
|
||||||
def handle_task_file_changes(current_task):
|
def handle_task_file_changes():
|
||||||
"""Check if task file changed and update task if needed. Returns (new_task, file_changed)"""
|
"""Check if task file changed and sync definitions to DB. Does not change current task."""
|
||||||
current_mtime = state.retrieve("current").get("filetime")
|
current_mtime = state.retrieve("current").get("filetime")
|
||||||
file_mtime = task.get_file_mtime(None)
|
file_mtime = task.get_file_mtime(None)
|
||||||
|
|
||||||
if current_mtime != file_mtime:
|
if current_mtime != file_mtime:
|
||||||
task_id = task.read_and_extract(None)
|
|
||||||
logger.debug(f"task_id:{task_id}")
|
|
||||||
task.file_to_db(None)
|
task.file_to_db(None)
|
||||||
if task_id != current_task:
|
state.save("current", filetime=file_mtime)
|
||||||
state.save("current", task=task_id)
|
logger.info("Task file changed, definitions synced to DB")
|
||||||
current_task = task_id
|
return True # File changed
|
||||||
|
|
||||||
return current_task, True # File changed
|
return False # No change
|
||||||
|
|
||||||
return current_task, False # No change
|
|
||||||
|
|
||||||
|
|
||||||
def update_workspace_state():
|
def update_workspace_state():
|
||||||
@@ -65,14 +61,19 @@ def update_workspace_state():
|
|||||||
|
|
||||||
|
|
||||||
def enforce_desktop_task(current_workspace, work_desktop_tasks, current_task):
|
def enforce_desktop_task(current_workspace, work_desktop_tasks, current_task):
|
||||||
"""Enforce assigned task for work desktops"""
|
"""Enforce assigned task for work desktops. Updates MongoDB state only, never writes to file."""
|
||||||
if current_workspace in work_desktop_tasks and work_desktop_tasks[current_workspace]:
|
if (
|
||||||
|
current_workspace in work_desktop_tasks
|
||||||
|
and work_desktop_tasks[current_workspace]
|
||||||
|
):
|
||||||
assigned_task = work_desktop_tasks[current_workspace]
|
assigned_task = work_desktop_tasks[current_workspace]
|
||||||
|
|
||||||
if current_task != assigned_task:
|
if current_task != assigned_task:
|
||||||
current_task = assigned_task
|
current_task = assigned_task
|
||||||
state.save("current", task=current_task)
|
state.save("current", task=current_task)
|
||||||
task.db_to_file_as_is(None)
|
logger.debug(
|
||||||
|
f"Enforced task {assigned_task} for workspace {current_workspace}"
|
||||||
|
)
|
||||||
|
|
||||||
return current_task
|
return current_task
|
||||||
|
|
||||||
@@ -121,8 +122,9 @@ def desktop(workspace_index):
|
|||||||
return unlabeled
|
return unlabeled
|
||||||
|
|
||||||
|
|
||||||
task.read_and_extract(None)
|
# Sync task definitions from file to DB on startup
|
||||||
task.file_to_db(None)
|
task.file_to_db(None)
|
||||||
|
state.save("current", filetime=task.get_file_mtime(None))
|
||||||
|
|
||||||
current_workspace = active_workspace()
|
current_workspace = active_workspace()
|
||||||
current_task = state.retrieve("current").get("task")
|
current_task = state.retrieve("current").get("task")
|
||||||
@@ -144,18 +146,21 @@ while True:
|
|||||||
# Load work_desktop_tasks from state
|
# Load work_desktop_tasks from state
|
||||||
work_desktop_tasks = state.retrieve_desktop_state()
|
work_desktop_tasks = state.retrieve_desktop_state()
|
||||||
|
|
||||||
# Handle task file changes
|
# Sync task definitions if file changed (does not change current task)
|
||||||
current_task, file_changed = handle_task_file_changes(current_task)
|
handle_task_file_changes()
|
||||||
|
|
||||||
# Update current task and workspace
|
# Get current state
|
||||||
current_task = state.retrieve("current").get("task")
|
current_task = state.retrieve("current").get("task")
|
||||||
current_workspace = update_workspace_state()
|
current_workspace = update_workspace_state()
|
||||||
|
|
||||||
# Enforce desktop task assignments (but skip if file just changed - user's manual change takes priority)
|
# Enforce work desktop task assignments
|
||||||
if not file_changed:
|
current_task = enforce_desktop_task(
|
||||||
current_task = enforce_desktop_task(current_workspace, work_desktop_tasks, current_task)
|
current_workspace, work_desktop_tasks, current_task
|
||||||
|
)
|
||||||
|
|
||||||
# Track workspace switches
|
# Track workspace switches
|
||||||
last_switch_time = track_workspace_switch(current_workspace, current_task, last_switch_time)
|
last_switch_time = track_workspace_switch(
|
||||||
|
current_workspace, current_task, last_switch_time
|
||||||
|
)
|
||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import state
|
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
from config import logger, tasks
|
from config import logger, tasks
|
||||||
|
|
||||||
@@ -59,7 +57,13 @@ def file_to_db(filepath: str):
|
|||||||
if task_id:
|
if task_id:
|
||||||
tasks.update_one(
|
tasks.update_one(
|
||||||
{"task_id": task_id},
|
{"task_id": task_id},
|
||||||
{"$set": {"path": full_path, "task_id": task_id, "historic": False}},
|
{
|
||||||
|
"$set": {
|
||||||
|
"path": full_path,
|
||||||
|
"task_id": task_id,
|
||||||
|
"historic": False,
|
||||||
|
}
|
||||||
|
},
|
||||||
upsert=True,
|
upsert=True,
|
||||||
)
|
)
|
||||||
elif full_path not in seen_paths:
|
elif full_path not in seen_paths:
|
||||||
@@ -68,96 +72,10 @@ def file_to_db(filepath: str):
|
|||||||
seen_paths.add(full_path)
|
seen_paths.add(full_path)
|
||||||
|
|
||||||
|
|
||||||
def format_task_line(
|
|
||||||
path_parts: list, indent_level: int, task_id: str, current_task: str
|
|
||||||
) -> str:
|
|
||||||
line = " " * (4 * indent_level) + path_parts[-1]
|
|
||||||
if task_id:
|
|
||||||
padding = max(1, 64 - len(line))
|
|
||||||
line = f"{line}{' ' * padding}|{task_id}"
|
|
||||||
if task_id == current_task:
|
|
||||||
line += " *"
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
def db_to_file_as_is(filepath: str):
|
|
||||||
"""Write tasks from MongoDB to file exactly as they were read."""
|
|
||||||
if filepath is None:
|
|
||||||
filepath = task_file
|
|
||||||
|
|
||||||
current_task = state.retrieve("current").get("task")
|
|
||||||
all_tasks = list(tasks.find())
|
|
||||||
|
|
||||||
with open(filepath, "w") as f:
|
|
||||||
for task in all_tasks:
|
|
||||||
if not task["path"]:
|
|
||||||
f.write("\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
path_parts = task["path"].split("/")
|
|
||||||
line = format_task_line(
|
|
||||||
path_parts, len(path_parts) - 1, task.get("task_id", ""), current_task
|
|
||||||
)
|
|
||||||
f.write(f"{line}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_tasks(prefix):
|
def get_all_tasks(prefix):
|
||||||
return list(tasks.find({"path": {"$ne": ""}}).sort("path", 1))
|
return list(tasks.find({"path": {"$ne": ""}}).sort("path", 1))
|
||||||
|
|
||||||
|
|
||||||
def db_to_file_consolidated(filepath: str):
|
|
||||||
"""Write tasks from MongoDB to file as a consolidated tree."""
|
|
||||||
current_task = state.retrieve("current").get("task")
|
|
||||||
all_tasks = list(tasks.find({"path": {"$ne": ""}}).sort("path", 1))
|
|
||||||
|
|
||||||
with open(filepath, "w") as f:
|
|
||||||
prev_parts = []
|
|
||||||
for task in all_tasks:
|
|
||||||
path_parts = task["path"].split("/")
|
|
||||||
common = 0
|
|
||||||
for i, (prev, curr) in enumerate(zip(prev_parts, path_parts)):
|
|
||||||
if prev != curr:
|
|
||||||
break
|
|
||||||
common = i + 1
|
|
||||||
|
|
||||||
for i, part in enumerate(path_parts[common:], common):
|
|
||||||
path_segment = path_parts[: i + 1]
|
|
||||||
task_id = task.get("task_id", "") if path_segment == path_parts else ""
|
|
||||||
line = format_task_line([part], i, task_id, current_task)
|
|
||||||
f.write(f"{line}\n")
|
|
||||||
|
|
||||||
prev_parts = path_parts
|
|
||||||
|
|
||||||
|
|
||||||
def extract(line: str) -> Optional[str]:
|
|
||||||
"""Extract task ID if line ends with * and has a valid ID."""
|
|
||||||
line = line.rstrip()
|
|
||||||
if line.endswith("*"):
|
|
||||||
pipe_index = line.find("|")
|
|
||||||
if pipe_index != -1:
|
|
||||||
# Extract everything between | and * and strip spaces
|
|
||||||
id_part = line[pipe_index + 1 : -1].strip()
|
|
||||||
if len(id_part) == 8:
|
|
||||||
return id_part
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def read_and_extract(filepath: str) -> Optional[str]:
|
|
||||||
"""Read file and update state if current task is found."""
|
|
||||||
if filepath is None:
|
|
||||||
filepath = task_file
|
|
||||||
|
|
||||||
mtime = get_file_mtime(filepath)
|
|
||||||
state.save("current", filetime=mtime)
|
|
||||||
|
|
||||||
with open(filepath, "r") as file:
|
|
||||||
for line in file:
|
|
||||||
task_id = extract(line)
|
|
||||||
if task_id:
|
|
||||||
return task_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_file_mtime(filepath: str) -> str:
|
def get_file_mtime(filepath: str) -> str:
|
||||||
"""Get file modification time as ISO format string."""
|
"""Get file modification time as ISO format string."""
|
||||||
if filepath is None:
|
if filepath is None:
|
||||||
|
|||||||
@@ -1,204 +1,235 @@
|
|||||||
import GObject from 'gi://GObject';
|
import GObject from "gi://GObject";
|
||||||
import St from 'gi://St';
|
import St from "gi://St";
|
||||||
import Gio from 'gi://Gio';
|
import Gio from "gi://Gio";
|
||||||
import GLib from 'gi://GLib';
|
import GLib from "gi://GLib";
|
||||||
import Clutter from 'gi://Clutter';
|
import Clutter from "gi://Clutter";
|
||||||
|
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
||||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
|
||||||
|
|
||||||
// Try common ports - worktree (10001) first, then default (10000)
|
// Try common ports - worktree (10001) first, then default (10000)
|
||||||
const DEFAULT_PORTS = [10001, 10000];
|
const DEFAULT_PORTS = [10001, 10000];
|
||||||
const DEBOUNCE_DELAY = 2200; // Wait 2.2s after workspace switch (dmcore polls every 2s)
|
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;
|
let DESKMETER_API_URL = null;
|
||||||
|
|
||||||
const TaskIndicator = GObject.registerClass(
|
const TaskIndicator = GObject.registerClass(
|
||||||
class TaskIndicator extends PanelMenu.Button {
|
class TaskIndicator extends PanelMenu.Button {
|
||||||
_init() {
|
_init() {
|
||||||
super._init(0.0, 'Deskmeter Task Indicator', false);
|
super._init(0.0, "Deskmeter Task Indicator", false);
|
||||||
|
|
||||||
// Create label for task display
|
// Create label for task display
|
||||||
this._label = new St.Label({
|
this._label = new St.Label({
|
||||||
text: 'detecting...',
|
text: "detecting...",
|
||||||
y_align: Clutter.ActorAlign.CENTER,
|
y_align: Clutter.ActorAlign.CENTER,
|
||||||
style_class: 'deskmeter-task-label'
|
style_class: "deskmeter-task-label",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.add_child(this._label);
|
this.add_child(this._label);
|
||||||
|
|
||||||
this._debounceTimeout = null;
|
this._debounceTimeout = null;
|
||||||
this._workspaceManager = global.workspace_manager;
|
this._periodicTimeout = null;
|
||||||
this._apiUrl = null;
|
this._workspaceManager = global.workspace_manager;
|
||||||
|
this._apiUrl = null;
|
||||||
|
|
||||||
// Connect to workspace switch signal
|
// Connect to workspace switch signal
|
||||||
this._workspaceSwitchedId = this._workspaceManager.connect(
|
this._workspaceSwitchedId = this._workspaceManager.connect(
|
||||||
'workspace-switched',
|
"workspace-switched",
|
||||||
this._onWorkspaceSwitched.bind(this)
|
this._onWorkspaceSwitched.bind(this),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Detect API port, then start updates
|
// Detect API port, then start updates
|
||||||
this._detectApiPort();
|
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() {
|
_detectApiPort() {
|
||||||
// Try each port in sequence
|
// Try each port in sequence
|
||||||
this._tryNextPort(0);
|
this._tryNextPort(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_tryNextPort(index) {
|
_tryNextPort(index) {
|
||||||
if (index >= DEFAULT_PORTS.length) {
|
if (index >= DEFAULT_PORTS.length) {
|
||||||
// No ports responded, use default and let it show "offline"
|
// No ports responded, use default and let it show "offline"
|
||||||
this._apiUrl = `http://localhost:${DEFAULT_PORTS[DEFAULT_PORTS.length - 1]}/api/current_task`;
|
this._apiUrl = `http://localhost:${DEFAULT_PORTS[DEFAULT_PORTS.length - 1]}/api/current_task`;
|
||||||
this._scheduleUpdate();
|
this._scheduleUpdate();
|
||||||
return;
|
this._startPeriodicRefresh();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const port = DEFAULT_PORTS[index];
|
const port = DEFAULT_PORTS[index];
|
||||||
const url = `http://localhost:${port}/api/current_task`;
|
const url = `http://localhost:${port}/api/current_task`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let file = Gio.File.new_for_uri(url);
|
let file = Gio.File.new_for_uri(url);
|
||||||
file.load_contents_async(null, (source, result) => {
|
file.load_contents_async(null, (source, result) => {
|
||||||
try {
|
try {
|
||||||
let [success, contents] = source.load_contents_finish(result);
|
let [success, contents] = source.load_contents_finish(result);
|
||||||
if (success) {
|
if (success) {
|
||||||
// Port responded, use it
|
// Port responded, use it
|
||||||
this._apiUrl = url;
|
this._apiUrl = url;
|
||||||
this._scheduleUpdate();
|
this._scheduleUpdate();
|
||||||
return;
|
this._startPeriodicRefresh();
|
||||||
}
|
return;
|
||||||
} catch (e) {
|
}
|
||||||
// This port failed, try next
|
} catch (e) {
|
||||||
this._tryNextPort(index + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// This port failed, try next
|
// This port failed, try next
|
||||||
this._tryNextPort(index + 1);
|
this._tryNextPort(index + 1);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// This port failed, try next
|
||||||
|
this._tryNextPort(index + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWorkspaceSwitched() {
|
_onWorkspaceSwitched() {
|
||||||
// Debounce updates - dmcore takes ~2 seconds to detect and update
|
// Debounce updates - dmcore takes ~2 seconds to detect and update
|
||||||
// We wait a bit to ensure the task has been updated in MongoDB
|
// We wait a bit to ensure the task has been updated in MongoDB
|
||||||
this._scheduleUpdate();
|
this._scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
_scheduleUpdate() {
|
_scheduleUpdate() {
|
||||||
// Clear any pending update
|
// Clear any pending update
|
||||||
if (this._debounceTimeout) {
|
if (this._debounceTimeout) {
|
||||||
GLib.source_remove(this._debounceTimeout);
|
GLib.source_remove(this._debounceTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule new update
|
// Schedule new update
|
||||||
this._debounceTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DEBOUNCE_DELAY, () => {
|
this._debounceTimeout = GLib.timeout_add(
|
||||||
this._updateTask();
|
GLib.PRIORITY_DEFAULT,
|
||||||
this._debounceTimeout = null;
|
DEBOUNCE_DELAY,
|
||||||
return GLib.SOURCE_REMOVE;
|
() => {
|
||||||
});
|
this._updateTask();
|
||||||
|
this._debounceTimeout = null;
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateTask() {
|
_updateTask() {
|
||||||
if (!this._apiUrl) {
|
if (!this._apiUrl) {
|
||||||
this._label.set_text('detecting...');
|
this._label.set_text("detecting...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create HTTP request
|
// Create HTTP request
|
||||||
let file = Gio.File.new_for_uri(this._apiUrl);
|
let file = Gio.File.new_for_uri(this._apiUrl);
|
||||||
file.load_contents_async(null, (source, result) => {
|
file.load_contents_async(null, (source, result) => {
|
||||||
try {
|
try {
|
||||||
let [success, contents] = source.load_contents_finish(result);
|
let [success, contents] = source.load_contents_finish(result);
|
||||||
if (success) {
|
if (success) {
|
||||||
let decoder = new TextDecoder('utf-8');
|
let decoder = new TextDecoder("utf-8");
|
||||||
let data = JSON.parse(decoder.decode(contents));
|
let data = JSON.parse(decoder.decode(contents));
|
||||||
|
|
||||||
// Update label with task path
|
// Update label with task path
|
||||||
let displayText = data.task_path || 'no task';
|
let displayText = data.task_path || "no task";
|
||||||
|
|
||||||
// Optionally truncate long paths
|
// Optionally truncate long paths
|
||||||
if (displayText.length > 40) {
|
if (displayText.length > 40) {
|
||||||
let parts = displayText.split('/');
|
let parts = displayText.split("/");
|
||||||
if (parts.length > 2) {
|
if (parts.length > 2) {
|
||||||
displayText = '.../' + parts.slice(-2).join('/');
|
displayText = ".../" + parts.slice(-2).join("/");
|
||||||
} else {
|
} else {
|
||||||
displayText = displayText.substring(0, 37) + '...';
|
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');
|
this._label.set_text(displayText);
|
||||||
logError(e, 'Failed to fetch deskmeter task');
|
}
|
||||||
}
|
} 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() {
|
destroy() {
|
||||||
try {
|
try {
|
||||||
if (this._debounceTimeout) {
|
if (this._debounceTimeout) {
|
||||||
GLib.source_remove(this._debounceTimeout);
|
GLib.source_remove(this._debounceTimeout);
|
||||||
this._debounceTimeout = null;
|
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export default class Extension {
|
||||||
constructor() {
|
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;
|
this._indicator = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enable() {
|
disable() {
|
||||||
try {
|
try {
|
||||||
this._indicator = new TaskIndicator();
|
if (this._indicator) {
|
||||||
|
this._indicator.destroy();
|
||||||
// Add to panel - position after workspace indicator
|
this._indicator = null;
|
||||||
// Panel boxes: left, center, right
|
}
|
||||||
// We'll add it to the left panel, after other items
|
} catch (e) {
|
||||||
Main.panel.addToStatusArea('deskmeter-task-indicator', this._indicator, 1, 'left');
|
// Log error but don't crash GNOME Shell
|
||||||
} catch (e) {
|
logError(e, "Failed to disable Deskmeter extension");
|
||||||
// Log error but don't crash GNOME Shell
|
this._indicator = null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ from flask import Blueprint, render_template
|
|||||||
from .dm import dmbp
|
from .dm import dmbp
|
||||||
from .get_period_times import (
|
from .get_period_times import (
|
||||||
get_period_totals,
|
get_period_totals,
|
||||||
read_and_extract,
|
get_work_period_totals,
|
||||||
task_file,
|
|
||||||
task_or_none,
|
task_or_none,
|
||||||
timezone,
|
timezone,
|
||||||
get_work_period_totals,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DMHTMLCalendar(calendar.HTMLCalendar):
|
class DMHTMLCalendar(calendar.HTMLCalendar):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -148,7 +145,9 @@ def workmonth(month=None, year=None):
|
|||||||
cal.setcalmonth(usemonth)
|
cal.setcalmonth(usemonth)
|
||||||
cal.setcalyear(useyear)
|
cal.setcalyear(useyear)
|
||||||
|
|
||||||
return render_template("calendar.html", content=cal.formatmonth(useyear, usemonth), auto_refresh=False)
|
return render_template(
|
||||||
|
"calendar.html", content=cal.formatmonth(useyear, usemonth), auto_refresh=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dmbp.route("/month")
|
@dmbp.route("/month")
|
||||||
@@ -174,4 +173,6 @@ def month(month=None, year=None, task=None):
|
|||||||
cal.setcalmonth(usemonth)
|
cal.setcalmonth(usemonth)
|
||||||
cal.setcalyear(useyear)
|
cal.setcalyear(useyear)
|
||||||
|
|
||||||
return render_template("calendar.html", content=cal.formatmonth(useyear, usemonth), auto_refresh=True)
|
return render_template(
|
||||||
|
"calendar.html", content=cal.formatmonth(useyear, usemonth), auto_refresh=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
timezone = ZoneInfo("America/Argentina/Buenos_Aires")
|
timezone = ZoneInfo("America/Argentina/Buenos_Aires")
|
||||||
utctz = ZoneInfo("UTC")
|
utctz = ZoneInfo("UTC")
|
||||||
@@ -38,8 +38,8 @@ def parse_task_line(line):
|
|||||||
|
|
||||||
|
|
||||||
def load_task_from_files(task_id):
|
def load_task_from_files(task_id):
|
||||||
"""Search task directory files for a task ID and load it into task_history."""
|
"""Search task directory files (recursively) for a task ID and load it into task_history."""
|
||||||
for task_filepath in task_dir.glob("*"):
|
for task_filepath in task_dir.glob("**/*"):
|
||||||
if not task_filepath.is_file():
|
if not task_filepath.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -65,12 +65,14 @@ def load_task_from_files(task_id):
|
|||||||
# Found it! Insert into task_history
|
# Found it! Insert into task_history
|
||||||
task_history.update_one(
|
task_history.update_one(
|
||||||
{"task_id": task_id},
|
{"task_id": task_id},
|
||||||
{"$set": {
|
{
|
||||||
"path": full_path,
|
"$set": {
|
||||||
"task_id": task_id,
|
"path": full_path,
|
||||||
"source_file": task_filepath.name
|
"task_id": task_id,
|
||||||
}},
|
"source_file": task_filepath.name,
|
||||||
upsert=True
|
}
|
||||||
|
},
|
||||||
|
upsert=True,
|
||||||
)
|
)
|
||||||
return full_path
|
return full_path
|
||||||
except:
|
except:
|
||||||
@@ -134,15 +136,10 @@ def get_task_time_seconds(start, end, task_id, workspaces=None):
|
|||||||
"$match": {
|
"$match": {
|
||||||
"date": {"$gte": start, "$lte": end},
|
"date": {"$gte": start, "$lte": end},
|
||||||
"task": task_id,
|
"task": task_id,
|
||||||
"workspace": {"$in": workspaces}
|
"workspace": {"$in": workspaces},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{"$group": {"_id": None, "total_seconds": {"$sum": "$delta"}}},
|
||||||
"$group": {
|
|
||||||
"_id": None,
|
|
||||||
"total_seconds": {"$sum": "$delta"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
result = list(switches.aggregate(pipeline))
|
result = list(switches.aggregate(pipeline))
|
||||||
@@ -154,12 +151,8 @@ def get_task_time_seconds(start, end, task_id, workspaces=None):
|
|||||||
|
|
||||||
|
|
||||||
def task_or_none(task=None):
|
def task_or_none(task=None):
|
||||||
if not task:
|
|
||||||
task = read_and_extract(task_file)
|
|
||||||
|
|
||||||
if task == "all":
|
if task == "all":
|
||||||
task = None
|
task = None
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
@@ -180,24 +173,6 @@ def convert_seconds(seconds, use_days=False):
|
|||||||
return "{:02d}:{:02d}:{:02d}".format(hours + days * 24, minutes, remaining_seconds)
|
return "{:02d}:{:02d}:{:02d}".format(hours + days * 24, minutes, remaining_seconds)
|
||||||
|
|
||||||
|
|
||||||
def extract(line):
|
|
||||||
if line.rstrip().endswith("*"):
|
|
||||||
pipe_index = line.find("|")
|
|
||||||
if pipe_index != -1 and len(line) > pipe_index + 8:
|
|
||||||
value = line[pipe_index + 1 : pipe_index + 9]
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def read_and_extract(file_path):
|
|
||||||
with open(file_path, "r") as file:
|
|
||||||
for line in file:
|
|
||||||
value = extract(line)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_work_period_totals(start, end):
|
def get_work_period_totals(start, end):
|
||||||
"""Get period totals grouped by task with full path."""
|
"""Get period totals grouped by task with full path."""
|
||||||
# Get all tasks with time in the period
|
# Get all tasks with time in the period
|
||||||
@@ -206,15 +181,10 @@ def get_work_period_totals(start, end):
|
|||||||
"$match": {
|
"$match": {
|
||||||
"date": {"$gte": start, "$lte": end},
|
"date": {"$gte": start, "$lte": end},
|
||||||
"workspace": {"$in": ["Plan", "Think", "Work"]},
|
"workspace": {"$in": ["Plan", "Think", "Work"]},
|
||||||
"task": {"$exists": True, "$ne": None}
|
"task": {"$exists": True, "$ne": None},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{"$group": {"_id": "$task", "total_seconds": {"$sum": "$delta"}}},
|
||||||
"$group": {
|
|
||||||
"_id": "$task",
|
|
||||||
"total_seconds": {"$sum": "$delta"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
results = list(switches.aggregate(pipeline))
|
results = list(switches.aggregate(pipeline))
|
||||||
@@ -228,17 +198,18 @@ def get_work_period_totals(start, end):
|
|||||||
# Get task path with history fallback
|
# Get task path with history fallback
|
||||||
task_path = get_task_path(task_id)
|
task_path = get_task_path(task_id)
|
||||||
|
|
||||||
combined_rows.append({
|
combined_rows.append(
|
||||||
"ws": task_path,
|
{"ws": task_path, "total": convert_seconds(total_seconds)}
|
||||||
"total": convert_seconds(total_seconds)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
# Sort by path for consistency
|
# Sort by path for consistency
|
||||||
combined_rows.sort(key=lambda x: x["ws"])
|
combined_rows.sort(key=lambda x: x["ws"])
|
||||||
return combined_rows
|
return combined_rows
|
||||||
|
|
||||||
|
|
||||||
def get_task_blocks_calendar(start, end, task=None, min_block_seconds=300, grid_hours=1):
|
def get_task_blocks_calendar(
|
||||||
|
start, end, task=None, min_block_seconds=300, grid_hours=1
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get task blocks for calendar-style visualization, aggregated by time grid.
|
Get task blocks for calendar-style visualization, aggregated by time grid.
|
||||||
Shows all tasks worked on during each grid period, with overlapping blocks.
|
Shows all tasks worked on during each grid period, with overlapping blocks.
|
||||||
@@ -263,7 +234,7 @@ def get_task_blocks_calendar(start, end, task=None, min_block_seconds=300, grid_
|
|||||||
|
|
||||||
match_query = {
|
match_query = {
|
||||||
"date": {"$gte": start, "$lte": end},
|
"date": {"$gte": start, "$lte": end},
|
||||||
"workspace": {"$in": ["Plan", "Think", "Work"]} # Only active workspaces
|
"workspace": {"$in": ["Plan", "Think", "Work"]}, # Only active workspaces
|
||||||
}
|
}
|
||||||
if task_query:
|
if task_query:
|
||||||
match_query["task"] = task_query
|
match_query["task"] = task_query
|
||||||
@@ -291,13 +262,14 @@ def get_task_blocks_calendar(start, end, task=None, min_block_seconds=300, grid_
|
|||||||
while remaining_duration > 0 and current_time < switch_end:
|
while remaining_duration > 0 and current_time < switch_end:
|
||||||
# Calculate grid period start (hour rounded down to grid_hours)
|
# Calculate grid period start (hour rounded down to grid_hours)
|
||||||
grid_hour = (current_time.hour // grid_hours) * grid_hours
|
grid_hour = (current_time.hour // grid_hours) * grid_hours
|
||||||
grid_start = current_time.replace(hour=grid_hour, minute=0, second=0, microsecond=0)
|
grid_start = current_time.replace(
|
||||||
|
hour=grid_hour, minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
grid_end = grid_start + timedelta(hours=grid_hours)
|
grid_end = grid_start + timedelta(hours=grid_hours)
|
||||||
|
|
||||||
# Time in this grid period
|
# Time in this grid period
|
||||||
time_in_grid = min(
|
time_in_grid = min(
|
||||||
(grid_end - current_time).total_seconds(),
|
(grid_end - current_time).total_seconds(), remaining_duration
|
||||||
remaining_duration
|
|
||||||
)
|
)
|
||||||
|
|
||||||
key = (current_time.date(), grid_hour, task_id)
|
key = (current_time.date(), grid_hour, task_id)
|
||||||
@@ -316,19 +288,23 @@ def get_task_blocks_calendar(start, end, task=None, min_block_seconds=300, grid_
|
|||||||
blocks = []
|
blocks = []
|
||||||
for (date, grid_hour, task_id), data in grid_task_time.items():
|
for (date, grid_hour, task_id), data in grid_task_time.items():
|
||||||
if data["duration"] >= min_block_seconds:
|
if data["duration"] >= min_block_seconds:
|
||||||
grid_start = datetime(date.year, date.month, date.day, grid_hour, 0, 0, tzinfo=timezone)
|
grid_start = datetime(
|
||||||
|
date.year, date.month, date.day, grid_hour, 0, 0, tzinfo=timezone
|
||||||
|
)
|
||||||
|
|
||||||
blocks.append({
|
blocks.append(
|
||||||
"task_id": task_id,
|
{
|
||||||
"task_path": data["task_path"],
|
"task_id": task_id,
|
||||||
"start": grid_start,
|
"task_path": data["task_path"],
|
||||||
"end": grid_start + timedelta(seconds=data["duration"]),
|
"start": grid_start,
|
||||||
"hour": grid_hour,
|
"end": grid_start + timedelta(seconds=data["duration"]),
|
||||||
"duration": int(data["duration"]),
|
"hour": grid_hour,
|
||||||
"active_seconds": int(data["duration"]),
|
"duration": int(data["duration"]),
|
||||||
"idle_seconds": 0,
|
"active_seconds": int(data["duration"]),
|
||||||
"active_ratio": 1.0
|
"idle_seconds": 0,
|
||||||
})
|
"active_ratio": 1.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return sorted(blocks, key=lambda x: (x["start"], x["task_path"]))
|
return sorted(blocks, key=lambda x: (x["start"], x["task_path"]))
|
||||||
|
|
||||||
@@ -360,13 +336,15 @@ def get_raw_switches(start, end, task=None):
|
|||||||
# Get task path with history fallback
|
# Get task path with history fallback
|
||||||
task_path = get_task_path(task_id) or "No Task"
|
task_path = get_task_path(task_id) or "No Task"
|
||||||
|
|
||||||
result.append({
|
result.append(
|
||||||
"workspace": switch["workspace"],
|
{
|
||||||
"task_id": task_id,
|
"workspace": switch["workspace"],
|
||||||
"task_path": task_path,
|
"task_id": task_id,
|
||||||
"date": switch["date"].replace(tzinfo=utctz).astimezone(timezone),
|
"task_path": task_path,
|
||||||
"delta": switch["delta"]
|
"date": switch["date"].replace(tzinfo=utctz).astimezone(timezone),
|
||||||
})
|
"delta": switch["delta"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user