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

+301
View File
@@ -0,0 +1,301 @@
from fabric.utils import Any, Gdk, cairo
from fabric.widgets.eventbox import EventBox
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.widgets.widget import Widget
from gi.repository import GtkLayerShell # type: ignore
from utils.monitors import HyprlandWithMonitors
from utils.roam import modus_service
class MouseCapture(Window):
"""A background overlay that captures outside clicks without blocking child window interactions"""
def __init__(self, layer: str, child_window: Window, **kwargs):
super().__init__(
layer=layer, # Use the passed layer
anchor="top bottom left right",
exclusivity="auto",
title="modus",
name="MouseCapture",
keyboard_mode="none", # Don't steal keyboard
all_visible=False,
visible=False,
**kwargs,
)
GtkLayerShell.set_exclusive_zone(self, -1)
self.child_window = child_window
# Ensure child window is on overlay layer to be above this capture
if hasattr(self.child_window, "layer"):
self.child_window.layer = "overlay"
if hasattr(self.child_window, "_init_mousecapture"):
self.child_window._init_mousecapture(self)
# Create transparent event box that captures clicks
self.event_box = EventBox(
events=[
"button-press-event",
"button-release-event",
"pointer-motion-mask", # Listen for mouse movement
"enter-notify-mask",
],
all_visible=True,
)
self.event_box.connect("button-press-event", self.on_overlay_click)
self.event_box.connect("motion-notify-event", self.on_mouse_motion)
self.event_box.connect("enter-notify-event", self.on_mouse_motion)
self.children = [self.event_box]
self._hyprland = HyprlandWithMonitors()
self._cursor_pointer = Gdk.Cursor.new_from_name(
Gdk.Display.get_default(), "pointer"
)
self._cursor_default = None # Resets to default arrow
# Make the overlay transparent
self.set_app_paintable(True)
self.connect("draw", self.on_draw)
self.connect("size-allocate", lambda *_: self.update_input_region())
self.connect("map", lambda *_: self.update_input_region())
self.connect("notify::visible", lambda *_: self.update_input_region())
# Add escape key binding to child window
if hasattr(self.child_window, "add_keybinding"):
self.child_window.add_keybinding("Escape", self.hide_child_window)
def on_draw(self, _widget, cr):
"""Make overlay transparent"""
cr.set_source_rgba(0, 0, 0, 0) # Fully transparent
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
return False
def _get_child_window_bounds(self) -> tuple[int, int, int, int]:
"""Calculates absolute screen bounds for the child window using Layer Shell properties"""
try:
# Get monitor geometry
monitor = self._hyprland.display.get_monitor_at_window(
self.child_window.get_window()
)
if not monitor:
monitor = self._hyprland.display.get_default_monitor()
geom = monitor.get_geometry()
mx, my, mw, mh = geom.x, geom.y, geom.width, geom.height
# Get child window properties
alloc = self.child_window.get_allocation()
ww, wh = alloc.width, alloc.height
# fabric's WaylandWindow usually has anchor and margin accessible
anchor = self.child_window.anchor
margin = self.child_window.margin # (top, right, bottom, left)
# Calculation logic based on Layer Shell anchors
# X coordinate
if GtkLayerShell.Edge.LEFT in anchor and GtkLayerShell.Edge.RIGHT in anchor:
x = mx + margin[3]
elif GtkLayerShell.Edge.LEFT in anchor:
x = mx + margin[3]
elif GtkLayerShell.Edge.RIGHT in anchor:
x = mx + mw - ww - margin[1]
else: # Centered horizontally
x = mx + (mw - ww) // 2 + margin[3] - margin[1]
# Y coordinate
if GtkLayerShell.Edge.TOP in anchor and GtkLayerShell.Edge.BOTTOM in anchor:
y = my + margin[0]
elif GtkLayerShell.Edge.TOP in anchor:
y = my + margin[0]
elif GtkLayerShell.Edge.BOTTOM in anchor:
y = my + mh - wh - margin[2]
else: # Centered vertically
y = my + (mh - wh) // 2 + margin[0] - margin[2]
return x, y, ww, wh
except Exception as e:
print(f"Error calculating child window bounds: {e}")
# Fallback to allocation if possible, though likely (0,0)
alloc = self.child_window.get_allocation()
return 0, 0, alloc.width, alloc.height
def _get_widget_absolute_bounds(self, widget: Widget) -> tuple[int, int, int, int]:
"""Calculates absolute screen bounds for a widget in another window"""
try:
toplevel = widget.get_toplevel()
if not toplevel:
return 0, 0, 0, 0
# Get monitor geometry
monitor = self._hyprland.display.get_monitor_at_window(
toplevel.get_window()
)
if not monitor:
monitor = self._hyprland.display.get_default_monitor()
geom = monitor.get_geometry()
mx, my = geom.x, geom.y
# Get relative position within toplevel
alloc = widget.get_allocation()
x, y = widget.translate_coordinates(toplevel, 0, 0) or (0, 0)
# If the toplevel is a Layer Shell window, its (0,0) is monitor relative
# but we need to account for its own margin/anchor if it's not full screen.
# However, for the Panel (which is usually top-anchored), (0,0) is (monitor_x, monitor_y).
return mx + x, my + y, alloc.width, alloc.height
except Exception as e:
print(f"Error calculating widget absolute bounds: {e}")
return 0, 0, 0, 0
def update_input_region(self):
"""Punches holes in the input region for the trigger button and child window"""
window = self.get_window()
if not window:
return
try:
# Full screen region
alloc = self.get_allocation()
region = cairo.Region(cairo.RectangleInt(0, 0, alloc.width, alloc.height))
# Punch hole for trigger button (relative to this overlay window)
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
# get_widget_absolute_bounds returns screen-absolute coords.
# Since overlay is full screen on its monitor, we just need to subtract its monitor's X/Y
px, py, pw, ph = self._get_widget_absolute_bounds(pointing_widget)
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
mx, my = monitor.get_geometry().x, monitor.get_geometry().y
region.subtract(cairo.RectangleInt(px - mx, py - my, pw, ph))
# Punch hole for child window
cx, cy, cw, ch = self._get_child_window_bounds()
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
mx, my = monitor.get_geometry().x, monitor.get_geometry().y
region.subtract(cairo.RectangleInt(cx - mx, cy - my, cw, ch))
window.input_shape_combine_region(region, 0, 0)
except Exception as e:
print(f"Error updating input region: {e}")
def on_mouse_motion(self, _widget, event):
"""Update cursor feedback based on position"""
# (This is mostly redundant now with input regions but kept for extra safety)
if not self.child_window.is_visible():
return False
return False
def on_overlay_click(self, _widget, event):
"""Handle overlay clicks - check if click is outside child window"""
if not self.child_window.is_visible():
return False
# Support double and triple clicks too
if event.type not in [
Gdk.EventType.BUTTON_PRESS,
Gdk.EventType._2BUTTON_PRESS,
Gdk.EventType._3BUTTON_PRESS,
]:
return False
# Use window-local coordinates for robust detection
click_x = event.x
click_y = event.y
# Get window position relative to monitor for absolute -> local conversion
window = self.get_window()
mx, my = 0, 0
if window:
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
geom = monitor.get_geometry()
mx, my = geom.x, geom.y
# Check if click is on the "trigger" (pointing) widget
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
px_abs, py_abs, pw, ph = self._get_widget_absolute_bounds(pointing_widget)
px, py = px_abs - mx, py_abs - my
if px <= click_x <= px + pw and py <= click_y <= py + ph:
# Clicked the button that opened us - hide and consume
self.hide_child_window()
return True
# Get child window bounds (monitor-relative)
cx_abs, cy_abs, cw, ch = self._get_child_window_bounds()
cx, cy = cx_abs - mx, cy_abs - my
# Check if click is inside child window bounds
inside_child = cx <= click_x <= cx + cw and cy <= click_y <= cy + ch
if not inside_child:
# Click is outside child window - hide it immediately
# No delay to avoid race conditions with toggle buttons
self.hide_child_window()
return True # Consume the event
# Click is inside child window - don't consume event
return False
def show_child_window(self, widget: Widget = None, event: Any = None) -> None:
self.set_child_window_visible(True)
def hide_child_window(self, widget: Widget = None, event: Any = None) -> None:
self.set_child_window_visible(False)
def set_child_window_visible(self, visible: bool) -> None:
if visible:
self.child_window.show()
self.show()
else:
self.child_window.hide()
self.hide()
# Update styling on the trigger button
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
if visible:
pointing_widget.add_style_class("active")
else:
pointing_widget.remove_style_class("active")
if hasattr(self.child_window, "_set_mousecapture"):
self.child_window._set_mousecapture(visible)
def toggle_mousecapture(self, *_) -> None:
if self.is_visible():
self.set_child_window_visible(False)
else:
self.set_child_window_visible(True)
class DropDownMouseCapture(MouseCapture):
"""A specialized MouseCapture for dropdown menus with service integration"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
modus_service.connect("dropdowns-hide-changed", self.dropdowns_hide_changed)
def hide_child_window(self, widget: Widget = None, event: Any = None) -> None:
"""Hide child window and update dropdown service state"""
# Update service state before hiding to prevent conflicts
if hasattr(self.child_window, "id"):
if str(modus_service.current_dropdown) == str(self.child_window.id):
modus_service.current_dropdown = None
super().hide_child_window(widget, event)
def dropdowns_hide_changed(self, widget: Widget = None, event: Any = None) -> None:
"""Handle dropdown service hide changes"""
if hasattr(self.child_window, "id"):
if modus_service.current_dropdown == self.child_window.id:
return
return self.hide_child_window(widget, event)