From 96434bd0365f4a2086d2f886059609c476d1ec0c Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 26 Jun 2025 16:10:17 +0545 Subject: [PATCH] updates --- config/__init__.py | 1 + config/assets/accounts.json | 9 + config/assets/bookmarks.json | 17 ++ config/assets/config.json | 65 +++++ config/{json => assets}/emoji.json | 0 config/{json => assets}/launcher.json | 0 config/assets/passwords.json | 11 + config/data.py | 8 +- config/settings/gui.py | 2 +- config/settings/utils.py | 6 +- main.py | 2 - modules/corners.py | 1 + modules/dock/components/__init__.py | 36 +-- modules/dock/components/applications.py | 9 +- modules/dock/components/battery.py | 5 +- modules/dock/components/controls.py | 5 +- modules/dock/components/indicators.py | 23 +- modules/dock/components/metrics.py | 5 +- modules/dock/components/music_player.py | 5 +- modules/dock/components/workspaces.py | 11 +- modules/launcher/plugins/README_screentime.md | 132 ---------- modules/launcher/plugins/applications.py | 3 +- modules/launcher/plugins/bluetooth.py | 3 +- modules/launcher/plugins/bookmarks.py | 100 +++++--- modules/launcher/plugins/caffeine.py | 5 +- modules/launcher/plugins/calendar.py | 3 +- modules/launcher/plugins/clipboard.py | 1 + modules/launcher/plugins/emoji.py | 5 +- modules/launcher/plugins/kanban.py | 3 +- modules/launcher/plugins/network.py | 3 +- modules/launcher/plugins/notifications.py | 3 +- modules/launcher/plugins/otp.py | 5 +- modules/launcher/plugins/password.py | 24 +- modules/launcher/plugins/power.py | 3 +- modules/launcher/plugins/process.py | 240 +++++++++++------- modules/launcher/plugins/screencapture.py | 1 + modules/launcher/plugins/system.py | 3 +- modules/launcher/plugins/wallpaper.py | 45 ++-- modules/launcher/plugins/websearch.py | 6 +- modules/launcher/trigger_config.py | 5 +- modules/notification_popup.py | 3 +- modules/osd.py | 3 +- modules/switcher.py | 3 +- services/battery.py | 26 +- services/brightness.py | 2 +- services/mpris.py | 11 +- services/network.py | 160 +++++++----- services/notification.py | 21 +- utils/animator.py | 1 - utils/audio_visualizer.py | 32 ++- utils/conversion.py | 97 ++++--- utils/custom_image.py | 3 +- utils/inhibit.py | 42 +-- utils/occlusion.py | 47 ++-- utils/wayland.py | 2 +- 55 files changed, 724 insertions(+), 543 deletions(-) create mode 100644 config/assets/accounts.json create mode 100644 config/assets/bookmarks.json create mode 100644 config/assets/config.json rename config/{json => assets}/emoji.json (100%) rename config/{json => assets}/launcher.json (100%) create mode 100644 config/assets/passwords.json delete mode 100644 modules/launcher/plugins/README_screentime.md diff --git a/config/__init__.py b/config/__init__.py index 4cde1a6..9a75817 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,6 +1,7 @@ """ Ax-Shell configuration package. """ + # Import only specific names actually defined in data.py # This prevents circular imports by not importing everything from .data import APP_NAME, APP_NAME_CAP, CACHE_DIR, CONFIG_FILE diff --git a/config/assets/accounts.json b/config/assets/accounts.json new file mode 100644 index 0000000..635df3d --- /dev/null +++ b/config/assets/accounts.json @@ -0,0 +1,9 @@ +{ + "test": { + "secret": "IVLCHLVRZ37QOPYWBMOSDHQY75YR54J4", + "issuer": "", + "algorithm": "SHA1", + "digits": 6, + "period": 30 + } +} \ No newline at end of file diff --git a/config/assets/bookmarks.json b/config/assets/bookmarks.json new file mode 100644 index 0000000..69bed60 --- /dev/null +++ b/config/assets/bookmarks.json @@ -0,0 +1,17 @@ +{ + "bookmarks": [ + { + "title": "GitHub", + "url": "https://github.com", + "description": "Code hosting platform", + "tags": [ + "code", + "git", + "development" + ], + "created": 1750669725.8057194, + "accessed": 0 + } + ], + "last_updated": 1750671530.1859832 +} \ No newline at end of file diff --git a/config/assets/config.json b/config/assets/config.json new file mode 100644 index 0000000..6cedc8e --- /dev/null +++ b/config/assets/config.json @@ -0,0 +1,65 @@ +{ + "prefix_restart": "ALT SHIFT", + "suffix_restart": "R", + "prefix_msg": "SUPER", + "suffix_msg": "A", + "prefix_application_switcher": "ALT", + "suffix_application_switcher": "TAB", + "prefix_launcher": "SUPER", + "suffix_launcher": "SPACE", + "prefix_app_launcher": "SUPER", + "suffix_app_launcher": "D", + "prefix_cliphist": "SUPER", + "suffix_cliphist": "V", + "prefix_wallpapers": "SUPER", + "suffix_wallpapers": "W", + "prefix_randwall": "ALT SHIFT", + "suffix_randwall": "W", + "prefix_emoji": "SUPER", + "suffix_emoji": "E", + "prefix_kanban": "SUPER", + "suffix_kanban": "T", + "prefix_power": "SUPER", + "suffix_power": "ESCAPE", + "prefix_caffeine": "SUPER SHIFT", + "suffix_caffeine": "M", + "prefix_settings": "SUPER", + "suffix_settings": "I", + "prefix_restart_inspector": "SUPER CTRL ALT", + "suffix_restart_inspector": "B", + "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", + "dock_position": "Bottom", + "vertical": false, + "dock_enabled": true, + "dock_auto_hide": true, + "dock_always_occluded": false, + "dock_hide_special_workspace": true, + "dock_icon_size": 20, + "window_switcher_items_per_row": 13, + "terminal_command": "kitty -e", + "corners_visible": true, + "dock_theme": "Pills", + "notif_pos": "Top", + "dock_workspace_visible": true, + "dock_metrics_visible": true, + "dock_date_time_visible": true, + "dock_battery_visible": true, + "dock_controls_visible": true, + "dock_indicators_visible": true, + "dock_applications_visible": true, + "dock_language_visible": true, + "dock_music_player_visible": true, + "workspace_dots": false, + "workspace_nums": false, + "workspace_use_chinese_numerals": false, + "metrics_visible": { + "cpu": true, + "ram": true, + "swap": true, + "disk": false, + "gpu": false + }, + "metrics_disks": [ + "/" + ] +} \ No newline at end of file diff --git a/config/json/emoji.json b/config/assets/emoji.json similarity index 100% rename from config/json/emoji.json rename to config/assets/emoji.json diff --git a/config/json/launcher.json b/config/assets/launcher.json similarity index 100% rename from config/json/launcher.json rename to config/assets/launcher.json diff --git a/config/assets/passwords.json b/config/assets/passwords.json new file mode 100644 index 0000000..caa9b31 --- /dev/null +++ b/config/assets/passwords.json @@ -0,0 +1,11 @@ +{ + "passwords": { + "git": { + "password": "BQoIGR8w", + "description": "", + "created": "2025-06-22T22:24:00.715166", + "last_accessed": "2025-06-22T22:31:47.987267" + } + }, + "last_modified": "2025-06-23T13:27:37.342660" +} \ No newline at end of file diff --git a/config/data.py b/config/data.py index 72db041..e6a1961 100644 --- a/config/data.py +++ b/config/data.py @@ -28,7 +28,7 @@ CURRENT_HEIGHT = screen.get_height() WALLPAPERS_DIR_DEFAULT = get_relative_path("../assets/wallpapers_example") -CONFIG_FILE = get_relative_path("../config/json/config.json") +CONFIG_FILE = get_relative_path("../config/assets/config.json") MATUGEN_STATE_FILE = os.path.join(CONFIG_DIR, "matugen") WORKSPACE_NUMS = False @@ -40,7 +40,7 @@ DOCK_THEME = "Pills" def load_config(): """Load the configuration from config.json""" - config_path = os.path.expanduser(f"~/{APP_NAME_CAP}/config/json/config.json") + config_path = os.path.expanduser(f"~/{APP_NAME_CAP}/config/assets/config.json") config = {} if os.path.exists(config_path): @@ -69,7 +69,7 @@ if os.path.exists(CONFIG_FILE): WORKSPACE_USE_CHINESE_NUMERAL = config.get("workspace_use_chinese_numerals", False) WORKSPACE_DOTS = config.get("workspace_dots", False) WINDOW_SWITCHER_ITEMS_PER_ROW = config.get("window_switcher_items_per_row", 13) - DOCK_HIDE_SPECIAL_WORKSPACE = config.get('dock_hide_special_workspace', True) + DOCK_HIDE_SPECIAL_WORKSPACE = config.get("dock_hide_special_workspace", True) NOTIF_POS = config.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) @@ -102,7 +102,7 @@ else: DOCK_ICON_SIZE = 20 WINDOW_SWITCHER_ITEMS_PER_ROW = 13 NOTIF_POS = NOTIF_POS_DEFAULT - DOCK_HIDE_SPECIAL_WORKSPACE = True + DOCK_HIDE_SPECIAL_WORKSPACE = True DOCK_COMPONENTS_VISIBILITY = { "workspaces": True, diff --git a/config/settings/gui.py b/config/settings/gui.py index 05fab19..779e56d 100644 --- a/config/settings/gui.py +++ b/config/settings/gui.py @@ -930,7 +930,7 @@ class HyprConfGUI(Window): print(f"{start_time:.4f}: Background task started.") config_json = os.path.expanduser( - f"~/.config/{APP_NAME_CAP}/config/json/config.json" + f"~/.config/{APP_NAME_CAP}/config/assets/config.json" ) os.makedirs(os.path.dirname(config_json), exist_ok=True) try: diff --git a/config/settings/utils.py b/config/settings/utils.py index ae53239..95acd14 100644 --- a/config/settings/utils.py +++ b/config/settings/utils.py @@ -194,7 +194,7 @@ def load_bind_vars(): bind_vars.update(settings_constants.DEFAULTS.copy()) config_json = os.path.expanduser( - f"~/.config/{APP_NAME_CAP}/config/json/config.json" + f"~/.config/{APP_NAME_CAP}/config/assets/config.json" ) if os.path.exists(config_json): try: @@ -245,8 +245,8 @@ def generate_hyprconf() -> str: """ home = os.path.expanduser("~") # Determine animation type based on bar position - bar_position = bind_vars.get("bar_position", "Top") - is_vertical = bar_position in ["Left", "Right"] + dock_position = bind_vars.get("dock_position", "Bottom") + is_vertical = dock_position in ["Left", "Right"] animation_type = "slidefadevert" if is_vertical else "slidefade" return f"""exec-once = uwsm-app $(python {home}/.config/{APP_NAME_CAP}/main.py) diff --git a/main.py b/main.py index 3b57c89..7b0ca69 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,5 @@ import os -import gi import setproctitle from fabric import Application from fabric.utils import exec_shell_command_async, get_relative_path, monitor_file @@ -14,7 +13,6 @@ from modules.notification_popup import NotificationPopup from modules.osd import OSD from modules.switcher import ApplicationSwitcher -gi.require_version("GLib", "2.0") for log in [ "fabric.hyprland.widgets", diff --git a/modules/corners.py b/modules/corners.py index 4873b34..4119ff7 100644 --- a/modules/corners.py +++ b/modules/corners.py @@ -1,5 +1,6 @@ from fabric.widgets.box import Box from fabric.widgets.shapes import Corner + from utils.wayland import WaylandWindow as Window diff --git a/modules/dock/components/__init__.py b/modules/dock/components/__init__.py index 9e03f6d..2aefa74 100644 --- a/modules/dock/components/__init__.py +++ b/modules/dock/components/__init__.py @@ -1,21 +1,25 @@ -import os import json -import config.data as data +import os + +from fabric.hyprland.service import HyprlandEvent +from fabric.hyprland.widgets import Language, get_hyprland_connection +from fabric.system_tray.widgets import SystemTray +from fabric.utils import get_relative_path from fabric.widgets.box import Box +from fabric.widgets.button import Button from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.system_tray.widgets import SystemTray -from .battery import Battery -from .metrics import Metrics -from .controls import Controls -from .workspaces import workspace -from .indicators import Indicators -from .applications import Applications -from .music_player import MusicPlayer -from fabric.hyprland.widgets import get_hyprland_connection, Language + +import config.data as data import utils.icons as icons -from fabric.hyprland.service import HyprlandEvent + +from .applications import Applications +from .battery import Battery +from .controls import Controls +from .indicators import Indicators +from .metrics import Metrics +from .music_player import MusicPlayer +from .workspaces import workspace class DockComponents(Box): @@ -114,7 +118,7 @@ class DockComponents(Box): # Special handling for applications component - let it manage its own visibility if component_name == "applications": # Only apply config visibility if the component has content - if hasattr(widget, '_update_visibility_based_on_content'): + if hasattr(widget, "_update_visibility_based_on_content"): widget._update_visibility_based_on_content() else: widget.set_visible(self.component_visibility[component_name]) @@ -143,9 +147,7 @@ class DockComponents(Box): self.component_visibility[component_name] ) - config_file = os.path.expanduser( - f"~/.config/{data.APP_NAME}/config/config.json" - ) + config_file = get_relative_path("../../../config/assets/config.json") if os.path.exists(config_file): try: with open(config_file, "r") as f: diff --git a/modules/dock/components/applications.py b/modules/dock/components/applications.py index 8430b86..fcf9c75 100644 --- a/modules/dock/components/applications.py +++ b/modules/dock/components/applications.py @@ -2,7 +2,6 @@ import json import logging import cairo -import config.data as data from fabric.hyprland.widgets import get_hyprland_connection from fabric.utils import exec_shell_command, exec_shell_command_async, get_relative_path from fabric.utils.helpers import get_desktop_applications @@ -10,11 +9,13 @@ from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.image import Image from gi.repository import Gdk, GLib, Gtk + +import config.data as data from utils.icon_resolver import IconResolver def read_config(): - config_path = get_relative_path("../../../config/json/dock.json") + config_path = get_relative_path("../../../config/assets/dock.json") try: with open(config_path, "r") as file: config_data = json.load(file) @@ -78,7 +79,7 @@ class Applications(Box): self.conn = get_hyprland_connection() self.icon_resolver = IconResolver() self.pinned = self.config.get("pinned_apps", []) - self.config_path = get_relative_path("../../../config/json/dock.json") + self.config_path = get_relative_path("../../../config/assets/dock.json") self.app_map = {} self._all_apps = get_desktop_applications() self.app_identifiers = self._build_app_identifiers_map() @@ -245,7 +246,7 @@ class Applications(Box): ) def update_pinned_apps_file(self): - config_path = get_relative_path("../../../config/json/dock.json") + config_path = get_relative_path("../../../config/assets/dock.json") try: with open(config_path, "w") as file: json.dump(self.config, file, indent=4) diff --git a/modules/dock/components/battery.py b/modules/dock/components/battery.py index dbfd132..879a3f0 100644 --- a/modules/dock/components/battery.py +++ b/modules/dock/components/battery.py @@ -1,10 +1,11 @@ -import config.data as data -import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.label import Label from gi.repository import Gdk, GLib + +import config.data as data +import utils.icons as icons from services.battery import Battery as BatteryService diff --git a/modules/dock/components/controls.py b/modules/dock/components/controls.py index 1d2e257..53f0db2 100644 --- a/modules/dock/components/controls.py +++ b/modules/dock/components/controls.py @@ -1,5 +1,3 @@ -import config.data as data -import utils.icons as icons from fabric.audio.service import Audio from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -8,6 +6,9 @@ from fabric.widgets.eventbox import EventBox from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay from gi.repository import Gdk, GLib + +import config.data as data +import utils.icons as icons from services.brightness import Brightness as BrightnessService diff --git a/modules/dock/components/indicators.py b/modules/dock/components/indicators.py index b8ecad7..4456a3b 100644 --- a/modules/dock/components/indicators.py +++ b/modules/dock/components/indicators.py @@ -1,14 +1,16 @@ import subprocess -import config.data as data -import utils.icons as icons + from fabric.bluetooth import BluetoothClient +from fabric.utils import get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay -from fabric.utils import get_relative_path from gi.repository import GLib + +import config.data as data +import utils.icons as icons from services.network import NetworkClient @@ -188,16 +190,15 @@ class RecordingIndicator(Button): # Set up periodic checking for recording status self.check_recording_status() - self.timeout_id = GLib.timeout_add(1000, self.check_recording_status) # Check every second + self.timeout_id = GLib.timeout_add( + 1000, self.check_recording_status + ) # Check every second def check_recording_status(self): """Check if recording is currently active and update visibility.""" try: result = subprocess.run( - [self.script_path, "status"], - capture_output=True, - text=True, - timeout=2 + [self.script_path, "status"], capture_output=True, text=True, timeout=2 ) is_recording = result.stdout.strip() == "true" @@ -209,7 +210,7 @@ class RecordingIndicator(Button): if self.get_visible(): self.hide() - except Exception as e: + except Exception: # If we can't check status, hide the indicator if self.get_visible(): self.hide() @@ -226,7 +227,7 @@ class RecordingIndicator(Button): def cleanup(self): """Clean up the timeout when the indicator is destroyed.""" - if hasattr(self, 'timeout_id'): + if hasattr(self, "timeout_id"): GLib.source_remove(self.timeout_id) @@ -245,5 +246,5 @@ class Indicators(Box): def cleanup(self): """Clean up all indicators when the dock is destroyed.""" - if hasattr(self, 'recording_indicator'): + if hasattr(self, "recording_indicator"): self.recording_indicator.cleanup() diff --git a/modules/dock/components/metrics.py b/modules/dock/components/metrics.py index 04abce0..fa62818 100644 --- a/modules/dock/components/metrics.py +++ b/modules/dock/components/metrics.py @@ -1,9 +1,7 @@ import json import subprocess -import config.data as data import psutil -import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar @@ -11,6 +9,9 @@ from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from gi.repository import GLib +import config.data as data +import utils.icons as icons + class MetricsProvider: def __init__(self): diff --git a/modules/dock/components/music_player.py b/modules/dock/components/music_player.py index 121bd12..7b96689 100644 --- a/modules/dock/components/music_player.py +++ b/modules/dock/components/music_player.py @@ -2,9 +2,7 @@ import os import tempfile import urllib.parse -import config.data as data import requests -import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox @@ -12,6 +10,9 @@ from fabric.widgets.eventbox import EventBox from fabric.widgets.label import Label from fabric.widgets.stack import Stack from gi.repository import Gdk, GdkPixbuf, GLib + +import config.data as data +import utils.icons as icons from services.mpris import MediaManager from utils.audio_visualizer import SpectrumRender from utils.circle_image import CircleImage diff --git a/modules/dock/components/workspaces.py b/modules/dock/components/workspaces.py index bf066eb..408a1bd 100644 --- a/modules/dock/components/workspaces.py +++ b/modules/dock/components/workspaces.py @@ -1,12 +1,13 @@ import json import subprocess -import config.data as data from fabric.hyprland.service import Hyprland from fabric.hyprland.widgets import WorkspaceButton, Workspaces from fabric.widgets.box import Box from fabric.widgets.button import Button +import config.data as data + CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"] @@ -102,8 +103,8 @@ workspaces = Workspaces( for i in range(1, 11) ], buttons_factory=None - if data.DOCK_HIDE_SPECIAL_WORKSPACE - else Workspaces.default_buttons_factory, + if data.DOCK_HIDE_SPECIAL_WORKSPACE + else Workspaces.default_buttons_factory, ) workspaces_num = Workspaces( @@ -127,8 +128,8 @@ workspaces_num = Workspaces( for i in range(1, 11) ], buttons_factory=None - if data.DOCK_HIDE_SPECIAL_WORKSPACE - else Workspaces.default_buttons_factory, + if data.DOCK_HIDE_SPECIAL_WORKSPACE + else Workspaces.default_buttons_factory, ) diff --git a/modules/launcher/plugins/README_screentime.md b/modules/launcher/plugins/README_screentime.md deleted file mode 100644 index 190e92b..0000000 --- a/modules/launcher/plugins/README_screentime.md +++ /dev/null @@ -1,132 +0,0 @@ -# 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 ` - 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 diff --git a/modules/launcher/plugins/applications.py b/modules/launcher/plugins/applications.py index 0d05d70..57deb52 100644 --- a/modules/launcher/plugins/applications.py +++ b/modules/launcher/plugins/applications.py @@ -4,6 +4,7 @@ from typing import List from fabric.utils import DesktopApp from fabric.utils.helpers import get_desktop_applications, get_relative_path + from modules.dock.main import Dock from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -46,7 +47,7 @@ class ApplicationsPlugin(PluginBase): if v is not None } - config_path = get_relative_path("../../../config/json/dock.json") + config_path = get_relative_path("../../../config/assets/dock.json") try: with open(config_path, "r") as file: data = json.load(file) diff --git a/modules/launcher/plugins/bluetooth.py b/modules/launcher/plugins/bluetooth.py index 129f1c3..c6989aa 100644 --- a/modules/launcher/plugins/bluetooth.py +++ b/modules/launcher/plugins/bluetooth.py @@ -1,7 +1,8 @@ from typing import List -import utils.icons as icons from fabric.bluetooth import BluetoothClient, BluetoothDevice + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/bookmarks.py b/modules/launcher/plugins/bookmarks.py index 2bb3064..424a80e 100644 --- a/modules/launcher/plugins/bookmarks.py +++ b/modules/launcher/plugins/bookmarks.py @@ -1,5 +1,4 @@ import json -import os import subprocess import threading import time @@ -7,8 +6,8 @@ from pathlib import Path from typing import Dict, List, Optional from urllib.parse import urlparse -from thefuzz import fuzz from fabric.utils.helpers import get_relative_path +from thefuzz import fuzz import utils.icons as icons from modules.launcher.plugin_base import PluginBase @@ -62,7 +61,9 @@ class BookmarkManager: current_time = time.time() # Check if cache is still valid - if (current_time - self.last_loaded) < self.cache_ttl and self.last_loaded > 0: + if ( + current_time - self.last_loaded + ) < self.cache_ttl and self.last_loaded > 0: return try: @@ -84,8 +85,6 @@ class BookmarkManager: self._load_bookmarks() return self.bookmarks - - def _save_bookmarks_unlocked(self): """Save bookmarks to JSON file without acquiring lock.""" try: @@ -107,7 +106,9 @@ class BookmarkManager: with self.cache_lock: self._save_bookmarks_unlocked() - def add_bookmark(self, title: str, url: str, description: str = "", tags: List[str] = None) -> bool: + def add_bookmark( + self, title: str, url: str, description: str = "", tags: List[str] = None + ) -> bool: """Add a new bookmark.""" try: url = self._normalize_url(url) @@ -123,7 +124,7 @@ class BookmarkManager: "description": description.strip(), "tags": tags or [], "created": time.time(), - "accessed": 0 + "accessed": 0, } self.bookmarks.append(new_bookmark) @@ -144,10 +145,11 @@ class BookmarkManager: identifier = identifier.lower().strip() for i, bookmark in enumerate(self.bookmarks): - if (bookmark["title"].lower() == identifier or - bookmark["url"].lower() == identifier or - self._extract_domain(bookmark["url"]).lower() == identifier): - + if ( + bookmark["title"].lower() == identifier + or bookmark["url"].lower() == identifier + or self._extract_domain(bookmark["url"]).lower() == identifier + ): self.bookmarks.pop(i) self._save_bookmarks() @@ -190,7 +192,9 @@ class BookmarksPlugin(PluginBase): self.description = "Manage and search your personal bookmarks" # Initialize bookmark manager with storage file - self.bookmark_file = Path(get_relative_path("../../../config/json/bookmarks.json")) + self.bookmark_file = Path( + get_relative_path("../../../config/assets/bookmarks.json") + ) self.bookmark_manager = BookmarkManager(self.bookmark_file) self.max_results = 15 @@ -223,7 +227,8 @@ class BookmarksPlugin(PluginBase): if ( not query_key.startswith(("add ", "remove ", "delete ", "rm ")) and query_key in self._results_cache - and (current_time - self._cache_timestamps.get(query_key, 0)) < self._cache_ttl + and (current_time - self._cache_timestamps.get(query_key, 0)) + < self._cache_ttl ): return self._results_cache[query_key] @@ -267,7 +272,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "info", "keep_launcher_open": True} + data={"type": "info", "keep_launcher_open": True}, ) ] @@ -282,7 +287,7 @@ class BookmarksPlugin(PluginBase): # Sort by relevance and limit results results.sort(key=lambda r: r.relevance, reverse=True) - return results[:self.max_results] + return results[: self.max_results] def _get_recent_bookmarks(self) -> List[Result]: """Get recent/popular bookmarks when no query is provided.""" @@ -297,7 +302,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "help", "keep_launcher_open": True} + data={"type": "help", "keep_launcher_open": True}, ), Result( title="Example: Add Google", @@ -306,12 +311,14 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=0.9, plugin_name=self.display_name, - data={"type": "example", "keep_launcher_open": True} - ) + data={"type": "example", "keep_launcher_open": True}, + ), ] # Sort by access time (most recent first) and show top 10 - sorted_bookmarks = sorted(bookmarks, key=lambda b: b.get("accessed", 0), reverse=True) + sorted_bookmarks = sorted( + bookmarks, key=lambda b: b.get("accessed", 0), reverse=True + ) results = [] for bookmark in sorted_bookmarks[:10]: result = self._create_bookmark_result(bookmark, 0.8) @@ -331,7 +338,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "help", "keep_launcher_open": True} + data={"type": "help", "keep_launcher_open": True}, ), Result( title="Example", @@ -340,8 +347,8 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=0.9, plugin_name=self.display_name, - data={"type": "example", "keep_launcher_open": True} - ) + data={"type": "example", "keep_launcher_open": True}, + ), ] # Parse arguments: title url [description] @@ -355,7 +362,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "error", "keep_launcher_open": True} + data={"type": "error", "keep_launcher_open": True}, ) ] @@ -366,7 +373,9 @@ class BookmarksPlugin(PluginBase): # Check if bookmark already exists normalized_url = self.bookmark_manager._normalize_url(url) existing_bookmarks = self.bookmark_manager.get_bookmarks() - already_exists = any(bookmark["url"] == normalized_url for bookmark in existing_bookmarks) + already_exists = any( + bookmark["url"] == normalized_url for bookmark in existing_bookmarks + ) if already_exists: return [ @@ -377,7 +386,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "error", "keep_launcher_open": True} + data={"type": "error", "keep_launcher_open": True}, ) ] @@ -395,7 +404,7 @@ class BookmarksPlugin(PluginBase): action=lambda: self._add_bookmark_action(title, url, description), relevance=1.0, plugin_name=self.display_name, - data={"type": "add", "name": title, "keep_launcher_open": True} + data={"type": "add", "name": title, "keep_launcher_open": True}, ) ] @@ -410,9 +419,12 @@ class BookmarksPlugin(PluginBase): matching_bookmarks = [] for bookmark in bookmarks: - if (bookmark["title"].lower() == identifier_lower or - bookmark["url"].lower() == identifier_lower or - self.bookmark_manager._extract_domain(bookmark["url"]).lower() == identifier_lower): + if ( + bookmark["title"].lower() == identifier_lower + or bookmark["url"].lower() == identifier_lower + or self.bookmark_manager._extract_domain(bookmark["url"]).lower() + == identifier_lower + ): matching_bookmarks.append(bookmark) if not matching_bookmarks: @@ -424,7 +436,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "error", "keep_launcher_open": True} + data={"type": "error", "keep_launcher_open": True}, ) ] @@ -441,7 +453,7 @@ class BookmarksPlugin(PluginBase): action=lambda: self._remove_bookmark_action(identifier), relevance=1.0, plugin_name=self.display_name, - data={"type": "remove", "name": title, "keep_launcher_open": True} + data={"type": "remove", "name": title, "keep_launcher_open": True}, ) ] @@ -456,7 +468,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=1.0, plugin_name=self.display_name, - data={"type": "help", "keep_launcher_open": True} + data={"type": "help", "keep_launcher_open": True}, ) ] @@ -470,7 +482,7 @@ class BookmarksPlugin(PluginBase): action=lambda: None, relevance=0.9, plugin_name=self.display_name, - data={"type": "info", "keep_launcher_open": True} + data={"type": "info", "keep_launcher_open": True}, ) ) @@ -486,7 +498,11 @@ class BookmarksPlugin(PluginBase): action=lambda t=title: self._remove_bookmark_action(t), relevance=0.8, plugin_name=self.display_name, - data={"type": "remove_option", "bookmark": bookmark, "keep_launcher_open": True} + data={ + "type": "remove_option", + "bookmark": bookmark, + "keep_launcher_open": True, + }, ) ) @@ -567,7 +583,9 @@ class BookmarksPlugin(PluginBase): return 0.0 - def _create_bookmark_result(self, bookmark: Dict, relevance: float) -> Optional[Result]: + def _create_bookmark_result( + self, bookmark: Dict, relevance: float + ) -> Optional[Result]: """Create a Result object for a bookmark.""" try: title = bookmark.get("title", "Untitled") @@ -598,8 +616,8 @@ class BookmarksPlugin(PluginBase): "domain": domain, "description": description, "keep_launcher_open": False, - "alt_action": lambda t=title: self._remove_bookmark_with_reset(t) - } + "alt_action": lambda t=title: self._remove_bookmark_with_reset(t), + }, ) except Exception as e: print(f"Error creating bookmark result: {e}") @@ -625,9 +643,7 @@ class BookmarksPlugin(PluginBase): # Open URL subprocess.Popen( - ["xdg-open", url], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception as e: print(f"Failed to open bookmark: {e}") @@ -659,7 +675,9 @@ class BookmarksPlugin(PluginBase): def _reset_to_trigger(self): """Reset launcher to trigger word and refresh.""" try: - if self._launcher_instance and hasattr(self._launcher_instance, "search_entry"): + if self._launcher_instance and hasattr( + self._launcher_instance, "search_entry" + ): # Get the current trigger (bookmark or bm) current_text = self._launcher_instance.search_entry.get_text() trigger = "bookmark " diff --git a/modules/launcher/plugins/caffeine.py b/modules/launcher/plugins/caffeine.py index ec1cc5a..433d0c1 100644 --- a/modules/launcher/plugins/caffeine.py +++ b/modules/launcher/plugins/caffeine.py @@ -2,11 +2,12 @@ import subprocess from threading import Timer from typing import List -import config.data as data import gi -import utils.icons as icons from fabric.utils import get_relative_path from fabric.utils.helpers import exec_shell_command_async + +import config.data as data +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/calendar.py b/modules/launcher/plugins/calendar.py index 1bfd27f..5d75ae5 100644 --- a/modules/launcher/plugins/calendar.py +++ b/modules/launcher/plugins/calendar.py @@ -3,9 +3,10 @@ import datetime from typing import List import gi -import utils.icons as icons from fabric.widgets.label import Label from gi.repository import Gtk + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/clipboard.py b/modules/launcher/plugins/clipboard.py index 8182c79..d868baa 100644 --- a/modules/launcher/plugins/clipboard.py +++ b/modules/launcher/plugins/clipboard.py @@ -8,6 +8,7 @@ from concurrent.futures import ThreadPoolExecutor from typing import Dict, List, Optional from gi.repository import GdkPixbuf, GLib + from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/emoji.py b/modules/launcher/plugins/emoji.py index 956f5b8..834d673 100644 --- a/modules/launcher/plugins/emoji.py +++ b/modules/launcher/plugins/emoji.py @@ -5,8 +5,9 @@ import time from collections import OrderedDict from typing import Dict, List -import config.data as data from fabric.utils import get_relative_path + +import config.data as data from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -22,7 +23,7 @@ class EmojiPlugin(PluginBase): self.display_name = "Emoji" self.description = "Search and copy emojis" self.emoji_data = {} - self.emoji_path = get_relative_path("../../../config/json/emoji.json") + self.emoji_path = get_relative_path("../../../config/assets/emoji.json") # Use cache directory for recent emojis (save directly in cache dir) self.recent_emoji_path = os.path.join(data.CACHE_DIR, "recent_emoji.json") diff --git a/modules/launcher/plugins/kanban.py b/modules/launcher/plugins/kanban.py index 6775eff..85fad39 100644 --- a/modules/launcher/plugins/kanban.py +++ b/modules/launcher/plugins/kanban.py @@ -5,11 +5,12 @@ from typing import List import cairo import gi -import utils.icons as icons from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import Gdk, GLib, GObject, Gtk + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/network.py b/modules/launcher/plugins/network.py index 101caba..643bdaf 100644 --- a/modules/launcher/plugins/network.py +++ b/modules/launcher/plugins/network.py @@ -1,9 +1,10 @@ from typing import List -import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.entry import Entry from gi.repository import GLib + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result from services.network import NetworkClient diff --git a/modules/launcher/plugins/notifications.py b/modules/launcher/plugins/notifications.py index d8753bc..5e4df12 100644 --- a/modules/launcher/plugins/notifications.py +++ b/modules/launcher/plugins/notifications.py @@ -2,11 +2,12 @@ import time from datetime import datetime from typing import List, Optional -import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.label import Label from gi.repository import GdkPixbuf + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result from services import notification_service diff --git a/modules/launcher/plugins/otp.py b/modules/launcher/plugins/otp.py index 832f004..b657367 100644 --- a/modules/launcher/plugins/otp.py +++ b/modules/launcher/plugins/otp.py @@ -6,8 +6,9 @@ import time from pathlib import Path from typing import Dict, List, Optional -import utils.icons as icons from fabric.utils import get_relative_path + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -28,7 +29,7 @@ class OTPPlugin(PluginBase): self.description = "Manage TOTP codes and 2FA authentication" self.secrets_file = Path( - get_relative_path("../../../config/json/accounts.json") + get_relative_path("../../../config/assets/accounts.json") ) self.secrets: Dict[str, Dict] = {} self.last_update = 0 diff --git a/modules/launcher/plugins/password.py b/modules/launcher/plugins/password.py index 7b03a65..18f1f65 100644 --- a/modules/launcher/plugins/password.py +++ b/modules/launcher/plugins/password.py @@ -1,16 +1,17 @@ -import json import base64 +import json import subprocess import threading import time -from pathlib import Path -from typing import List, Dict, Optional from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional + +from fabric.utils import get_relative_path import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -from fabric.utils import get_relative_path class PasswordManager: @@ -166,7 +167,7 @@ class PasswordPlugin(PluginBase): # Initialize password manager self.password_file = Path( - get_relative_path("../../../config/json/passwords.json") + get_relative_path("../../../config/assets/passwords.json") ) self.password_manager = PasswordManager(self.password_file) @@ -271,10 +272,10 @@ class PasswordPlugin(PluginBase): # Check if password is revealed if name in self.revealed_passwords: title = f"{name}: {self.revealed_passwords[name]}" - subtitle = f"Password revealed - Enter: copy | Shift+Enter: hide" + subtitle = "Password revealed - Enter: copy | Shift+Enter: hide" else: title = f"{name}: {'*' * 8}" - subtitle = f"Enter: copy | Shift+Enter: reveal password" + subtitle = "Enter: copy | Shift+Enter: reveal password" if description: subtitle += f" | {description}" @@ -328,7 +329,7 @@ class PasswordPlugin(PluginBase): results.append( Result( title=f"Update password for '{name}'?", - subtitle=f"Password already exists. Click to update it.", + subtitle="Password already exists. Click to update it.", icon_markup=icons.key, action=lambda: self._add_password_action( name, password, description, update=True @@ -342,7 +343,7 @@ class PasswordPlugin(PluginBase): results.append( Result( title=f"Add password for '{name}'", - subtitle=f"Click to save password" + subtitle="Click to save password" + (f" | {description}" if description else ""), icon_markup=icons.plus, action=lambda: self._add_password_action( @@ -449,10 +450,10 @@ class PasswordPlugin(PluginBase): # Check if password is revealed if name in self.revealed_passwords: title = f"{name}: {self.revealed_passwords[name]}" - subtitle = f"Password revealed - Enter: copy | Shift+Enter: hide" + subtitle = "Password revealed - Enter: copy | Shift+Enter: hide" else: title = f"{name}: {'*' * 8}" - subtitle = f"Enter: copy | Shift+Enter: reveal password" + subtitle = "Enter: copy | Shift+Enter: reveal password" if description: subtitle += f" | {description}" @@ -510,7 +511,6 @@ class PasswordPlugin(PluginBase): try: success = self.password_manager.remove_password(name) if success: - # Remove from revealed passwords if present self.revealed_passwords.pop(name, None) diff --git a/modules/launcher/plugins/power.py b/modules/launcher/plugins/power.py index de68dce..ffdc919 100644 --- a/modules/launcher/plugins/power.py +++ b/modules/launcher/plugins/power.py @@ -1,7 +1,8 @@ from typing import List -import utils.icons as icons from fabric.utils import exec_shell_command_async + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/process.py b/modules/launcher/plugins/process.py index 204f330..f631249 100644 --- a/modules/launcher/plugins/process.py +++ b/modules/launcher/plugins/process.py @@ -1,8 +1,9 @@ -import time import threading +import time from typing import List import psutil + import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -18,8 +19,6 @@ class ProcessPlugin(PluginBase): self.display_name = "Process Manager" self.description = "View and manage running processes" - - # Threading for auto-refresh self.refresh_thread = None self.stop_refresh = threading.Event() @@ -39,13 +38,17 @@ class ProcessPlugin(PluginBase): def _initialize_cpu_monitoring(self): """Initialize CPU monitoring for all processes to get meaningful readings immediately.""" try: - for proc in psutil.process_iter(['pid']): + for proc in psutil.process_iter(["pid"]): try: - pid = proc.info['pid'] + pid = proc.info["pid"] # Initialize CPU monitoring - this establishes baseline for future readings proc.cpu_percent() self._cpu_cache[pid] = proc - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess, + ): continue self._last_cpu_update = time.time() except Exception as e: @@ -67,27 +70,31 @@ class ProcessPlugin(PluginBase): # Initialize CPU monitoring if this is the first call or enough time has passed if current_time - self._last_cpu_update > 0.1: # Update every 100ms # Pre-populate CPU cache for all processes - for proc in psutil.process_iter(['pid']): + for proc in psutil.process_iter(["pid"]): try: - pid = proc.info['pid'] + pid = proc.info["pid"] # Initialize CPU monitoring - this call establishes baseline proc.cpu_percent() self._cpu_cache[pid] = proc - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess, + ): continue self._last_cpu_update = current_time # Group processes by application name app_groups = {} - for proc in psutil.process_iter(['pid', 'name', 'status', 'ppid']): + for proc in psutil.process_iter(["pid", "name", "status", "ppid"]): try: info = proc.info - if info['status'] == psutil.STATUS_ZOMBIE: + if info["status"] == psutil.STATUS_ZOMBIE: continue - pid = info['pid'] - name = info['name'] or 'Unknown' + pid = info["pid"] + name = info["name"] or "Unknown" # Get CPU percentage - use cached process if available for better readings if pid in self._cpu_cache: @@ -110,13 +117,13 @@ class ProcessPlugin(PluginBase): memory_mb = memory_info.rss / (1024 * 1024) if memory_info else 0 process_data = { - 'pid': pid, - 'ppid': info['ppid'], - 'name': name, - 'cpu_percent': cpu_percent, - 'memory_percent': memory_percent, - 'memory_mb': memory_mb, - 'status': info['status'] + "pid": pid, + "ppid": info["ppid"], + "name": name, + "cpu_percent": cpu_percent, + "memory_percent": memory_percent, + "memory_mb": memory_mb, + "status": info["status"], } # Group processes by application name @@ -125,7 +132,11 @@ class ProcessPlugin(PluginBase): app_groups[app_name] = [] app_groups[app_name].append(process_data) - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess, + ): continue # Create grouped results @@ -134,37 +145,44 @@ class ProcessPlugin(PluginBase): if len(app_processes) == 1: # Single process - show as individual proc = app_processes[0] - proc['process_count'] = 1 + proc["process_count"] = 1 grouped_processes.append(proc) else: # Multiple processes - group them and sum resources - total_cpu = sum(p['cpu_percent'] for p in app_processes) - total_memory_mb = sum(p['memory_mb'] for p in app_processes) - total_memory_percent = sum(p['memory_percent'] for p in app_processes) + total_cpu = sum(p["cpu_percent"] for p in app_processes) + total_memory_mb = sum(p["memory_mb"] for p in app_processes) + total_memory_percent = sum( + p["memory_percent"] for p in app_processes + ) # Find the main process (usually the one with lowest PID) - main_process = min(app_processes, key=lambda x: x['pid']) + main_process = min(app_processes, key=lambda x: x["pid"]) grouped_process = { - 'pid': main_process['pid'], # Use main process PID for killing - 'name': app_name, - 'cpu_percent': total_cpu, - 'memory_percent': total_memory_percent, - 'memory_mb': total_memory_mb, - 'status': main_process['status'], - 'process_count': len(app_processes), - 'child_pids': [p['pid'] for p in app_processes] + # Use main process PID for killing + "pid": main_process["pid"], + "name": app_name, + "cpu_percent": total_cpu, + "memory_percent": total_memory_percent, + "memory_mb": total_memory_mb, + "status": main_process["status"], + "process_count": len(app_processes), + "child_pids": [p["pid"] for p in app_processes], } grouped_processes.append(grouped_process) # Clean up old entries from cache all_pids = set() for app_processes in app_groups.values(): - all_pids.update(p['pid'] for p in app_processes) - self._cpu_cache = {pid: proc for pid, proc in self._cpu_cache.items() if pid in all_pids} + all_pids.update(p["pid"] for p in app_processes) + self._cpu_cache = { + pid: proc for pid, proc in self._cpu_cache.items() if pid in all_pids + } # Sort by combined CPU and memory usage for better display - grouped_processes.sort(key=lambda x: (x['cpu_percent'] + x['memory_percent']), reverse=True) + grouped_processes.sort( + key=lambda x: (x["cpu_percent"] + x["memory_percent"]), reverse=True + ) return grouped_processes except Exception as e: @@ -177,34 +195,50 @@ class ProcessPlugin(PluginBase): name = process_name.lower() # Handle common browser patterns - if 'firefox' in name: - return 'Firefox' - elif 'chrome' in name or 'chromium' in name: - return 'Chrome' - elif 'zen' in name or name in ['socket process', 'privileged cont', 'rdd process', - 'isolated web co', 'web content', 'webextensions', - 'utility process', 'isolated servic']: - return 'Zen Browser' - elif 'code' in name and ('helper' in name or 'oss' in name or name == 'code'): - return 'VS Code' - elif name.startswith('python') and len(name) > 6: - return 'Python' - elif name.startswith('node') and len(name) > 4: - return 'Node.js' - elif 'electron' in name: - return 'Electron' - elif 'java' in name: - return 'Java' - elif 'gnome' in name: - return 'GNOME' - elif 'gtk' in name: - return 'GTK App' + if "firefox" in name: + return "Firefox" + elif "chrome" in name or "chromium" in name: + return "Chrome" + elif "zen" in name or name in [ + "socket process", + "privileged cont", + "rdd process", + "isolated web co", + "web content", + "webextensions", + "utility process", + "isolated servic", + ]: + return "Zen Browser" + elif "code" in name and ("helper" in name or "oss" in name or name == "code"): + return "VS Code" + elif name.startswith("python") and len(name) > 6: + return "Python" + elif name.startswith("node") and len(name) > 4: + return "Node.js" + elif "electron" in name: + return "Electron" + elif "java" in name: + return "Java" + elif "gnome" in name: + return "GNOME" + elif "gtk" in name: + return "GTK App" # Remove common suffixes - suffixes = ['-bin', '-real', '-wrapped', '.bin', '.exe', '-helper', '-gpu', '-renderer'] + suffixes = [ + "-bin", + "-real", + "-wrapped", + ".bin", + ".exe", + "-helper", + "-gpu", + "-renderer", + ] for suffix in suffixes: if name.endswith(suffix): - name = name[:-len(suffix)] + name = name[: -len(suffix)] break # Capitalize first letter @@ -222,8 +256,9 @@ class ProcessPlugin(PluginBase): filtered_processes = processes if query: filtered_processes = [ - proc for proc in processes - if query in proc['name'].lower() or query in str(proc['pid']) + proc + for proc in processes + if query in proc["name"].lower() or query in str(proc["pid"]) ] # Limit to top 20 processes to avoid overwhelming the UI @@ -237,12 +272,12 @@ class ProcessPlugin(PluginBase): def _create_process_result(self, proc_data: dict) -> Result: """Create a Result object for a process or grouped application.""" try: - pid = proc_data['pid'] - name = proc_data['name'] - cpu_percent = proc_data['cpu_percent'] - memory_percent = proc_data['memory_percent'] - memory_mb = proc_data['memory_mb'] - process_count = proc_data.get('process_count', 1) + pid = proc_data["pid"] + name = proc_data["name"] + cpu_percent = proc_data["cpu_percent"] + memory_percent = proc_data["memory_percent"] + memory_mb = proc_data["memory_mb"] + process_count = proc_data.get("process_count", 1) # Format the title with process count if grouped if process_count > 1: @@ -251,10 +286,14 @@ class ProcessPlugin(PluginBase): title = f"{name} (PID: {pid})" # Add visual indicators for high usage - cpu_indicator = "🔥" if cpu_percent > 80 else "⚡" if cpu_percent > 50 else "" + cpu_indicator = ( + "🔥" if cpu_percent > 80 else "⚡" if cpu_percent > 50 else "" + ) mem_indicator = "💾" if memory_percent > 80 else "" - subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {memory_mb:.1f}MB ({memory_percent:.1f}%){mem_indicator}" + subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: { + memory_mb:.1f + }MB ({memory_percent:.1f}%){mem_indicator}" # Choose icon based on CPU usage if cpu_percent > 50: @@ -279,7 +318,7 @@ class ProcessPlugin(PluginBase): "memory_percent": memory_percent, "memory_mb": memory_mb, "process_count": process_count, - "child_pids": proc_data.get('child_pids', [pid]), + "child_pids": proc_data.get("child_pids", [pid]), "keep_launcher_open": True, # Keep launcher open to see live updates "alt_action": lambda data=proc_data: self._kill_process_group(data), }, @@ -288,18 +327,16 @@ class ProcessPlugin(PluginBase): print(f"ProcessPlugin: Error creating process result: {e}") return None - - def _kill_process_group(self, proc_data: dict): """Kill a process group (application with all its subprocesses).""" try: - process_count = proc_data.get('process_count', 1) - child_pids = proc_data.get('child_pids', [proc_data['pid']]) - app_name = proc_data['name'] + process_count = proc_data.get("process_count", 1) + child_pids = proc_data.get("child_pids", [proc_data["pid"]]) + app_name = proc_data["name"] if process_count == 1: # Single process - self._kill_process(proc_data['pid']) + self._kill_process(proc_data["pid"]) else: # Multiple processes - kill all killed_count = 0 @@ -307,7 +344,11 @@ class ProcessPlugin(PluginBase): if self._kill_process(pid, silent=True): killed_count += 1 - print(f"✓ Terminated {killed_count}/{len(child_pids)} processes for '{app_name}'") + print( + f"✓ Terminated {killed_count}/{len(child_pids)} processes for '{ + app_name + }'" + ) except Exception as e: print(f"✗ Error killing process group: {e}") @@ -325,7 +366,9 @@ class ProcessPlugin(PluginBase): try: proc.wait(timeout=3) if not silent: - print(f"✓ Successfully terminated process '{proc_name}' (PID: {pid})") + print( + f"✓ Successfully terminated process '{proc_name}' (PID: {pid})" + ) return True except psutil.TimeoutExpired: # Force kill if graceful termination failed @@ -340,7 +383,9 @@ class ProcessPlugin(PluginBase): return False except psutil.AccessDenied: if not silent: - print(f"✗ Access denied - cannot kill process {pid} (insufficient permissions)") + print( + f"✗ Access denied - cannot kill process {pid} (insufficient permissions)" + ) return False except Exception as e: if not silent: @@ -351,7 +396,8 @@ class ProcessPlugin(PluginBase): """Start background thread for auto-refreshing process data in milliseconds.""" def refresh_loop(): - while not self.stop_refresh.wait(0.5): # Check every 500ms for real-time updates + # Check every 500ms for real-time updates + while not self.stop_refresh.wait(0.5): current_time = time.time() if current_time - self.last_update >= 0.5: # Update every 500ms self.last_update = current_time @@ -368,6 +414,7 @@ class ProcessPlugin(PluginBase): """Update process data in existing result items (like OTP plugin).""" try: import gc + from gi.repository import GLib def do_update(): @@ -408,11 +455,12 @@ class ProcessPlugin(PluginBase): # Run data collection in background to avoid blocking UI import concurrent.futures + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(get_data_async) try: current_processes = future.result(timeout=1.0) # 1 second timeout - process_dict = {proc['pid']: proc for proc in current_processes} + process_dict = {proc["pid"]: proc for proc in current_processes} for child in results_box.get_children(): if ( @@ -438,26 +486,32 @@ class ProcessPlugin(PluginBase): return proc_data = process_dict[pid] - cpu_percent = proc_data['cpu_percent'] - memory_percent = proc_data['memory_percent'] - memory_mb = proc_data['memory_mb'] + cpu_percent = proc_data["cpu_percent"] + memory_percent = proc_data["memory_percent"] + memory_mb = proc_data["memory_mb"] # CPU is already normalized in _get_live_process_data, so no need to normalize again # Update the subtitle with live data - cpu_indicator = "🔥" if cpu_percent > 80 else "⚡" if cpu_percent > 50 else "" + cpu_indicator = ( + "🔥" if cpu_percent > 80 else "⚡" if cpu_percent > 50 else "" + ) mem_indicator = "💾" if memory_percent > 80 else "" - new_subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {memory_mb:.1f}MB ({memory_percent:.1f}%){mem_indicator}" + new_subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: { + memory_mb:.1f + }MB ({memory_percent:.1f}%){mem_indicator}" # Find and update the subtitle label widget self._find_and_update_subtitle_label(result_item, new_subtitle) # Update the result data for consistency - result_item.result.data.update({ - "cpu_percent": cpu_percent, - "memory_percent": memory_percent, - "memory_mb": memory_mb, - }) + result_item.result.data.update( + { + "cpu_percent": cpu_percent, + "memory_percent": memory_percent, + "memory_mb": memory_mb, + } + ) except Exception as e: print(f"Error updating process result item: {e}") diff --git a/modules/launcher/plugins/screencapture.py b/modules/launcher/plugins/screencapture.py index 57fb64e..7608f94 100644 --- a/modules/launcher/plugins/screencapture.py +++ b/modules/launcher/plugins/screencapture.py @@ -2,6 +2,7 @@ import subprocess from typing import List from fabric.utils import get_relative_path + from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result from utils.icons import screenrecord, screenshots, ssfull, ssregion, stop diff --git a/modules/launcher/plugins/system.py b/modules/launcher/plugins/system.py index af5fa07..96296b8 100644 --- a/modules/launcher/plugins/system.py +++ b/modules/launcher/plugins/system.py @@ -1,9 +1,10 @@ import os -import time import shlex +import time from typing import List, Union from fabric.utils import exec_shell_command_async + import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result diff --git a/modules/launcher/plugins/wallpaper.py b/modules/launcher/plugins/wallpaper.py index 7b31436..66af94b 100644 --- a/modules/launcher/plugins/wallpaper.py +++ b/modules/launcher/plugins/wallpaper.py @@ -8,13 +8,14 @@ import threading import time from typing import Dict, List, Optional -import config.data as data -import utils.icons as icons from fabric.utils.helpers import exec_shell_command_async from gi.repository import GdkPixbuf +from PIL import Image + +import config.data as data +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -from PIL import Image class WallpaperPlugin(PluginBase): @@ -319,34 +320,36 @@ class WallpaperPlugin(PluginBase): wallpaper_path = os.readlink(current_wall) if os.path.exists(wallpaper_path): # Apply the new scheme to current wallpaper - exec_shell_command_async(f'matugen image "{wallpaper_path}" -t {scheme}') + exec_shell_command_async( + f'matugen image "{wallpaper_path}" -t {scheme}' + ) # Send notification exec_shell_command_async( - f"notify-send '🎨 Color Scheme' 'Applied {scheme_name} scheme' -a '{ - data.APP_NAME_CAP - }' -e" + f"notify-send '🎨 Color Scheme' 'Applied { + scheme_name + } scheme' -a '{data.APP_NAME_CAP}' -e" ) else: # No current wallpaper, just show scheme change notification exec_shell_command_async( - f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (will apply to next wallpaper)' -a '{ - data.APP_NAME_CAP - }' -e" + f"notify-send '🎨 Color Scheme' 'Set to { + scheme_name + } (will apply to next wallpaper)' -a '{data.APP_NAME_CAP}' -e" ) else: # No current wallpaper, just show scheme change notification exec_shell_command_async( - f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (will apply to next wallpaper)' -a '{ - data.APP_NAME_CAP - }' -e" + f"notify-send '🎨 Color Scheme' 'Set to { + scheme_name + } (will apply to next wallpaper)' -a '{data.APP_NAME_CAP}' -e" ) else: # Matugen is disabled, just save the setting exec_shell_command_async( - f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (matugen disabled)' -a '{ - data.APP_NAME_CAP - }' -e" + f"notify-send '🎨 Color Scheme' 'Set to { + scheme_name + } (matugen disabled)' -a '{data.APP_NAME_CAP}' -e" ) def _apply_hex_color(self, hex_color: str, scheme: str = None): @@ -501,7 +504,8 @@ class WallpaperPlugin(PluginBase): status_text }", icon_markup=icons.palette, - action=lambda c=hex_color, s=scheme: self._apply_hex_color_direct(c, s), + action=lambda c=hex_color, + s=scheme: self._apply_hex_color_direct(c, s), relevance=1.0, plugin_name=self.display_name, data={ @@ -541,7 +545,8 @@ class WallpaperPlugin(PluginBase): status_text }", icon_markup=icons.palette, - action=lambda c=hex_color, s=scheme: self._apply_hex_color_direct(c, s) + action=lambda c=hex_color, + s=scheme: self._apply_hex_color_direct(c, s) if not matugen_enabled else None, relevance=0.9, @@ -588,7 +593,9 @@ class WallpaperPlugin(PluginBase): results.append( Result( title=f"Random Hex Color{indicator_text}", - subtitle=f"Generate and apply random color with {scheme_name} scheme • {status_text}", + subtitle=f"Generate and apply random color with { + scheme_name + } scheme • {status_text}", icon_markup=icons.palette, action=lambda s=scheme: self._apply_hex_color_direct( self._generate_random_hex_color(), s diff --git a/modules/launcher/plugins/websearch.py b/modules/launcher/plugins/websearch.py index d8b8dd2..575492a 100644 --- a/modules/launcher/plugins/websearch.py +++ b/modules/launcher/plugins/websearch.py @@ -147,8 +147,6 @@ class WebSearchPlugin(PluginBase): return "", query - - def _is_url(self, text: str) -> bool: """Check if the text is a URL.""" text = text.strip().lower() @@ -257,4 +255,6 @@ class WebSearchPlugin(PluginBase): """Show help for a specific search engine.""" engine_info = self.search_engines.get(engine_id) if engine_info: - print(f"Search engine: {engine_info['name']} - {engine_info['description']}") + print( + f"Search engine: {engine_info['name']} - {engine_info['description']}" + ) diff --git a/modules/launcher/trigger_config.py b/modules/launcher/trigger_config.py index 0608676..cb67687 100644 --- a/modules/launcher/trigger_config.py +++ b/modules/launcher/trigger_config.py @@ -2,14 +2,15 @@ import json import os from typing import Any, Dict, List -import utils.icons as icons from fabric.utils import get_relative_path +import utils.icons as icons + class TriggerConfig: def __init__(self, config_path: str = None): if config_path is None: - config_path = get_relative_path("../../config/json/launcher.json") + config_path = get_relative_path("../../config/assets/launcher.json") self.config_path = config_path diff --git a/modules/notification_popup.py b/modules/notification_popup.py index c772667..45797f3 100644 --- a/modules/notification_popup.py +++ b/modules/notification_popup.py @@ -1,6 +1,5 @@ import os -import utils.icons as icons from fabric.notifications import ( Notification, NotificationAction, @@ -15,6 +14,8 @@ from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from gi.repository import Gdk, GdkPixbuf, GLib from loguru import logger + +import utils.icons as icons from services import notification_service from utils.animator import Animator from utils.custom_image import CustomImage diff --git a/modules/osd.py b/modules/osd.py index 9840350..820e146 100644 --- a/modules/osd.py +++ b/modules/osd.py @@ -1,13 +1,14 @@ import time from typing import ClassVar, Literal -import utils.icons as icons from fabric.audio import Audio from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from fabric.widgets.scale import Scale, ScaleMark from gi.repository import GLib, GObject + +import utils.icons as icons from services.brightness import Brightness from utils.animator import Animator from utils.wayland import WaylandWindow as Window diff --git a/modules/switcher.py b/modules/switcher.py index 2babfb7..246c39d 100644 --- a/modules/switcher.py +++ b/modules/switcher.py @@ -1,13 +1,14 @@ import json import subprocess -import config.data as data from fabric.hyprland.widgets import get_hyprland_connection from fabric.widgets.box import Box from fabric.widgets.eventbox import EventBox from fabric.widgets.image import Image from fabric.widgets.label import Label from gi.repository import Gdk + +import config.data as data from utils.icon_resolver import IconResolver from utils.wayland import WaylandWindow as Window diff --git a/services/battery.py b/services/battery.py index e0306e0..19e01ed 100644 --- a/services/battery.py +++ b/services/battery.py @@ -1,4 +1,4 @@ -from fabric.core import Service, Property, Signal +from fabric.core import Property, Service, Signal from pydbus import SystemBus DeviceState = { @@ -31,7 +31,11 @@ class Battery(Service): @Property(str, "readable") def temperature(self): - return f"{self._battery.Temperature}°C" if hasattr(self._battery, "Temperature") else "N/A" + return ( + f"{self._battery.Temperature}°C" + if hasattr(self._battery, "Temperature") + else "N/A" + ) @Property(str, "readable") def time_to_empty(self): @@ -69,7 +73,7 @@ class Battery(Service): self.profile_changed.emit(profile) self.changed.emit() return True - except Exception as e: + except Exception: return False def __init__(self): @@ -78,17 +82,23 @@ class Battery(Service): # Battery device try: - self._battery = self._bus.get("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/battery_BAT0") + self._battery = self._bus.get( + "org.freedesktop.UPower", "/org/freedesktop/UPower/devices/battery_BAT0" + ) self._battery.onPropertiesChanged = self.handle_battery_change - except Exception as e: + except Exception: return # PowerProfiles try: - self._profile_proxy = self._bus.get("net.hadess.PowerProfiles", "/net/hadess/PowerProfiles") + self._profile_proxy = self._bus.get( + "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles" + ) # Use onPropertiesChanged for consistency with battery device - self._profile_proxy.onPropertiesChanged = lambda _, changed, __: self._handle_profile_props_changed(changed) - except Exception as e: + self._profile_proxy.onPropertiesChanged = ( + lambda _, changed, __: self._handle_profile_props_changed(changed) + ) + except Exception: self._profile_proxy = None self.changed.emit() diff --git a/services/brightness.py b/services/brightness.py index 1cd2915..88d0d2e 100644 --- a/services/brightness.py +++ b/services/brightness.py @@ -15,7 +15,7 @@ try: screen_device = os.listdir("/sys/class/backlight") screen_device = screen_device[0] if screen_device else "" except FileNotFoundError: - logger.error(f"No backlight devices found, brightness control disabled") + logger.error("No backlight devices found, brightness control disabled") screen_device = "" diff --git a/services/mpris.py b/services/mpris.py index 5772460..3125890 100644 --- a/services/mpris.py +++ b/services/mpris.py @@ -1,12 +1,11 @@ -from gi.repository import Playerctl -from typing import Optional -import gi import contextlib -from loguru import logger +from typing import Optional -from fabric.core.service import Service, Signal, Property +import gi +from fabric.core.service import Property, Service, Signal from fabric.utils.helpers import get_enum_member_name, snake_case_to_kebab_case - +from gi.repository import Playerctl +from loguru import logger gi.require_version("Playerctl", "2.0") diff --git a/services/network.py b/services/network.py index a3c7a94..1124b4a 100644 --- a/services/network.py +++ b/services/network.py @@ -1,8 +1,9 @@ -from gi.repository import NM, GLib +from typing import Optional + import gi -from typing import List, Optional from fabric.core.service import Property, Service, Signal from fabric.utils import bulk_connect, get_enum_member_name, snake_case_to_kebab_case +from gi.repository import NM, GLib from loguru import logger gi.require_version("NM", "1.0") # Ensure the correct version is loaded @@ -65,8 +66,9 @@ class NetworkClient(Service): if not self._client: return "disconnected" return snake_case_to_kebab_case( - get_enum_member_name(self._client.get_property( - "state"), default="disconnected") + get_enum_member_name( + self._client.get_property("state"), default="disconnected" + ) ) @Property(str, "readable") @@ -75,8 +77,9 @@ class NetworkClient(Service): if not self._client: return "disconnected" return snake_case_to_kebab_case( - get_enum_member_name(self._client.get_property( - "connectivity"), default="disconnected") + get_enum_member_name( + self._client.get_property("connectivity"), default="disconnected" + ) ) @Property(list, "readable") @@ -92,7 +95,9 @@ class NetworkClient(Service): @Property(bool, "read-write", default_value=False) def networking_enabled(self) -> bool: """Checks if networking is enabled.""" - return self._client.get_property("networking_enabled") if self._client else False + return ( + self._client.get_property("networking_enabled") if self._client else False + ) @networking_enabled.setter def networking_enabled(self, value: bool): @@ -126,19 +131,26 @@ class NetworkClient(Service): def on_client_ready(self, source, result): """Callback when NM.Client is ready.""" try: - self._client = NM.Client.new_finish( - result) # Retrieve client instance + self._client = NM.Client.new_finish(result) # Retrieve client instance logger.info("[Network] NM.Client initialized successfully!") # Connect signals bulk_connect( self._client, { - "device-added": lambda _, device: self.on_device_added(device=device), - "device-removed": lambda _, device: self.on_device_removed(device=device), - "notify::state": lambda *args: self.notifier('state'), - "notify::networking-enabled": lambda *args: self.notifier('networking-enabled'), - "notify::wireless-enabled": lambda *args: self.notifier('wireless-enabled'), + "device-added": lambda _, device: self.on_device_added( + device=device + ), + "device-removed": lambda _, device: self.on_device_removed( + device=device + ), + "notify::state": lambda *args: self.notifier("state"), + "notify::networking-enabled": lambda *args: self.notifier( + "networking-enabled" + ), + "notify::wireless-enabled": lambda *args: self.notifier( + "wireless-enabled" + ), # "notify::primary-connection": lambda *args: self.notifier('primary-connection'), # "notify::active-connection": lambda *args: self.notifier('active-connection'), # "active-connection-added": lambda *args: self.emit("changed"), @@ -150,10 +162,10 @@ class NetworkClient(Service): for device in self.do_get_raw_devices(): self.on_device_added(device=device) - self.notify('state') - self.notify('networking-enabled') - self.notify('wireless-enabled') - self.notify('is-ready') + self.notify("state") + self.notify("networking-enabled") + self.notify("wireless-enabled") + self.notify("is-ready") self.ready.emit() except Exception as e: @@ -194,10 +206,8 @@ class NetworkClient(Service): self.networking_enabled = not self.networking_enabled def deactivate_connection(self, connection): - """ Disconnect """ - self._client.deactivate_connection_async( - connection, None, None - ) + """Disconnect""" + self._client.deactivate_connection_async(connection, None, None) def notifier(self, name): self.notify(name) @@ -206,6 +216,7 @@ class NetworkClient(Service): class AccessPoint(Service): """A service to manage access points""" + @Signal def changed(self) -> None: ... @@ -215,7 +226,7 @@ class AccessPoint(Service): @Property(int, "readable") def strength(self) -> int: - return self._ap.get_property('strength') + return self._ap.get_property("strength") @Property(int, "readable") def frequency(self) -> int: @@ -254,7 +265,10 @@ class AccessPoint(Service): connection = None for setting in settings: wifi_setting = setting.get_setting_wireless() - if wifi_setting and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid: + if ( + wifi_setting + and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid + ): connection = setting break if not connection: @@ -273,10 +287,10 @@ class AccessPoint(Service): self._device: Wifi = device self._ap: NM.AccessPoint = ap - self._ap.connect("notify::strength", - lambda *args: self.notifier("strength")) - self._device.connect("notify::active-access-point", - lambda *args: self.notifier("is-active")) + self._ap.connect("notify::strength", lambda *args: self.notifier("strength")) + self._device.connect( + "notify::active-access-point", lambda *args: self.notifier("is-active") + ) def notifier(self, name: str, *args): self.notify(name) @@ -298,7 +312,7 @@ class Wifi(Service): @Property(NetworkClient, "readable") def client(self) -> NetworkClient: - """Returns the client """ + """Returns the client""" return self._client @Property(bool, "read-write", default_value=False) @@ -312,7 +326,9 @@ class Wifi(Service): @Property(list[AccessPoint], "readable") def access_points(self) -> list[AccessPoint]: - return sorted(self._access_points.values(), key=lambda x: x.is_active, reverse=True) + return sorted( + self._access_points.values(), key=lambda x: x.is_active, reverse=True + ) @Property(AccessPoint, "readable") def active_access_point(self) -> Optional[AccessPoint]: @@ -330,13 +346,16 @@ class Wifi(Service): { "notify::active-access-point": lambda *args: self.on_access_point_activated(), "access-point-added": lambda _, ap: self.on_access_point_added(ap=ap), - "access-point-removed": lambda _, ap: self.on_access_point_removed(ap=ap), + "access-point-removed": lambda _, ap: self.on_access_point_removed( + ap=ap + ), # "state-changed": lambda device, new, old, reason: self.on_state_changed(new), }, ) - self._client.connect("notify::wireless-enabled", - lambda *args: self.notifier('wireless-enabled')) + self._client.connect( + "notify::wireless-enabled", lambda *args: self.notifier("wireless-enabled") + ) for ap in self.do_get_access_points(): self.on_access_point_added(ap=ap) @@ -353,10 +372,7 @@ class Wifi(Service): ssid = ap.get_ssid() ssid = NM.utils_ssid_to_utf8(ssid.get_data()) if ssid else "Unknown" - access_point: AccessPoint = AccessPoint( - ap=ap, - device=self - ) + access_point: AccessPoint = AccessPoint(ap=ap, device=self) self._access_points[ssid] = access_point @@ -382,8 +398,7 @@ class Wifi(Service): def on_access_point_activated(self): if self._device.get_active_access_point(): self._active_access_point: AccessPoint = AccessPoint( - ap=self._device.get_active_access_point(), - device=self + ap=self._device.get_active_access_point(), device=self ) else: @@ -404,7 +419,7 @@ class Wifi(Service): None, lambda device, result: [ device.request_scan_finish(result), - self.notifier('access-points'), + self.notifier("access-points"), ], ) logger.info("[Wifi] Scan started") @@ -430,7 +445,10 @@ class Wifi(Service): for setting in settings: wifi_setting = setting.get_setting_wireless() - if wifi_setting and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid: + if ( + wifi_setting + and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid + ): connection = setting break @@ -443,16 +461,17 @@ class Wifi(Service): s_con = NM.SettingConnection.new() s_con.set_property(NM.SETTING_CONNECTION_ID, ssid) s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") - s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, - self._device.get_iface()) # Set interface name + s_con.set_property( + NM.SETTING_CONNECTION_INTERFACE_NAME, self._device.get_iface() + ) # Set interface name connection.add_setting(s_con) # Wireless settings s_wifi = NM.SettingWireless.new() - s_wifi.set_property(NM.SETTING_WIRELESS_SSID, - GLib.Bytes.new(ssid.encode())) - s_wifi.set_property(NM.SETTING_WIRELESS_MODE, - "infrastructure") # Ensure mode is correct + s_wifi.set_property(NM.SETTING_WIRELESS_SSID, GLib.Bytes.new(ssid.encode())) + s_wifi.set_property( + NM.SETTING_WIRELESS_MODE, "infrastructure" + ) # Ensure mode is correct connection.add_setting(s_wifi) # Security settings (only if password is required and provided) @@ -462,8 +481,7 @@ class Wifi(Service): return False s_sec = NM.SettingWirelessSecurity.new() - s_sec.set_property( - NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") + s_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") s_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password) connection.add_setting(s_sec) @@ -482,16 +500,19 @@ class Wifi(Service): try: new_connection = client.add_connection_finish(result) if not new_connection: - logger.error( - f"Failed to create connection for '{ssid}'") + logger.error(f"Failed to create connection for '{ssid}'") return logger.info(f"Connection for '{ssid}' added successfully") client.activate_connection_async( - new_connection, device, None, None, - lambda c, r: logger.info(f"Connected to '{ssid}'") if c else logger.error( - f"Failed to connect to '{ssid}'") + new_connection, + device, + None, + None, + lambda c, r: logger.info(f"Connected to '{ssid}'") + if c + else logger.error(f"Failed to connect to '{ssid}'"), ) except Exception as e: logger.error(f"Failed to add connection: {e}") @@ -501,13 +522,16 @@ class Wifi(Service): connection, True, None, - lambda c, r: on_connection_added(c, r, ssid, self._device) + lambda c, r: on_connection_added(c, r, ssid, self._device), ) # Activate the connection self._client._client.activate_connection_async( - connection, self._device, None, None, - lambda client, result: logger.info(f"Connected to '{ssid}'") + connection, + self._device, + None, + None, + lambda client, result: logger.info(f"Connected to '{ssid}'"), ) return True @@ -535,16 +559,16 @@ class Ethernet(Service): @Property(str, "readable") def state(self) -> str: return snake_case_to_kebab_case( - get_enum_member_name(self._device.get_state(), - default="disconnected") + get_enum_member_name(self._device.get_state(), default="disconnected") ) @Property(str, "readable") def internet(self) -> str: if self._active_connection: return snake_case_to_kebab_case( - get_enum_member_name(self._active_connection.get_state(), - default="disconnected") + get_enum_member_name( + self._active_connection.get_state(), default="disconnected" + ) ) return "disconnected" @@ -568,7 +592,8 @@ class Ethernet(Service): self._active_connection = None self._device.connect( - "state-changed", lambda *args: self.on_network_state_changed()) + "state-changed", lambda *args: self.on_network_state_changed() + ) self.update_active_connection() @@ -586,15 +611,20 @@ class Ethernet(Service): if active_connection: self._active_connection = active_connection self._active_connection.connect( - "state-changed", lambda *args: self.emit('changed')) + "state-changed", lambda *args: self.emit("changed") + ) def get_network_stats(self): """Fetch received and transmitted bytes from the system files""" try: # Read data from /sys/class/net for accurate speed - with open(f"/sys/class/net/{self.iface}/statistics/rx_bytes", "r") as rx_file: + with open( + f"/sys/class/net/{self.iface}/statistics/rx_bytes", "r" + ) as rx_file: rx_bytes = int(rx_file.read().strip()) - with open(f"/sys/class/net/{self.iface}/statistics/tx_bytes", "r") as tx_file: + with open( + f"/sys/class/net/{self.iface}/statistics/tx_bytes", "r" + ) as tx_file: tx_bytes = int(tx_file.read().strip()) return rx_bytes, tx_bytes except FileNotFoundError: diff --git a/services/notification.py b/services/notification.py index 4da918b..126ae3b 100644 --- a/services/notification.py +++ b/services/notification.py @@ -1,15 +1,16 @@ +import config.data as data +from gi.repository import GLib import json import os from typing import List +import gi from fabric import Signal from fabric.notifications import Notification, Notifications from loguru import logger -import gi -gi.require_version('GLib', '2.0') -from gi.repository import GLib -import config.data as data +gi.require_version("GLib", "2.0") + NOTIFICATION_CACHE_FILE = f"{data.CACHE_DIR}/notifications.json" @@ -116,11 +117,9 @@ class CustomNotifications(Notifications): new_id = self._count timestamp = GLib.get_real_time() / 1000000 # Convert microseconds to seconds serialized_data = data.serialize() - serialized_data.update({ - "id": new_id, - "app_name": data.app_name, - "timestamp": timestamp - }) + serialized_data.update( + {"id": new_id, "app_name": data.app_name, "timestamp": timestamp} + ) # Append the new notification self.all_notifications.append(serialized_data) @@ -164,7 +163,9 @@ class CustomNotifications(Notifications): try: notif = Notification.deserialize(notification) # Store the timestamp in the notification object - notif.timestamp = notification.get("timestamp", GLib.get_real_time() / 1000000) + notif.timestamp = notification.get( + "timestamp", GLib.get_real_time() / 1000000 + ) return notif except Exception as e: logger.error( diff --git a/utils/animator.py b/utils/animator.py index ed60e9e..1799ae6 100644 --- a/utils/animator.py +++ b/utils/animator.py @@ -1,6 +1,5 @@ from typing import cast -import fabric from fabric import Property, Service, Signal from gi.repository import GLib, Gtk diff --git a/utils/audio_visualizer.py b/utils/audio_visualizer.py index 82b9090..1b1e909 100644 --- a/utils/audio_visualizer.py +++ b/utils/audio_visualizer.py @@ -16,12 +16,14 @@ from loguru import logger def get_bars(file_path): config = configparser.ConfigParser() config.read(file_path) - return int(config['general']['bars']) + return int(config["general"]["bars"]) + CAVA_CONFIG = get_relative_path("../config/cavalcade/cava.ini") bars = get_bars(CAVA_CONFIG) + def set_death_signal(): """ Set the death signal of the child process to SIGTERM so that if the parent @@ -31,11 +33,13 @@ def set_death_signal(): PR_SET_PDEATHSIG = 1 libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM) + class Cava: """ CAVA wrapper. Launch cava process with certain settings and read output. """ + NONE = 0 RUNNING = 1 RESTARTING = 2 @@ -54,7 +58,9 @@ class Cava: self.env["LC_ALL"] = "en_US.UTF-8" # not sure if it's necessary is_16bit = True - self.byte_type, self.byte_size, self.byte_norm = ("H", 2, 65535) if is_16bit else ("B", 1, 255) + self.byte_type, self.byte_size, self.byte_norm = ( + ("H", 2, 65535) if is_16bit else ("B", 1, 255) + ) if not os.path.exists(self.path): os.mkfifo(self.path) @@ -71,7 +77,8 @@ class Cava: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=self.env, - preexec_fn=set_death_signal # Ensure cava gets killed when the parent dies. + # Ensure cava gets killed when the parent dies. + preexec_fn=set_death_signal, ) logger.debug("cava successfully launched!") self.state = self.RUNNING @@ -84,13 +91,15 @@ class Cava: self.fifo_fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK) # Open dummy write end to prevent getting an EOF on our FIFO self.fifo_dummy_fd = os.open(self.path, os.O_WRONLY | os.O_NONBLOCK) - self.io_watch_id = GLib.io_add_watch(self.fifo_fd, GLib.IO_IN, self._io_callback) + self.io_watch_id = GLib.io_add_watch( + self.fifo_fd, GLib.IO_IN, self._io_callback + ) def _io_callback(self, source, condition): chunk = self.byte_size * self.bars # number of bytes for given format try: data = os.read(self.fifo_fd, chunk) - except OSError as e: + except OSError: # logger.error("Error reading FIFO: {}".format(e)) return False @@ -145,16 +154,20 @@ class Cava: if os.path.exists(self.path): os.remove(self.path) + class AttributeDict(dict): """Dictionary with keys as attributes. Does nothing but easy reading""" + def __getattr__(self, attr): return self.get(attr, 3) def __setattr__(self, attr, value): self[attr] = value + class Spectrum: """Spectrum drawing""" + def __init__(self): self.silence_value = 0 self.audio_sample = [] @@ -242,6 +255,7 @@ class Spectrum: blue = int(color[5:7], 16) / 255 self.color = Gdk.RGBA(red=red, green=green, blue=blue, alpha=1.0) + class SpectrumRender: def __init__(self, mode=None, **kwargs): self.mode = mode @@ -252,7 +266,7 @@ class SpectrumRender: def get_spectrum_box(self): # Get the spectrum box - box = Overlay(name="cavalcade", h_align='center', v_align='center') + box = Overlay(name="cavalcade", h_align="center", v_align="center") box.set_size_request(90, 34) # Better size for music player box.add_overlay(self.draw.area) box.show_all() # Ensure everything is visible @@ -264,6 +278,7 @@ class AudioVisualizer(Overlay): Simple AudioVisualizer wrapper around existing spectrum components for compatibility with the music player component. """ + def __init__(self, name=None, width=150, height=24, bars=20, **kwargs): super().__init__(name=name, **kwargs) @@ -291,7 +306,10 @@ class AudioVisualizer(Overlay): super().set_visible(visible) if visible and self.is_playing: # Ensure spectrum is active when visible and playing - if hasattr(self.spectrum_render, 'cava') and self.spectrum_render.cava.state == 0: # NONE + if ( + hasattr(self.spectrum_render, "cava") + and self.spectrum_render.cava.state == 0 + ): # NONE self.spectrum_render.cava.start() elif not visible: # Optionally pause spectrum when not visible to save resources diff --git a/utils/conversion.py b/utils/conversion.py index db045bb..0f15f9a 100644 --- a/utils/conversion.py +++ b/utils/conversion.py @@ -1,19 +1,23 @@ -import requests -import time import threading +import time from concurrent.futures import ThreadPoolExecutor from typing import Dict, Optional, Tuple +import requests + class CurrencyCache: """Thread-safe currency exchange rate cache with background updates.""" def __init__(self): - self._cache: Dict[str, Dict] = {} # {base_currency: {rates_data, timestamp}} + # {base_currency: {rates_data, timestamp}} + self._cache: Dict[str, Dict] = {} self._cache_lock = threading.Lock() self._cache_ttl = 3600 # 1 hour in seconds self._request_timeout = 2 # 2 seconds for faster response - self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="currency") + self._executor = ThreadPoolExecutor( + max_workers=2, thread_name_prefix="currency" + ) self._pending_requests: Dict[str, threading.Event] = {} def get_rate(self, from_code: str, to_code: str) -> Optional[Tuple[float, float]]: @@ -33,19 +37,22 @@ class CurrencyCache: # Check if we have fresh cached data if from_lower in self._cache: cache_entry = self._cache[from_lower] - cache_age = current_time - cache_entry['timestamp'] + cache_age = current_time - cache_entry["timestamp"] - if cache_age < self._cache_ttl and to_lower in cache_entry['rates']: - rate = cache_entry['rates'][to_lower]['rate'] + if cache_age < self._cache_ttl and to_lower in cache_entry["rates"]: + rate = cache_entry["rates"][to_lower]["rate"] return rate, cache_age # Check if we're already fetching this currency if from_lower in self._pending_requests: # Don't wait, return cached data if available (even if stale) - if from_lower in self._cache and to_lower in self._cache[from_lower]['rates']: + if ( + from_lower in self._cache + and to_lower in self._cache[from_lower]["rates"] + ): cache_entry = self._cache[from_lower] - cache_age = current_time - cache_entry['timestamp'] - rate = cache_entry['rates'][to_lower]['rate'] + cache_age = current_time - cache_entry["timestamp"] + rate = cache_entry["rates"][to_lower]["rate"] return rate, cache_age return None @@ -57,10 +64,13 @@ class CurrencyCache: # Return stale cached data if available with self._cache_lock: - if from_lower in self._cache and to_lower in self._cache[from_lower]['rates']: + if ( + from_lower in self._cache + and to_lower in self._cache[from_lower]["rates"] + ): cache_entry = self._cache[from_lower] - cache_age = current_time - cache_entry['timestamp'] - rate = cache_entry['rates'][to_lower]['rate'] + cache_age = current_time - cache_entry["timestamp"] + rate = cache_entry["rates"][to_lower]["rate"] return rate, cache_age return None @@ -77,8 +87,8 @@ class CurrencyCache: with self._cache_lock: self._cache[from_code] = { - 'rates': rates_data, - 'timestamp': current_time + "rates": rates_data, + "timestamp": current_time, } except Exception as e: @@ -102,7 +112,7 @@ class CurrencyCache: _currency_cache = CurrencyCache() -class Units(): +class Units: def __init__(self): self.WEIGHT_CHART: dict[str, tuple[float, float]] = { "kilogram": (1, 1), @@ -198,12 +208,18 @@ class Units(): self.TEMPERATURE_CHART = { "celsius": (lambda v: v + 273.15, lambda v: v - 273.15), "c": (lambda v: v + 273.15, lambda v: v - 273.15), - "fahrenheit": (lambda v: (v - 32) * 5/9 + 273.15, lambda v: (v - 273.15) * 9/5 + 32), - "f": (lambda v: (v - 32) * 5/9 + 273.15, lambda v: (v - 273.15) * 9/5 + 32), + "fahrenheit": ( + lambda v: (v - 32) * 5 / 9 + 273.15, + lambda v: (v - 273.15) * 9 / 5 + 32, + ), + "f": ( + lambda v: (v - 32) * 5 / 9 + 273.15, + lambda v: (v - 273.15) * 9 / 5 + 32, + ), "kelvin": (lambda v: v, lambda v: v), "k": (lambda v: v, lambda v: v), - "rankine": (lambda v: v * 5/9, lambda v: v * 9/5), - "reaumur": (lambda v: v * 5/4 + 273.15, lambda v: (v - 273.15) * 4/5), + "rankine": (lambda v: v * 5 / 9, lambda v: v * 9 / 5), + "reaumur": (lambda v: v * 5 / 4 + 273.15, lambda v: (v - 273.15) * 4 / 5), } self.TIME_CHART: dict[str, float] = { @@ -404,7 +420,7 @@ class Units(): # We no longer use currency_converter here. -class Conversion(): +class Conversion: def __init__(self): self.units = Units() self.currency_cache = _currency_cache @@ -464,7 +480,12 @@ class Conversion(): # 2) If both are currency codes (e.g. “USD”, “ARS”) # we assume they are uppercase and have 3 letters. - if len(from_type) == 3 and len(to_type) == 3 and from_type.isalpha() and to_type.isalpha(): + if ( + len(from_type) == 3 + and len(to_type) == 3 + and from_type.isalpha() + and to_type.isalpha() + ): result = self._convert_currency_fast(value, from_type, to_type) if result is not None: return result @@ -474,7 +495,9 @@ class Conversion(): # 3) If it doesn't fall into any case, error. raise ValueError(f"Unsupported conversion: {from_type} to {to_type}") - def _convert_currency_fast(self, value: float, from_code: str, to_code: str) -> Optional[float]: + def _convert_currency_fast( + self, value: float, from_code: str, to_code: str + ) -> Optional[float]: """ Fast currency conversion using cached exchange rates. Returns None if rate is not available in cache. @@ -485,7 +508,9 @@ class Conversion(): return value * rate return None - def _convert_currency_via_floatrates(self, value: float, from_code: str, to_code: str) -> float: + def _convert_currency_via_floatrates( + self, value: float, from_code: str, to_code: str + ) -> float: """ Converts using the JSON from floatrates.com: - Makes GET to https://www.floatrates.com/daily/{from_lower}.json @@ -504,7 +529,11 @@ class Conversion(): data = resp.json() if to_lower not in data: - raise ValueError(f"Target currency '{to_code}' not found in floatrates response for '{from_code}'") + raise ValueError( + f"Target currency '{to_code}' not found in floatrates response for '{ + from_code + }'" + ) rate = data[to_lower]["rate"] return value * rate @@ -516,7 +545,9 @@ class Conversion(): if "and" in parts: # value unit1 and value2 unit2 _ to target_unit parts.remove("and") if len(parts) != 6: - raise ValueError("Invalid format. Expected: 'value from_type and value2 from_type2 _ to_type'") + raise ValueError( + "Invalid format. Expected: 'value from_type and value2 from_type2 _ to_type'" + ) value1, from_type1, value2, from_type2, _, to_type = parts value1, value2 = float(value1), float(value2) @@ -525,7 +556,9 @@ class Conversion(): to_type = self.clean_type(to_type) if from_type1 == from_type2: - return self.convert(value1 + value2, from_type1, to_type), to_type + addition + return self.convert( + value1 + value2, from_type1, to_type + ), to_type + addition else: res = 0 res += self.convert(value1, from_type1, to_type) @@ -533,7 +566,9 @@ class Conversion(): return res, to_type + addition else: if len(parts) != 4: - raise ValueError("Invalid format. Expected: 'value from_type _ to_type'") + raise ValueError( + "Invalid format. Expected: 'value from_type _ to_type'" + ) value, from_type, _, to_type = parts value = float(value) from_type = self.clean_type(from_type) @@ -544,7 +579,7 @@ class Conversion(): """ If it's currency (3 letters), convert to uppercase. If it ends in 's' (and is not 'celsius'), remove the 's' for - other units. """ + other units.""" if len(type) == 3 and type.isalpha(): return type.upper() if type.endswith("s") and type.lower() != "celsius": @@ -561,7 +596,9 @@ class Conversion(): """Cleanup resources.""" self.currency_cache.cleanup() - def get_currency_cache_info(self, from_code: str, to_code: str) -> Optional[Tuple[bool, float]]: + def get_currency_cache_info( + self, from_code: str, to_code: str + ) -> Optional[Tuple[bool, float]]: """ Get currency cache information for UI display. Returns (is_fresh, cache_age_seconds) or None if not cached. diff --git a/utils/custom_image.py b/utils/custom_image.py index 1aae7f6..8096a6a 100644 --- a/utils/custom_image.py +++ b/utils/custom_image.py @@ -2,9 +2,8 @@ import math from typing import cast import cairo -from gi.repository import Gtk - from fabric.widgets.image import Image +from gi.repository import Gtk class CustomImage(Image): diff --git a/utils/inhibit.py b/utils/inhibit.py index d967e4f..6b5a106 100644 --- a/utils/inhibit.py +++ b/utils/inhibit.py @@ -1,19 +1,19 @@ # From https://github.com/stwa/wayland-idle-inhibitor # License: WTFPL Version 2 -import sys -import os -import time import argparse +import os import subprocess +import sys from dataclasses import dataclass from signal import SIGINT, SIGTERM, signal from threading import Event, Timer import setproctitle from pywayland.client.display import Display -from pywayland.protocol.idle_inhibit_unstable_v1.zwp_idle_inhibit_manager_v1 import \ - ZwpIdleInhibitManagerV1 +from pywayland.protocol.idle_inhibit_unstable_v1.zwp_idle_inhibit_manager_v1 import ( + ZwpIdleInhibitManagerV1, +) from pywayland.protocol.wayland.wl_compositor import WlCompositor from pywayland.protocol.wayland.wl_registry import WlRegistryProxy from pywayland.protocol.wayland.wl_surface import WlSurface @@ -30,18 +30,20 @@ def parse_duration(duration_str: str) -> int: Examples: '1h', '30m', '45s', '1.5h', '1.5m', '30.5s', 'on', 'off' """ try: - if duration_str.lower() in ['on', 'off']: + if duration_str.lower() in ["on", "off"]: return 0 - elif duration_str.endswith('h'): + elif duration_str.endswith("h"): return int(float(duration_str[:-1]) * 3600) - elif duration_str.endswith('m'): + elif duration_str.endswith("m"): return int(float(duration_str[:-1]) * 60) - elif duration_str.endswith('s'): + elif duration_str.endswith("s"): return int(float(duration_str[:-1])) else: return int(duration_str) except ValueError: - raise ValueError("Invalid duration format. Use '1h', '30m', '45s', 'on', 'off', etc.") + raise ValueError( + "Invalid duration format. Use '1h', '30m', '45s', 'on', 'off', etc." + ) def kill_existing_inhibit_processes(): @@ -49,7 +51,7 @@ def kill_existing_inhibit_processes(): try: # Find all modus-inhibit processes except the current one output = subprocess.check_output(["pgrep", "-f", "modus-inhibit"], text=True) - pids = output.strip().split('\n') + pids = output.strip().split("\n") current_pid = str(os.getpid()) killed_count = 0 @@ -92,13 +94,19 @@ def handle_registry_global( def main() -> None: - parser = argparse.ArgumentParser(description='Inhibit system idle for a specified duration') - parser.add_argument('duration', nargs='?', default='0', - help='Duration to inhibit (e.g., "1h", "30m", "45s", "on", "off"). Use "on" for indefinite, "off" to stop.') + parser = argparse.ArgumentParser( + description="Inhibit system idle for a specified duration" + ) + parser.add_argument( + "duration", + nargs="?", + default="0", + help='Duration to inhibit (e.g., "1h", "30m", "45s", "on", "off"). Use "on" for indefinite, "off" to stop.', + ) args = parser.parse_args() # Handle "off" argument - kill existing processes and exit - if args.duration.lower() == 'off': + if args.duration.lower() == "off": kill_existing_inhibit_processes() sys.exit(0) @@ -132,7 +140,9 @@ def main() -> None: if global_registry.inhibit_manager is None: print("Error: Your Wayland compositor does not support idle inhibition.") print("This usually means either:") - print("1. Your compositor (like Hyprland) doesn't support the idle-inhibit protocol") + print( + "1. Your compositor (like Hyprland) doesn't support the idle-inhibit protocol" + ) print("2. The protocol is not enabled in your compositor") print("\nFor Hyprland, you can enable it by adding to your config:") print("misc:disable_autoreload = true") diff --git a/utils/occlusion.py b/utils/occlusion.py index 6f49e46..b81ee75 100644 --- a/utils/occlusion.py +++ b/utils/occlusion.py @@ -1,61 +1,64 @@ -import subprocess import json +import subprocess import config.data as data + def get_current_workspace(): """ Get the current workspace ID using hyprctl. """ try: result = subprocess.run( - ["hyprctl", "activeworkspace"], - capture_output=True, - text=True + ["hyprctl", "activeworkspace"], capture_output=True, text=True ) # Assume the output similar to: "ID " # Extracting the number from the output parts = result.stdout.split() for i, part in enumerate(parts): if part == "ID" and i + 1 < len(parts): - return int(parts[i+1]) + return int(parts[i + 1]) except Exception as e: print(f"Error getting current workspace: {e}") return -1 + def get_screen_dimensions(): """ Get screen dimensions from hyprctl. - + Returns: tuple: (width, height) of the monitor containing the current workspace """ try: # Get current workspace workspace_id = get_current_workspace() - + # Get monitor information result = subprocess.run( - ["hyprctl", "-j", "monitors"], - capture_output=True, - text=True + ["hyprctl", "-j", "monitors"], capture_output=True, text=True ) monitors = json.loads(result.stdout) - + # Find the monitor containing our workspace for monitor in monitors: if monitor.get("activeWorkspace", {}).get("id") == workspace_id: - return monitor.get("width", data.CURRENT_WIDTH), monitor.get("height", data.CURRENT_HEIGHT) - + return monitor.get("width", data.CURRENT_WIDTH), monitor.get( + "height", data.CURRENT_HEIGHT + ) + # Fallback to first monitor if monitors: - return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get("height", data.CURRENT_HEIGHT) + return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get( + "height", data.CURRENT_HEIGHT + ) except Exception as e: print(f"Error getting screen dimensions: {e}") - + # Default fallback values return data.CURRENT_WIDTH, data.CURRENT_HEIGHT + def check_occlusion(occlusion_region, workspace=None): """ Check if a region is occupied by any window on a given workspace. @@ -72,14 +75,14 @@ def check_occlusion(occlusion_region, workspace=None): """ if workspace is None: workspace = get_current_workspace() - + # Handle simplified side-based format if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2: side, size = occlusion_region if isinstance(side, str): # Convert side-based format to coordinates screen_width, screen_height = get_screen_dimensions() - + if side.lower() == "bottom": occlusion_region = (0, screen_height - size, screen_width, size) elif side.lower() == "top": @@ -88,7 +91,7 @@ def check_occlusion(occlusion_region, workspace=None): occlusion_region = (0, 0, size, screen_height) elif side.lower() == "right": occlusion_region = (screen_width - size, 0, size, screen_height) - + # Ensure occlusion_region is in the correct format (x, y, width, height) if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4: print(f"Invalid occlusion region format: {occlusion_region}") @@ -96,9 +99,7 @@ def check_occlusion(occlusion_region, workspace=None): try: result = subprocess.run( - ["hyprctl", "-j", "clients"], - capture_output=True, - text=True + ["hyprctl", "-j", "clients"], capture_output=True, text=True ) clients = json.loads(result.stdout) except Exception as e: @@ -131,7 +132,9 @@ def check_occlusion(occlusion_region, workspace=None): win_x2, win_y2 = x + width, y + height # Check for intersection between the window and occlusion region - if not (win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2): + if not ( + win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2 + ): return True # Occlusion region is occupied return False # No window overlaps the occlusion region diff --git a/utils/wayland.py b/utils/wayland.py index 0dcbe51..472dc40 100644 --- a/utils/wayland.py +++ b/utils/wayland.py @@ -1,4 +1,3 @@ -from gi.repository import Gdk, GObject, Gtk import re from collections.abc import Iterable from enum import Enum @@ -9,6 +8,7 @@ import gi from fabric.core.service import Property from fabric.utils.helpers import extract_css_values, get_enum_member from fabric.widgets.window import Window +from gi.repository import Gdk, GObject, Gtk from loguru import logger gi.require_version("Gtk", "3.0")