✨ feat(launcher): add Digital Wellbeing plugin with screen time tracking

This commit is contained in:
sankalp committed 2025-06-24 11:26:24 +05:45
1 parent ffa535bcca
commit c77b45115b
7 files changed
+1047 -4

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ config/json/dock.json
config/json/accounts.json
config/json/passwords.json
config/json/bookmarks.json
config/json/screentime.json
config/hypr/modus.conf
config/hypr/colors.conf
styles/colors.css
-4
View File
@@ -83,10 +83,6 @@
"ps": {
"icon": "process",
"description": "Process Manager - View and manage running processes"
},
"weather": {
"icon": "cloud",
"description": "Weather - Get current weather information"
}
},
"settings": {
+1
View File
@@ -97,6 +97,7 @@ class PluginManager:
"password",
"bookmarks",
"process",
"screentime",
]
for plugin_name in default_plugins:
@@ -0,0 +1,132 @@
# Digital Wellbeing Plugin
The Digital Wellbeing plugin provides comprehensive screen time tracking and statistics, inspired by GNOME 48's Digital Wellbeing feature. It monitors application usage and displays beautiful, modern statistics in the Modus launcher.
## Features
- **🎯 GNOME-style Interface**: Modern, clean design matching GNOME 48's Digital Wellbeing
- **📊 Comprehensive Statistics**: Large time display, summary cards, and grid-based app display
- **⏱️ Real-time Tracking**: Monitors active windows using Hyprland's `hyprctl` commands
- **🎯 Real Application Icons**: Shows actual application icons from system icon theme
- **📱 Grid Layout**: Applications displayed in a clean 3-column grid format
- **📅 Weekly Insights**: Compare daily usage with weekly averages and trends
- **🔍 Smart Search**: Find specific app usage with enhanced details and real icons
- **💾 Persistent Data**: Stores usage data in JSON files for historical tracking
- **🎨 Compact Design**:
- Real application icons (32px) optimized for launcher
- Clean 3-column grid layout (540x250px)
- Compact card design (80x70px) with hover effects
- Usage time display (no percentages for cleaner look)
- Optimized for launcher's 550x260 scroll window
## Usage
### Trigger Keywords
- `screentime` - Show full screen time statistics
- `st` - Short alias for screen time
### Commands
- `screentime` or `st` - Display compact Digital Wellbeing summary
- `st list` - Show detailed app usage grid
- `st apps` - Same as list (alternative command)
- `st week` - Display weekly statistics and trends
- `st <app_name>` - Search for specific application with weekly comparison
### Examples
```
screentime # Compact summary (header + cards only)
st # Short alias for compact view
st list # Full app usage grid
st apps # Alternative for app list
st week # Weekly statistics
st firefox # Firefox usage details
st code # VS Code usage details
```
## Interface Components
### 🏠 Main Dashboard (Default View)
- **Large Time Display**: Prominent display of total screen time today
- **Summary Cards**:
- Most Used App with time spent
- Total Apps Used count
- Average Session Length per app
- **Compact Layout**: Clean summary without app grid (540x150px)
### 📱 App List View (`st list`)
- **Full App Grid**: Top 12 applications displayed in a 3-column grid layout
- **Detailed View**: Shows individual app icons, names, and usage times
- **Expanded Layout**: Larger widget (540x250px) to accommodate grid
### 📊 Statistics Display
- **Real Application Icons**: Displays actual application icons from system theme
- **Usage Comparison**: Compare today vs yesterday with difference indicators
- **Weekly Averages**: Show daily averages for each application
- **Compact Grid Layout**:
- 32px application icons optimized for launcher
- 3-column grid layout (80x70px cards)
- App name and usage time display (no percentages)
- Automatic fallback to default application icon
- Icon caching for improved performance
- **Launcher-Optimized Design**:
- Fits perfectly in launcher's 550x260 scroll window
- Compact card layout with minimal spacing
- Hover effects for better interactivity
- Clean, uncluttered information display
## Data Storage
Usage data is stored in `config/json/screentime/` directory:
- `usage_YYYY-MM-DD.json` - Daily usage data files
- Data includes application class names and total seconds used
- Automatic cleanup of old data (configurable retention period)
- Weekly aggregation for trend analysis
## How It Works
1. **Window Tracking**: Uses `hyprctl -j activewindow` to detect the currently focused application
2. **Time Recording**: Tracks time spent in each application by monitoring window focus changes
3. **Data Persistence**: Saves usage data every 30 seconds and on plugin cleanup
4. **Visualization**: Creates custom GTK widgets with progress bars to show usage statistics
## Technical Details
- **Background Monitoring**: Runs a background thread that checks active window every 2 seconds
- **Application Detection**: Uses window class names from Hyprland for application identification
- **Memory Efficient**: Only stores essential data and cleans up old cache entries
- **Error Handling**: Gracefully handles window manager errors and missing data
## Customization
The plugin can be customized by modifying:
- Update frequency (currently 2 seconds)
- Number of top applications shown (currently 10)
- Minimum usage time threshold (currently 1 minute)
- Bar graph colors and styling in `styles/launcher.css`
## CSS Classes
The plugin uses these CSS classes for modern GNOME-style styling:
- `#screentime-widget` - Main widget container with rounded corners
- `.screentime-title` - Main title styling
- `.screentime-time-display` - Large time display with custom font
- `.screentime-card` - Summary cards with hover effects
- `.screentime-app-item` - Individual app items with modern styling
- `.screentime-app-icon` - App icon placeholders with gradients
- `.screentime-progress` - Modern progress bars with gradients
- `.screentime-progress.high-usage` - High usage bars (red gradient)
- `.screentime-progress.medium-usage` - Medium usage bars (yellow gradient)
- `.screentime-progress.low-usage` - Low usage bars (green gradient)
## Requirements
- Hyprland window manager
- `hyprctl` command available in PATH
- GTK 3.0
- Python 3.7+
## Privacy
- All data is stored locally on your system
- No network connections or external data sharing
- Data files can be manually deleted if desired
@@ -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
+148
View File
@@ -213,3 +213,151 @@
font-size: 12px;
font-weight: 600;
}
/* Screen Time Widget Styling - GNOME Digital Wellbeing Style */
#screentime-widget {
background-color: var(--surface);
border-radius: 20px;
padding: 0;
margin: 8px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Title styling */
.screentime-title {
color: var(--foreground);
margin-bottom: 8px;
}
/* Large time display */
.screentime-time-display {
margin: 12px 0;
font-family: "SF Pro Display", system-ui, sans-serif;
font-weight: 300;
}
/* Summary cards */
.screentime-card {
background-color: var(--surface-bright);
border-radius: 12px;
padding: 16px 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
}
.screentime-card:hover {
background-color: var(--surface-container);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* App item styling */
.screentime-app-item {
background-color: var(--surface-bright);
border-radius: 12px;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
}
.screentime-app-item:hover {
background-color: var(--surface-container);
}
/* Grid card styling - compact for launcher */
.screentime-grid-card {
background-color: var(--surface-bright);
border-radius: 8px;
padding: 6px 4px;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
min-width: 80px;
min-height: 70px;
}
.screentime-grid-card:hover {
background-color: var(--surface-container);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Grid icon styling */
.screentime-grid-icon {
background: linear-gradient(135deg, var(--primary), var(--primary-bright));
border-radius: 8px;
min-width: 32px;
min-height: 32px;
}
/* App icon placeholder */
.screentime-app-icon {
background: linear-gradient(135deg, var(--primary), var(--primary-bright));
border-radius: 8px;
min-width: 32px;
min-height: 32px;
}
/* Progress bar container */
.screentime-progress-container {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 3px;
padding: 0;
}
/* Modern progress bars */
.screentime-progress {
border-radius: 3px;
background-color: transparent;
}
.screentime-progress trough {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 3px;
min-height: 6px;
}
.screentime-progress progress {
border-radius: 3px;
min-height: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Modern usage intensity colors */
.screentime-progress.high-usage progress {
background: linear-gradient(90deg, #ff6b6b, #ee5a52);
}
.screentime-progress.medium-usage progress {
background: linear-gradient(90deg, #ffd93d, #ffb700);
}
.screentime-progress.low-usage progress {
background: linear-gradient(90deg, #51cf66, #37b24d);
}
/* Scrolled window styling */
#screentime-widget scrolledwindow {
border-radius: 12px;
background-color: transparent;
border: none;
}
#screentime-widget scrolledwindow scrollbar {
background-color: transparent;
border-radius: 6px;
margin: 2px;
}
#screentime-widget scrolledwindow scrollbar slider {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 6px;
min-width: 6px;
transition: all 0.2s ease;
}
#screentime-widget scrolledwindow scrollbar slider:hover {
background-color: rgba(255, 255, 255, 0.3);
}
/* Labels */
#screentime-widget label {
color: var(--foreground);
}
+1
View File
@@ -186,6 +186,7 @@ auth: str = "&#xeca0;"
bookmark: str = "&#xea3a;"
star: str = "&#xeb2e;"
process: str = "&#xf292;"
screentime: str = "&#xf1c0;"
# otp