feat(desktop): updates
This commit is contained in:
1 parent
fc7f2851c9
commit
29f3519965
13 files changed
+549
-29
No files matched your search
+30
-1
@@ -19,6 +19,7 @@ Entry point is `start.py`, delegates to `src/main.py`. `main.py` instantiates
|
||||
- **OSD**: Volume/brightness overlays (`src/window/osd/`)
|
||||
- **Settings**: Configuration GUI (`src/window/settings/`)
|
||||
- **Wallpaper Picker**: Wallpaper browser and management (`src/window/wallpaper/`)
|
||||
- **Desktop Widgets**: User-defined widgets on the desktop background (`src/window/desktop/`)
|
||||
|
||||
## 2. Spotlight Search Engine
|
||||
|
||||
@@ -87,7 +88,35 @@ macOS-style menu bar in the top panel.
|
||||
- Python daemon (`src/window/globalmenu/service.py`) listens for exported menus and
|
||||
renders them as Fabric widgets in the panel
|
||||
|
||||
## 5. State Management & Services
|
||||
## 5. Desktop Widgets
|
||||
|
||||
Desktop widgets render GTK widgets directly on the wallpaper background.
|
||||
|
||||
### Widget System
|
||||
|
||||
- **Registry** (`src/window/desktop/registry.py`): Stores widget classes, sizes, and default positions keyed by string id.
|
||||
- **Built-in widgets** (`src/window/desktop/*.py`): Date, weather, calendar, CPU/RAM info. Each module registers itself at import time.
|
||||
- **User widgets** (`config/desktop/*.py`): Drop-in `.py` files that call `DesktopWidgetRegistry.register()`. Loaded dynamically at startup and watched for live reload.
|
||||
- **Position Manager** (`src/window/desktop/main.py`): Reads widget positions from `config/desktop.toml` (per-monitor, fractional coords). Merges with defaults from registry. Drag/drop saves overrides.
|
||||
- **Desktop Window** (`src/window/desktop/main.py`): `DesktopWidgetWindow` uses a `Gtk.Fixed` container to position widgets at fractional screen coordinates. Handles edit mode (drag and drop), fade animations, and file monitoring.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```
|
||||
Startup:
|
||||
1. Import built-in widget modules → register widgets
|
||||
2. Scan config/desktop/*.py → import & register user widgets
|
||||
3. Read config/desktop.toml → get positions (or populate defaults)
|
||||
4. Create DesktopWidgetWindow per monitor → render widgets
|
||||
|
||||
Runtime:
|
||||
File monitor on config/desktop/ → detect add/delete
|
||||
→ unregister stale widgets → re-import new widgets → rebuild
|
||||
2-second poll fallback for missed file monitor events
|
||||
Drag/drop in edit mode → save position to desktop.toml
|
||||
```
|
||||
|
||||
## 6. State Management & Services
|
||||
|
||||
Services in `src/services/` monitor system state asynchronously and emit
|
||||
signals:
|
||||
|
||||
@@ -9,6 +9,8 @@ Modus is configured via TOML files in the `config/` directory, plus a graphical
|
||||
| `config.toml` | Main settings (features, widgets, paths) | Restart |
|
||||
| `mods.toml` | Custom panel buttons (menus, shell commands) | Instant |
|
||||
| `dock.toml` | Pinned dock apps | Restart |
|
||||
| `desktop.toml` | Desktop widget positions (per-monitor) | Restart |
|
||||
| `config/desktop/*.py` | User desktop widgets | Instant (live reload) |
|
||||
|
||||
## Settings Window
|
||||
|
||||
@@ -156,3 +158,59 @@ pinned = [
|
||||
```
|
||||
|
||||
Use the exact `.desktop` file name or application ID.
|
||||
|
||||
---
|
||||
|
||||
## Desktop Widgets (`config/desktop/` + `desktop.toml`)
|
||||
|
||||
Desktop widgets render directly on your wallpaper. Drop a `.py` file in `config/desktop/` and it appears live.
|
||||
|
||||
### Adding a Widget
|
||||
|
||||
```bash
|
||||
cp examples/desktop/example.py config/desktop/example.py
|
||||
```
|
||||
|
||||
The widget file calls `DesktopWidgetRegistry.register()` with a key, class, size, and default position:
|
||||
|
||||
```python
|
||||
DesktopWidgetRegistry.register("my_widget", MyWidget, (174, 174), (0.5, 0.5))
|
||||
```
|
||||
|
||||
### Positioning
|
||||
|
||||
Positions live in `config/desktop.toml`, one entry per monitor:
|
||||
|
||||
```toml
|
||||
[[0]]
|
||||
key = "my_widget"
|
||||
px = 0.5
|
||||
py = 0.5
|
||||
```
|
||||
|
||||
- `0` = monitor ID
|
||||
- `px`, `py` = 0.0–1.0 fractions of screen
|
||||
|
||||
Drag widgets in edit mode (right-click desktop → Edit Widgets) to reposition.
|
||||
|
||||
### Removing a Widget
|
||||
|
||||
Delete the `.py` file from `config/desktop/`:
|
||||
|
||||
```bash
|
||||
rm config/desktop/example.py
|
||||
```
|
||||
|
||||
Widget disappears within 2 seconds. The entry is also removed from `desktop.toml`.
|
||||
|
||||
### Built-in Widgets
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `date` | Date display |
|
||||
| `weather` | Weather info |
|
||||
| `calendar` | Monthly calendar |
|
||||
| `cpu_info` | CPU usage |
|
||||
| `ram_info` | RAM usage |
|
||||
|
||||
See [Desktop Widgets Guide](desktop_widgets.md) for full documentation and examples.
|
||||
@@ -0,0 +1,210 @@
|
||||
# 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
|
||||
@@ -49,6 +49,25 @@ Update `weather_location` in `config.toml` (e.g., `"London, UK"` or
|
||||
- The venv lives at `config/plugins/.venvs/<plugin_name>/`
|
||||
- Delete that directory to force re-install on next load
|
||||
|
||||
### 8. Desktop widget not appearing?
|
||||
|
||||
- Check the file is in `config/desktop/` (not a subdirectory)
|
||||
- Ensure it calls `DesktopWidgetRegistry.register()` at module level
|
||||
- Check `journalctl --user -f -t modus` for `[DesktopWidgets]` errors
|
||||
- Ensure the widget `key` is unique (not `"date"`, `"weather"`, etc.)
|
||||
|
||||
### 9. Desktop widget still showing after deleting the file?
|
||||
|
||||
The cleanup runs within 2 seconds via file monitor + poll fallback. If it persists:
|
||||
- Check `journalctl --user -f -t modus` for stale widget messages
|
||||
- Restart Modus: `uv run start`
|
||||
|
||||
### 10. Desktop widget position resets after restart?
|
||||
|
||||
- Positions are stored in `config/desktop.toml`
|
||||
- If you delete `desktop.toml`, positions revert to defaults from `register()` calls
|
||||
- Ensure `config/desktop.toml` is writable
|
||||
|
||||
---
|
||||
|
||||
## Pro Tips
|
||||
@@ -56,6 +75,10 @@ Update `weather_location` in `config.toml` (e.g., `"London, UK"` or
|
||||
- **Live Reload for Mods**: `config/mods.toml` is monitored for changes. Edits
|
||||
apply instantly — no restart needed
|
||||
|
||||
- **Desktop Widgets**: Drop `.py` files in `config/desktop/` for instant widgets.
|
||||
Edit `config/desktop.toml` or drag in edit mode to reposition.
|
||||
Remove by deleting the `.py` file — disappears within 2 seconds.
|
||||
|
||||
- **Pinning Apps**: Use exact `.desktop` filename in `dock.toml` (e.g.,
|
||||
`"org.gnome.Nautilus"`, not `"Files"`)
|
||||
|
||||
|
||||
Reference in new issue
Block a user