Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/Pixelorama-Linux
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"process_name": "godot-runner",
"window_keyword": "Pixelorama",
"project": "Pixelroma",
"language": "Pixel-art",
"plugin": "bearnard"
},
94 changes: 68 additions & 26 deletions tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
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):
Expand All @@ -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:
Expand All @@ -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}")
Expand All @@ -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()
main()