I ported, debugged, and shipped a window preview feature for Dash to Dock — one of the most popular GNOME Shell extensions. This post documents the journey: what worked, what broke repeatedly, and the hard-won lessons about GNOME Shell extension development on Wayland.

The Starting Point: A Stale PR

It began with PR #574 on the upstream repository — an old pull request that added window preview popups when hovering over dock icons. The code was outdated and no longer compatible with current GNOME Shell. I pulled it down, analysed it, and set about porting it to the modern GNOME Shell API.

The goal was straightforward: when a user hovers over an application icon in the dock, show live thumbnail previews of that application’s open windows — similar to the taskbar preview feature in Windows.

The Focus Nightmare

The single hardest problem was focus management. GNOME Shell’s popup menu system uses a modal grab model — when a popup opens, it captures input events. This is fine for right-click context menus, but for hover-triggered preview windows it creates a nightmare:

  • The preview popup would steal focus — once shown, moving the mouse to another dock icon did nothing. The user was trapped.
  • The only escape was clicking elsewhere — hover-out events were swallowed by the modal grab.
  • Fixing focus broke the context menu — every attempt to make the preview release focus would break the right-click context menu. This happened at least 5 times during development.

The solution required carefully managing the popup’s reactive state: the preview needed to be open for rendering purposes but not capture input in the way a traditional menu does. We had to iterate through all dock icons on enter/leave events, explicitly closing previews for other applications.

Debugging on Wayland

GNOME Shell extensions on Wayland present unique debugging challenges. You cannot simply restart the shell — you need a nested Wayland session:

dbus-run-session -- gnome-shell --nested --wayland

Copy-paste does not work between the nested session and the host, so I added file-based logging early on to /tmp/dash-to-dock-DATE.log. This turned out to be essential. The log output was invaluable for analysis, creating a tight feedback loop: run the extension, reproduce the bug, analyse the logs, apply a fix, rebuild, repeat.

The Open/Close/Open Flicker Bug

One persistent bug: on the first hover over any icon, the preview would appear, disappear, then appear again — a visible flicker. It only happened once per icon, then worked fine afterward.

The root cause was a race condition between the hover-open timeout (300ms delay before showing) and the menu system’s own open/close signals. When the popup first rendered, GNOME Shell’s menu manager would fire a menu-closed signal during initial layout, which triggered our close handler, which then got re-triggered by the still-active hover state.

The fix was a justOpened guard flag that suppressed close events for a brief window after the initial open.

Window Preview Sizing

Getting the preview thumbnails to size correctly went through several iterations:

  1. First attempt: Fixed-size preview boxes — resulted in disproportionate previews with large empty spaces for narrow windows.
  2. Second attempt: Scale app thumbnails to fit the preview box — made everything look like squashed squares.
  3. Final approach: Dynamic width based on the application window’s aspect ratio, with a fixed height and a maximum width cap at 90% of desktop horizontal space. Single windows get a tight fit; multiple windows of the same app expand horizontally.

Aero Peek: Fading Away the Clutter

With previews working, I added an Aero Peek feature inspired by Windows: when hovering over a specific window thumbnail in the preview popup, all other windows fade to near-transparency (1% opacity), letting the user see the target window beneath the stack.

I initially tried full transparency (0% opacity) with a glow outline around windows, but GNOME Shell’s compositor does not support per-window glow effects without custom shaders. The simple opacity fade at 1% proved effective enough — windows are essentially invisible but technically still rendered, avoiding compositor edge cases.

Animation Styles That Refused to Animate

I added a settings dropdown for preview animation styles: Instant, Fade, Slide, Scale, Expand, and Dissolve. The settings UI worked, the values were stored and read correctly, the animation config function returned the right style… but nothing visually changed.

Over a dozen debug sessions later, the issue was that the animation transitions were being applied after the popup was already visible. The Clutter animation framework needs the initial state set before the actor is shown, then the target state set to trigger the transition. We were setting both states after show(), so there was nothing to animate between.

The Translation Gap

The feature introduced new user-facing strings — Show window previews on mouse hover, animation style names, etc. These all needed translations across the extension’s supported languages. GNOME Shell extensions use gettext, and each string needed entries in every .po file.

Key Takeaways for GNOME Shell Extension Development

  1. File-based logging is non-negotiable on Wayland — console.log goes to the journal, but you cannot easily copy from a nested session. Write to /tmp/.
  2. The menu/popup system is a minefield — GNOME Shell’s PopupMenu assumes modal interaction. If you need non-modal popups (like hover previews), expect to fight the framework.
  3. Test the right-click menu after every change — focus and event handling changes have a remarkable ability to break context menus silently.
  4. Race conditions are the default — hover events, timeouts, animation completions, and menu signals all fire asynchronously. Guard everything.
  5. Aspect ratio math is harder than it sounds — single window vs. multi-window layout, mixed landscape/portrait windows, and screen size limits all interact.
  6. Use a nested Wayland session — dbus-run-session gnome-shell nested wayland saves you from logging out after every crash.

The Result

The final feature, submitted as PR #2470, adds:

  • Hover-triggered window preview popups with live thumbnails
  • Dynamic sizing based on window aspect ratios
  • Clickable previews that bring windows to focus
  • Close buttons on individual previews
  • Aero Peek transparency effect
  • Multiple animation styles (fade, slide, scale, expand, dissolve)
  • Late-arriving window detection (preview appears when an app finishes launching)
  • Full translation support

Dozens of iterations, countless regressions, and one very stubborn focus-stealing bug later — it works.