feat(root): implement dynamic todo list refresh and UI enhancements

This change adds a mechanism to refresh the todo list dynamically when the underlying service updates, improving UI responsiveness and user experience. It also updates documentation and removes deprecated files.

- New Features:
  - Added `_refresh_todos` method to rebuild the todo list when `get_todo_service().todos` changes.
  - Introduced `TodoListCapture` class for mouse capture handling of the todo list window.
- Enhancements:
  - Updated README with corrected image paths, installation tips, and manual installation commands.
  - Introduced `start.py` to launch the application or lock mode.
  - Removed deprecated `config.json` and `debug_memory.py`.
- Bug Fixes:
  - Fixed lock screen screenshot path in README.
This commit is contained in:
sankalp committed 2026-04-10 17:11:32 +05:45
1 parent 235f53b6a2
commit e4813f35a0
267 files changed
+24582 -394

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,106 @@
from pathlib import Path
import weakref
from fabric.system_tray.widgets import SystemTrayItem
from fabric.utils import GdkPixbuf, GLib, logger
from services.config import get_config, on_config_change
# Track active tray items for hot-reloading
_tracked_items = weakref.WeakSet()
def should_hide(item) -> bool:
"""Check if a tray item should be hidden based on config."""
ignore_list = get_config("systray_ignore", [])
if not ignore_list:
return False
title = item.title if item.title else ""
identifier = item.identifier if item.identifier else ""
# Check both title and identifier (case-insensitive for convenience)
for pattern in ignore_list:
p = pattern.lower()
if p in title.lower() or p in identifier.lower():
return True
return False
def patched_do_update_properties(self, *_):
# Determine the icon name to use
item = self._item
icon_name = item.icon_name
attention_icon_name = item.attention_icon_name
# Log item info for user debugging
logger.info(
f"[SysTray] Item: title='{getattr(item, 'title', '')}', identifier='{getattr(item, 'identifier', '')}'"
)
# Visibility check
is_hidden = should_hide(item)
self.set_visible(not is_hidden)
if is_hidden:
return
if item.status == "NeedsAttention" and attention_icon_name:
preferred_icon_name = attention_icon_name
else:
preferred_icon_name = icon_name
# 1. Try the standard fabric/GTK implementation first
pixbuf = item.get_preferred_icon_pixbuf(self._icon_size)
# 2. If standard lookup fails, try your specific absolute path logic
if pixbuf is None and preferred_icon_name:
path = Path(preferred_icon_name)
if path.is_absolute() and path.exists():
try:
target_size = self._icon_size if self._icon_size else 24
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
str(path),
target_size,
target_size,
True, # Preserve aspect ratio
)
except GLib.GError:
pixbuf = None
# Apply the resulting pixbuf or the fallback "missing" icon
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
else:
self._image.set_from_icon_name("image-missing", self._icon_size)
tooltip = item.tooltip
self.set_tooltip_markup(
tooltip.description
or tooltip.title
or (item.title.title() if item.title else None)
or "Unknown"
)
def apply_enhanced_system_tray():
# Patch __init__ to track instances
original_init = SystemTrayItem.__init__
def patched_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
_tracked_items.add(self)
SystemTrayItem.__init__ = patched_init
# Replace the method on the class so all tray items use this logic
SystemTrayItem.do_update_properties = patched_do_update_properties
# Setup hot-reload listener
def on_reload(new_config, old_config):
if new_config.get("systray_ignore") != old_config.get("systray_ignore"):
logger.info("[SysTray] Config changed, updating tray item visibility")
for item_widget in _tracked_items:
item_widget.do_update_properties()
on_config_change(on_reload)
+337
View File
@@ -0,0 +1,337 @@
from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow
from fabric.utils import FormattedString, exec_shell_command_async
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from shared.dialogs.about import About, AboutApp
from shared.window.dropdown import ModusDropdown, dropdown_divider, dropdowns
from shared.window.mousecapture import DropDownMouseCapture
from utils.app_name_resolver import format_window
from utils.roam import modus_service
from utils.utils import setup_cursor_hover
def has_active_window():
return (
modus_service.current_active_app_name
and modus_service.current_active_app_name != "Finder"
)
def create_menu_button(label, on_clicked=None):
button_kwargs = {
"label": label,
"name": "global-menu",
}
if on_clicked is not None:
button_kwargs["on_clicked"] = on_clicked
button = Button(**button_kwargs)
setup_cursor_hover(button, "pointer")
return button
def create_dropdown_with_capture(dropdown_id, parent, dropdown_children, layer="top"):
dropdown = ModusDropdown(
dropdown_id=dropdown_id,
parent=parent,
dropdown_children=dropdown_children,
)
mouse_capture = DropDownMouseCapture(layer=layer, child_window=dropdown)
return mouse_capture
def manage_button_style_classes(buttons, active_button=None, style_class="active"):
for button in buttons:
if button: # Check if button exists (some might be None)
button.remove_style_class(style_class)
if active_button:
active_button.add_style_class(style_class)
def show_about_app(_=None):
if not has_active_window():
return
app_name = modus_service.current_active_app_name
wmclass = getattr(modus_service, "current_active_wm_class", "")
about_window = AboutApp(app_name=app_name, wmclass=wmclass)
about_window.toggle(None)
def on_click_subthread(button, on_clicked, on_click):
if on_clicked:
on_clicked(button)
else:
exec_shell_command_async(on_click, lambda *_: None)
for dropdown in dropdowns:
if dropdown.is_visible():
dropdown.hide_via_mousecapture()
break
def dropdown_option(
label: str = "",
keybind: str = "",
on_click='echo "ModusPanelDropdown Action"',
on_clicked=None,
):
btn = Button(
child=CenterBox(
start_children=[
Label(label=label, h_align="start", name="dropdown-option-label"),
],
end_children=[
Label(label=keybind, h_align="end", name="dropdown-option-keybind")
],
orientation="horizontal",
h_align="fill",
h_expand=True,
v_expand=True,
),
name="dropdown-option",
h_align="fill",
on_clicked=lambda button: on_click_subthread(button, on_clicked, on_click),
h_expand=True,
v_expand=True,
)
setup_cursor_hover(btn, "pointer")
return btn
class SystemDropdown(ModusDropdown):
def __init__(self, parent, **kwargs):
super().__init__(
dropdown_id="os-menu",
parent=parent,
dropdown_children=[
dropdown_option(
"About this PC", on_clicked=lambda _: About().toggle(_)
),
dropdown_divider("---------------------"),
dropdown_option(
"System Settings...",
# TODO: Open Modus own setting
# on_click="xdg-open settings",
),
dropdown_divider("---------------------"),
dropdown_option("Force Quit", "", "hyprctl kill"),
dropdown_divider("---------------------"),
dropdown_option("Sleep", "", "systemctl suspend"),
dropdown_option("Restart", "", "systemctl reboot"),
dropdown_option("Shut Down", "", "shutdown now"),
dropdown_divider("---------------------"),
dropdown_option("Lock Screen", "󰘳 L", "hyprlock"),
],
**kwargs,
)
class GlobalMenuDropdowns:
def __init__(self, parent):
self.parent = parent
self.system_dropdown = SystemDropdown(parent=parent)
self.menu_button_dropdown = DropDownMouseCapture(
layer="top", child_window=self.system_dropdown
)
self.menu_button = Button(
label="Modus",
name="global-menu",
on_clicked=lambda _: self.menu_button_dropdown.toggle_mousecapture(),
)
self.menu_button_dropdown.child_window.set_pointing_to(self.menu_button)
self.global_title_menu_about = dropdown_option(
f"About {modus_service.current_active_app_name}",
on_clicked=show_about_app,
)
self.global_menu_title = create_dropdown_with_capture(
"global-menu-title",
parent,
[self.global_title_menu_about],
)
self.global_menu_view = create_dropdown_with_capture(
"global-menu-view",
parent,
[
dropdown_option(
"Enter Full Screen",
on_click="hyprctl dispatch fullscreen",
),
],
)
self.global_menu_window = create_dropdown_with_capture(
"global-menu-window",
parent,
[
dropdown_option(
"Zoom In",
"󰍉 +",
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '.float * 1.1')",
),
dropdown_option(
"Zoom Out",
"󰍉 -",
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '(.float * 0.9) | if . < 1 then 1 else . end')",
),
dropdown_divider("---------------------"),
dropdown_option(
"Move Window to Left",
on_click="hyprctl dispatch movewindow l",
),
dropdown_option(
"Move Window to Right",
on_click="hyprctl dispatch movewindow r",
),
dropdown_option(
"Cycle Through Windows",
on_click="hyprctl dispatch cyclenext",
),
dropdown_divider("---------------------"),
dropdown_option("Float", on_click="hyprctl dispatch togglefloating"),
dropdown_option("Quit", on_click="hyprctl dispatch killactive"),
dropdown_option("Pseudo", on_click="hyprctl dispatch pseudo"),
dropdown_option(
"Toggle Split", on_click="hyprctl dispatch togglesplit"
),
dropdown_option("Center", on_click="hyprctl dispatch centerwindow"),
dropdown_option("Group", on_click="hyprctl dispatch togglegroup"),
dropdown_option(
"Pin",
on_clicked=lambda _: exec_shell_command_async(
"bash ~/.config/scripts/winpin.sh", lambda *_: None
),
),
],
)
self.global_menu_help = create_dropdown_with_capture(
"global-menu-help",
parent,
[
dropdown_option(
"Modus",
on_click="xdg-open https://github.com/S4NKALP/Modus/issues",
),
dropdown_divider("---------------------"),
dropdown_option(
"Hyprland Wiki", on_click="xdg-open https://wiki.hyprland.org/"
),
],
)
modus_service.connect(
"current-active-app-name-changed", self._on_active_app_changed
)
self.global_menu_button_title = Button(
child=ActiveWindow(
formatter=FormattedString(
"{ format_window(win_title, win_class) }",
format_window=format_window,
)
),
name="global-menu",
on_clicked=self._on_title_button_clicked,
)
self.global_menu_title.child_window.set_pointing_to(
self.global_menu_button_title
)
# File, Edit and Go buttons are placeholders - no dropdowns implemented yet
self.global_menu_button_file = create_menu_button("File")
self.global_menu_button_edit = create_menu_button("Edit")
self.global_menu_button_go = create_menu_button("Go")
self.global_menu_button_view = create_menu_button(
"View",
lambda _: self.global_menu_view.toggle_mousecapture(),
)
self.global_menu_view.child_window.set_pointing_to(self.global_menu_button_view)
self.global_menu_button_window = create_menu_button(
"Window",
lambda _: self.global_menu_window.toggle_mousecapture(),
)
self.global_menu_window.child_window.set_pointing_to(
self.global_menu_button_window
)
self.global_menu_button_help = create_menu_button(
"Help",
lambda _: self.global_menu_help.toggle_mousecapture(),
)
self.global_menu_help.child_window.set_pointing_to(self.global_menu_button_help)
self.all_menu_buttons = [
self.menu_button,
self.global_menu_button_title,
self.global_menu_button_file,
self.global_menu_button_edit,
self.global_menu_button_view,
self.global_menu_button_go,
self.global_menu_button_window,
self.global_menu_button_help,
]
self.dropdown_button_map = {
"os-menu": self.menu_button,
"global-menu-title": self.global_menu_button_title,
"global-menu-file": self.global_menu_button_file,
"global-menu-edit": self.global_menu_button_edit,
"global-menu-view": self.global_menu_button_view,
"global-menu-go": self.global_menu_button_go,
"global-menu-window": self.global_menu_button_window,
"global-menu-help": self.global_menu_button_help,
}
modus_service.connect("current-dropdown-changed", self.changed_dropdown)
modus_service.connect("dropdowns-hide-changed", self.hide_dropdowns)
def _on_title_button_clicked(self, _):
if has_active_window():
self.global_menu_title.toggle_mousecapture()
def _on_active_app_changed(self, _, value):
self.global_title_menu_about.set_property("label", f"About {value}")
def hide_dropdowns(self, *_):
manage_button_style_classes(self.all_menu_buttons)
def changed_dropdown(self, _, dropdown_id):
active_button = self.dropdown_button_map.get(dropdown_id)
manage_button_style_classes(self.all_menu_buttons, active_button)
class GlobalMenu(Box):
def __init__(self, parent_window=None, **kwargs):
if parent_window is None:
parent_window = kwargs.pop("parent_window", None)
super().__init__(
name="globalmenu", orientation="horizontal", spacing=0, **kwargs
)
self.dropdown_system = GlobalMenuDropdowns(parent=parent_window)
self.children = [
self.dropdown_system.global_menu_button_title,
self.dropdown_system.global_menu_button_file,
self.dropdown_system.global_menu_button_edit,
self.dropdown_system.global_menu_button_view,
self.dropdown_system.global_menu_button_go,
self.dropdown_system.global_menu_button_window,
self.dropdown_system.global_menu_button_help,
]
def show_system_dropdown(self, imac_button):
self.dropdown_system.menu_button_dropdown.child_window.set_pointing_to(
imac_button
)
mouse_capture = self.dropdown_system.menu_button_dropdown
mouse_capture.set_child_window_visible(not mouse_capture.is_visible())
+459
View File
@@ -0,0 +1,459 @@
from fabric.bluetooth import BluetoothClient
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from fabric.widgets.wayland import WaylandWindow as Window
from services.battery import BatteryService, DeviceState
from services.network import NetworkClient
from shared.window.battery_widget import BatteryControl
from shared.window.mousecapture import DropDownMouseCapture
from utils.functions import format_duration, get_wifi_icon_for_strength
from utils.roam import modus_service
from utils.utils import setup_cursor_hover, svg_file
from window.controlcenter.bluetooth import BluetoothConnections
from window.controlcenter.wifi import WifiConnections
def create_control_window(name_prefix):
return Window(
layer="overlay",
title="modus",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
name=f"{name_prefix}-window",
visible=False,
)
def create_mouse_capture(window):
return DropDownMouseCapture(layer="top", child_window=window)
def setup_control_center(
show_window, name_prefix, widget_class, parent, **widget_kwargs
):
if not show_window:
return None, None, None
window = create_control_window(name_prefix)
widget = widget_class(parent, **widget_kwargs)
window.children = [widget]
mouse_capture = create_mouse_capture(window)
return window, widget, mouse_capture
def handle_indicator_click(mouse_capture):
if mouse_capture:
mouse_capture.toggle_mousecapture()
def hide_control_center(mouse_capture):
if mouse_capture:
mouse_capture.hide_child_window()
class BluetoothIndicator(Box):
def __init__(self, show_window=True, **kwargs):
super().__init__(name="bluetooth-indicator", orientation="h", **kwargs)
self.show_window = show_window
self.bluetooth = BluetoothClient()
self.bt_icon = svg_file("applets/bluetooth-clear.svg", size=22)
self.bt_button = Button(
name="bt-button", child=self.bt_icon, on_clicked=self.on_bluetooth_clicked
)
self.add(self.bt_button)
# Set pointer cursor on hover
setup_cursor_hover(self.bt_button, "pointer")
# Setup control center using shared function
(
self.bluetooth_window,
self.bluetooth_widget,
self.bluetooth_mousecapture,
) = setup_control_center(
self.show_window,
"bluetooth",
BluetoothConnections,
self,
show_back_button=False,
)
if self.bluetooth_window:
self.bluetooth_window._pointing_widget = self.bt_button
modus_service.connect("bluetooth-changed", self.on_bluetooth_changed)
self.bluetooth.connect("changed", self.on_bluetooth_direct_changed)
self.bluetooth.connect("device-added", self.on_device_added)
self.bluetooth.connect("device-removed", self.on_device_removed)
self.update_modus_service_bluetooth_state()
self.update_state()
def update_state(self):
if not self.bluetooth.enabled:
self.bt_icon.dynamic_file("applets/bluetooth-off-clear.svg")
tooltip = "Bluetooth disabled"
else:
connected_devices = self.bluetooth.connected_devices
if connected_devices:
self.bt_icon.dynamic_file("applets/bluetooth-clear.svg")
if len(connected_devices) >= 1:
self.bt_icon.dynamic_file("applets/bluetooth-paired.svg")
device = connected_devices[0]
tooltip = f"Connected to {device.alias}"
if device.battery_percentage > 0:
tooltip += f" ({device.battery_percentage:.0f}%)"
else:
tooltip = f"Connected to {len(connected_devices)} devices"
else:
self.bt_icon.dynamic_file("applets/bluetooth-clear.svg")
tooltip = "No devices connected"
self.bt_button.set_tooltip_text(tooltip)
def on_bluetooth_changed(self, service, new_bluetooth_state):
self.update_state()
def on_bluetooth_direct_changed(self, *args):
self.update_modus_service_bluetooth_state()
self.update_state()
def on_device_added(self, _, address):
self.update_modus_service_bluetooth_state()
self.update_state()
def on_device_removed(self, _, address):
self.update_modus_service_bluetooth_state()
self.update_state()
def update_modus_service_bluetooth_state(self):
if not self.bluetooth.enabled:
bluetooth_state = "disabled"
else:
connected_devices = self.bluetooth.connected_devices
if connected_devices:
if len(connected_devices) == 1:
device = connected_devices[0]
bluetooth_state = f"connected:{device.alias}"
if (
hasattr(device, "battery_percentage")
and device.battery_percentage > 0
):
bluetooth_state += f":{device.battery_percentage:.0f}%"
else:
bluetooth_state = f"connected:{len(connected_devices)}_devices"
else:
bluetooth_state = "enabled"
modus_service.bluetooth = bluetooth_state
def on_bluetooth_clicked(self, *args):
handle_indicator_click(self.bluetooth_mousecapture)
def close_bluetooth(self, *args):
hide_control_center(self.bluetooth_mousecapture)
def hide_controlcenter(self, *args):
hide_control_center(self.bluetooth_mousecapture)
class NetworkIndicator(Box):
def __init__(self, show_window=True, **kwargs):
super().__init__(name="network-indicator", orientation="h", **kwargs)
self.show_window = show_window
self.network_service = NetworkClient()
self.network_icon = svg_file("applets/wifi-clear.svg", size=22)
self.network_button = Button(
name="network-button",
child=self.network_icon,
on_clicked=self.on_wifi_clicked,
)
self.add(self.network_button)
# Set pointer cursor on hover
setup_cursor_hover(self.network_button, "pointer")
# Setup control center using shared function
(
self.wifi_window,
self.wifi_widget,
self.wifi_mousecapture,
) = setup_control_center(
self.show_window,
"wifi",
WifiConnections,
self,
show_back_button=False,
)
if self.wifi_window:
self.wifi_window._pointing_widget = self.network_button
modus_service.connect("wlan-changed", self.on_wlan_changed)
self.network_service.connect("wifi-device-added", self.on_wifi_device_added)
self.network_service.connect(
"ethernet-device-added", self.on_ethernet_device_added
)
self.network_service.connect("changed", self.on_network_changed)
self.update_modus_service_wlan_state()
self.update_state()
def on_wlan_changed(self, service, new_wlan_state):
self.update_state()
def on_wifi_device_added(self, *args):
"""Called when WiFi device is added"""
if self.network_service.wifi_device:
self.network_service.wifi_device.connect(
"changed", self.on_network_direct_changed
)
self.update_modus_service_wlan_state()
self.update_state()
def on_ethernet_device_added(self, *args):
if self.network_service.ethernet_device:
self.network_service.ethernet_device.connect(
"changed", self.on_network_direct_changed
)
self.update_modus_service_wlan_state()
self.update_state()
def on_network_direct_changed(self, *args):
self.update_modus_service_wlan_state()
self.update_state()
def on_network_changed(self, *args):
self.update_modus_service_wlan_state()
self.update_state()
def update_modus_service_wlan_state(self):
wlan_state = "disconnected"
if self.network_service.wifi_device:
wifi = self.network_service.wifi_device
if not wifi.wireless_enabled:
wlan_state = "disabled"
elif wifi.active_access_point:
ap = wifi.active_access_point
wlan_state = f"connected:{ap.ssid}"
if ap.strength >= 0:
wlan_state += f":{ap.strength}%"
else:
wlan_state = "enabled"
# Check Ethernet if WiFi is not connected
elif self.network_service.ethernet_device:
ethernet = self.network_service.ethernet_device
if ethernet.internet == "activated":
wlan_state = "ethernet:connected"
if hasattr(ethernet, "speed") and ethernet.speed:
wlan_state += f":{ethernet.speed}"
elif ethernet.internet == "activating":
wlan_state = "ethernet:connecting"
else:
wlan_state = "ethernet:disconnected"
modus_service.wlan = wlan_state
def update_state(self):
tooltip = "No network connection"
icon_file = "wifi-off-clear.svg"
# Check WiFi first (prioritize WiFi over Ethernet)
if self.network_service.wifi_device:
wifi = self.network_service.wifi_device
if not wifi.wireless_enabled:
icon_file = "wifi-off-clear.svg"
tooltip = "WiFi disabled"
elif wifi.active_access_point:
ap = wifi.active_access_point
# Use dynamic WiFi icon based on signal strength
wifi_icon_path = get_wifi_icon_for_strength(ap.strength)
self.network_icon.set_from_file(wifi_icon_path)
tooltip = f"Connected to {ap.ssid}"
if ap.strength >= 0:
tooltip += f" ({ap.strength}%)"
self.network_button.set_tooltip_text(tooltip)
return # Early return to avoid setting icon again
else:
icon_file = "wifi-off-clear.svg"
tooltip = "WiFi disconnected"
# Check Ethernet if WiFi is not connected
elif self.network_service.ethernet_device:
ethernet = self.network_service.ethernet_device
if ethernet.internet == "activated":
icon_file = "network-wired.svg"
tooltip = "Ethernet connected"
if hasattr(ethernet, "speed") and ethernet.speed:
tooltip += f" ({ethernet.speed})"
elif ethernet.internet == "activating":
icon_file = "network-wired.svg"
tooltip = "Ethernet connecting..."
else:
icon_file = "network-wired-offline.svg"
tooltip = "Ethernet disconnected"
self.network_icon.dynamic_file(f"applets/{icon_file}")
self.network_button.set_tooltip_text(tooltip)
def on_wifi_clicked(self, *args):
handle_indicator_click(self.wifi_mousecapture)
def close_wifi(self, *args):
hide_control_center(self.wifi_mousecapture)
def hide_controlcenter(self, *args):
hide_control_center(self.wifi_mousecapture)
class BatteryIndicator(Box):
def __init__(self, show_window=True, **kwargs):
super().__init__(name="battery-indicator", orientation="h", **kwargs)
self.show_window = show_window
self.battery_service = BatteryService()
self.battery_icon = svg_file("battery/battery-100.svg", size=23)
self.battery_button = Button(
name="battery-button",
child=self.battery_icon,
on_clicked=self.on_battery_clicked,
)
self.battery_label = Label(name="battery-label", label="--- %")
self.add(self.battery_label)
self.add(self.battery_button)
# Set pointer cursor on hover
setup_cursor_hover(self.battery_button, "pointer")
(
self.battery_window,
self.battery_widget,
self.battery_mousecapture,
) = setup_control_center(
self.show_window,
"battery",
BatteryControl,
self,
show_back_button=False,
)
if self.battery_window:
self.battery_window._pointing_widget = self.battery_button
modus_service.connect("battery-changed", self.on_battery_changed)
self.battery_service.connect("changed", self.on_battery_direct_changed)
self.update_modus_service_battery_state()
self.update_state()
def on_battery_changed(self, service, new_battery_state):
self.update_state()
def on_battery_direct_changed(self, *args):
self.update_modus_service_battery_state()
self.update_state()
def _get_percentage(self):
return int(self.battery_service.get_property("Percentage") or 0)
def _get_state(self):
state_value = self.battery_service.get_property("State")
return DeviceState.get(state_value, "UNKNOWN")
def _is_present(self):
return bool(self.battery_service.get_property("IsPresent"))
def _get_time_to_empty(self):
return int(self.battery_service.get_property("TimeToEmpty") or 0)
def _get_time_to_full(self):
return int(self.battery_service.get_property("TimeToFull") or 0)
def _format_time(self, seconds: int) -> str:
return format_duration(seconds)
def _get_battery_icon_file(self, percentage: int, is_charging: bool) -> str:
clamped = max(0, min(100, percentage))
step = (clamped // 10) * 10
filename = f"battery-{step:03d}{'-charging' if is_charging else ''}.svg"
return f"battery/{filename}"
def update_modus_service_battery_state(self):
if not self._is_present():
battery_state = "not_present"
else:
percentage = self._get_percentage()
state_str = self._get_state().lower()
battery_state = f"{state_str}:{percentage}%"
if state_str == "discharging":
time_to_empty = self._format_time(self._get_time_to_empty())
if time_to_empty != "N/A":
battery_state += f":{time_to_empty}"
elif state_str == "charging":
time_to_full = self._format_time(self._get_time_to_full())
if time_to_full != "N/A":
battery_state += f":{time_to_full}"
modus_service.battery = battery_state
def get_battery_tooltip(self, percentage, state):
tooltip = f"Battery: {percentage}%"
if state == "CHARGING":
tooltip += " (Charging)"
time_to_full = self._format_time(self._get_time_to_full())
if time_to_full != "N/A":
tooltip += f" - {time_to_full} until full"
elif state == "DISCHARGING":
time_to_empty = self._format_time(self._get_time_to_empty())
if time_to_empty != "N/A":
tooltip += f" - {time_to_empty} remaining"
elif state == "FULLY_CHARGED":
tooltip += " (Fully charged)"
return tooltip
def update_state(self):
if not self._is_present():
self.set_visible(False)
return
else:
self.set_visible(True)
percentage = self._get_percentage()
state = self._get_state()
is_charging = state in ["CHARGING", "FULLY_CHARGED"]
icon_file = self._get_battery_icon_file(percentage, is_charging)
tooltip = self.get_battery_tooltip(percentage, state)
percentage_text = f"{percentage}%"
self.battery_icon.dynamic_file(icon_file)
self.battery_button.set_tooltip_text(tooltip)
self.battery_label.set_label(percentage_text)
def on_battery_clicked(self, *args):
handle_indicator_click(self.battery_mousecapture)
def close_battery(self, *args):
hide_control_center(self.battery_mousecapture)
def hide_controlcenter(self, *args):
hide_control_center(self.battery_mousecapture)
@@ -0,0 +1,212 @@
from fabric.utils import GLib, os, time
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from services.screencapture import screen_capture_service
from utils.utils import setup_cursor_hover, svg_file
class RecordingIndicator(Button):
def __init__(self, **kwargs):
super().__init__(name="panel-button", visible=True, **kwargs)
self.recording_start_time = None
self.last_process_check = 0
self.process_check_interval = 1.0
self.timer_update_interval = 1000
self.status_check_interval = 2000
self.timer_timeout_id = None
self.status_timeout_id = None
self.recording_icon = svg_file("misc/media-record.svg", size=24)
self.time_label = Label(
name="recording-time-label",
markup="00:00",
max_width_chars=5,
ellipsize="none",
)
self.recording_box = Box(
orientation="h",
spacing=2,
children=[self.recording_icon, self.time_label],
size=(80, -1),
)
self.add(self.recording_box)
# Prevent container.show_all() from forcing this visible when no recording
try:
self.set_no_show_all(True)
except Exception:
pass
self.connect("clicked", self.on_stop_recording)
try:
setup_cursor_hover(self, "pointer")
except Exception:
pass
self.connect("button-press-event", self.on_button_press)
self.set_visible(False)
GLib.timeout_add(100, self._delayed_init)
def on_button_press(self, *args):
GLib.timeout_add(100, lambda: self.remove_style_class("pressed") or False)
return False
def is_recorder_running(self):
return screen_capture_service.is_recording
def check_recording_status(self):
current_time = time.time()
self.last_process_check = current_time
try:
is_recording = self.is_recorder_running()
if is_recording:
if not self.get_visible():
self.set_visible(True)
if self.timer_timeout_id is None:
self.timer_timeout_id = GLib.timeout_add(
self.timer_update_interval, self.update_timer_display
)
if self.recording_start_time is None:
self.recording_start_time = self.get_recording_start_time()
self.update_timer_display()
else:
# Ensure it stays hidden when not recording
self.set_visible(False)
self.cleanup_recording_state()
except Exception as e:
print(f"[DEBUG] Error checking recording status: {e}")
if self.get_visible():
self.set_visible(False)
self.cleanup_recording_state()
return True
def update_timer_display(self):
if not self.get_visible() or self.recording_start_time is None:
return False
try:
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 ({time_text}) - Click to stop"
)
return True
except Exception as e:
print(f"[DEBUG] Error updating timer display: {e}")
return False
def cleanup_recording_state(self):
self.recording_start_time = None
if self.timer_timeout_id:
GLib.source_remove(self.timer_timeout_id)
self.timer_timeout_id = None
def get_recording_start_time(self):
wf_file = "/tmp/recording_start_time.txt"
gpu_file = "/tmp/gpu_recording_start_time.txt"
def read_timestamp(path):
try:
with open(path, "r") as f:
content = f.read().strip()
if content:
t = float(content)
if abs(t - time.time()) <= 3600:
return t
return os.path.getmtime(path)
except (OSError, ValueError):
return None
if os.path.exists(wf_file):
t = read_timestamp(wf_file)
if t:
print("[DEBUG] Using wf-recorder start time")
return t
if os.path.exists(gpu_file):
t = read_timestamp(gpu_file)
if t:
print("[DEBUG] Using gpu-screen-recorder start time")
return t
print("[DEBUG] No start time file found, using current time")
return time.time()
def on_stop_recording(self, *args):
try:
self.set_visible(False)
self.cleanup_recording_state()
screen_capture_service.stop_recording()
GLib.timeout_add(500, self._verify_recording_stopped)
GLib.timeout_add(1500, self._verify_recording_stopped)
GLib.timeout_add(3000, self._verify_recording_stopped)
except Exception:
self.set_visible(False)
self.cleanup_recording_state()
def _verify_recording_stopped(self):
try:
if self.is_recorder_running():
if self.recording_start_time is None:
self.recording_start_time = self.get_recording_start_time()
if self.recording_start_time:
self.set_visible(True)
self.update_timer_display()
if self.timer_timeout_id is None:
self.timer_timeout_id = GLib.timeout_add(
self.timer_update_interval, self.update_timer_display
)
else:
self.set_visible(False)
self.cleanup_recording_state()
except Exception:
self.set_visible(False)
self.cleanup_recording_state()
return False
def _delayed_init(self):
try:
self.check_recording_status()
self.status_timeout_id = GLib.timeout_add(
self.status_check_interval, self.check_recording_status
)
except Exception as e:
print(f"[DEBUG] Error in delayed recording indicator init: {e}")
return False
def destroy(self):
if self.timer_timeout_id:
GLib.source_remove(self.timer_timeout_id)
self.timer_timeout_id = None
if self.status_timeout_id:
GLib.source_remove(self.status_timeout_id)
self.status_timeout_id = None
super().destroy()
+79
View File
@@ -0,0 +1,79 @@
from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton
from fabric.widgets.box import Box
from services.config import get_config, on_config_change
from utils.functions import is_special_workspace_id
from utils.utils import setup_cursor_hover
# TODO: Support multi-monitor setups
class WorkspaceIndicator(Box):
def __init__(self, **kwargs):
Box.__init__(
self, name="workspace-indicator", orientation="h", spacing=4, **kwargs
)
self._current_config = {"hide_special_workspace": True}
on_config_change(self._on_config_changed)
self.workspaces = HyprlandWorkspaces(
name="workspaces",
spacing=4,
buttons_factory=self._get_button_factory(),
)
self.add(self.workspaces)
self.show_all()
self._apply_initial_config()
def _apply_initial_config(self):
new_value = get_config("hide_special_workspace", True)
if new_value != self._current_config["hide_special_workspace"]:
self._current_config["hide_special_workspace"] = new_value
self.update_config({"hide_special_workspace": new_value})
def _on_config_changed(self, new_config: dict, old_config: dict):
if "hide_special_workspace" in new_config and new_config.get(
"hide_special_workspace"
) != old_config.get("hide_special_workspace"):
self._current_config["hide_special_workspace"] = new_config.get(
"hide_special_workspace", True
)
self.update_config(
{
"hide_special_workspace": self._current_config[
"hide_special_workspace"
]
}
)
def _get_button_factory(self):
if self._current_config.get("hide_special_workspace", True):
return self._create_filtered_button
else:
return lambda ws_id: self._create_button_with_hover(ws_id)
def _create_filtered_button(self, ws_id):
if self._current_config.get(
"hide_special_workspace", True
) and is_special_workspace_id(ws_id):
return None
return self._create_button_with_hover(ws_id)
def _create_button_with_hover(self, ws_id):
button = WorkspaceButton(id=ws_id, label=str(ws_id))
setup_cursor_hover(button, "pointer")
return button
def update_config(self, new_config: dict):
if "hide_special_workspace" in new_config:
button_factory = self._get_button_factory()
self.remove(self.workspaces)
self.workspaces = HyprlandWorkspaces(
name="workspaces",
spacing=4,
buttons_factory=button_factory,
)
self.add(self.workspaces)
self.workspaces.show_all()