♻️ refactor(launcher): improve code structure and plugin functionality

This commit is contained in:
sankalp committed 2025-07-30 13:04:22 +05:45
1 parent 4822693298
commit 5495233667
31 files changed
+536 -618

No files matched your search

@@ -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
+40 -15
View File
@@ -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
+6 -8
View File
@@ -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 "):
+14 -26
View File
@@ -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
+46 -44
View File
@@ -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