diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2025-08-05 17:25:30 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2025-08-05 17:25:30 +0800 |
| commit | f9efa56caeeb5289d6b2314e50041a1b7055a3c8 (patch) | |
| tree | 8a9e5b59f8690555ba8ad51f653db02d9483e2a7 /services | |
| parent | f6b5ac3cfba03602fea9d8c30b13075514dbd131 (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.qml | 96 | ||||
| -rw-r--r-- | services/Clip.qml | 149 |
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(); + } + } + } +} |
