From 2cb0f939e0392f2520b516fad397053d523210c2 Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Thu, 7 Aug 2025 17:26:38 +0800 Subject: feat(Launcher): Clip --- config/Launcher.qml | 3 ++ launcher/components/Clip.qml | 124 +++++++++++++++++++++++++++++++++++++++++++ launcher/todo.md | 2 + services/Clip.qml | 42 +++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/config/Launcher.qml b/config/Launcher.qml index ca0748d..a3ce98a 100644 --- a/config/Launcher.qml +++ b/config/Launcher.qml @@ -31,6 +31,9 @@ Singleton { readonly property int predictiveCompletionRadius: 6 readonly property list widgets: [ + Component { + Clip {} + }, Component { Spell {} }, diff --git a/launcher/components/Clip.qml b/launcher/components/Clip.qml index e69de29..2bad8d6 100644 --- a/launcher/components/Clip.qml +++ b/launcher/components/Clip.qml @@ -0,0 +1,124 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property var clipHist: active ? Clip.fuzzyQuery(inputCleaned) : [] + property int selectedIndex: -1 + + name: "Clipboard" + + prefix: "clip " + + implicitHeight: valid ? layout.height : 0 + + process: function () { + const isValid = clipHist.length > 0; + const first = isValid && clipHist[0]; + return { + valid: isValid, + priority: isValid, + answer: first ? first.icon : "" + }; + } + + exec: function () { + Clip.decodeAndCopy(clipHist[selectedIndex].raw); + } + + up: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > repeater.count - 1) + return true; + selectedIndex++; + } + + onClipHistChanged: { + selectedIndex = 0; + // selectedIndexChanged dont get called somehow + syncSelectionState(); + } + + onSelectedIndexChanged: { + syncSelectionState(); + } + + function syncSelectionState() { + Qt.callLater(() => { + // fucking Qt why tf is repeater not ready when this is called + SelectionState.selected = repeater.itemAt(selectedIndex); + SelectionState.exec = root.exec; + }); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(innerLayout.height + titleBar.height, Launcher.widgetHeight * 1.5) + + Flickable { + id: flickable + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: innerLayout.width + contentHeight: innerLayout.height + + ColumnLayout { + id: innerLayout + spacing: 0 + + Repeater { + id: repeater + model: clipHist + + Item { + required property var modelData + required property int index + Layout.margins: Launcher.innerMargin * 2 + implicitWidth: item.width + implicitHeight: item.height + + RowLayout { + id: item + spacing: Launcher.innerMargin * 2 + + IText { + text: modelData?.isImage ? "Image" : "Text" || "" + } + IText { + text: modelData?.data || "" + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.selectedIndex = index; + } + onExited: { + root.selectedIndex = 0; + } + onPressed: { + root.exec(); + } + } + } + } + } + } + } +} diff --git a/launcher/todo.md b/launcher/todo.md index bfc2103..e974b09 100644 --- a/launcher/todo.md +++ b/launcher/todo.md @@ -31,3 +31,5 @@ when finished: - [ ] color - [ ] time +emoji picker +desktopentry action diff --git a/services/Clip.qml b/services/Clip.qml index af9ee7b..6fee97c 100644 --- a/services/Clip.qml +++ b/services/Clip.qml @@ -3,6 +3,7 @@ pragma Singleton import Quickshell import Quickshell.Io import QtQuick +import "../utils/levendist.js" as Levendist Singleton { id: root @@ -15,6 +16,8 @@ Singleton { property string toCopy: "" property string decoded: "" + property var fuzzyQueryCache: ({}); + property var clipHist: { const arr = []; if (!_clipHist?.length) @@ -57,6 +60,45 @@ Singleton { return arr; } + function fuzzyQuery(search) { + if (!search) + return root.clipHist; + if (root.fuzzyQueryCache[search]) { + return root.fuzzyQueryCache[search]; + } + let source = root.clipHist; + let previousLists = Object.keys(root.fuzzyQueryCache).filter(key => key && search.startsWith(key)); + if (previousLists.length > 0) { + var longest = previousLists.reduce((a, b) => a.length >= b.length ? a : b); + source = root.fuzzyQueryCache[longest]; + } + const p = [], f = []; + for (const s of source) { + const t = (s.isImage ? "image" : (s.data || "")).toLowerCase(); + if (t.startsWith(search)) { + p.push(s); + continue; + } + let pos = 0; + for (const c of search) { + pos = t.indexOf(c, pos); + if (pos < 0) { + pos = 0; + break; + } + pos++; + } + if (pos) + f.push({ + s, + score: Levendist.distance(search, t) + }); + } + const results = p.concat(f.sort((a, b) => a.score - b.score).map(({s}) => s)); + root.fuzzyQueryCache[search] = results; + return results; + } + component SClip_t: QtObject { property int index property bool isImage -- cgit v1.2.3