# Desktop Widgets Guide Modus displays widgets directly on your desktop background. Drop a `.py` file in `config/desktop/` and it appears live — no restart needed. ## Quick Start Copy the example widget: ```bash cp examples/desktop/example.py config/desktop/example.py ``` The widget appears on your desktop immediately. Edit positions by dragging in edit mode (right-click desktop → Edit Widgets) or by editing `config/desktop.toml`. ## How It Works 1. On startup, Modus scans `config/desktop/` for `.py` files. 2. Each file is imported. It calls `DesktopWidgetRegistry.register()` to register a widget class with a key, size, and default position. 3. `config/desktop.toml` stores per-monitor positions (`px`, `py` as 0.0–1.0 fractions). 4. A file monitor + 2-second poll watches `config/desktop/` — adding or removing files live-reloads widgets. ## Writing a Desktop Widget ### Minimal Example File: `config/desktop/example.py` ```python from fabric.widgets.box import Box from fabric.widgets.label import Label from window.desktop.registry import DesktopWidgetRegistry class ExampleClockWidget(Box): def __init__(self, **kwargs): super().__init__( name="example-clock-widget", orientation="v", h_expand=True, v_expand=True, h_align="center", v_align="center", **kwargs, ) self.label = Label( name="example-clock-label", label="Hello Desktop", justification="center", ) self.add(self.label) DesktopWidgetRegistry.register( "example", # unique key (used in desktop.toml) ExampleClockWidget, # GTK widget class (174, 174), # size (width, height) in pixels (0.45, 0.45), # default position (px, py) as 0.0–1.0 fractions ) ``` ### `DesktopWidgetRegistry.register()` Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `key` | `str` | Yes | Unique identifier (e.g., `"my_widget"`). Used in `desktop.toml`. | | `widget_cls` | `class` | Yes | GTK widget class (must inherit from a Fabric/GTK widget). | | `size` | `tuple[int, int]` | Yes | `(width, height)` in pixels. | | `default_position` | `tuple[float, float]` | No | `(px, py)` as 0.0–1.0 screen fractions. Default `(0.0, 0.0)`. | ### Widget Rules - Widget must be a GTK widget (inherit from `fabric.widgets.box.Box`, `fabric.widgets.label.Label`, etc.) - Use `name=` for CSS styling (maps to `#example-clock-widget` in CSS) - Set `h_expand=True` / `v_expand=True` for flexible sizing - Call `DesktopWidgetRegistry.register()` at module level (not inside a function) - The `key` must be unique across all widgets ## Built-in Widgets | Key | Widget | Default Position | |-----|--------|-----------------| | `date` | Date display | Top-left `(0.0, 0.0)` | | `weather` | Weather info | Top-left `(0.097, 0.0)` | | `calendar` | Monthly calendar | Top-left `(0.191, 0.001)` | | `cpu_info` | CPU usage | Bottom-right `(0.810, 0.827)` | | `ram_info` | RAM usage | Bottom-right `(0.905, 0.827)` | ## Positioning ### Edit Mode (Drag and Drop) 1. Right-click desktop → **Edit Widgets** 2. Drag widgets to new positions 3. Right-click → **Done Edit** 4. Positions save to `config/desktop.toml` ### Config File Edit `config/desktop.toml` directly: ```toml [[0]] key = "my_widget" px = 0.5 py = 0.5 ``` - `0` = monitor ID (0 for primary) - `px`, `py` = position as 0.0–1.0 fractions of screen ### Default Position Set in your widget file's `register()` call. Used when no entry exists in `desktop.toml`: ```python DesktopWidgetRegistry.register("my_widget", MyWidget, (174, 174), (0.3, 0.7)) ``` ## Lifecycle | Action | What Happens | |--------|-------------| | Add `.py` file to `config/desktop/` | Widget appears on desktop (live) | | Delete `.py` file from `config/desktop/` | Widget disappears (within 2s) | | Edit `.py` file in `config/desktop/` | Widget rebuilds on next file change | | Drag widget in edit mode | Position saves to `desktop.toml` | | Edit `desktop.toml` manually | Position updates on restart | ## Styling Widgets Use the widget's `name` parameter for CSS targeting: ```python self.label = Label(name="my-widget-label", label="Hello") ``` In your CSS (`src/styles/`), target with: ```css #my-widget-label { font-size: 24px; color: @theme_text_color; } #my-widget-label:selected { color: @accent_color; } ``` ## Advanced: Using Services Widgets can access Modus services for live data: ```python from fabric.utils import GLib, invoke_repeater from fabric.widgets.box import Box from fabric.widgets.label import Label from window.desktop.registry import DesktopWidgetRegistry class LiveClockWidget(Box): def __init__(self, **kwargs): super().__init__( name="live-clock", orientation="v", h_align="center", v_align="center", **kwargs, ) self.label = Label(name="clock-label", label="--:--") self.add(self.label) self._timer_id = invoke_repeater(1000, self._update) def _update(self) -> bool: import datetime now = datetime.datetime.now() self.label.set_label(now.strftime("%H:%M")) return True def destroy(self): if hasattr(self, "_timer_id") and self._timer_id: GLib.source_remove(self._timer_id) super().destroy() DesktopWidgetRegistry.register("live_clock", LiveClockWidget, (174, 174), (0.5, 0.0)) ``` ## Troubleshooting ### Widget not appearing? - Check the file is in `config/desktop/` (not a subdirectory) - Check `journalctl --user -f -t modus` for `[DesktopWidgets]` errors - Ensure the file calls `DesktopWidgetRegistry.register()` at module level - Ensure the `key` is unique (not conflicting with built-in widgets) ### Widget still showing after deleting the file? The file monitor + poll should clean up within 2 seconds. If not: 1. Check `journalctl --user -f -t modus` for `[DesktopWidgets] poll found stale:` messages 2. Restart Modus: `uv run start` ### Position resets after restart? - `desktop.toml` stores your custom positions - If you delete `desktop.toml`, positions revert to defaults from `register()` calls - Ensure `config/desktop.toml` is writable