summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2025-08-05 17:25:30 +0800
committerlittleblack111 <littleblack11111@gmail.com>2025-08-05 17:25:30 +0800
commitf9efa56caeeb5289d6b2314e50041a1b7055a3c8 (patch)
tree8a9e5b59f8690555ba8ad51f653db02d9483e2a7 /services
parentf6b5ac3cfba03602fea9d8c30b13075514dbd131 (diff)
feat: add launcher support and image clipboard history utility
- Integrated a new Launcher component into the shell.qml for improved application launching. - Implemented an IPC handler to toggle the launcher visibility. - Added a new script (cliphist-img.sh) to manage image history from the clipboard. - Removed the outdated fuzzysort.js utility to streamline the codebase. - Refactored the Levenshtein distance algorithm for better performance and clarity.
Diffstat (limited to 'services')
-rw-r--r--services/AppSearch.qml96
-rw-r--r--services/Clip.qml149
2 files changed, 196 insertions, 49 deletions
diff --git a/services/AppSearch.qml b/services/AppSearch.qml
index afab48f..9cd10d4 100644
--- a/services/AppSearch.qml
+++ b/services/AppSearch.qml
@@ -1,6 +1,5 @@
pragma Singleton
-import "../utils/fuzzysort.js" as Fuzzy
import "../utils/levendist.js" as Levendist
import Quickshell
import Quickshell.Io
@@ -13,8 +12,7 @@ import qs.config
Singleton {
id: root
- property bool sloppySearch: General.sloppySearch ?? false
- property real scoreThreshold: 0.2
+ property int scoreThreshold: General.appSearchFuzzySearchThreshold
property var substitutions: ({
"code-url-handler": "visual-studio-code",
@@ -53,40 +51,54 @@ Singleton {
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name))
- readonly property var preppedNames: list.map(a => ({
- name: Fuzzy.prepare(`${a.name} `),
- entry: a
- }))
-
/**
* Returns an Array<DesktopEntry> for a given search.
- * Results are cached by exact search string.
+ * Results are cached by exact search string and, if the
+ * search starts with a previously cached key, uses that
+ * cached result as the source for filtering.
*/
- function fuzzyQuery(search) {
+ function fuzzyQuery(search) { // search is expected to be .toLowerCase()
if (root.fuzzyQueryCache[search]) {
return root.fuzzyQueryCache[search];
}
- let results;
- if (root.sloppySearch) {
- results = list.map(obj => ({
- entry: obj,
- score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase())
- })).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score).map(item => item.entry);
- } else {
- results = Fuzzy.go(search, preppedNames, {
- all: true,
- key: "name"
- }).map(r => r.obj.entry);
+ let source = list;
+ 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.name.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;
}
- /**
- * Caches whether an icon actually exists on disk.
- */
function iconExists(iconName) {
if (!iconName || iconName.length === 0) {
return false;
@@ -99,10 +111,6 @@ Singleton {
return exists;
}
- /**
- * Heavy logic to guess an icon name. We memoize results by the
- * exact input string.
- */
function guessIcon(str) {
if (!str || str.length === 0) {
return "image-missing";
@@ -111,21 +119,18 @@ Singleton {
return root.guessIconCache[str];
}
- let result;
+ var result;
- // 1) direct substitution table
if (substitutions[str]) {
result = substitutions[str];
- } else
- // 2) regex rules
- {
+ } else {
result = null;
- for (let i = 0; i < regexSubstitutions.length; i++) {
- const {
+ for (var i = 0; i < regexSubstitutions.length; i++) {
+ var {
regex,
replace
} = regexSubstitutions[i];
- const replaced = str.replace(regex, replace);
+ var replaced = str.replace(regex, replace);
if (replaced !== str) {
result = replaced;
break;
@@ -133,30 +138,24 @@ Singleton {
}
}
- // 3) if still not found, see if the original exists
- if (!result) {
- if (root.iconExists(str)) {
- result = str;
- }
+ if (!result && root.iconExists(str)) {
+ result = str;
}
- // 4) try domain-reverse name
if (!result) {
- let guessStr = str.split(".").slice(-1)[0].toLowerCase();
+ var guessStr = str.split(".").slice(-1)[0].toLowerCase();
if (root.iconExists(guessStr)) {
result = guessStr;
}
}
- // 5) kebab-case
if (!result) {
- let guessStr = str.toLowerCase().replace(/\s+/g, "-");
- if (root.iconExists(guessStr)) {
- result = guessStr;
+ var kebab = str.toLowerCase().replace(/\s+/g, "-");
+ if (root.iconExists(kebab)) {
+ result = kebab;
}
}
- // 6) fallback to first fuzzy match’s icon
if (!result) {
const matches = root.fuzzyQuery(str);
if (matches.length > 0) {
@@ -167,7 +166,6 @@ Singleton {
}
}
- // 7) give up
if (!result) {
result = str;
}
diff --git a/services/Clip.qml b/services/Clip.qml
new file mode 100644
index 0000000..31ac2d4
--- /dev/null
+++ b/services/Clip.qml
@@ -0,0 +1,149 @@
+pragma Singleton
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+Singleton {
+ id: root
+
+ property list<string> _clipHist: []
+ property list<string> _clipImg: []
+ property list<string> _clipImgIds: _clipImg.map(p => p.split("/").pop().split(".").shift())
+
+ property string toDecode: ""
+ property string toCopy: ""
+ property string decoded: ""
+
+ property var clipHist: {
+ const arr = [];
+ if (!_clipHist?.length)
+ return arr;
+
+ const imgMap = {};
+ const nimg = Math.min(_clipImg.length, _clipImgIds.length);
+ for (let i = 0; i < nimg; i++) {
+ const id = _clipImgIds[i];
+ if (id)
+ imgMap[id] = _clipImg[i];
+ }
+
+ for (let i = 0; i < _clipHist.length; i++) {
+ const raw = _clipHist[i];
+ if (!raw)
+ continue;
+ const tab = raw.indexOf("\t");
+ let idNum = -1;
+ let payload = raw;
+ if (tab > 0) {
+ const n = parseInt(raw.slice(0, tab), 10);
+ if (!Number.isNaN(n)) {
+ idNum = n;
+ payload = raw.slice(tab + 1); // strip "ID<TAB>"
+ }
+ }
+
+ const idStr = idNum >= 0 ? "" + idNum : "";
+ const imgPath = idStr ? imgMap[idStr] || "" : "";
+ const isImg = !!imgPath;
+
+ arr.push({
+ index: idNum,
+ isImage: isImg,
+ data: isImg ? imgPath : payload,
+ raw: raw
+ });
+ }
+ return arr;
+ }
+
+ component SClip_t: QtObject {
+ property int index
+ property bool isImage
+ property string data
+ }
+
+ function decodeAndCopy(text) {
+ toDecode = text;
+ decodeAndCopyProc.running = true;
+ }
+
+ function copy(text) {
+ toCopy = text;
+ copyProc.running = true;
+ }
+
+ function _update() {
+ clipProc.running = true;
+ imgProc.running = true;
+ }
+
+ Process {
+ id: decodeAndCopyProc
+ running: false
+ command: ["sh", "-c", "cliphist decode" + toDecode + " | wl-copy"]
+ stdout: StdioCollector {
+ onStreamFinished: () => {
+ decoded = data;
+ }
+ }
+ onStarted: {
+ decoded = "";
+ }
+ onExited: {
+ toDecode = "";
+ }
+ }
+
+ Process {
+ id: clipProc
+ running: true
+ command: ["cliphist", "-preview-width", 9 ** 9, "-max-items", 9 ** 9, "list"]
+ property bool finished: false
+ stdout: SplitParser {
+ onRead: line => {
+ if (clipProc.finished) {
+ _clipHist = [];
+ clipProc.finished = false;
+ }
+ if (line)
+ _clipHist.push(line);
+ }
+ }
+ onExited: {
+ finished = true;
+ }
+ }
+
+ Process {
+ id: imgProc
+ running: true
+ command: ["sh", Quickshell.shellDir + "/utils/cliphist-img.sh"]
+ stdout: SplitParser {
+ onRead: line => {
+ if (line)
+ _clipImg.push(line);
+ }
+ }
+ }
+
+ Process {
+ id: copyProc
+ running: false
+ command: ["wl-copy", toCopy]
+ onExited: {
+ toCopy = "";
+ }
+ }
+
+ Process {
+ id: watchProc
+ running: true
+ command: ["wl-paste", "-w", "echo"]
+ stdout: SplitParser {
+ onRead: line => {
+ _update();
+ }
+ }
+ }
+}