Skip to content

Clipboard Manager

The Problem

Wayland's security model prevents clients from reading the clipboard without focus. The clipboard contents are owned by the last client that copied text — if that client closes, the content is lost. A shell needs a persistent clipboard manager that stores history and re-owns the selection.

The Naive Approach

Polling wl-paste -p every second. This only works when the shell window is focused (Wayland data-device protocol), consumes CPU, and loses entries between copies.

💡Mental Model

Wayland's wlr-data-control-unstable-v1 protocol (supported by wlroots-based compositors like Hyprland, Sway) allows a privileged client to read the selection at any time. Quickshell's Clipboard service uses this protocol to capture clipboard content when it changes, without focus requirements.

The Idea

Use Quickshell's Clipboard service to monitor clipboard changes. Store each unique entry in a history list. Display recent entries in a popup. On click, write the selected entry back to the clipboard via Clipboard.setText().

Let's Build It

qml
import Quickshell
import Quickshell.Services.Clipboard

Column {
  spacing: 6

  ClipboardManager {
    id: clipManager
    maxHistory: 50
  }

  Button {
    text: "Clipboard History"
    onClicked: historyPopup.open()
  }

  PopupWindow {
    id: historyPopup

    Column {
      Repeater {
        model: clipManager.history
        delegate: Rectangle {
          required property var modelData
          width: 300; height: 32; radius: 4
          color: mouse.containsMouse ? "#5294e2" : "#2b2b2b"
          Text {
            text: modelData.text.length > 40 ? modelData.text.substring(0, 40) + "..." : modelData.text
            anchors.centerIn: parent
            elide: Text.ElideRight
          }
          TapHandler {
            onTapped: {
              Clipboard.setText(modelData.text)
              historyPopup.close()
            }
          }
          HoverHandler { id: mouse }
        }
      }
    }
  }
}
🛠️Let's Build It

ClipboardManager (from Quickshell.Services.Clipboard) monitors the wlr-data-control protocol. Every clipboard change fires onClipboardChanged. The history property is a ListModel of { text, timestamp } objects. Clipboard.setText() writes the selected entry back to the Wayland selection.

Let's Improve It

Add support for images (Pixmap entries) and pinning favorites. Show a thumbnail for image entries. Add a "clear" button and search filtering.

qml
TextField {
  placeholderText: "Search clipboard..."
  onTextChanged: clipManager.filter(text)
}
⚠️Common Mistake

Assuming clipboard content is always text. Wayland supports multiple MIME types (text, images, files). Always check entry.mimeType before rendering. Images may need to be displayed via Image { source: entry.data } if the data is a URL or blob.

Under the Hood

The wlr-data-control protocol works via a Wayland global. Quickshell creates a zwlr_data_control_source_v1 and zwlr_data_control_offer_v1. When the selection changes, it reads all offered MIME types and stores the content in memory. Text is decoded as UTF-8, images as raw bytes storable as QByteArray. The clipboard is also re-seated when the shell reappears after a compositor restart.

Exercises

Beginner: Show the last 5 clipboard entries in a simple popup.

Intermediate: Add a "favorites" system where pinned entries survive a history clear. Support drag-and-drop reordering.

Advanced: Implement a clipboard sync across machines using a custom SSH-based protocol or a pastebin service.

What You've Learned
  • Clipboard on Wayland requires wlr-data-control protocol
  • Quickshell ClipboardManager captures text/image with history
  • Supports paste, favorites, and persistence across restarts

Built with VitePress. Licensed under MIT.