- QML 77.7%
- Python 11.7%
- JavaScript 7%
- Shell 3.6%
|
|
||
|---|---|---|
| desktop | ||
| docs | ||
| helper | ||
| kwin/zoomdock-edge | ||
| package | ||
| thunderbird | ||
| install.sh | ||
| README.md | ||
Zoom Dock
A macOS-style magnifying dock for KDE Plasma 6. Icons swell under the pointer, running apps get live window previews, and unread counts land on the icon. Every frame is drawn on the GPU.
Plasma 6.0+ · Wayland · QML + one small Python relay · GPL-2.0+
Contents
- What you get
- Install
- Set up the panel
- Using it
- Settings
- Unread badges and progress
- The Mac-style desktop (
desktop/layout.js) - Updating and uninstalling
- Troubleshooting
- How it works, and why it's built this way
- Repository layout
What you get
The zoom moves with the pointer. These are three real screenshots, taken seconds apart:
At rest, it's a quiet row of icons on a translucent shelf:
| 🔍 Magnification | A smooth raised-cosine bump that follows the pointer. The resting size, magnified size and spread are all adjustable. |
| 🪟 Live window previews | Hover over a running app to see a PipeWire thumbnail of every window it has, each with its own close button. |
| 🔴 Unread badges and progress | Counts (capped at 99+) and progress bars from any app that uses the Unity LauncherEntry API: Telegram, Discover, Electron apps (app.setBadgeCount), and others, plus Thunderbird through a bundled add-on (it doesn't badge on Linux by itself). Unlike Plasma's own task manager, it never badges a count of unread notifications: that number doesn't say what it's counting. |
| ⚪ Running indicators | A dot under a running app for each open window, up to three. They use your accent colour for the app in front and turn orange when the app wants attention; the icon also hops three times, like the Mac's. |
| 🏀 Launch bounce | The icon hops while the app starts, until its first window appears (or 10 seconds pass). |
| ↔️ Drag to reorder | Drag an icon along the dock to move it. The order is saved when you let go. |
| 📌 Drop to pin, drop to open | Drop a .desktop file on the dock to pin that app. Drop any other file on an icon to open it with that app. |
| 🖱️ Scroll to switch | Scrolling over an app steps through its windows. |
| ➗ Divider | A thin line separates pinned apps from apps that are running but not pinned. |
| 🎵 Media controls | A preview shows what the app is playing, with previous, play/pause and next. A speaker on the icon of any app making sound mutes or unmutes it with a click. |
| 🕘 Recent apps | The last few apps you used but haven't pinned (3 by default) stay on the dock after the divider, Mac-style, until newer ones push them off. |
| 📄 Recent files | Right-click an app for the files it opened most recently, from Plasma's activity history. |
| 📥 Downloads and Bin | After a second divider: a Downloads stack that shows the latest dozen files (click to open, drag out to move), and a Bin to drop files on, which shows when it's full. |
| 🖥️ All desktops | By default, apps on other virtual desktops still show as running, and clicking one switches to its desktop rather than opening a second copy. A window on another desktop has a hollow dot (○) instead of a filled one, its preview names the desktop, and Move Window Here (right-click, or the button on its preview) brings it to the one you're on. |
Install
Requirements
| Need | For | Fedora | Debian / Ubuntu | Arch |
|---|---|---|---|---|
| KDE Plasma 6.0+ | the dock | — | — | — |
kpackagetool6 |
installing the widget | kf6-kpackage |
ships with Plasma 6 | ships with Plasma 6 |
| Python 3 + PyGObject | the badge relay | python3-gobject-base |
python3-gi |
python-gobject |
| Plasma 6.4+ | instant badges (6.0–6.3 polls every 2 s instead) | — | — | — |
| Thunderbird 128+ | Thunderbird badges (optional) | — | — | — |
| PipeWire + KWin screencasting | live previews (optional) | default on Plasma Wayland |
Without the optional pieces you still get the dock. On Plasma 6.0–6.3 badges still work, but they update by polling every 2 seconds rather than instantly. Without screencasting, a preview shows the app's icon instead of the window.
One command
git clone ssh://git@git.bathit.co.uk:2026/EZ/zoomdock.git
cd zoomdock
./install.sh
install.sh is safe to re-run; it installs or updates in place. It does four things:
- Installs the widget with
kpackagetool6, which puts it in~/.local/share/plasma/plasmoids/uk.co.bathit.zoomdock. - Installs the badge relay at
~/.local/lib/zoomdock/and enables it as the systemd user servicezoomdock-badges.service. - Sets up Thunderbird badges, if you have a Thunderbird profile. See Thunderbird.
- Restarts
plasmashellso the new QML loads. Your panels blink and redraw. That's expected, and no windows are touched.
Note
Everything installs into your home directory: no
sudo, and nothing written to/usr.
Set up the panel
A panel is a fixed-size window, so an icon can only grow into room the panel already has. The dock therefore wants a thick panel with a transparent background, and it draws its own shelf behind the icons.
- Right-click the desktop → Add Panel → Empty Panel, then move it to the bottom edge.
- In the panel's edit mode, set:
- Height: about 110 px. The resting icons use
restRatioof that height, and the rest is headroom for the zoom. - Width: Fit content, centred.
- Visibility: Auto-hide, or Dodge windows. When an auto-hidden dock slides up under a pointer resting on the bottom edge, Wayland sends it no pointer position until the pointer moves, so
install.shalso installs a small KWin script (kwin/zoomdock-edge) that reports where the pointer is on the bottom edge. The dock zooms there straight away (Plasma 6.4+). - Opacity: Translucent. For a fully see-through panel, add the Panel Colorizer widget to it and give the panel a transparent background.
- Height: about 110 px. The resting icons use
- Add Widgets… → search for Zoom Dock → drag it onto the panel.
- Pin your apps: drag them in from the app launcher, or right-click a running app → Pin to Dock.
If you're replacing the stock task manager, your old pinned apps aren't copied over automatically. desktop/layout.js does that for you.
Using it
| Do this | On | What happens |
|---|---|---|
| Click | a pinned app that isn't running | Launches it, with the bounce |
| Click | a running app | Brings it to the front |
| Click | the app that's already in front | Minimises it |
| Click | an app with several windows | Cycles through its windows |
| Middle-click | any app | Opens a new window on the current desktop (the app's own New Window action where it has one, otherwise a new instance) |
| Right-click | any app | Its recent files, the app's own actions from its .desktop file (Chrome's New Window and New Incognito Window, Firefox's New Private Window…), then Unpin from Dock / Keep in Dock, Open New Window (only for apps without their own), Quit (closes every window) |
| Click | the speaker on an icon | Mutes or unmutes that app |
| Drag | a pinned or recent icon, up off the dock | Fades it; let go there to remove it from the dock |
| Click | Downloads | Shows the latest downloads. Right-click opens the folder |
| Drop | files, onto the Bin | Moves them to the bin. Click opens it; right-click → Empty Bin… asks first |
| Scroll | an app with windows | Steps through its windows |
| Hover | a running app | Live previews. Click a thumbnail to switch to it; middle-click or ✕ closes it |
| Drag | an icon, along the dock | Reorders it (saved on release) |
| Drop | a .desktop file, anywhere on the dock |
Pins that app |
| Drop | any file, onto an icon | Opens the file with that app |
What the dots mean:
- ● under an icon: the app is running, one dot per window (up to three).
- ○ hollow: that window is on another desktop.
- ● in your accent colour: the app is in front.
- ● orange, after three hops: the app wants your attention.
Settings
Right-click the dock → Configure Zoom Dock…
| Setting | Key | Range | Default | What it does |
|---|---|---|---|---|
| Resting size | restRatio |
0.30 – 0.90 | 0.55 | Icon size at rest, as a fraction of the panel's height |
| Magnified size | zoomRatio |
0.50 – 1.00 | 0.95 | Size of the icon right under the pointer |
| Magnification spread | spread |
1 – 5 | 2.5 | How many neighbouring icons, on each side, grow with it |
| Show apps from every virtual desktop | showAllDesktops |
on / off | on | Lists apps whose windows are on other desktops (macOS behaviour) |
| Draw a dock background | showBackground |
on / off | on | The translucent shelf behind the icons |
Tuning tips
- A bigger zoom with the same footprint: lower Resting size. This is the headroom trade-off; the Mac layout uses
0.4/0.97. - A punchier, more local zoom: lower spread to about 1.5.
- A gentle wave across the whole dock: raise spread to 4–5.
- A Magnified size close to 1.0 lets icons touch the top of the panel. Keep it a little under 1.0, or the launch bounce has nowhere to go.
Unread badges and progress
The counts above were sent by the test snippet below.
Apps report unread counts and progress through the Unity LauncherEntry D-Bus API, sending them from their own bus name and object path. Plasma's QML D-Bus watcher can only listen to one fixed service and path, so the dock can't hear those signals directly. A ~100-line relay bridges the two:
The relay keeps the latest state for each app and forgets an app the moment its bus connection closes, so a badge never outlives the app that set it.
On Plasma 6.0–6.3 there is no QML D-Bus watcher, so the dock falls back to BadgesPoll.qml, which asks the relay for its table every 2 seconds.
Thunderbird
Thunderbird doesn't badge its icon on Linux; its unread-count code (MailNotificationManager) only runs on Windows and macOS. So the repo ships a small add-on that does it instead:
- The add-on (
thunderbird/) counts unread mail in every folder of every account, except Trash, Spam, Sent, Drafts, Archives, Templates and Outbox. Mail your filters file into subfolders counts too, which Thunderbird's own Inbox-only badge would miss. - It passes the count to
helper/zoomdock-thunderbird.py, a native-messaging helper that Thunderbird starts and keeps running. - The helper announces the count over LauncherEntry like any other app, under every Thunderbird
.desktopid it finds (thunderbird,org.mozilla.Thunderbird,net.thunderbird.Thunderbird, depending on how it was packaged).
install.sh puts the add-on in every profile listed in ~/.thunderbird/profiles.ini, writes the helper's manifest to ~/.mozilla/native-messaging-hosts/ (where Thunderbird looks for it), and adds extensions.autoDisableScopes = 14 to each profile's user.js. Without that setting, an add-on placed in a profile starts disabled until someone approves it.
Note
The badge appears after Thunderbird restarts, and sometimes only after the second restart. The first start after install can register the add-on without running it. If
pgrep -af '[z]oomdock-thunderbird'shows nothing once Thunderbird is up, restart it once more.
Check that it's running
systemctl --user status zoomdock-badges
gdbus call --session -d uk.co.bathit.ZoomDock -o /Badges -m uk.co.bathit.ZoomDock.Badges.GetAll
# ('{"net.thunderbird.Thunderbird": {"count": 12, "countVisible": true}}',)
Send yourself a test badge
The sender has to stay connected, because the relay drops a badge as soon as its sender goes away. A one-shot gdbus emit therefore flashes and vanishes. This keeps one up for 30 seconds:
python3 - <<'EOF'
import time, gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
bus.emit_signal(None, "/test", "com.canonical.Unity.LauncherEntry", "Update",
GLib.Variant("(sa{sv})", ("application://net.thunderbird.Thunderbird.desktop",
{"count": GLib.Variant("x", 12), "count-visible": GLib.Variant("b", True)})))
bus.flush_sync(None)
time.sleep(30)
EOF
Swap in any app's .desktop file name. Supported keys: count, count-visible, progress (0–1), progress-visible, urgent.
The Mac-style desktop (desktop/layout.js)
desktop/layout.js is a Plasma shell script that turns a stock single-panel desktop into the layout above:
- The existing panel becomes the top bar: 36 px, opaque, auto-hiding. It gets the launcher, the window title (
org.kde.windowtitle.Fork, if installed), the global menu and a spacer, and your tray, clock and Show Desktop stay as they were. - A new bottom panel holds only Zoom Dock: 110 px, fit, centred, auto-hiding, with
restRatio 0.4andzoomRatio 0.97. Panel Colorizer makes its background transparent. - Your pinned apps carry over from the old task manager.
It refuses to run twice. If Zoom Dock is already on a panel, it stops with an error and changes nothing.
Warning
The script expects a runner (
apply.sh) that fills in its two placeholders (__DRY_RUN__,__COLORIZER_SETTINGS__) and applies the widget order it prints on its last line. That runner isn't in this repository yet. Until it is, set the panels up by hand as in Set up the panel. You can still do a dry run, which shows the plan and changes nothing:sed -e 's/__DRY_RUN__/true/' -e 's/__COLORIZER_SETTINGS__/""/' desktop/layout.js > /tmp/zoomdock-layout.js qdbus6 org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$(cat /tmp/zoomdock-layout.js)"(On Fedora the binary is
qdbus-qt6.)
Updating and uninstalling
Update:
cd zoomdock && git pull && ./install.sh
The plasmashell restart is required: Plasma caches the QML, so new code doesn't load without it.
Uninstall: first remove the dock from its panel (or delete the panel), so Plasma isn't left holding a widget that no longer exists. Then:
systemctl --user disable --now zoomdock-badges.service
rm ~/.config/systemd/user/zoomdock-badges.service
rm -r ~/.local/lib/zoomdock
rm -f ~/.mozilla/native-messaging-hosts/uk.co.bathit.zoomdock.json ~/.thunderbird/native-messaging-hosts/uk.co.bathit.zoomdock.json
rm -f ~/.thunderbird/*/extensions/zoomdock-badge@bathit.co.uk.xpi # and the Zoom Dock line in each profile's user.js
systemctl --user daemon-reload
kpackagetool6 -t Plasma/Applet -r uk.co.bathit.zoomdock
systemctl --user restart plasma-plasmashell
Troubleshooting
No badges at all
Work down the chain:
- Is the relay up?
systemctl --user status zoomdock-badges. If it failed withNo module named 'gi', install PyGObject (see Requirements). - Does the relay have data? Run the
GetAllcall above. An empty{}means no app has sent anything yet. - Is the app actually running? Badges come from the running app and disappear when it quits.
- Look at what the dock logged:
journalctl --user -b | grep zoomdock. On Plasma 6.0–6.3 you'll seepolling the badge relay, which is expected. - No Thunderbird badge? Check that the helper is running:
pgrep -af '[z]oomdock-thunderbird'. If it isn't, restart Thunderbird (see the note above). If the relay shows"count": 0, there's simply no unread mail in your folders.
The icons don't grow, or they're clipped at the top
The panel is too thin. Icons grow into the headroom the panel already has. Make it thicker (about 110 px), or lower Resting size to free up room.
Previews show the app's icon instead of the window
Live thumbnails come from KWin's screencasting over PipeWire, which you get on a Plasma Wayland session. Check that PipeWire is running (systemctl --user status pipewire). On X11 you'll see the icon fallback.
I changed the code and nothing happened
Run ./install.sh again. It reinstalls the package and restarts plasmashell, and Plasma won't pick up new QML without that restart.
How it works, and why it's built this way
Each of these was a real bug before it became a rule:
- The icon is rendered once at full zoom size and scaled down on the GPU. Resizing a
Kirigami.Iconre-rasterises it every frame and snaps to whole pixels, which is what made early versions judder. - The pointer is mapped onto the resting layout. If magnification were measured against the magnified positions, every move would shift the icons under the pointer and the zoom would chase itself.
- The widget's width is fixed at "resting row + the most the row can ever grow". If it weren't, the panel would resize while you swept along it.
- Leaving is debounced by 150 ms. Moving from one icon to the next reports a ~8 ms "un-hover", and without the debounce the zoom snapped shut and reopened between icons.
- Launchers are bound into the task model, never assigned later. They have to exist before windows are sorted, or each window keeps whatever slot it arrived in rather than its pinned position.
- Badges load through a
Loader, from their own file.DBus.SignalWatcheronly exists from Plasma 6.4, and importing it frommain.qmlstopped the whole dock loading on 6.3. Now, when it fails to load, theLoaderswitches toBadgesPoll.qml. - A badge's sender has to stay connected. The relay drops an app's badge when its D-Bus connection closes, which is what clears a badge when an app quits. It's also why the Thunderbird helper is a long-running process rather than a script that fires once per change.
- Window geometry isn't published before layout. The minimise animation needs each icon's position on screen, and before the dock is laid out there isn't one.
Repository layout
zoomdock/
├── install.sh install / update (widget, relay, Thunderbird, plasmashell restart)
├── package/ the Plasma widget (uk.co.bathit.zoomdock)
│ ├── metadata.json
│ └── contents/
│ ├── config/main.xml settings schema and defaults
│ ├── config/config.qml
│ └── ui/
│ ├── main.qml the dock: zoom, previews, menu, drag & drop
│ ├── Badges.qml D-Bus listener (loaded separately, Plasma 6.4+)
│ ├── BadgesPoll.qml fallback for Plasma 6.0–6.3: polls the relay
│ └── configGeneral.qml the settings page
├── helper/
│ ├── zoomdock-badges.py LauncherEntry → one fixed D-Bus address; relays the edge pointer
│ ├── zoomdock-badges.service systemd user unit
│ └── zoomdock-thunderbird.py native-messaging helper: Thunderbird count → LauncherEntry
├── thunderbird/ the Thunderbird add-on (unread count across your folders)
├── kwin/zoomdock-edge/ KWin script: pointer position on the bottom edge, for auto-hide
├── desktop/layout.js Mac-style two-panel layout (shell script)
└── docs/
├── diagrams.py regenerates docs/images/*.svg
└── images/
The screenshots in docs/images/*.png are real captures of the dock, cropped to the panel. The diagrams are generated by docs/diagrams.py, and the magnification figure uses the same formula as sizeFor() in main.qml:
python3 -I docs/diagrams.py docs/images




