// Clipboard history, backed by Vicinae's existing database. Opening the panel // performs one read-only query; nothing polls or repaints while it is closed. import QtQuick import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import qs.config import qs.services import qs.widgets import qs.modules.quicksettings PanelWindow { id: root visible: ShellState.clipboardOpen color: "transparent" anchors { top: true right: true } margins { top: Theme.barHeight + Theme.barGap * 2 right: Theme.barSideMargin } exclusiveZone: 0 implicitWidth: 430 implicitHeight: 550 WlrLayershell.namespace: "qs-popover-clipboard" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand property string filter: "" readonly property var filteredEntries: Clipboard.entries.filter(entry => { const needle = root.filter.trim().toLowerCase(); if (!needle) return true; return [entry.preview, entry.source, entry.host, entry.typeLabel] .join(" ").toLowerCase().includes(needle); }) function close(): void { ShellState.close(); } function activateFirst(): void { if (root.filteredEntries.length === 0) return; Clipboard.copyEntry(root.filteredEntries[0].id); root.close(); } onVisibleChanged: { if (root.visible) { root.filter = ""; search.clear(); Clipboard.refresh(); openAnim.restart(); Qt.callLater(search.grab); } } HyprlandFocusGrab { windows: [root] active: root.visible onCleared: root.close() } Rectangle { id: surface anchors.fill: parent radius: Theme.popoverRadius color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha) border.width: 1 border.color: Theme.alpha(Theme.fg, 0.08) PrismEdge { anchors { top: parent.top left: parent.left right: parent.right topMargin: 1 } inset: parent.radius } transform: Scale { id: openScale origin.x: surface.width origin.y: 0 xScale: 1 yScale: 1 } Column { anchors.fill: parent anchors.margins: Theme.popoverPadding spacing: Theme.sectionSpacing Item { width: parent.width height: 30 Row { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter spacing: 9 ThemedIcon { anchors.verticalCenter: parent.verticalCenter size: 17 icon: "edit-paste-symbolic" iconFallback: "edit-copy-symbolic" tint: Theme.accent } Text { anchors.verticalCenter: parent.verticalCenter text: "Clipboard" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeTitle font.weight: Font.DemiBold } } Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: Clipboard.entries.length === 1 ? "1 item" : Clipboard.entries.length + " items" color: Theme.fgMuted font.family: Theme.fontFamily font.features: Theme.tabularFigures font.pixelSize: Theme.fontSizeSmall } } SearchField { id: search width: parent.width placeholder: "Search clipboard history" onTextChanged: root.filter = text onAccepted: root.activateFirst() onDismissed: root.close() } Rectangle { width: parent.width height: 1 border.width: 0 color: Theme.alpha(Theme.fg, 0.1) } Item { width: parent.width height: parent.height - y Text { anchors.centerIn: parent visible: Clipboard.loading text: "Loading clipboard history…" color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } Text { anchors.centerIn: parent width: parent.width - 40 visible: !Clipboard.loading && !Clipboard.available horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap text: "Clipboard history is unavailable. Start Vicinae to restore it." color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } Text { anchors.centerIn: parent visible: !Clipboard.loading && Clipboard.available && root.filteredEntries.length === 0 text: root.filter ? "No matching clipboard items" : "Nothing copied yet" color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } ScrollColumn { anchors.fill: parent maxHeight: parent.height spacing: 4 visible: !Clipboard.loading && Clipboard.available && root.filteredEntries.length > 0 Repeater { model: root.filteredEntries ClipboardEntry { required property var modelData width: parent.width entry: modelData onActivated: { Clipboard.copyEntry(modelData.id); root.close(); } } } } } } } NumberAnimation { id: openAnim target: openScale property: "yScale" from: 0.96 to: 1 duration: Theme.durFast easing.type: Easing.OutCubic } }