♻️ refactor(launcher): improve code structure and plugin functionality
This commit is contained in:
31 files changed
+536
-618
No files matched your search
@@ -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",
|
||||
|
||||
+3
-3
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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}"
|
||||
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
)
|
||||
)
|
||||
+5
-3
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -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))
|
||||
|
||||
+19
-11
@@ -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)
|
||||
self.player_vanish(player)
|
||||
+5
-3
@@ -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}")
|
||||
|
||||
+12
-4
@@ -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):
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
@@ -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))):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
)
|
||||
+31
-21
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user