summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2025-08-07 17:26:38 +0800
committerlittleblack111 <littleblack11111@gmail.com>2025-08-07 17:33:18 +0800
commit2cb0f939e0392f2520b516fad397053d523210c2 (patch)
tree23c256c87d6cbe54d64d07b4e42d626dc347bcf3
parent20d7bed965bca4becba613f6e59473e9db759735 (diff)
feat(Launcher): Clip
-rw-r--r--config/Launcher.qml3
-rw-r--r--launcher/components/Clip.qml124
-rw-r--r--launcher/todo.md2
-rw-r--r--services/Clip.qml42
4 files changed, 171 insertions, 0 deletions
diff --git a/config/Launcher.qml b/config/Launcher.qml
index ca0748d..a3ce98a 100644
--- a/config/Launcher.qml
+++ b/config/Launcher.qml
@@ -32,6 +32,9 @@ Singleton {
readonly property list<Component> widgets: [
Component {
+ Clip {}
+ },
+ Component {
Spell {}
},
Component {
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