KDE Plasma 6 dock widget with macOS-style icon magnification
  • QML 77.7%
  • Python 11.7%
  • JavaScript 7%
  • Shell 3.6%
Find a file
Gary c241902289 Save the launcher list only when it changed (no binding loop)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:33:56 +00:00
desktop Load the badge watcher separately so Plasma 6.3 loses only the badges 2026-10-15 18:18:20 +01:00
docs README: Thunderbird does not send badges on Linux 2026-10-15 22:54:04 +01:00
helper Zoom under a still pointer when the auto-hidden dock slides up 2026-10-16 13:18:30 +01:00
kwin/zoomdock-edge Zoom under a still pointer when the auto-hidden dock slides up 2026-10-16 13:18:30 +01:00
package Save the launcher list only when it changed (no binding loop) 2026-10-27 16:33:56 +00:00
thunderbird Thunderbird badge counts unread mail in every folder, not only Inboxes 2026-10-16 12:30:37 +01:00
install.sh Zoom under a still pointer when the auto-hidden dock slides up 2026-10-16 13:18:30 +01:00
README.md Desktop-aware dots and Move Window Here 2026-10-20 11:40:03 +01:00

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.

Zoom Dock magnifying Chrome under the pointer

Plasma 6.0+ · Wayland · QML + one small Python relay · GPL-2.0+


Contents


What you get

The zoom moves with the pointer. These are three real screenshots, taken seconds apart:

Pointer over Sublime Text Pointer between WhatsApp and the solitaire app

At rest, it's a quiet row of icons on a translucent shelf:

The dock at rest

🔍 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:

  1. Installs the widget with kpackagetool6, which puts it in ~/.local/share/plasma/plasmoids/uk.co.bathit.zoomdock.
  2. Installs the badge relay at ~/.local/lib/zoomdock/ and enables it as the systemd user service zoomdock-badges.service.
  3. Sets up Thunderbird badges, if you have a Thunderbird profile. See Thunderbird.
  4. Restarts plasmashell so 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.

Anatomy of the dock panel

  1. Right-click the desktop → Add Panel → Empty Panel, then move it to the bottom edge.
  2. In the panel's edit mode, set:
    • Height: about 110 px. The resting icons use restRatio of 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.sh also 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.
  3. Add Widgets… → search for Zoom Dock → drag it onto the panel.
  4. 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

How the zoom is computed

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

Test badges on the Thunderbird and VS Code icons

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:

Badge relay data flow

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:

  1. 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.
  2. It passes the count to helper/zoomdock-thunderbird.py, a native-messaging helper that Thunderbird starts and keeps running.
  3. The helper announces the count over LauncherEntry like any other app, under every Thunderbird .desktop id 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)

Before and after the Mac-style layout

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.4 and zoomRatio 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:

  1. Is the relay up? systemctl --user status zoomdock-badges. If it failed with No module named 'gi', install PyGObject (see Requirements).
  2. Does the relay have data? Run the GetAll call above. An empty {} means no app has sent anything yet.
  3. Is the app actually running? Badges come from the running app and disappear when it quits.
  4. Look at what the dock logged: journalctl --user -b | grep zoomdock. On Plasma 6.0–6.3 you'll see polling the badge relay, which is expected.
  5. 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.Icon re-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.SignalWatcher only exists from Plasma 6.4, and importing it from main.qml stopped the whole dock loading on 6.3. Now, when it fails to load, the Loader switches to BadgesPoll.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

Made for KDE Plasma by Bath IT · GPL-2.0-or-later