✨ feat(launcher): add Digital Wellbeing plugin with screen time tracking
This commit is contained in:
7 files changed
+1047
-4
No files matched your search
@@ -0,0 +1,764 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from gi.repository import GLib, Gtk, Gdk
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
import utils.icons as icons
|
||||
from utils.icon_resolver import IconResolver
|
||||
from modules.launcher.plugin_base import PluginBase
|
||||
from modules.launcher.result import Result
|
||||
|
||||
|
||||
class ScreenTimeBarWidget(Gtk.Box):
|
||||
"""Custom widget to display screen time statistics like GNOME Digital Wellbeing."""
|
||||
|
||||
def __init__(self, usage_data: Dict[str, float], total_time: float, weekly_data: Dict[str, float] = None):
|
||||
super().__init__(
|
||||
name="screentime-widget",
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
spacing=12
|
||||
)
|
||||
|
||||
# Compact size for summary view only
|
||||
self.set_size_request(540, 150)
|
||||
self.set_vexpand(False)
|
||||
self.set_hexpand(True)
|
||||
|
||||
self.usage_data = usage_data
|
||||
self.total_time = total_time
|
||||
self.weekly_data = weekly_data or {}
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the GNOME-style digital wellbeing UI."""
|
||||
# Main container with minimal padding for launcher
|
||||
main_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
main_container.set_margin_left(12)
|
||||
main_container.set_margin_right(12)
|
||||
main_container.set_margin_top(0)
|
||||
main_container.set_margin_bottom(0)
|
||||
|
||||
# Header section with large time display
|
||||
self.create_header_section(main_container)
|
||||
|
||||
# Summary cards section
|
||||
self.create_summary_cards(main_container)
|
||||
|
||||
self.pack_start(main_container, True, True, 0)
|
||||
self.show_all()
|
||||
|
||||
def create_header_section(self, container):
|
||||
"""Create the main header with large time display."""
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
|
||||
# Main title
|
||||
title_label = Gtk.Label()
|
||||
title_label.set_markup('<span size="medium" weight="bold">Digital Wellbeing</span>')
|
||||
title_label.set_halign(Gtk.Align.CENTER)
|
||||
title_label.get_style_context().add_class("screentime-title")
|
||||
|
||||
# Large time display
|
||||
total_hours = int(self.total_time // 3600)
|
||||
total_minutes = int((self.total_time % 3600) // 60)
|
||||
|
||||
time_display = Gtk.Label()
|
||||
if total_hours > 0:
|
||||
time_text = f"{total_hours}h {total_minutes}m"
|
||||
else:
|
||||
time_text = f"{total_minutes}m"
|
||||
|
||||
time_display.set_markup(f'<span size="xx-large" weight="bold" color="#3584e4">{time_text}</span>')
|
||||
time_display.set_halign(Gtk.Align.CENTER)
|
||||
time_display.get_style_context().add_class("screentime-time-display")
|
||||
|
||||
# Subtitle
|
||||
subtitle = Gtk.Label()
|
||||
subtitle.set_markup('<span color="#666666">Screen time today</span>')
|
||||
subtitle.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
header_box.pack_start(title_label, False, False, 0)
|
||||
header_box.pack_start(time_display, False, False, 0)
|
||||
header_box.pack_start(subtitle, False, False, 0)
|
||||
|
||||
container.pack_start(header_box, False, False, 0)
|
||||
|
||||
def create_summary_cards(self, container):
|
||||
"""Create summary statistics cards."""
|
||||
if not self.usage_data:
|
||||
return
|
||||
|
||||
cards_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
cards_box.set_homogeneous(True)
|
||||
|
||||
# Most used app card
|
||||
most_used_app = max(self.usage_data.items(), key=lambda x: x[1])
|
||||
most_used_hours = int(most_used_app[1] // 3600)
|
||||
most_used_minutes = int((most_used_app[1] % 3600) // 60)
|
||||
most_used_time = f"{most_used_hours}h {most_used_minutes}m" if most_used_hours > 0 else f"{most_used_minutes}m"
|
||||
|
||||
most_used_card = self.create_info_card("Most Used", most_used_app[0], most_used_time)
|
||||
|
||||
# App count card
|
||||
app_count = len([app for app, time in self.usage_data.items() if time >= 60]) # Apps with 1+ minute
|
||||
app_count_card = self.create_info_card("Apps Used", str(app_count), "applications")
|
||||
|
||||
# Average session card (estimate)
|
||||
if app_count > 0:
|
||||
avg_session = self.total_time / app_count / 60 # minutes
|
||||
avg_text = f"{int(avg_session)}m"
|
||||
else:
|
||||
avg_text = "0m"
|
||||
avg_card = self.create_info_card("Avg Session", avg_text, "per app")
|
||||
|
||||
cards_box.pack_start(most_used_card, True, True, 0)
|
||||
cards_box.pack_start(app_count_card, True, True, 0)
|
||||
cards_box.pack_start(avg_card, True, True, 0)
|
||||
|
||||
container.pack_start(cards_box, False, False, 0)
|
||||
|
||||
def create_info_card(self, title, value, subtitle):
|
||||
"""Create an information card widget."""
|
||||
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
card.get_style_context().add_class("screentime-card")
|
||||
card.set_margin_left(8)
|
||||
card.set_margin_right(8)
|
||||
card.set_margin_top(8)
|
||||
card.set_margin_bottom(8)
|
||||
|
||||
title_label = Gtk.Label()
|
||||
title_label.set_markup(f'<span size="small" color="#666666">{title}</span>')
|
||||
title_label.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
value_label = Gtk.Label()
|
||||
value_label.set_markup(f'<span size="large" weight="bold">{value}</span>')
|
||||
value_label.set_halign(Gtk.Align.CENTER)
|
||||
value_label.set_ellipsize(3) # ELLIPSIZE_END
|
||||
value_label.set_max_width_chars(12)
|
||||
|
||||
subtitle_label = Gtk.Label()
|
||||
subtitle_label.set_markup(f'<span size="x-small" color="#888888">{subtitle}</span>')
|
||||
subtitle_label.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
card.pack_start(title_label, False, False, 0)
|
||||
card.pack_start(value_label, False, False, 0)
|
||||
card.pack_start(subtitle_label, False, False, 0)
|
||||
|
||||
return card
|
||||
|
||||
def create_app_usage_section(self, container):
|
||||
"""Create the application usage section with grid layout."""
|
||||
if not self.usage_data:
|
||||
no_data_label = Gtk.Label()
|
||||
no_data_label.set_markup('<span color="#666666">No usage data available</span>')
|
||||
no_data_label.set_halign(Gtk.Align.CENTER)
|
||||
no_data_label.set_margin_top(20)
|
||||
container.pack_start(no_data_label, True, True, 0)
|
||||
return
|
||||
|
||||
# Section title
|
||||
section_title = Gtk.Label()
|
||||
section_title.set_markup('<span weight="bold">App Usage</span>')
|
||||
section_title.set_halign(Gtk.Align.START)
|
||||
section_title.set_margin_top(8)
|
||||
|
||||
# Scrollable area for app grid
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scrolled.set_vexpand(True)
|
||||
scrolled.set_min_content_height(120)
|
||||
|
||||
# Container for app grid
|
||||
self.grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
scrolled.add(self.grid_box)
|
||||
|
||||
container.pack_start(section_title, False, False, 0)
|
||||
container.pack_start(scrolled, True, True, 0)
|
||||
|
||||
self.create_app_grid()
|
||||
|
||||
def create_app_grid(self):
|
||||
"""Create grid layout for app usage display."""
|
||||
if not self.usage_data:
|
||||
return
|
||||
|
||||
# Sort apps by usage time (descending)
|
||||
sorted_apps = sorted(
|
||||
self.usage_data.items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Show top 12 apps in grid format
|
||||
max_apps = 12
|
||||
apps_to_show = [app for app in sorted_apps[:max_apps] if app[1] >= 60] # 1+ minute
|
||||
|
||||
if not apps_to_show:
|
||||
return
|
||||
|
||||
# Create grid with 3 columns
|
||||
cols = 3
|
||||
rows = (len(apps_to_show) + cols - 1) // cols # Ceiling division
|
||||
|
||||
for row in range(rows):
|
||||
row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
row_box.set_homogeneous(True)
|
||||
|
||||
for col in range(cols):
|
||||
app_index = row * cols + col
|
||||
if app_index < len(apps_to_show):
|
||||
app_name, usage_time = apps_to_show[app_index]
|
||||
app_card = self.create_app_grid_card(app_name, usage_time)
|
||||
row_box.pack_start(app_card, True, True, 0)
|
||||
else:
|
||||
# Empty placeholder to maintain grid alignment
|
||||
placeholder = Gtk.Box()
|
||||
row_box.pack_start(placeholder, True, True, 0)
|
||||
|
||||
self.grid_box.pack_start(row_box, False, False, 0)
|
||||
|
||||
def create_app_grid_card(self, app_name: str, usage_time: float) -> Gtk.Box:
|
||||
"""Create a grid card for application usage display."""
|
||||
# Main card container
|
||||
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
card.get_style_context().add_class("screentime-grid-card")
|
||||
card.set_margin_left(1)
|
||||
card.set_margin_right(1)
|
||||
card.set_margin_top(1)
|
||||
card.set_margin_bottom(1)
|
||||
|
||||
# Icon container
|
||||
icon_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
icon_container.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
# Get real application icon (compact size for launcher)
|
||||
icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 32)
|
||||
if not icon_pixbuf:
|
||||
# Fallback to default application icon
|
||||
icon_pixbuf = self.icon_resolver.get_icon_pixbuf("application-x-executable", 32)
|
||||
|
||||
if icon_pixbuf:
|
||||
icon_image = Gtk.Image()
|
||||
icon_image.set_from_pixbuf(icon_pixbuf)
|
||||
icon_image.set_size_request(32, 32)
|
||||
else:
|
||||
# Final fallback to colored box
|
||||
icon_image = Gtk.Box()
|
||||
icon_image.set_size_request(32, 32)
|
||||
icon_image.get_style_context().add_class("screentime-grid-icon")
|
||||
|
||||
icon_container.pack_start(icon_image, False, False, 0)
|
||||
|
||||
# App name
|
||||
display_name = app_name if len(app_name) <= 10 else app_name[:7] + "..."
|
||||
name_label = Gtk.Label()
|
||||
name_label.set_markup(f'<span weight="500" size="x-small">{display_name}</span>')
|
||||
name_label.set_halign(Gtk.Align.CENTER)
|
||||
name_label.set_ellipsize(3) # ELLIPSIZE_END
|
||||
name_label.set_max_width_chars(10)
|
||||
|
||||
# Usage time
|
||||
hours = int(usage_time // 3600)
|
||||
minutes = int((usage_time % 3600) // 60)
|
||||
time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m"
|
||||
|
||||
time_label = Gtk.Label()
|
||||
time_label.set_markup(f'<span size="x-small" weight="500" color="#3584e4">{time_text}</span>')
|
||||
time_label.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
# Pack elements
|
||||
card.pack_start(icon_container, False, False, 0)
|
||||
card.pack_start(name_label, False, False, 0)
|
||||
card.pack_start(time_label, False, False, 0)
|
||||
|
||||
return card
|
||||
|
||||
def create_modern_app_bar(self, app_name: str, usage_time: float, max_time: float) -> Gtk.Box:
|
||||
"""Create a modern GNOME-style application usage bar."""
|
||||
# Main container with rounded corners styling
|
||||
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
container.get_style_context().add_class("screentime-app-item")
|
||||
container.set_margin_left(4)
|
||||
container.set_margin_right(4)
|
||||
container.set_margin_top(4)
|
||||
container.set_margin_bottom(4)
|
||||
|
||||
# Top row with app name and time
|
||||
top_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
|
||||
# App icon and name
|
||||
app_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
|
||||
# Get real application icon
|
||||
icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 32)
|
||||
if not icon_pixbuf:
|
||||
# Fallback to default application icon
|
||||
icon_pixbuf = self.icon_resolver.get_icon_pixbuf("application-x-executable", 32)
|
||||
|
||||
if icon_pixbuf:
|
||||
icon_image = Gtk.Image()
|
||||
icon_image.set_from_pixbuf(icon_pixbuf)
|
||||
icon_image.set_size_request(32, 32)
|
||||
else:
|
||||
# Final fallback to colored box if no icon found
|
||||
icon_image = Gtk.Box()
|
||||
icon_image.set_size_request(32, 32)
|
||||
icon_image.get_style_context().add_class("screentime-app-icon")
|
||||
|
||||
# App name with better typography
|
||||
display_name = app_name if len(app_name) <= 20 else app_name[:17] + "..."
|
||||
name_label = Gtk.Label()
|
||||
name_label.set_markup(f'<span weight="500">{display_name}</span>')
|
||||
name_label.set_halign(Gtk.Align.START)
|
||||
name_label.set_ellipsize(3) # ELLIPSIZE_END
|
||||
|
||||
app_info_box.pack_start(icon_image, False, False, 0)
|
||||
app_info_box.pack_start(name_label, False, False, 0)
|
||||
|
||||
# Time and percentage info
|
||||
time_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
time_info_box.set_halign(Gtk.Align.END)
|
||||
|
||||
# Usage time
|
||||
hours = int(usage_time // 3600)
|
||||
minutes = int((usage_time % 3600) // 60)
|
||||
time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m"
|
||||
|
||||
time_label = Gtk.Label()
|
||||
time_label.set_markup(f'<span weight="500">{time_text}</span>')
|
||||
time_label.set_halign(Gtk.Align.END)
|
||||
|
||||
# Percentage of total time
|
||||
percentage = (usage_time / self.total_time * 100) if self.total_time > 0 else 0
|
||||
percentage_label = Gtk.Label()
|
||||
percentage_label.set_markup(f'<span size="small" color="#666666">{percentage:.0f}%</span>')
|
||||
percentage_label.set_halign(Gtk.Align.END)
|
||||
|
||||
time_info_box.pack_start(time_label, False, False, 0)
|
||||
time_info_box.pack_start(percentage_label, False, False, 0)
|
||||
|
||||
top_row.pack_start(app_info_box, False, False, 0)
|
||||
top_row.pack_start(time_info_box, True, True, 0)
|
||||
|
||||
# Modern progress bar with rounded corners
|
||||
progress_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
progress_container.get_style_context().add_class("screentime-progress-container")
|
||||
|
||||
progress_bar = Gtk.ProgressBar()
|
||||
progress_bar.set_fraction(usage_time / max_time if max_time > 0 else 0)
|
||||
progress_bar.set_show_text(False)
|
||||
progress_bar.set_size_request(-1, 6)
|
||||
progress_bar.get_style_context().add_class("screentime-progress")
|
||||
|
||||
# Color based on usage intensity with modern colors
|
||||
if percentage > 25:
|
||||
progress_bar.get_style_context().add_class("high-usage")
|
||||
elif percentage > 10:
|
||||
progress_bar.get_style_context().add_class("medium-usage")
|
||||
else:
|
||||
progress_bar.get_style_context().add_class("low-usage")
|
||||
|
||||
progress_container.pack_start(progress_bar, True, True, 0)
|
||||
|
||||
container.pack_start(top_row, False, False, 0)
|
||||
container.pack_start(progress_container, False, False, 0)
|
||||
|
||||
return container
|
||||
|
||||
|
||||
class ScreenTimePlugin(PluginBase):
|
||||
"""
|
||||
Screen time tracking plugin that monitors application usage.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.display_name = "Screen Time"
|
||||
self.description = "Track application usage and screen time"
|
||||
|
||||
# Data storage - single JSON file
|
||||
self.data_file = get_relative_path("../../../config/json/screentime.json")
|
||||
|
||||
# Icon resolver for application icons
|
||||
self.icon_resolver = IconResolver()
|
||||
|
||||
# Current tracking state
|
||||
self.current_app = None
|
||||
self.current_start_time = None
|
||||
self.screentime_data = {"sessions": []} # Main data structure
|
||||
|
||||
# Threading for monitoring
|
||||
self.monitor_thread = None
|
||||
self.stop_monitoring = threading.Event()
|
||||
|
||||
# Last activity time for session tracking
|
||||
self.last_activity_time = None
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the screen time plugin."""
|
||||
self.set_triggers(["screentime", "st"])
|
||||
self.load_data()
|
||||
self.start_monitoring()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the screen time plugin."""
|
||||
self.stop_monitoring.set()
|
||||
if self.monitor_thread:
|
||||
self.monitor_thread.join(timeout=2)
|
||||
self.save_data()
|
||||
|
||||
def load_data(self):
|
||||
"""Load screentime data from the JSON file."""
|
||||
try:
|
||||
if os.path.exists(self.data_file):
|
||||
with open(self.data_file, 'r') as f:
|
||||
self.screentime_data = json.load(f)
|
||||
if "sessions" not in self.screentime_data:
|
||||
self.screentime_data["sessions"] = []
|
||||
else:
|
||||
self.screentime_data = {"sessions": []}
|
||||
|
||||
# Initialize today's session if it doesn't exist
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
today_session = self.get_session_for_date(today)
|
||||
if not today_session:
|
||||
self.screentime_data["sessions"].append({
|
||||
"date": today,
|
||||
"active_time": 0,
|
||||
"apps": {}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"ScreenTimePlugin: Error loading data: {e}")
|
||||
self.screentime_data = {"sessions": []}
|
||||
|
||||
def save_data(self):
|
||||
"""Save screentime data to the JSON file."""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
|
||||
|
||||
with open(self.data_file, 'w') as f:
|
||||
json.dump(self.screentime_data, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"ScreenTimePlugin: Error saving data: {e}")
|
||||
|
||||
def get_session_for_date(self, date_str: str) -> Optional[Dict]:
|
||||
"""Get session data for a specific date."""
|
||||
for session in self.screentime_data["sessions"]:
|
||||
if session["date"] == date_str:
|
||||
return session
|
||||
return None
|
||||
|
||||
def get_today_active_time(self) -> float:
|
||||
"""Get today's total active time in seconds."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(today)
|
||||
return session["active_time"] if session else 0
|
||||
|
||||
def update_today_active_time(self, additional_seconds: float):
|
||||
"""Add time to today's active time."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(today)
|
||||
|
||||
if session:
|
||||
session["active_time"] += additional_seconds
|
||||
else:
|
||||
# Create new session for today
|
||||
self.screentime_data["sessions"].append({
|
||||
"date": today,
|
||||
"active_time": additional_seconds,
|
||||
"apps": {}
|
||||
})
|
||||
|
||||
def update_app_usage(self, app_name: str, additional_seconds: float):
|
||||
"""Add time to today's app usage."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(today)
|
||||
|
||||
if session:
|
||||
if "apps" not in session:
|
||||
session["apps"] = {}
|
||||
if app_name not in session["apps"]:
|
||||
session["apps"][app_name] = 0
|
||||
session["apps"][app_name] += additional_seconds
|
||||
else:
|
||||
# Create new session for today
|
||||
self.screentime_data["sessions"].append({
|
||||
"date": today,
|
||||
"active_time": 0,
|
||||
"apps": {app_name: additional_seconds}
|
||||
})
|
||||
|
||||
def get_today_apps(self) -> Dict[str, float]:
|
||||
"""Get today's app usage data."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(today)
|
||||
return session.get("apps", {}) if session else {}
|
||||
|
||||
def get_weekly_total(self) -> float:
|
||||
"""Get total active time for the last 7 days."""
|
||||
today = datetime.now()
|
||||
total_time = 0
|
||||
|
||||
for i in range(7):
|
||||
date = (today - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(date)
|
||||
if session:
|
||||
total_time += session["active_time"]
|
||||
|
||||
return total_time
|
||||
|
||||
def _get_yesterday_total(self) -> float:
|
||||
"""Get yesterday's total active time."""
|
||||
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
session = self.get_session_for_date(yesterday)
|
||||
return session["active_time"] if session else 0
|
||||
|
||||
def start_monitoring(self):
|
||||
"""Start background monitoring of active windows."""
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
return
|
||||
|
||||
self.stop_monitoring.clear()
|
||||
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""Main monitoring loop that tracks active time and app usage."""
|
||||
while not self.stop_monitoring.wait(2.0): # Check every 2 seconds
|
||||
try:
|
||||
current_time = time.time()
|
||||
current_app = self._get_active_window_class()
|
||||
|
||||
# If app changed, record time for previous app
|
||||
if self.current_app and self.current_app != current_app:
|
||||
if self.current_start_time:
|
||||
elapsed = current_time - self.current_start_time
|
||||
# Only count if elapsed time is reasonable (not more than 5 seconds)
|
||||
if elapsed <= 5.0:
|
||||
self.update_today_active_time(elapsed)
|
||||
self.update_app_usage(self.current_app, elapsed)
|
||||
|
||||
# Update current app tracking
|
||||
if current_app:
|
||||
self.current_app = current_app
|
||||
self.current_start_time = current_time
|
||||
else:
|
||||
# No active window, reset tracking
|
||||
self.current_app = None
|
||||
self.current_start_time = None
|
||||
|
||||
# Save data periodically (every 30 seconds)
|
||||
if int(current_time) % 30 == 0:
|
||||
self.save_data()
|
||||
|
||||
except Exception as e:
|
||||
print(f"ScreenTimePlugin: Error in monitor loop: {e}")
|
||||
time.sleep(5) # Wait longer on error
|
||||
|
||||
def _get_active_window_class(self) -> Optional[str]:
|
||||
"""Get the class name of the currently active window."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hyprctl", "-j", "activewindow"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
window_data = json.loads(result.stdout)
|
||||
# Use class name, fallback to title if class is empty
|
||||
app_class = window_data.get("class", "").strip()
|
||||
if not app_class:
|
||||
app_class = window_data.get("title", "Unknown").strip()
|
||||
|
||||
# Filter out empty or system windows
|
||||
if app_class and app_class not in ["", "Unknown", "Desktop"]:
|
||||
return app_class
|
||||
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as e:
|
||||
print(f"ScreenTimePlugin: Error getting active window: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def query(self, query_string: str) -> List[Result]:
|
||||
"""Process screen time queries with GNOME Digital Wellbeing style."""
|
||||
results = []
|
||||
query = query_string.strip().lower()
|
||||
|
||||
# Get today's data
|
||||
today_time = self.get_today_active_time()
|
||||
today_apps = self.get_today_apps()
|
||||
weekly_time = self.get_weekly_total()
|
||||
|
||||
# Main screen time display (compact view)
|
||||
if not query or query in ["today", "stats", "usage", "wellbeing"]:
|
||||
today_hours = int(today_time // 3600)
|
||||
today_minutes = int((today_time % 3600) // 60)
|
||||
|
||||
# Enhanced subtitle with more context
|
||||
if today_time > 0:
|
||||
subtitle = f"Today: {today_hours}h {today_minutes}m"
|
||||
|
||||
# Add comparison with yesterday if available
|
||||
yesterday_total = self._get_yesterday_total()
|
||||
if yesterday_total > 0:
|
||||
if today_time > yesterday_total:
|
||||
diff = today_time - yesterday_total
|
||||
diff_minutes = int(diff // 60)
|
||||
subtitle += f" (+{diff_minutes}m vs yesterday)"
|
||||
elif today_time < yesterday_total:
|
||||
diff = yesterday_total - today_time
|
||||
diff_minutes = int(diff // 60)
|
||||
subtitle += f" (-{diff_minutes}m vs yesterday)"
|
||||
|
||||
# Add weekly average
|
||||
weekly_avg = weekly_time / 7 if weekly_time > 0 else 0
|
||||
if weekly_avg > 0:
|
||||
avg_hours = int(weekly_avg // 3600)
|
||||
avg_minutes = int((weekly_avg % 3600) // 60)
|
||||
avg_text = f"{avg_hours}h {avg_minutes}m" if avg_hours > 0 else f"{avg_minutes}m"
|
||||
subtitle += f" • Weekly avg: {avg_text}/day"
|
||||
else:
|
||||
subtitle = "No screen time recorded today"
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title="Digital Wellbeing",
|
||||
subtitle=subtitle,
|
||||
icon_markup=icons.screentime,
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "screentime_display"}
|
||||
)
|
||||
)
|
||||
|
||||
# App list view (individual result items)
|
||||
elif query in ["list", "apps", "applications"]:
|
||||
if today_apps:
|
||||
# Sort apps by usage time (descending)
|
||||
sorted_apps = sorted(
|
||||
today_apps.items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Show top 12 apps as individual results
|
||||
for i, (app_name, usage_time) in enumerate(sorted_apps[:12]):
|
||||
if usage_time < 60: # Skip apps with less than 1 minute
|
||||
continue
|
||||
|
||||
hours = int(usage_time // 3600)
|
||||
minutes = int((usage_time % 3600) // 60)
|
||||
time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m"
|
||||
|
||||
# Calculate percentage of total time
|
||||
percentage = (usage_time / today_time * 100) if today_time > 0 else 0
|
||||
|
||||
subtitle = f"Used for {time_text} today ({percentage:.0f}%)"
|
||||
|
||||
# Get application icon
|
||||
app_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 48)
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title=app_name,
|
||||
subtitle=subtitle,
|
||||
icon=app_icon_pixbuf,
|
||||
icon_markup=icons.screentime if not app_icon_pixbuf else None,
|
||||
action=lambda: None,
|
||||
relevance=1.0 - (i * 0.1), # Decrease relevance for lower usage
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "app_usage_item", "app": app_name}
|
||||
)
|
||||
)
|
||||
else:
|
||||
results.append(
|
||||
Result(
|
||||
title="No App Usage Data",
|
||||
subtitle="Start using applications to see usage statistics",
|
||||
icon_markup=icons.info,
|
||||
action=lambda: None,
|
||||
relevance=0.8,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "no_data"}
|
||||
)
|
||||
)
|
||||
|
||||
# Weekly summary option
|
||||
elif query in ["week", "weekly"]:
|
||||
weekly_hours = int(weekly_time // 3600)
|
||||
weekly_minutes = int((weekly_time % 3600) // 60)
|
||||
daily_avg = weekly_time / 7
|
||||
avg_hours = int(daily_avg // 3600)
|
||||
avg_minutes = int((daily_avg % 3600) // 60)
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title="Weekly Screen Time",
|
||||
subtitle=f"Total: {weekly_hours}h {weekly_minutes}m • Daily avg: {avg_hours}h {avg_minutes}m",
|
||||
icon_markup=icons.calendar,
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "weekly_stats"}
|
||||
)
|
||||
)
|
||||
|
||||
# Show help if no data
|
||||
if not today_apps and not query:
|
||||
results.append(
|
||||
Result(
|
||||
title="Digital Wellbeing",
|
||||
subtitle="Start using applications to see your screen time statistics",
|
||||
icon_markup=icons.info,
|
||||
action=lambda: None,
|
||||
relevance=0.8,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "help"}
|
||||
)
|
||||
)
|
||||
|
||||
# Quick stats for specific app queries
|
||||
if query and query not in ["today", "stats", "usage", "wellbeing", "week", "weekly"]:
|
||||
matching_apps = [
|
||||
(app, time_spent) for app, time_spent in today_apps.items()
|
||||
if query in app.lower()
|
||||
]
|
||||
|
||||
for app_name, time_spent in matching_apps[:3]:
|
||||
hours = int(time_spent // 3600)
|
||||
minutes = int((time_spent % 3600) // 60)
|
||||
percentage = (time_spent / today_time * 100) if today_time > 0 else 0
|
||||
time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m"
|
||||
|
||||
subtitle = f"Today: {time_text} ({percentage:.0f}%)"
|
||||
|
||||
# Get application icon pixbuf
|
||||
app_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 48)
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title=f"{app_name}",
|
||||
subtitle=subtitle,
|
||||
icon=app_icon_pixbuf,
|
||||
icon_markup=icons.screentime if not app_icon_pixbuf else None,
|
||||
action=lambda: None,
|
||||
relevance=0.9,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "app_stat", "app": app_name}
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
Reference in new issue
Block a user