From 4e89dfe3848dafa99b18c1a39327ce0e1a550e72 Mon Sep 17 00:00:00 2001 From: dragonruler1000 Date: Thu, 26 Jun 2025 10:09:53 -0500 Subject: [PATCH 1/3] addied linux support. --- tracker.py | 94 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/tracker.py b/tracker.py index 1cc1455..79a9ad5 100644 --- a/tracker.py +++ b/tracker.py @@ -4,25 +4,29 @@ import urllib.error import json import psutil +import platform +import subprocess +import os from datetime import datetime -import pygetwindow as gw -import pyautogui -WAKATIME_API_KEY = 'YOUR_WAKATIME_API_KEY' -HEARTBEAT_INTERVAL = 120 -CHECK_INTERVAL = 30 +try: + import pyautogui +except ImportError: + pyautogui = None + +WAKATIME_API_KEY = '1cf98d16-a859-4fcb-8d40-946a04a7f54c' +HEARTBEAT_INTERVAL = 120 # seconds +CHECK_INTERVAL = 30 # seconds APPS = [ - """ - Insert code block from /apps here { - "process_name": "", - "window_keyword": "", - "project": "", - "language": "", + "process_name": "godot-runner", # Example: VS Code + "window_keyword": "Pixelorama", + "project": "Pixelroma", + "language": "Pixel-art", "plugin": "general-wakatime" }, - """ + # Add more tracked apps here ] def is_process_running(name): @@ -32,19 +36,54 @@ def is_process_running(name): return False def get_active_window_title(): + system = platform.system() + session_type = os.environ.get("XDG_SESSION_TYPE", "").lower() + try: - win = gw.getActiveWindow() - if win: - return win.title.strip() - except: - pass + if system in ["Windows", "Darwin"]: + import pygetwindow as gw + win = gw.getActiveWindow() + if win: + return win.title.strip() + + elif system == "Linux": + if session_type == "x11": + return subprocess.check_output( + ["xdotool", "getactivewindow", "getwindowname"] + ).decode().strip() + + elif session_type == "wayland": + # KDE Plasma Wayland: D-Bus calls may block compositor or prompt user + # Safer fallback: just return None or a placeholder + print("[WARN] Window title detection is limited on KDE Wayland. Falling back to process-only detection.") + return None + except Exception as e: + print(f"[WARN] D-Bus Wayland error: {e}") return None + def user_is_active(window_title=None, threshold_seconds=CHECK_INTERVAL): - pos1 = pyautogui.position() - time.sleep(threshold_seconds) - pos2 = pyautogui.position() - return pos1 != pos2 and get_active_window_title() == window_title + session_type = os.environ.get("XDG_SESSION_TYPE", "").lower() + + if platform.system() == "Linux" and session_type == "wayland": + # Under Wayland, just check if the window stayed focused + time.sleep(threshold_seconds) + return get_active_window_title() == window_title + + elif pyautogui: + try: + pos1 = pyautogui.position() + time.sleep(threshold_seconds) + pos2 = pyautogui.position() + return pos1 != pos2 and get_active_window_title() == window_title + except Exception as e: + print(f"[WARN] pyautogui failed: {e}") + return False + + else: + # Fallback if pyautogui is missing + time.sleep(threshold_seconds) + return get_active_window_title() == window_title def save_heartbeat_local(payload): with open("heartbeats.json", "a", encoding='utf-8') as f: @@ -67,15 +106,17 @@ def send_heartbeat(entity, app_config): "language": app_config['language'], "plugin": app_config['plugin'] } + req = urllib.request.Request( url='https://hackatime.hackclub.com/api/hackatime/v1/users/current/heartbeats', data=json.dumps(payload).encode('utf-8'), headers=headers, method='POST' ) + try: with urllib.request.urlopen(req) as response: - print(f"[{datetime.now()}] Heartbeat: {entity} -> {app_config['project']} ({response.status})") + print(f"[{datetime.now()}] Heartbeat sent for '{entity}' -> {app_config['project']} ({response.status})") save_heartbeat_local(payload) except urllib.error.HTTPError as e: print(f"[{datetime.now()}] HTTP error: {e.code} - {e.reason}") @@ -92,12 +133,13 @@ def main(): for app in APPS: if is_process_running(app['process_name']): - if current_title and app['window_keyword'].lower() in current_title.lower(): + # On KDE Wayland, current_title may be None + if current_title is None or (current_title and app['window_keyword'].lower() in current_title.lower()): if user_is_active(window_title=current_title): if now - last_sent[app['process_name']] > HEARTBEAT_INTERVAL or current_title != last_window[app['process_name']]: - send_heartbeat(current_title, app) + send_heartbeat(current_title or app['process_name'], app) last_sent[app['process_name']] = now - last_window[app['process_name']] = current_title + last_window[app['process_name']] = current_title or "" if __name__ == "__main__": - main() \ No newline at end of file + main() From 83c0d13df695ec5f5c73d7a3df2c003d06d36623 Mon Sep 17 00:00:00 2001 From: dragonruler1000 Date: Thu, 26 Jun 2025 10:26:31 -0500 Subject: [PATCH 2/3] Update tracker.py --- tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracker.py b/tracker.py index 79a9ad5..89baa6c 100644 --- a/tracker.py +++ b/tracker.py @@ -14,7 +14,7 @@ except ImportError: pyautogui = None -WAKATIME_API_KEY = '1cf98d16-a859-4fcb-8d40-946a04a7f54c' +WAKATIME_API_KEY = '' HEARTBEAT_INTERVAL = 120 # seconds CHECK_INTERVAL = 30 # seconds From 5e54b8ecfd853bb832b9f457c20f5f41eab98ab8 Mon Sep 17 00:00:00 2001 From: dragonruler1000 Date: Fri, 27 Jun 2025 21:29:59 -0500 Subject: [PATCH 3/3] Create Pixelorama-Linux Add the app info for the Linux release of Pixelorama --- apps/Pixelorama-Linux | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/Pixelorama-Linux diff --git a/apps/Pixelorama-Linux b/apps/Pixelorama-Linux new file mode 100644 index 0000000..0102eba --- /dev/null +++ b/apps/Pixelorama-Linux @@ -0,0 +1,7 @@ + { + "process_name": "godot-runner", + "window_keyword": "Pixelorama", + "project": "Pixelroma", + "language": "Pixel-art", + "plugin": "bearnard" + },