130 lines
6.3 KiB
Markdown
130 lines
6.3 KiB
Markdown
# Kadence — the native desktop app
|
|
|
|
Not Electron. **PyQt6 + QML with Qt Quick Controls' Material style**: the window
|
|
is a native Wayland/X11 surface drawn by Qt, using the session's own fonts,
|
|
scaling and colour scheme. The only dependency is PyQt6, which on Arch is the
|
|
system package `python-pyqt6`; everything else — HTTP, config, process scanning —
|
|
is the Python standard library.
|
|
|
|
It is also the piece that *enforces* things, because it is the only part running
|
|
on your machine.
|
|
|
|
```bash
|
|
sudo pacman -S --needed python-pyqt6 qt6-declarative # Arch
|
|
./packaging/install-desktop.sh # menu entry, icon, launcher
|
|
sudo ./helper/install.sh # privileged helper (website blocking)
|
|
kadence-desktop
|
|
```
|
|
|
|
Run it straight from the checkout instead: `python3 -m kadence_desktop`.
|
|
|
|
---
|
|
|
|
## What "native" buys you
|
|
|
|
| Feature | How |
|
|
| --- | --- |
|
|
| **Tray icon that *is* the timer** | `QSystemTrayIcon` (a StatusNotifierItem on KDE). The icon is painted at runtime: a ring that drains as the interval runs, tinted by focus / break / idle, with a pause glyph when held. Middle-click toggles the timer. |
|
|
| **Real notifications with buttons** | `org.freedesktop.Notifications` over D-Bus, with actions: *Start break* · *Another pomodoro* · *+5 min*. Replaced in place, so a day of pomodoros never stacks a column of toasts. |
|
|
| **Scriptable over D-Bus** | `org.kadence.Desktop` exports `StartWork`, `Toggle`, `Pause`, `Complete`, `Extend`, `Interrupt`, `Status`, `SendReport`, `Raise`. Bind any of them to a global shortcut in System Settings — no key-grabbing hacks. |
|
|
| **Screen stays awake** | `org.freedesktop.ScreenSaver.Inhibit` while you focus, released the moment the interval ends. |
|
|
| **Do Not Disturb** | Other apps' banners are suppressed during focus (Plasma's `Notifications.Inhibit`; on GNOME the `show-banners` gsetting, put back afterwards). |
|
|
| **Your music pauses** | Any MPRIS player (Spotify, VLC, mpv, browsers) is paused when focus starts, and *only the ones we paused* resume on the break. |
|
|
| **Taskbar progress** | `com.canonical.Unity.LauncherEntry` — a progress bar under the dock icon and a badge with today's count. |
|
|
| **Knows when you walked away** | logind `Lock`/`Unlock` and `PrepareForSleep` pause and resume the timer; optional idle-pause uses KDE's or Mutter's idle monitor. |
|
|
| **Starts with the session** | XDG autostart entry, or a **systemd user unit** that restarts on failure and logs to the journal. |
|
|
| **Single instance** | Owning the D-Bus name is the lock; a second launch raises the first window and exits. |
|
|
| **Launcher actions** | Right-click the icon: *Start a pomodoro*, *Start a break*, *Start / pause*, *Send today's report*. |
|
|
|
|
Every one of these degrades to a no-op where the desktop does not provide it, so
|
|
the same build runs on KDE, GNOME or a bare window manager.
|
|
|
|
---
|
|
|
|
## Blocking
|
|
|
|
The rules (which sites, which apps, when) live on the backend so the web app can
|
|
edit them and a second machine obeys the same list. This app enforces them:
|
|
|
|
* **Websites** — the managed region of `/etc/hosts` is kept in sync with the
|
|
policy. Writing it needs root, which a desktop app must not have, so it calls
|
|
`kadence-helper` through **pkexec**.
|
|
* **Applications** — `/proc` is scanned every few seconds for your own processes
|
|
matching the list; they are terminated (SIGTERM, then SIGKILL) or merely
|
|
reported, and every attempt is sent to the backend for the evening report.
|
|
* Everything is released when the interval ends — and on exit, including a crash,
|
|
because `shutdown()` clears the block and the inhibits.
|
|
|
|
### The privileged helper
|
|
|
|
`helper/kadence-helper` is the entire privileged surface, and it is deliberately
|
|
dull:
|
|
|
|
* every hostname must match a strict pattern, the redirect must parse as an IP;
|
|
* only the region between its own markers is rewritten — your own entries above
|
|
and below survive byte for byte;
|
|
* the input list is opened `O_NOFOLLOW` and must be a regular file;
|
|
* the write is atomic (temp file + `rename`), and the first run backs up
|
|
`/etc/hosts` to `/etc/hosts.kadence-backup`;
|
|
* `localhost` can never be blocked.
|
|
|
|
```bash
|
|
sudo ./helper/install.sh # polkit asks once per session
|
|
sudo ./helper/install.sh --passwordless # or never, for your user only
|
|
sudo ./helper/install.sh --uninstall # unblocks first, then removes itself
|
|
```
|
|
|
|
---
|
|
|
|
## Global shortcuts
|
|
|
|
System Settings → Shortcuts → Custom Shortcuts → New → D-Bus command:
|
|
|
|
| Field | Value |
|
|
| --- | --- |
|
|
| Service | `org.kadence.Desktop` |
|
|
| Path | `/Timer` |
|
|
| Method | `org.kadence.Timer.Toggle` |
|
|
|
|
Or bind a command: `kadence-desktop --toggle`. The verbs talk to the running
|
|
instance instead of starting a second one:
|
|
|
|
```bash
|
|
kadence-desktop --toggle # start → pause → resume
|
|
kadence-desktop --start # start a focus interval
|
|
kadence-desktop --break
|
|
kadence-desktop --complete
|
|
kadence-desktop --status # "Focus 18:42 · Write the docs · 3/8 today"
|
|
kadence-desktop --report # send today's report to Telegram
|
|
```
|
|
|
|
In-window shortcuts: **Space** start/pause · **Ctrl+Enter** finish ·
|
|
**Ctrl+E** +5 min · **Ctrl+R** refresh sources · **Ctrl+1…5** pages ·
|
|
**Ctrl+W** hide to tray.
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
`~/.config/kadence/desktop.json` holds only what is about *this machine* — the
|
|
backend URL and token, and which integrations this box may use. Timer lengths,
|
|
task sources, block lists and Telegram all live on the server, so they are the
|
|
same everywhere. The **Settings** page edits both, clearly separated.
|
|
|
|
## Layout
|
|
|
|
```
|
|
kadence_desktop/
|
|
app.py # QApplication, QML engine, tray, D-Bus, single instance
|
|
controller.py # the object QML binds to; wires transitions to integrations
|
|
worker.py # the network thread (every HTTP call happens here)
|
|
api.py # stdlib HTTP client for the backend
|
|
config.py # ~/.config/kadence/desktop.json
|
|
tray.py # the painted countdown icon and its menu
|
|
integrations/ # notifications, inhibit/DND, MPRIS, launcher, logind, D-Bus, autostart
|
|
blocking/ # hosts file, /proc scanning, and the manager that syncs policy
|
|
qml/ # Material UI: Timer, Tasks, Blocker, Report, Settings
|
|
helper/ # the privileged helper + polkit action + installer
|
|
packaging/ # .desktop entry, icon, user installer
|
|
```
|