From 0f07104205973a8df700aedb8145e574293b9e16 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 30 Jul 2025 13:04:22 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(launcher):=20impr?= =?UTF-8?q?ove=20code=20structure=20and=20plugin=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 10 +- modules/dock.py | 6 +- modules/launcher/main.py | 4 - modules/launcher/plugins/bash_scripts.py | 161 +++++++++++----- modules/launcher/plugins/calculator.py | 2 - modules/launcher/plugins/clipboard.py | 6 +- modules/launcher/plugins/process.py | 1 - modules/launcher/plugins/reminders.py | 5 +- modules/launcher/plugins/screencapture.py | 5 +- modules/launcher/plugins/system.py | 43 +++-- modules/launcher/plugins/tmux.py | 118 +++++++----- modules/launcher/plugins/wallpaper.py | 33 ++-- modules/launcher/result_item.py | 4 +- modules/osd.py | 3 +- .../panel/components/enhanced_system_tray.py | 20 +- modules/panel/components/indicators.py | 55 ++++-- modules/panel/components/menu_actions.py | 14 +- modules/panel/components/menu_contents.py | 40 ++-- modules/panel/components/menubar.py | 90 ++++----- .../panel/components/recording_indicator.py | 77 ++++++++ modules/panel/main.py | 9 +- modules/switcher.py | 8 +- services/battery.py | 2 +- services/mpris.py | 30 +-- services/network.py | 8 +- utils/circle_image.py | 16 +- utils/conversion.py | 7 +- utils/icon_resolver.py | 8 +- utils/popup_window.py | 143 -------------- utils/wayland.py | 52 +++--- utils/wiggle_bar.py | 174 ------------------ 31 files changed, 536 insertions(+), 618 deletions(-) create mode 100644 modules/panel/components/recording_indicator.py delete mode 100644 utils/popup_window.py delete mode 100644 utils/wiggle_bar.py diff --git a/main.py b/main.py index 43265c6..5fdb6b0 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,16 @@ import setproctitle -from fabric import Application -from fabric.utils import get_relative_path, monitor_file from loguru import logger from config.data import APP_NAME +from fabric import Application +from fabric.utils import get_relative_path, monitor_file +from modules.dock import Dock +from modules.launcher.main import Launcher +from modules.osd import OSD # from modules.corners import Corners from modules.panel.main import Panel -from modules.dock import Dock from modules.switcher import ApplicationSwitcher -from modules.launcher.main import Launcher -from modules.osd import OSD for log in [ "fabric.hyprland.widgets", diff --git a/modules/dock.py b/modules/dock.py index 4ffbce8..f87cd3e 100644 --- a/modules/dock.py +++ b/modules/dock.py @@ -2,6 +2,7 @@ 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, @@ -17,13 +18,12 @@ from fabric.widgets.eventbox import EventBox from fabric.widgets.image import Image from fabric.widgets.revealer import Revealer from gi.repository import Gdk, GLib, Gtk - -import config.data as data -from modules.corners import MyCorner from utils.icon_resolver import IconResolver from utils.occlusion import check_occlusion from utils.wayland import WaylandWindow as Window +from modules.corners import MyCorner + def read_config(): """Read and return the full configuration from the JSON file, handling missing file.""" diff --git a/modules/launcher/main.py b/modules/launcher/main.py index 46a9dca..4bf0bb6 100644 --- a/modules/launcher/main.py +++ b/modules/launcher/main.py @@ -1197,8 +1197,6 @@ class Launcher(Window): self.focus_mode = "search" self._focus_search_entry_without_selection() - - def _focus_search_entry_without_selection(self): """Focus search entry and position cursor at end without selecting text.""" # First grab focus @@ -1245,8 +1243,6 @@ class Launcher(Window): # Also try with a small delay as backup GLib.timeout_add(CURSOR_POSITION_DELAY_MS, clear_selection) - - def _navigate_results_forward(self): """Navigate to next result or wrap around to first result.""" if self.results and self.selected_index < len(self.results) - 1: diff --git a/modules/launcher/plugins/bash_scripts.py b/modules/launcher/plugins/bash_scripts.py index 7a242a6..dff923b 100644 --- a/modules/launcher/plugins/bash_scripts.py +++ b/modules/launcher/plugins/bash_scripts.py @@ -82,6 +82,7 @@ class BashScriptsPlugin(PluginBase): """Get reference to the battery service.""" try: from services.battery import Battery + self._battery_service = Battery() except Exception as e: print(f"BashScriptsPlugin: Error getting battery service: {e}") @@ -95,7 +96,10 @@ class BashScriptsPlugin(PluginBase): try: # Check if profile proxy is available - if not hasattr(self._battery_service, '_profile_proxy') or not self._battery_service._profile_proxy: + if ( + not hasattr(self._battery_service, "_profile_proxy") + or not self._battery_service._profile_proxy + ): print("BashScriptsPlugin: Power profile proxy not available") return False @@ -114,7 +118,7 @@ class BashScriptsPlugin(PluginBase): profile_mapping = { "power-saver": ["power-saver", "powersave", "power_saver"], "balanced": ["balanced", "balance"], - "performance": ["performance", "performance-mode"] + "performance": ["performance", "performance-mode"], } # Find the actual profile name @@ -129,18 +133,20 @@ class BashScriptsPlugin(PluginBase): if actual_profile: self._battery_service._profile_proxy.ActiveProfile = actual_profile - print(f"BashScriptsPlugin: Successfully set power profile to {actual_profile}") + print( + f"BashScriptsPlugin: Successfully set power profile to {actual_profile}" + ) return True else: - print(f"BashScriptsPlugin: No matching profile found for '{profile}' in available profiles: {available_profiles}") + print( + f"BashScriptsPlugin: No matching profile found for '{profile}' in available profiles: {available_profiles}" + ) return False except Exception as e: print(f"BashScriptsPlugin: Error setting power profile: {e}") return False - - def _load_scripts_cache(self): """Load scripts cache from JSON file.""" try: @@ -150,7 +156,9 @@ class BashScriptsPlugin(PluginBase): self._scripts_cache = cache_data.get("scripts", {}) self._last_cache_update = cache_data.get("last_update", 0) else: - print("BashScriptsPlugin: No cache file found, will build cache in background") + print( + "BashScriptsPlugin: No cache file found, will build cache in background" + ) except Exception as e: print(f"BashScriptsPlugin: Error loading scripts cache: {e}") self._scripts_cache = {} @@ -172,16 +180,17 @@ class BashScriptsPlugin(PluginBase): def _start_background_cache_update(self): """Start background thread to update scripts cache.""" current_time = time.time() - + # Check if cache needs updating - if (current_time - self._last_cache_update > self._cache_update_interval or - not self._scripts_cache): - + if ( + current_time - self._last_cache_update > self._cache_update_interval + or not self._scripts_cache + ): + if not self._cache_building: self._cache_building = True self._cache_thread = threading.Thread( - target=self._build_scripts_cache_background, - daemon=True + target=self._build_scripts_cache_background, daemon=True ) self._cache_thread.start() @@ -192,11 +201,15 @@ class BashScriptsPlugin(PluginBase): new_cache = {} # Scan Modus scripts directory for discovered scripts - if os.path.exists(self.modus_scripts_dir) and os.path.isdir(self.modus_scripts_dir): + if os.path.exists(self.modus_scripts_dir) and os.path.isdir( + self.modus_scripts_dir + ): try: self._scan_directory_for_scripts(self.modus_scripts_dir, new_cache) except (PermissionError, FileNotFoundError, OSError) as e: - print(f"BashScriptsPlugin: Error scanning Modus scripts directory: {e}") + print( + f"BashScriptsPlugin: Error scanning Modus scripts directory: {e}" + ) # Update cache atomically self._scripts_cache = new_cache @@ -228,11 +241,13 @@ class BashScriptsPlugin(PluginBase): cache[script_name] = { "path": script_path, "name": script_name, - "description": self._get_script_description(script_path), + "description": self._get_script_description( + script_path + ), "type": "discovered", "executable": os.access(script_path, os.X_OK), "args": [], - "category": os.path.basename(directory) + "category": os.path.basename(directory), } except (PermissionError, FileNotFoundError, OSError) as e: @@ -244,14 +259,16 @@ class BashScriptsPlugin(PluginBase): """Check if a file is a bash script.""" try: # Check file extension first (most common case) - if file_path.endswith(('.sh', '.bash')): + if file_path.endswith((".sh", ".bash")): return True # For files without extension, check shebang try: - with open(file_path, 'rb') as f: - first_line = f.readline(100).decode('utf-8', errors='ignore') - if first_line.startswith('#!') and ('bash' in first_line or 'sh' in first_line): + with open(file_path, "rb") as f: + first_line = f.readline(100).decode("utf-8", errors="ignore") + if first_line.startswith("#!") and ( + "bash" in first_line or "sh" in first_line + ): return True except (PermissionError, FileNotFoundError, UnicodeDecodeError): pass @@ -263,13 +280,13 @@ class BashScriptsPlugin(PluginBase): def _get_script_description(self, script_path: str) -> str: """Extract description from script comments.""" try: - with open(script_path, 'r', encoding='utf-8', errors='ignore') as f: + with open(script_path, "r", encoding="utf-8", errors="ignore") as f: lines = f.readlines() # Look for description in first few comment lines for line in lines[:10]: line = line.strip() - if line.startswith('#') and not line.startswith('#!'): + if line.startswith("#") and not line.startswith("#!"): # Remove leading # and whitespace desc = line[1:].strip() if desc and len(desc) > 5: # Meaningful description @@ -284,7 +301,9 @@ class BashScriptsPlugin(PluginBase): query = query_string.strip() # Start background update if needed (non-blocking) - if not self._scripts_cache or (time.time() - self._last_cache_update > self._cache_update_interval): + if not self._scripts_cache or ( + time.time() - self._last_cache_update > self._cache_update_interval + ): self._start_background_cache_update() results = [] @@ -313,11 +332,15 @@ class BashScriptsPlugin(PluginBase): max_results = 20 # Sort scripts by name for consistent ordering - sorted_scripts = sorted(self._scripts_cache.items(), key=lambda x: x[1].get("name", "")) + sorted_scripts = sorted( + self._scripts_cache.items(), key=lambda x: x[1].get("name", "") + ) # Add scripts (limit to max_results) for script_name, script_info in sorted_scripts: - script_results = self._create_script_results_with_args(script_name, script_info, 0.8) + script_results = self._create_script_results_with_args( + script_name, script_info, 0.8 + ) for script_result in script_results: if len(results) < max_results: results.append(script_result) @@ -345,7 +368,10 @@ class BashScriptsPlugin(PluginBase): description_lower = script_info.get("description", "").lower() # Skip if no match at all - if query_lower not in script_name_lower and query_lower not in description_lower: + if ( + query_lower not in script_name_lower + and query_lower not in description_lower + ): continue # Categorize matches @@ -359,11 +385,15 @@ class BashScriptsPlugin(PluginBase): description_matches.append((script_name, script_info, 0.5)) # Combine results in priority order - all_matches = exact_matches + prefix_matches + partial_matches + description_matches + all_matches = ( + exact_matches + prefix_matches + partial_matches + description_matches + ) # Convert to Result objects for script_name, script_info, relevance in all_matches: - script_results = self._create_script_results_with_args(script_name, script_info, relevance) + script_results = self._create_script_results_with_args( + script_name, script_info, relevance + ) for script_result in script_results: if len(results) < max_results: results.append(script_result) @@ -387,10 +417,12 @@ class BashScriptsPlugin(PluginBase): description_lower = info["description"].lower() # Check for exact match or partial match - if (query_lower == cmd_lower or - cmd_lower.startswith(query_lower) or - query_lower in cmd_lower or - query_lower in description_lower): + if ( + query_lower == cmd_lower + or cmd_lower.startswith(query_lower) + or query_lower in cmd_lower + or query_lower in description_lower + ): relevance = 1.0 if query_lower == cmd_lower else 0.9 @@ -401,7 +433,11 @@ class BashScriptsPlugin(PluginBase): action=self._create_power_action(info["profile"]), relevance=relevance, plugin_name=self.display_name, - data={"command": cmd, "profile": info["profile"], "type": "power_manager"} + data={ + "command": cmd, + "profile": info["profile"], + "type": "power_manager", + }, ) results.append(result) @@ -419,7 +455,11 @@ class BashScriptsPlugin(PluginBase): action=self._create_power_action(info["profile"]), relevance=0.8, plugin_name=self.display_name, - data={"command": cmd, "profile": info["profile"], "type": "power_manager"} + data={ + "command": cmd, + "profile": info["profile"], + "type": "power_manager", + }, ) results.append(result) @@ -427,6 +467,7 @@ class BashScriptsPlugin(PluginBase): def _create_power_action(self, profile: str): """Create an action function for setting power profile.""" + def action(): success = self._set_power_profile(profile) if success: @@ -439,16 +480,21 @@ class BashScriptsPlugin(PluginBase): if app and hasattr(app, "launcher"): launcher = app.launcher if launcher and hasattr(launcher, "search_entry"): + def clear_search(): launcher.search_entry.set_text("") return False + # Use a small delay to ensure the action completes first GLib.timeout_add(50, clear_search) except Exception as e: print(f"BashScriptsPlugin: Could not clear search query: {e}") + return action - def _create_script_result(self, script_name: str, script_info: Dict, relevance: float) -> Result: + def _create_script_result( + self, script_name: str, script_info: Dict, relevance: float + ) -> Result: """Create a Result object for a script.""" script_path = script_info.get("path", "") description = script_info.get("description", "") @@ -465,7 +511,9 @@ class BashScriptsPlugin(PluginBase): if not executable: subtitle_parts.append("(not executable)") - subtitle = " | ".join(subtitle_parts) if subtitle_parts else f"Execute: {script_name}" + subtitle = ( + " | ".join(subtitle_parts) if subtitle_parts else f"Execute: {script_name}" + ) # Choose icon based on script type and status if not executable: @@ -482,10 +530,16 @@ class BashScriptsPlugin(PluginBase): action=self._create_script_action(script_name, script_info), relevance=relevance, plugin_name=self.display_name, - data={"script_name": script_name, "script_path": script_path, "type": script_type} + data={ + "script_name": script_name, + "script_path": script_path, + "type": script_type, + }, ) - def _create_script_results_with_args(self, script_name: str, script_info: Dict, relevance: float) -> List[Result]: + def _create_script_results_with_args( + self, script_name: str, script_info: Dict, relevance: float + ) -> List[Result]: """Create multiple Result objects for scripts that support arguments.""" results = [] @@ -495,7 +549,7 @@ class BashScriptsPlugin(PluginBase): variants = [ ("-rgb", "Pick RGB color"), ("-hex", "Pick HEX color"), - ("-hsv", "Pick HSV color") + ("-hsv", "Pick HSV color"), ] for arg, desc in variants: @@ -503,32 +557,48 @@ class BashScriptsPlugin(PluginBase): title=f"{script_name} {arg}", subtitle=f"{desc} | [scripts]", icon_markup=icons.terminal, - action=self._create_script_action_with_args(script_name, script_info, [arg]), - relevance=relevance + 0.1, # Slightly higher relevance for specific variants + action=self._create_script_action_with_args( + script_name, script_info, [arg] + ), + relevance=relevance + + 0.1, # Slightly higher relevance for specific variants plugin_name=self.display_name, - data={"script_name": script_name, "script_path": script_info.get("path", ""), "type": script_info.get("type", "discovered"), "args": [arg]} + data={ + "script_name": script_name, + "script_path": script_info.get("path", ""), + "type": script_info.get("type", "discovered"), + "args": [arg], + }, ) results.append(variant_result) else: # For other scripts, create the basic result - basic_result = self._create_script_result(script_name, script_info, relevance) + basic_result = self._create_script_result( + script_name, script_info, relevance + ) results.append(basic_result) return results def _create_script_action(self, script_name: str, script_info: Dict): """Create an action function for executing a script.""" + def action(): self._execute_script(script_name, script_info) + return action - def _create_script_action_with_args(self, script_name: str, script_info: Dict, args: List[str]): + def _create_script_action_with_args( + self, script_name: str, script_info: Dict, args: List[str] + ): """Create an action function for executing a script with specific arguments.""" + def action(): # Create a modified script_info with the specific arguments modified_script_info = script_info.copy() modified_script_info["args"] = args self._execute_script(script_name, modified_script_info) + return action def _execute_script(self, script_name: str, script_info: Dict): @@ -549,4 +619,3 @@ class BashScriptsPlugin(PluginBase): except Exception as e: print(f"BashScriptsPlugin: Error executing script '{script_name}': {e}") - diff --git a/modules/launcher/plugins/calculator.py b/modules/launcher/plugins/calculator.py index 1468154..e90e468 100644 --- a/modules/launcher/plugins/calculator.py +++ b/modules/launcher/plugins/calculator.py @@ -95,8 +95,6 @@ class CalculatorPlugin(PluginBase): self._conversion_cache[cache_key] = result subtitle = f"{value} {from_unit} = {result:.6g} {to_unit}" - - return [ Result( title=f"{result:.6g} {to_unit}", diff --git a/modules/launcher/plugins/clipboard.py b/modules/launcher/plugins/clipboard.py index d86bd5a..e332d19 100644 --- a/modules/launcher/plugins/clipboard.py +++ b/modules/launcher/plugins/clipboard.py @@ -380,9 +380,9 @@ class ClipboardPlugin(PluginBase): result = Result( title=display_text, subtitle="Text from clipboard", - description=content - if len(content) <= 100 - else content[:97] + "...", + description=( + content if len(content) <= 100 else content[:97] + "..." + ), icon_name="edit-paste", relevance=relevance, plugin_name=self.name, diff --git a/modules/launcher/plugins/process.py b/modules/launcher/plugins/process.py index d94bfbe..0384bfb 100644 --- a/modules/launcher/plugins/process.py +++ b/modules/launcher/plugins/process.py @@ -342,7 +342,6 @@ class ProcessPlugin(PluginBase): if self._kill_process(pid, silent=True): killed_count += 1 - except Exception as e: print(f"✗ Error killing process group: {e}") diff --git a/modules/launcher/plugins/reminders.py b/modules/launcher/plugins/reminders.py index bcfbb26..947f1bd 100644 --- a/modules/launcher/plugins/reminders.py +++ b/modules/launcher/plugins/reminders.py @@ -408,8 +408,9 @@ class RemindersPlugin(PluginBase): target_time_str })", icon_markup=icons.timer_on, - action=lambda ts=time_str, - msg=message: self._create_and_confirm_reminder(ts, msg), + action=lambda ts=time_str, msg=message: self._create_and_confirm_reminder( + ts, msg + ), relevance=1.0, plugin_name=self.display_name, data={ diff --git a/modules/launcher/plugins/screencapture.py b/modules/launcher/plugins/screencapture.py index 3db965b..8cbea99 100644 --- a/modules/launcher/plugins/screencapture.py +++ b/modules/launcher/plugins/screencapture.py @@ -466,8 +466,9 @@ class ScreencapturePlugin(PluginBase): title=f"Convert {file_param} to {format_type.upper()}", subtitle=f"Convert specified file to {format_type} format", icon_markup=screenrecord, - action=lambda fp=file_param, - ft=format_type: self._run_script_with_file(ft, fp), + action=lambda fp=file_param, ft=format_type: self._run_script_with_file( + ft, fp + ), relevance=1.0, plugin_name=self.display_name, ) diff --git a/modules/launcher/plugins/system.py b/modules/launcher/plugins/system.py index 65142f1..c8bb04d 100644 --- a/modules/launcher/plugins/system.py +++ b/modules/launcher/plugins/system.py @@ -57,7 +57,9 @@ class SystemPlugin(PluginBase): self._bin_cache = set(cache_data.get("binaries", [])) self._last_bin_update = cache_data.get("last_update", 0) else: - print("SystemPlugin: No cache file found, will build cache in background") + print( + "SystemPlugin: No cache file found, will build cache in background" + ) except Exception as e: print(f"SystemPlugin: Error loading binary cache: {e}") self._bin_cache = set() @@ -72,7 +74,7 @@ class SystemPlugin(PluginBase): cache_data = { "binaries": sorted(list(self._bin_cache)), "last_update": self._last_bin_update, - "cache_version": "1.0" + "cache_version": "1.0", } with open(self.bin_cache_file, "w", encoding="utf-8") as f: @@ -85,14 +87,15 @@ class SystemPlugin(PluginBase): current_time = time.time() # Check if cache needs updating - if (current_time - self._last_bin_update > self._bin_update_interval or - not self._bin_cache): + if ( + current_time - self._last_bin_update > self._bin_update_interval + or not self._bin_cache + ): if not self._cache_building: self._cache_building = True self._cache_thread = threading.Thread( - target=self._build_bin_cache_background, - daemon=True + target=self._build_bin_cache_background, daemon=True ) self._cache_thread.start() @@ -113,8 +116,9 @@ class SystemPlugin(PluginBase): # Use os.scandir for better performance than os.listdir with os.scandir(path) as entries: for entry in entries: - if (entry.is_file(follow_symlinks=False) and - os.access(entry.path, os.X_OK)): + if entry.is_file(follow_symlinks=False) and os.access( + entry.path, os.X_OK + ): new_cache.add(entry.name) except (PermissionError, FileNotFoundError, OSError): continue @@ -126,7 +130,6 @@ class SystemPlugin(PluginBase): # Save to disk self._save_bin_cache() - except Exception as e: print(f"SystemPlugin: Error building binary cache: {e}") finally: @@ -151,7 +154,9 @@ class SystemPlugin(PluginBase): # Check system binaries # Start background update if needed (non-blocking) - but only if cache is empty or very old - if not self._bin_cache or (time.time() - self._last_bin_update > self._bin_update_interval): + if not self._bin_cache or ( + time.time() - self._last_bin_update > self._bin_update_interval + ): self._start_background_cache_update() # Optimize search with early termination and result limiting @@ -174,19 +179,25 @@ class SystemPlugin(PluginBase): display_command = full_command command_to_execute = full_command relevance = 1.0 - exact_matches.append((binary, display_command, command_to_execute, relevance)) + exact_matches.append( + (binary, display_command, command_to_execute, relevance) + ) elif binary_lower.startswith(binary_query): # Prefix match - high priority display_command = binary command_to_execute = binary relevance = 0.9 - prefix_matches.append((binary, display_command, command_to_execute, relevance)) + prefix_matches.append( + (binary, display_command, command_to_execute, relevance) + ) else: # Partial match - lower priority display_command = binary command_to_execute = binary relevance = 0.7 - partial_matches.append((binary, display_command, command_to_execute, relevance)) + partial_matches.append( + (binary, display_command, command_to_execute, relevance) + ) # Early termination if we have enough good matches if len(exact_matches) + len(prefix_matches) >= max_results: @@ -196,7 +207,9 @@ class SystemPlugin(PluginBase): all_matches = exact_matches + prefix_matches + partial_matches # Convert to Result objects (limit to max_results) - for binary, display_command, command_to_execute, relevance in all_matches[:max_results]: + for binary, display_command, command_to_execute, relevance in all_matches[ + :max_results + ]: result = Result( title=display_command, subtitle=f"Execute: {display_command}", @@ -210,8 +223,6 @@ class SystemPlugin(PluginBase): return results # Already sorted by priority - - def _create_action(self, command: Union[str, List[str]]): """Create an action function for the given command.""" diff --git a/modules/launcher/plugins/tmux.py b/modules/launcher/plugins/tmux.py index 23e1267..3fba132 100644 --- a/modules/launcher/plugins/tmux.py +++ b/modules/launcher/plugins/tmux.py @@ -46,8 +46,7 @@ class TmuxPlugin(PluginBase): """Start background thread to refresh session cache.""" if not self.refresh_thread or not self.refresh_thread.is_alive(): self.refresh_thread = threading.Thread( - target=self._refresh_sessions_background, - daemon=True + target=self._refresh_sessions_background, daemon=True ) self.refresh_thread.start() @@ -59,7 +58,7 @@ class TmuxPlugin(PluginBase): if current_time - self._last_cache_update > self._cache_ttl: self._sessions_cache = self._get_tmux_sessions() self._last_cache_update = current_time - + # Sleep for a short interval self.stop_refresh.wait(1) except Exception as e: @@ -73,12 +72,18 @@ class TmuxPlugin(PluginBase): ["tmux", "list-sessions", "-F", "#{session_name}"], capture_output=True, text=True, - timeout=5 + timeout=5, ) if result.returncode == 0: - return [s.strip() for s in result.stdout.strip().split('\n') if s.strip()] + return [ + s.strip() for s in result.stdout.strip().split("\n") if s.strip() + ] return [] - except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError) as e: + except ( + subprocess.TimeoutExpired, + subprocess.CalledProcessError, + FileNotFoundError, + ) as e: print(f"TmuxPlugin: Error getting tmux sessions: {e}") return [] @@ -92,28 +97,32 @@ class TmuxPlugin(PluginBase): if current_time - self._last_cache_update > self._cache_ttl: self._sessions_cache = self._get_tmux_sessions() self._last_cache_update = current_time - + sessions = self._sessions_cache # Handle specific commands if query.startswith("new ") or query.startswith("create "): session_name = query.split(" ", 1)[1].strip() if " " in query else "" results.append(self._create_new_session_result(session_name)) - + elif query.startswith("kill ") or query.startswith("delete "): session_name = query.split(" ", 1)[1].strip() if " " in query else "" if session_name: - matching_sessions = [s for s in sessions if session_name.lower() in s.lower()] + matching_sessions = [ + s for s in sessions if session_name.lower() in s.lower() + ] for session in matching_sessions: results.append(self._create_kill_session_result(session)) - + elif query.startswith("rename "): parts = query.split(" ", 2) if len(parts) >= 3: old_name, new_name = parts[1], parts[2] if old_name in sessions: - results.append(self._create_rename_session_result(old_name, new_name)) - + results.append( + self._create_rename_session_result(old_name, new_name) + ) + else: # Show existing sessions for attachment if sessions: @@ -122,13 +131,19 @@ class TmuxPlugin(PluginBase): filtered_sessions = [s for s in sessions if query in s.lower()] else: filtered_sessions = sessions - + for session in filtered_sessions: results.append(self._create_attach_session_result(session)) - + # Always show option to create new session if not query or "new" in query or "create" in query: - results.append(self._create_new_session_result(query if query and not any(cmd in query for cmd in ["new", "create"]) else "")) + results.append( + self._create_new_session_result( + query + if query and not any(cmd in query for cmd in ["new", "create"]) + else "" + ) + ) return results @@ -140,7 +155,7 @@ class TmuxPlugin(PluginBase): icon_markup=icons.terminal, action=lambda: self._attach_to_session(session_name), relevance=0.9, - data={"type": "attach", "session": session_name} + data={"type": "attach", "session": session_name}, ) def _create_new_session_result(self, session_name: str = "") -> Result: @@ -152,7 +167,7 @@ class TmuxPlugin(PluginBase): icon_markup=icons.plus, action=lambda: self._create_session(session_name), relevance=0.8, - data={"type": "create", "session": session_name} + data={"type": "create", "session": session_name}, ) def _create_kill_session_result(self, session_name: str) -> Result: @@ -163,7 +178,7 @@ class TmuxPlugin(PluginBase): icon_markup=icons.trash, action=lambda: self._kill_session(session_name), relevance=0.7, - data={"type": "kill", "session": session_name} + data={"type": "kill", "session": session_name}, ) def _create_rename_session_result(self, old_name: str, new_name: str) -> Result: @@ -174,14 +189,15 @@ class TmuxPlugin(PluginBase): icon_markup=icons.config, action=lambda: self._rename_session(old_name, new_name), relevance=0.6, - data={"type": "rename", "old_session": old_name, "new_session": new_name} + data={"type": "rename", "old_session": old_name, "new_session": new_name}, ) - def _attach_to_session(self, session_name: str): """Attach to an existing tmux session.""" try: - terminal_cmd = self._get_terminal_command(f"tmux attach-session -t '{session_name}'") + terminal_cmd = self._get_terminal_command( + f"tmux attach-session -t '{session_name}'" + ) exec_shell_command_async(terminal_cmd) print(f"TmuxPlugin: Attaching to session '{session_name}'") except Exception as e: @@ -197,25 +213,25 @@ class TmuxPlugin(PluginBase): while str(counter) in existing_sessions: counter += 1 session_name = str(counter) - + # Clean the session name clean_name = session_name.strip().replace(" ", "_") - + # Create session subprocess.run( - ["tmux", "new-session", "-d", "-s", clean_name], - check=True, - timeout=10 + ["tmux", "new-session", "-d", "-s", clean_name], check=True, timeout=10 ) - + # Launch terminal and attach - terminal_cmd = self._get_terminal_command(f"tmux attach-session -t '{clean_name}'") + terminal_cmd = self._get_terminal_command( + f"tmux attach-session -t '{clean_name}'" + ) exec_shell_command_async(terminal_cmd) - + # Refresh cache self._sessions_cache = self._get_tmux_sessions() self._last_cache_update = time.time() - + print(f"TmuxPlugin: Created and attached to session '{clean_name}'") except Exception as e: print(f"TmuxPlugin: Error creating session '{session_name}': {e}") @@ -224,15 +240,13 @@ class TmuxPlugin(PluginBase): """Kill a tmux session.""" try: subprocess.run( - ["tmux", "kill-session", "-t", session_name], - check=True, - timeout=10 + ["tmux", "kill-session", "-t", session_name], check=True, timeout=10 ) - + # Refresh cache self._sessions_cache = self._get_tmux_sessions() self._last_cache_update = time.time() - + print(f"TmuxPlugin: Killed session '{session_name}'") except Exception as e: print(f"TmuxPlugin: Error killing session '{session_name}': {e}") @@ -244,32 +258,39 @@ class TmuxPlugin(PluginBase): subprocess.run( ["tmux", "rename-session", "-t", old_name, clean_name], check=True, - timeout=10 + timeout=10, ) - + # Refresh cache self._sessions_cache = self._get_tmux_sessions() self._last_cache_update = time.time() - + print(f"TmuxPlugin: Renamed session '{old_name}' to '{clean_name}'") except Exception as e: - print(f"TmuxPlugin: Error renaming session '{old_name}' to '{new_name}': {e}") + print( + f"TmuxPlugin: Error renaming session '{old_name}' to '{new_name}': {e}" + ) def _get_terminal_command(self, cmd: str) -> str: """Get terminal command based on configured terminal or available terminals.""" # First try to use the configured terminal command - if hasattr(data, 'TERMINAL_COMMAND') and data.TERMINAL_COMMAND: + if hasattr(data, "TERMINAL_COMMAND") and data.TERMINAL_COMMAND: parts = data.TERMINAL_COMMAND.split() terminal = parts[0] - + try: # Check if the configured terminal is available - subprocess.run(["which", terminal], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run( + ["which", terminal], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) return f"{data.TERMINAL_COMMAND} {cmd}" except subprocess.CalledProcessError: # If configured terminal is not available, fall back to defaults pass - + # Fallback to checking available terminals terminals = [ ("kitty", f"kitty -e {cmd}"), @@ -279,14 +300,19 @@ class TmuxPlugin(PluginBase): ("konsole", f"konsole -e {cmd}"), ("xfce4-terminal", f"xfce4-terminal -e '{cmd}'"), ] - + for term, term_cmd in terminals: try: # Check if terminal is available - subprocess.run(["which", term], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run( + ["which", term], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) return term_cmd except subprocess.CalledProcessError: continue - + # Default fallback return f"kitty -e {cmd}" diff --git a/modules/launcher/plugins/wallpaper.py b/modules/launcher/plugins/wallpaper.py index e349a80..64deea1 100644 --- a/modules/launcher/plugins/wallpaper.py +++ b/modules/launcher/plugins/wallpaper.py @@ -132,6 +132,7 @@ class WallpaperPlugin(PluginBase): if app and hasattr(app, "launcher"): launcher = app.launcher if launcher and hasattr(launcher, "search_entry"): + def clear_and_refresh(): # Clear the search entry to just the trigger launcher.search_entry.set_text("wall ") @@ -150,11 +151,9 @@ class WallpaperPlugin(PluginBase): import gc for obj in gc.get_objects(): - if ( - hasattr(obj, "__class__") - and obj.__class__.__name__ == "Launcher" - ): + if hasattr(obj, "__class__") and obj.__class__.__name__ == "Launcher": if hasattr(obj, "search_entry") and hasattr(obj, "_perform_search"): + def clear_and_refresh(): obj.search_entry.set_text("wall ") obj.search_entry.set_position(-1) @@ -554,8 +553,9 @@ 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={ @@ -595,10 +595,11 @@ class WallpaperPlugin(PluginBase): status_text }", icon_markup=icons.palette, - action=lambda c=hex_color, - s=scheme: self._apply_hex_color_direct(c, s) - if not matugen_enabled - else None, + action=lambda c=hex_color, s=scheme: ( + self._apply_hex_color_direct(c, s) + if not matugen_enabled + else None + ), relevance=0.9, plugin_name=self.display_name, data={ @@ -683,11 +684,13 @@ class WallpaperPlugin(PluginBase): title=f"Random Hex Color{indicator_text}", subtitle=f"Generate and apply random color • {status_text}", icon_markup=icons.palette, - action=lambda s=scheme: self._apply_hex_color_direct( - self._generate_random_hex_color(), s - ) - if not matugen_enabled - else None, + action=lambda s=scheme: ( + self._apply_hex_color_direct( + self._generate_random_hex_color(), s + ) + if not matugen_enabled + else None + ), relevance=0.8, plugin_name=self.display_name, data={ diff --git a/modules/launcher/result_item.py b/modules/launcher/result_item.py index 27dcf82..e18fd9e 100644 --- a/modules/launcher/result_item.py +++ b/modules/launcher/result_item.py @@ -25,7 +25,9 @@ class ResultItem(EventBox): """Emitted when result is hovered.""" pass - def __init__(self, result: Result, selected: bool = False, index: int = 0, **kwargs): + def __init__( + self, result: Result, selected: bool = False, index: int = 0, **kwargs + ): super().__init__(name="launcher-result-item", **kwargs) self.result = result diff --git a/modules/osd.py b/modules/osd.py index 3667080..74ffb7d 100644 --- a/modules/osd.py +++ b/modules/osd.py @@ -1,14 +1,13 @@ 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/panel/components/enhanced_system_tray.py b/modules/panel/components/enhanced_system_tray.py index cee539f..d171fa6 100644 --- a/modules/panel/components/enhanced_system_tray.py +++ b/modules/panel/components/enhanced_system_tray.py @@ -52,10 +52,10 @@ def patched_do_update_properties(self, *_): def _try_load_icon_from_path(self, icon_path): try: # Check if it's a file path and handle it directly - if os.path.isabs(icon_path) or '/' in icon_path: + if os.path.isabs(icon_path) or "/" in icon_path: # Try to load as SVG from the original path if it exists if os.path.exists(icon_path): - if icon_path.lower().endswith('.svg'): + if icon_path.lower().endswith(".svg"): # Load SVG directly pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( icon_path, self._icon_size, self._icon_size @@ -80,11 +80,13 @@ def _try_load_icon_from_path(self, icon_path): # Remove extension for theme lookup name_without_ext = os.path.splitext(filename)[0] default_theme = Gtk.IconTheme.get_default() - + # Try filename without extension if default_theme.has_icon(name_without_ext): pixbuf = default_theme.load_icon( - name_without_ext, self._icon_size, Gtk.IconLookupFlags.FORCE_SIZE + name_without_ext, + self._icon_size, + Gtk.IconLookupFlags.FORCE_SIZE, ) if pixbuf: self._image.set_from_pixbuf(pixbuf) @@ -109,9 +111,9 @@ def _try_load_icon_from_path(self, icon_path): "/usr/local/share/icons", "/usr/local/share/pixmaps", os.path.expanduser("~/.local/share/icons"), - os.path.expanduser("~/.icons") + os.path.expanduser("~/.icons"), ] - + filename = os.path.basename(icon_path) for icon_dir in common_icon_dirs: potential_path = os.path.join(icon_dir, filename) @@ -129,7 +131,7 @@ def _try_load_icon_from_path(self, icon_path): except Exception: pass - + return False @@ -146,11 +148,11 @@ def apply_enhanced_system_tray(): # Store original method global original_do_update_properties original_do_update_properties = SystemTrayItem.do_update_properties - + # Attach helper methods to SystemTrayItem class SystemTrayItem._try_load_icon_from_path = _try_load_icon_from_path SystemTrayItem._set_tooltip = _set_tooltip - + # Replace the do_update_properties method SystemTrayItem.do_update_properties = patched_do_update_properties diff --git a/modules/panel/components/indicators.py b/modules/panel/components/indicators.py index 421cf79..a2155d2 100644 --- a/modules/panel/components/indicators.py +++ b/modules/panel/components/indicators.py @@ -54,26 +54,36 @@ class WifiIndicator(Button): self.network.ethernet_device and self.network.ethernet_device.state == "activated" ): - self.wifi_icon.set_from_file(get_relative_path("../../../config/assets/icons/wifi.svg")) + self.wifi_icon.set_from_file( + get_relative_path("../../../config/assets/icons/wifi.svg") + ) tooltip = f"Connected to Ethernet ({self.network.ethernet_device.speed})" self.set_tooltip_text(tooltip) return if not self.network.wifi_device: - self.wifi_icon.set_from_file(get_relative_path("../../../config/assets/icons/wifi-off.svg")) + self.wifi_icon.set_from_file( + get_relative_path("../../../config/assets/icons/wifi-off.svg") + ) tooltip = "No WiFi device found" elif not self.network.wifi_device.wireless_enabled: - self.wifi_icon.set_from_file(get_relative_path("../../../config/assets/icons/wifi-off.svg")) + self.wifi_icon.set_from_file( + get_relative_path("../../../config/assets/icons/wifi-off.svg") + ) tooltip = "WiFi disabled" else: active_ap = self.network.wifi_device.active_access_point if active_ap: strength = active_ap.strength # Use wifi.svg for all connected states since we don't have signal strength icons - self.wifi_icon.set_from_file(get_relative_path("../../../config/assets/icons/wifi.svg")) + self.wifi_icon.set_from_file( + get_relative_path("../../../config/assets/icons/wifi.svg") + ) tooltip = f"Connected to {active_ap.ssid} ({strength}%)" else: - self.wifi_icon.set_from_file(get_relative_path("../../../config/assets/icons/wifi-off.svg")) + self.wifi_icon.set_from_file( + get_relative_path("../../../config/assets/icons/wifi-off.svg") + ) tooltip = "Not connected" self.set_tooltip_text(tooltip) @@ -127,12 +137,16 @@ class BluetoothIndicator(Button): def update_state(self): if not self.bluetooth.enabled: - self.bt_icon.set_from_file(get_relative_path("../../../config/assets/icons/bluetooth-off.svg")) + self.bt_icon.set_from_file( + get_relative_path("../../../config/assets/icons/bluetooth-off.svg") + ) tooltip = "Bluetooth disabled" else: connected_devices = self.bluetooth.connected_devices if connected_devices: - self.bt_icon.set_from_file(get_relative_path("../../../config/assets/icons/bluetooth.svg")) + self.bt_icon.set_from_file( + get_relative_path("../../../config/assets/icons/bluetooth.svg") + ) if len(connected_devices) == 1: device = connected_devices[0] tooltip = f"Connected to {device.alias}" @@ -141,7 +155,9 @@ class BluetoothIndicator(Button): else: tooltip = f"Connected to {len(connected_devices)} devices" else: - self.bt_icon.set_from_file(get_relative_path("../../../config/assets/icons/bluetooth.svg")) + self.bt_icon.set_from_file( + get_relative_path("../../../config/assets/icons/bluetooth.svg") + ) tooltip = "No devices connected" self.set_tooltip_text(tooltip) @@ -155,11 +171,10 @@ class BluetoothIndicator(Button): def on_device_removed(self, _, address): self.update_state() + class Battery(Box): def __init__(self, **kwargs): - super().__init__( - orientation="v", visible=True, **kwargs - ) + super().__init__(orientation="v", visible=True, **kwargs) self._battery = BatteryService() self._battery.changed.connect(self.update_battery) @@ -167,7 +182,9 @@ class Battery(Box): self.icon = Svg( name="indicators-icon", size=24, - svg_file=get_relative_path("../../../config/assets/icons/battery/battery-100.svg"), + svg_file=get_relative_path( + "../../../config/assets/icons/battery/battery-100.svg" + ), ) self.battery_button = Button( child=self.icon, @@ -259,21 +276,29 @@ class Battery(Box): self.icon.remove_style_class("alert") if state == "FULLY_CHARGED" or (percentage >= 100 and state == "CHARGING"): - self.icon.set_from_file(get_relative_path("../../../config/assets/icons/battery/battery-100-charging.svg")) + self.icon.set_from_file( + get_relative_path( + "../../../config/assets/icons/battery/battery-100-charging.svg" + ) + ) elif state == "CHARGING": # Calculate battery level and use corresponding charging icon level = min(100, max(0, int(percentage // 10) * 10)) if level == 0 and percentage > 0: level = 10 icon_name = f"battery-{level:03d}-charging.svg" - self.icon.set_from_file(get_relative_path(f"../../../config/assets/icons/battery/{icon_name}")) + self.icon.set_from_file( + get_relative_path(f"../../../config/assets/icons/battery/{icon_name}") + ) else: # Calculate battery level and use corresponding icon level = min(100, max(0, int(percentage // 10) * 10)) if level == 0 and percentage > 0: level = 10 icon_name = f"battery-{level:03d}.svg" - self.icon.set_from_file(get_relative_path(f"../../../config/assets/icons/battery/{icon_name}")) + self.icon.set_from_file( + get_relative_path(f"../../../config/assets/icons/battery/{icon_name}") + ) self.set_visible(True) return True diff --git a/modules/panel/components/menu_actions.py b/modules/panel/components/menu_actions.py index 072b1e7..be7a51a 100644 --- a/modules/panel/components/menu_actions.py +++ b/modules/panel/components/menu_actions.py @@ -8,10 +8,10 @@ from fabric.utils import exec_shell_command class MenuActionHandler: - + def __init__(self): pass - + def execute_action(self, action): try: # Window menu actions @@ -34,16 +34,13 @@ class MenuActionHandler: elif action == "Quit": Hyprland.send_command("dispatch killactive") - # Go menu actions elif action == "Back": Hyprland.send_command("dispatch workspace e-1") elif action == "Forward": Hyprland.send_command("dispatch workspace e+1") - # Edit menu actions - # View menu actions elif action == "Full Screen": @@ -63,7 +60,7 @@ class MenuActionHandler: elif action == "Report a Bug...": exec_shell_command("xdg-open https://github.com/S4NKALP/Modus/issues") - # System menu actions + # System menu actions elif action == "About This PC": exec_shell_command("gnome-system-monitor") elif action == "Force Quit": @@ -77,13 +74,14 @@ class MenuActionHandler: elif action == "Lock": exec_shell_command("hyprlock") - # App menu actions (for hiding apps) elif action.startswith("Hide ") and not action == "Hide Others": action.replace("Hide ", "") Hyprland.send_command("dispatch movetoworkspacesilent special:hidden") elif action == "Hide Others": - Hyprland.send_command("dispatch movetoworkspacesilent special:hidden,^(activewindow)") + Hyprland.send_command( + "dispatch movetoworkspacesilent special:hidden,^(activewindow)" + ) elif action == "Show All": Hyprland.send_command("dispatch movetoworkspace e+0,special:hidden") elif action.startswith("Quit "): diff --git a/modules/panel/components/menu_contents.py b/modules/panel/components/menu_contents.py index 6706520..9e09089 100644 --- a/modules/panel/components/menu_contents.py +++ b/modules/panel/components/menu_contents.py @@ -6,19 +6,10 @@ This file contains all the menu items and their organization. def get_default_menu_contents(): return { - "Hyprland": [ - ], - "File": [ - ], - "Edit": [ - ], - "View": [ - "Full Screen", - "---", - "Zoom In", - "Zoom Out", - "Actual Size" - ], + "Hyprland": [], + "File": [], + "Edit": [], + "View": ["Full Screen", "---", "Zoom In", "Zoom Out", "Actual Size"], "Go": [ "Back", "Forward", @@ -33,7 +24,7 @@ def get_default_menu_contents(): "Center", "Group", "Pin", - "Quit" + "Quit", ], "Help": [ "---", @@ -41,33 +32,30 @@ def get_default_menu_contents(): "Arch Wiki", "Keyboard Shortcuts", "---", - "Report a Bug..." - ] + "Report a Bug...", + ], } def get_app_menu_template(): - return [ - "About {app_name}", - "---", - "Hide {app_name}", - "---", - "Quit {app_name}" - ] + return ["About {app_name}", "---", "Hide {app_name}", "---", "Quit {app_name}"] def create_app_menu(app_name): template = get_app_menu_template() - return [item.format(app_name=app_name) if "{app_name}" in item else item for item in template] + return [ + item.format(app_name=app_name) if "{app_name}" in item else item + for item in template + ] def get_menu_contents_for_app(app_name): contents = get_default_menu_contents().copy() - + if app_name and app_name != "Hyprland": # Replace the Hyprland menu with app-specific menu contents["Hyprland"] = create_app_menu(app_name) - + return contents diff --git a/modules/panel/components/menubar.py b/modules/panel/components/menubar.py index 8c84ca3..c0ba1fa 100644 --- a/modules/panel/components/menubar.py +++ b/modules/panel/components/menubar.py @@ -13,13 +13,13 @@ class DropdownWindow(WaylandWindow): super().__init__( name="dropdown-window", title="Menu Dropdown", - layer="overlay", - anchor="top left", - exclusivity="none", - margin=f"{y}px 0px 0px {x}px", - visible=False, - all_visible=True, - **kwargs + layer="overlay", + anchor="top left", + exclusivity="none", + margin=f"{y}px 0px 0px {x}px", + visible=False, + all_visible=True, + **kwargs, ) # Store reference to parent menubar for closing dropdown @@ -51,14 +51,16 @@ class DropdownWindow(WaylandWindow): if event.detail != 2: # 2 = Gdk.NotifyType.INFERIOR (child widget) # Start auto-close timer when mouse leaves if not self.auto_close_timer: - self.auto_close_timer = GLib.timeout_add(300, self.auto_close_callback) # 300ms delay + self.auto_close_timer = GLib.timeout_add( + 300, self.auto_close_callback + ) # 300ms delay return False def auto_close_callback(self): if self.parent_menubar: self.parent_menubar.hide_dropdown() self.auto_close_timer = None - return False + return False class MenuBar(Box): @@ -82,38 +84,32 @@ class MenuBar(Box): self.action_handler = MenuActionHandler() # Menu items in macOS style order (excluding the first item which will be dynamic) - self.static_menu_items = [ - "File", - "Edit", - "View", - "Go", - "Window", - "Help" - ] + self.static_menu_items = ["File", "Edit", "View", "Go", "Window", "Help"] self.menu_contents = get_default_menu_contents() self.app_button = Button( name="menubar-button", - child=Label( - name="activewindow-label", - label="Hyprland" - ), + child=Label(name="activewindow-label", label="Hyprland"), + ) + self.app_button.connect( + "clicked", lambda widget: self.toggle_dropdown("Hyprland", widget) ) - self.app_button.connect("clicked", lambda widget: self.toggle_dropdown("Hyprland", widget)) self.add(self.app_button) self.menu_buttons = [] for item in self.static_menu_items: button = Button( name="menubar-button", - child=Label( - name="menubar-label", - label=item - ), + child=Label(name="menubar-label", label=item), ) - button.connect("clicked", lambda widget, menu_item=item: self.on_menu_button_clicked(menu_item, widget)) + button.connect( + "clicked", + lambda widget, menu_item=item: self.on_menu_button_clicked( + menu_item, widget + ), + ) self.menu_buttons.append(button) self.add(button) @@ -145,9 +141,9 @@ class MenuBar(Box): if menu_item in context_dependent_menus: # Only allow if there's an active window (not just showing "Hyprland") has_active_window = ( - self.current_window_title and - self.current_window_title.strip() and - self.app_button.get_child().get_label() != "Hyprland" + self.current_window_title + and self.current_window_title.strip() + and self.app_button.get_child().get_label() != "Hyprland" ) return has_active_window @@ -171,9 +167,11 @@ class MenuBar(Box): def toggle_dropdown(self, menu_item, button): # If same menu is clicked and dropdown is visible, hide it - if (self.current_dropdown == menu_item and - self.dropdown_window and - self.dropdown_window.get_visible()): + if ( + self.current_dropdown == menu_item + and self.dropdown_window + and self.dropdown_window.get_visible() + ): self.hide_dropdown() return @@ -209,7 +207,7 @@ class MenuBar(Box): separator = Box( name="menu-separator", size=(150, 1), - style="background-color: #444; margin: 2px 0;" + style="background-color: #444; margin: 2px 0;", ) self.dropdown_window.content_box.add(separator) else: @@ -219,9 +217,9 @@ class MenuBar(Box): child=Label( name="dropdown-label", label=item, - h_align="start" # Align text to the left + h_align="start", # Align text to the left ), - on_clicked=lambda *_, action=item: self.on_menu_action(action) + on_clicked=lambda *_, action=item: self.on_menu_action(action), ) self.dropdown_window.content_box.add(menu_button) @@ -236,7 +234,9 @@ class MenuBar(Box): # Get button allocation allocation = button.get_allocation() - print(f"Button allocation: x={allocation.x}, y={allocation.y}, width={allocation.width}, height={allocation.height}") + print( + f"Button allocation: x={allocation.x}, y={allocation.y}, width={allocation.width}, height={allocation.height}" + ) # Use the exact button position for perfect alignment dropdown_x = allocation.x @@ -291,9 +291,11 @@ class MenuBar(Box): def show_system_dropdown(self, button): # If same system dropdown is open, hide it (toggle behavior) - if (self.current_dropdown == "System" and - self.dropdown_window and - self.dropdown_window.get_visible()): + if ( + self.current_dropdown == "System" + and self.dropdown_window + and self.dropdown_window.get_visible() + ): self.hide_dropdown() return @@ -324,7 +326,7 @@ class MenuBar(Box): "Shutdown", "Restart", "Sleep", - "Lock" + "Lock", ] # Add menu items to dropdown @@ -334,7 +336,7 @@ class MenuBar(Box): separator = Box( name="menu-separator", size=(150, 1), - style="background-color: #444; margin: 2px 0;" + style="background-color: #444; margin: 2px 0;", ) self.dropdown_window.content_box.add(separator) else: @@ -344,9 +346,9 @@ class MenuBar(Box): child=Label( name="dropdown-label", label=item, - h_align="start" # Align text to the left + h_align="start", # Align text to the left ), - on_clicked=lambda *_, action=item: self.on_menu_action(action) + on_clicked=lambda *_, action=item: self.on_menu_action(action), ) self.dropdown_window.content_box.add(menu_button) diff --git a/modules/panel/components/recording_indicator.py b/modules/panel/components/recording_indicator.py new file mode 100644 index 0000000..0ccc0d8 --- /dev/null +++ b/modules/panel/components/recording_indicator.py @@ -0,0 +1,77 @@ +from fabric.widgets.button import Button +from fabric.widgets.label import Label +import time +import subprocess +from fabric.widgets.box import Box +from gi.repository import GLib +from fabric.utils import get_relative_path +from fabric.widgets.svg import Svg + + +class RecordingIndicator(Button): + def __init__(self, **kwargs): + super().__init__(name="panel-button", visible=False, **kwargs) + + self.script_path = get_relative_path("../../../scripts/screen-capture.sh") + self.recording_start_time = None + + self.recording_icon = Svg( + name="indicators-icon", + size=24, + svg_file=get_relative_path("../../../config/assets/icons/media-record.svg"), + ) + self.time_label = Label(name="recording-time-label", markup="00:00") + + self.recording_box = Box( + orientation="h", + spacing=2, + children=self.recording_icon, + ) + + self.add(self.recording_box) + + self.connect("clicked", self.on_stop_recording) + self.hide() + + GLib.timeout_add(1, self._delayed_init) + + def check_recording_status(self): + try: + result = subprocess.run( + [self.script_path, "status"], capture_output=True, text=True, timeout=2 + ) + is_recording = result.stdout.strip() == "true" + + if is_recording: + if not self.get_visible(): + self.show() + + # Get the recording start time if we don't have it + if self.recording_start_time is None: + self.recording_start_time = self.get_recording_start_time() + + # Update the recording time display + if self.recording_start_time: + elapsed_seconds = int(time.time() - self.recording_start_time) + minutes = elapsed_seconds // 60 + seconds = elapsed_seconds % 60 + time_text = f"{minutes:02d}:{seconds:02d}" + self.time_label.set_markup(time_text) + self.set_tooltip_text( + f"Recording in progress ({minutes:02d}:{ + seconds:02d}) - Click to stop" + ) + else: + self.set_tooltip_text("Recording in progress - Click to stop") + else: + if self.get_visible(): + self.hide() + self.recording_start_time = None + + except Exception: + # If we can't check status, hide the indicator + if self.get_visible(): + self.hide() + self.recording_start_time = None + + return True # Continue the timeout diff --git a/modules/panel/main.py b/modules/panel/main.py index acac968..d11fd49 100644 --- a/modules/panel/main.py +++ b/modules/panel/main.py @@ -7,10 +7,13 @@ from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from fabric.widgets.svg import Svg +from utils.wayland import WaylandWindow as Window + +from modules.panel.components.enhanced_system_tray import ( + apply_enhanced_system_tray, +) # ignore from modules.panel.components.indicators import Indicators from modules.panel.components.menubar import MenuBar -from modules.panel.components.enhanced_system_tray import apply_enhanced_system_tray -from utils.wayland import WaylandWindow as Window # Apply enhanced system tray icon handling apply_enhanced_system_tray() @@ -131,4 +134,4 @@ class Panel(Window): else: self.chevron_button.get_child().set_from_file( get_relative_path("../../config/assets/icons/chevron-right.svg") - ) \ No newline at end of file + ) diff --git a/modules/switcher.py b/modules/switcher.py index 7223a1b..b0b8737 100644 --- a/modules/switcher.py +++ b/modules/switcher.py @@ -1,13 +1,12 @@ import json +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 @@ -102,7 +101,10 @@ class ApplicationSwitcher(Window): if c.get("hidden", False): continue # Skip clients in special workspaces if the setting is enabled - if data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS and self._is_special_workspace(c): + if ( + data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS + and self._is_special_workspace(c) + ): continue filtered_windows.append(c) diff --git a/services/battery.py b/services/battery.py index b6b8e3c..f690519 100644 --- a/services/battery.py +++ b/services/battery.py @@ -48,7 +48,7 @@ class Battery(Service): @Property(str, "readable") def time_to_empty(self): if self._use_psutil_fallback: - if self._psutil_battery and hasattr(self._psutil_battery, 'secsleft'): + if self._psutil_battery and hasattr(self._psutil_battery, "secsleft"): return self.seconds_to_hours_minutes(self._psutil_battery.secsleft) return "N/A" return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToEmpty", 0)) diff --git a/services/mpris.py b/services/mpris.py index 99dd76a..6d7aa7f 100644 --- a/services/mpris.py +++ b/services/mpris.py @@ -55,7 +55,11 @@ class PlayerService(Service): return self._player.get_position() except Exception: # Player is no longer available, stop the fabricator - if hasattr(self, 'pos_fabricator') and self.pos_fabricator and self._fabricator_running: + if ( + hasattr(self, "pos_fabricator") + and self.pos_fabricator + and self._fabricator_running + ): try: self.pos_fabricator.stop() self._fabricator_running = False @@ -69,7 +73,7 @@ class PlayerService(Service): return # Already cleaned up try: - if hasattr(self, 'pos_fabricator') and self.pos_fabricator: + if hasattr(self, "pos_fabricator") and self.pos_fabricator: if self._fabricator_running: self.pos_fabricator.stop() self._fabricator_running = False @@ -79,9 +83,11 @@ class PlayerService(Service): print(f"[DEBUG] Error during PlayerService cleanup: {e}") def on_seeked(self, player, position): - if (not self._cleanup_done and - self.status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" and - not self._fabricator_running): + if ( + not self._cleanup_done + and self.status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" + and not self._fabricator_running + ): try: self.pos_fabricator.start() self._fabricator_running = True @@ -128,7 +134,7 @@ class PlayerService(Service): return # Validate player object - if not self._player or not hasattr(self._player, 'get_position'): + if not self._player or not hasattr(self._player, "get_position"): return # Get position safely @@ -147,9 +153,11 @@ class PlayerService(Service): # Get duration safely from multiple sources dur = 0.0 # Default to 0 - if (hasattr(self._player, 'props') and - hasattr(self._player.props, 'metadata') and - self._player.props.metadata): + if ( + hasattr(self._player, "props") + and hasattr(self._player.props, "metadata") + and self._player.props.metadata + ): metadata = self._player.props.metadata @@ -173,7 +181,7 @@ class PlayerService(Service): # Try to get duration from player directly (some players support this) if dur <= 0: try: - if hasattr(self._player, 'get_metadata'): + if hasattr(self._player, "get_metadata"): player_metadata = self._player.get_metadata() if player_metadata: raw_dur = player_metadata["mpris:length"] @@ -241,4 +249,4 @@ class PlayerManager(Service): self.new_player(player) def _on_player_vanished(self, sender, player, manager): - self.player_vanish(player) \ No newline at end of file + self.player_vanish(player) diff --git a/services/network.py b/services/network.py index 1124b4a..54016d9 100644 --- a/services/network.py +++ b/services/network.py @@ -510,9 +510,11 @@ class Wifi(Service): device, None, None, - lambda c, r: logger.info(f"Connected to '{ssid}'") - if c - else logger.error(f"Failed to connect to '{ssid}'"), + 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}") diff --git a/utils/circle_image.py b/utils/circle_image.py index acfdc6d..5afc189 100644 --- a/utils/circle_image.py +++ b/utils/circle_image.py @@ -32,8 +32,12 @@ class CircleImage(Gtk.DrawingArea, Widget): style: str | None = None, tooltip_text: str | None = None, tooltip_markup: str | None = None, - h_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None, - v_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None, + h_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, + v_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, h_expand: bool = False, v_expand: bool = False, size: int | None = None, @@ -57,7 +61,9 @@ class CircleImage(Gtk.DrawingArea, Widget): ) self.size = size if size is not None else 100 # Default size if not provided self._angle = 0 - self._orig_image: GdkPixbuf.Pixbuf | None = None # Original image for reprocessing + self._orig_image: GdkPixbuf.Pixbuf | None = ( + None # Original image for reprocessing + ) self._image: GdkPixbuf.Pixbuf | None = None if image_file: pix = GdkPixbuf.Pixbuf.new_from_file(image_file) @@ -79,7 +85,9 @@ class CircleImage(Gtk.DrawingArea, Widget): else: square_size = width if square_size != self.size: - pixbuf = pixbuf.scale_simple(self.size, self.size, GdkPixbuf.InterpType.BILINEAR) + pixbuf = pixbuf.scale_simple( + self.size, self.size, GdkPixbuf.InterpType.BILINEAR + ) return pixbuf def on_draw(self, widget: "CircleImage", ctx: cairo.Context): diff --git a/utils/conversion.py b/utils/conversion.py index 0f15f9a..c39b4da 100644 --- a/utils/conversion.py +++ b/utils/conversion.py @@ -556,9 +556,10 @@ 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) diff --git a/utils/icon_resolver.py b/utils/icon_resolver.py index a80c8f6..58ac1e4 100644 --- a/utils/icon_resolver.py +++ b/utils/icon_resolver.py @@ -16,7 +16,9 @@ if not os.path.exists(data.CACHE_DIR): class IconResolver: - def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"): + def __init__( + self, default_applicaiton_icon: str = "application-x-executable-symbolic" + ): if os.path.exists(ICON_CACHE_FILE): with open(ICON_CACHE_FILE) as f: try: @@ -79,7 +81,9 @@ class IconResolver: data_dir = os.path.join(data_dir, "applications") if os.path.exists(data_dir): files = os.listdir(data_dir) - matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()] + matching = [ + s for s in files if "".join(app_id.lower().split()) in s.lower() + ] if matching: return os.path.join(data_dir, matching[0]) for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))): diff --git a/utils/popup_window.py b/utils/popup_window.py deleted file mode 100644 index 24d1af3..0000000 --- a/utils/popup_window.py +++ /dev/null @@ -1,143 +0,0 @@ -from fabric.widgets.box import Box -from utils.wayland import WaylandWindow - -from config.c import c - -import gi # type: ignore -gi.require_version("GtkLayerShell", "0.1") - -from gi.repository import Gtk, GtkLayerShell # type: ignore - - -class PopupWindow(WaylandWindow): - def __init__( - self, - parent: WaylandWindow, - pointing_to: Gtk.Widget | None = None, - margin: tuple[int, ...] | str = "0px 0px 0px 0px", - **kwargs, - ): - super().__init__(**kwargs) - self.exclusivity = "none" - - self._parent = parent - self._pointing_widget = pointing_to - self._base_margin = self.extract_margin(margin) - self.margin = self._base_margin.values() - - self.connect("notify::visible", self.do_update_handlers) - - def get_coords_for_widget(self, widget: Gtk.Widget) -> tuple[int, int]: - if not ((toplevel := widget.get_toplevel()) and toplevel.is_toplevel()): # type: ignore - return 0, 0 - allocation = widget.get_allocation() - x, y = widget.translate_coordinates(toplevel, allocation.x, allocation.y) or ( - 0, - 0, - ) - return round(x / 2), round(y / 2) - - def set_pointing_to(self, widget: Gtk.Widget | None): - if self._pointing_widget: - try: - self._pointing_widget.disconnect_by_func(self.do_handle_size_allocate) - except Exception: - pass - self._pointing_widget = widget - return self.do_update_handlers() - - def do_update_handlers(self, *_): - if not self._pointing_widget: - return - - if not self.get_visible(): - try: - self._pointing_widget.disconnect_by_func(self.do_handle_size_allocate) - self.disconnect_by_func(self.do_handle_size_allocate) - except Exception: - pass - return - - self._pointing_widget.connect("size-allocate", self.do_handle_size_allocate) - self.connect("size-allocate", self.do_handle_size_allocate) - - return self.do_handle_size_allocate() - - def do_handle_size_allocate(self, *_): - return self.do_reposition(self.do_calculate_edges()) - - def do_calculate_edges(self): - move_axe = "x" - parent_anchor = self._parent.anchor - - if len(parent_anchor) != 3: - return move_axe - - if ( - GtkLayerShell.Edge.LEFT in parent_anchor - and GtkLayerShell.Edge.RIGHT in parent_anchor - ): - # horizontal -> move on x-axies - move_axe = "x" - if GtkLayerShell.Edge.TOP in parent_anchor: - self.anchor = "left top" - else: - self.anchor = "left bottom" - elif ( - GtkLayerShell.Edge.TOP in parent_anchor - and GtkLayerShell.Edge.BOTTOM in parent_anchor - ): - # vertical -> move on y-axies - move_axe = "y" - if GtkLayerShell.Edge.RIGHT in parent_anchor: - self.anchor = "top right" - else: - self.anchor = "top left" - - return move_axe - - def do_reposition(self, move_axe: str): - parent_margin = self._parent.margin - parent_x_margin, parent_y_margin = parent_margin[0], parent_margin[3] - - height = self.get_allocated_height() - width = self.get_allocated_width() - - if self._pointing_widget: - coords = self.get_coords_for_widget(self._pointing_widget) - if coords[0] + width > int(c.get_rule("Display.resolution").split("x")[0]): - coords_centered = ( - round(coords[0] - (width - self._pointing_widget.get_allocated_width())), - round(coords[1] + self._pointing_widget.get_allocated_height() / 2), - ) - else: - coords_centered = ( - round(coords[0]), - round(coords[1] + self._pointing_widget.get_allocated_height() / 2), - ) - else: - coords_centered = ( - round(self._parent.get_allocated_width() / 2), - round(self._parent.get_allocated_height() / 2), - ) - self.margin = tuple( - a + b - for a, b in zip( - ( - ( - 0, - 0, - 0, - round((parent_x_margin + coords_centered[0])), - ) - if move_axe == "x" - else ( - round((parent_y_margin + coords_centered[1]) - (height / 2)), - 0, - 0, - 0, - ) - ), - self._base_margin.values(), - ) - ) diff --git a/utils/wayland.py b/utils/wayland.py index 472dc40..76978da 100644 --- a/utils/wayland.py +++ b/utils/wayland.py @@ -132,13 +132,15 @@ class WaylandWindow(Window): @keyboard_mode.setter def keyboard_mode( self, - value: Literal[ - "none", - "exclusive", - "on-demand", - "entry-number", - ] - | KeyboardMode, + value: ( + Literal[ + "none", + "exclusive", + "on-demand", + "entry-number", + ] + | KeyboardMode + ), ): self._keyboard_mode = get_enum_member( KeyboardMode, value, default=KeyboardMode.NONE @@ -226,10 +228,12 @@ class WaylandWindow(Window): layer: Literal["background", "bottom", "top", "overlay"] | Layer = Layer.TOP, anchor: str = "", margin: str | Iterable[int] = "0px 0px 0px 0px", - exclusivity: Literal["auto", "normal", "none"] - | WaylandWindowExclusivity = WaylandWindowExclusivity.NONE, - keyboard_mode: Literal["none", "exclusive", "on-demand"] - | KeyboardMode = KeyboardMode.NONE, + exclusivity: ( + Literal["auto", "normal", "none"] | WaylandWindowExclusivity + ) = WaylandWindowExclusivity.NONE, + keyboard_mode: ( + Literal["none", "exclusive", "on-demand"] | KeyboardMode + ) = KeyboardMode.NONE, pass_through: bool = False, monitor: int | Gdk.Monitor | None = None, title: str = "fabric", @@ -242,12 +246,12 @@ class WaylandWindow(Window): style_classes: Iterable[str] | str | None = None, tooltip_text: str | None = None, tooltip_markup: str | None = None, - h_align: Literal["fill", "start", "end", "center", "baseline"] - | Gtk.Align - | None = None, - v_align: Literal["fill", "start", "end", "center", "baseline"] - | Gtk.Align - | None = None, + h_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, + v_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, h_expand: bool = False, v_expand: bool = False, size: Iterable[int] | int | None = None, @@ -292,7 +296,11 @@ class WaylandWindow(Window): self.keyboard_mode = keyboard_mode self.exclusivity = exclusivity self.pass_through = pass_through - self.show_all() if all_visible is True else self.show() if visible is True else None + ( + self.show_all() + if all_visible is True + else self.show() if visible is True else None + ) def steal_input(self) -> None: return GtkLayerShell.set_keyboard_interactivity(self, True) @@ -347,9 +355,11 @@ class WaylandWindow(Window): margins = ( extract_css_values(input.lower()) if isinstance(input, str) - else input - if isinstance(input, (tuple, list)) and len(input) == 4 - else (0, 0, 0, 0) + else ( + input + if isinstance(input, (tuple, list)) and len(input) == 4 + else (0, 0, 0, 0) + ) ) return { Edge.TOP: margins[0], diff --git a/utils/wiggle_bar.py b/utils/wiggle_bar.py deleted file mode 100644 index 69298c2..0000000 --- a/utils/wiggle_bar.py +++ /dev/null @@ -1,174 +0,0 @@ -import math -from fabric.core.service import Service, Signal -from gi.repository import Gtk, Gdk, GLib -import gi - -gi.require_version("Gtk", "3.0") - - -class WigglyWidget(Gtk.DrawingArea, Service): - @Signal - def on_seek(self, ratio: float) -> None: ... - - def __init__(self): - super().__init__(name="wiggle-bar") - self.phase = 0 - self.set_size_request(-1, 20) - self.connect("draw", self.on_draw) - - # mouse events - self.add_events( - Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.BUTTON_RELEASE_MASK - | Gdk.EventMask.POINTER_MOTION_MASK - ) - - self.connect("button-press-event", self.on_button_press) - self.connect("motion-notify-event", self.on_motion) - self.connect("button-release-event", self.on_button_release) - - GLib.timeout_add(16, self.update) # 60 FPS - self.value = 0.0 - self.amplitude = 2 - self.dragging = False - self.pause = False - - self.show_all() - - def animate_amplitude_to(self): - if abs(self.amplitude - self.amplitude_target) < 0.01: - self.amplitude = self.amplitude_target - self.queue_draw() - return False - else: - self.amplitude += self.amplitude_step - self.queue_draw() - return True - - def update_amplitude(self, decrease): - if decrease: - self.amplitude_target = 0 - self.amplitude_step = (0 - self.amplitude) / 10.0 - else: - self.amplitude_target = 2 - self.amplitude_step = (2 - self.amplitude) / 10.0 - - # animation stops when False is returned btw - GLib.timeout_add(16, self.animate_amplitude_to) - - def update_value_from_x(self, x): - width = self.get_allocated_width() - self.value = max(0.0, min(1.0, x / width)) - self.on_seek(self.value) - self.queue_draw() - - def on_button_press(self, widget, event): - self.dragging = True - self.update_amplitude(True) - self.update_value_from_x(event.x) - return True - - # def animate_progress_to(self): - # print("frame") - # if self.value < self.value_target: - # self.value = self.value_target - # print(self.value) - # self.queue_draw() - # return False - # else: - # self.value += self.value_step - # self.queue_draw() - # return True - - # def update_value_from_signal(self, new_value): - # self.value_target = min(1.0, new_value) - # steps = 60 # 1s / 16ms ≈ 60 frames - # self.value_step = (self.value_target - self.value) / steps - # GLib.timeout_add(16, self.animate_progress_to) - - def update_value_from_signal(self, new_value): - self.value = min(1.0, new_value) - - def on_motion(self, widget, event): - if self.dragging and not self.pause: - self.update_value_from_x(event.x) - - cursor = Gdk.Cursor.new(Gdk.CursorType.HAND1) - widget.get_window().set_cursor(cursor) - - return True - - def on_button_release(self, widget, event): - if self.pause == False: - self.dragging = False - self.update_amplitude(False) - return True - - def update(self): - if self.dragging == False: - self.phase += 0.1 - # if (self.value<1): - # self.value += 0.001 - self.queue_draw() - return True - - def on_draw(self, widget, cr): - alloc_width = self.get_allocated_width() - height = self.get_allocated_height() - center_y = height / 2 - # self.amplitude = 2 - frequency = 0.3 - slider_diameter = 6 - - width = int(alloc_width * self.value) - slider_diameter + 1 - - cr.set_source_rgb(1, 1, 1) # color here btw - cr.set_line_width(2) - - last_x = 0 # fallback values in case the slider gets dragged out of range - last_y = 0 - - cr.move_to(0, center_y) - for x in range(width): - y = center_y + self.amplitude * math.sin((x * frequency) + self.phase) - cr.line_to(x, y) - last_x, last_y = x, y - - cr.stroke() - - # Set the color using CSS - the widget should have CSS styling applied - # that sets the color to var(--primary). For now, use a fallback approach. - style_context = self.get_style_context() - - try: - # Get the foreground color which should be styled with CSS - color = style_context.get_color(Gtk.StateFlags.NORMAL) - cr.set_source_rgba(color.red, color.green, color.blue, color.alpha) - except: - # Fallback to a nice blue color that matches the primary theme - cr.set_source_rgb(0.2, 0.6, 1.0) - - rect_width = 6 - arc_radius = slider_diameter / 2 - self.draw_rounded_rect(cr, last_x, height, rect_width, rect_width, arc_radius) - cr.fill() - - cr.stroke() - - cr.set_source_rgb(1, 1, 1) - cr.set_line_width(1) - - # This is for anti aliasing. When line width becomes 1px and the position is not an integer, two lines of opacity 50% is drawn on two pixel rows. - # This moves the line to make it pixel perfect hence not needing anti aliasing - cr.translate(0, 0.5) - - cr.move_to(last_x + 2 * arc_radius, height / 2) - cr.line_to(alloc_width, height / 2) - - cr.stroke() - - def draw_rounded_rect(self, cr, x, y, width, _height, radius): - cr.new_sub_path() - cr.arc(x + width - radius, radius, radius, -math.pi, 0) - cr.arc(x + width - radius, y - radius, radius, 0, -math.pi) - cr.close_path()