From d219f493ce82a9de1e9c76a92f08d02a3f768837 Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Tue, 19 Aug 2025 23:31:39 -0400 Subject: [PATCH] launcher v1 --- constants/Icons.qml | 1 + modules/Shell.qml | 3 +- modules/Shortcuts.qml | 4 +- modules/launcher/AppList.qml | 51 ++ modules/launcher/Launcher.qml | 105 +++ modules/launcher/items/AppItem.qml | 57 ++ modules/launcher/services/Apps.qml | 22 + services/Visibility.qml | 1 + utils/FuzzySearch.qml | 39 + utils/scripts/fzf.js | 1307 ++++++++++++++++++++++++++++ widgets/LucideIcon.qml | 11 + widgets/StyledTextField.qml | 8 + 12 files changed, 1605 insertions(+), 4 deletions(-) create mode 100644 modules/launcher/AppList.qml create mode 100644 modules/launcher/Launcher.qml create mode 100644 modules/launcher/items/AppItem.qml create mode 100644 modules/launcher/services/Apps.qml create mode 100644 utils/FuzzySearch.qml create mode 100644 utils/scripts/fzf.js create mode 100644 widgets/LucideIcon.qml create mode 100644 widgets/StyledTextField.qml diff --git a/constants/Icons.qml b/constants/Icons.qml index b905db8..2fee3c8 100644 --- a/constants/Icons.qml +++ b/constants/Icons.qml @@ -17,6 +17,7 @@ Singleton { property string gpu: "\u{E66f}" property string hardDrive: "\u{E0f1}" property string memoryStick: "\u{E44a}" + property string search: "\u{E155}" property string wifiOff: "\u{E1af}" property string wifiLow: "\u{E5fd}" property string wifiHigh: "\u{E5fc}" diff --git a/modules/Shell.qml b/modules/Shell.qml index 8e92771..875a431 100644 --- a/modules/Shell.qml +++ b/modules/Shell.qml @@ -1,8 +1,8 @@ import "bar" +import "launcher" import "powermenu" import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland import QtQuick import QtQuick.Effects @@ -15,6 +15,7 @@ Variants { required property ShellScreen modelData Bar {} + Launcher {} PowerMenu {} } } diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index b0dcb10..430f3ea 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -8,9 +8,7 @@ Scope { LuxShortcut { name: 'launcher' description: 'Open the application launcher' - onPressed: { - console.log("Launcher shortcut pressed"); - } + onPressed: Visibility.launcher = !Visibility.launcher } LuxShortcut { diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml new file mode 100644 index 0000000..471f020 --- /dev/null +++ b/modules/launcher/AppList.qml @@ -0,0 +1,51 @@ +pragma ComponentBehavior: Bound + +import "items" +import "services" +import qs.config +import qs.widgets +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +StyledListView { + id: root + + required property TextField search + + Layout.fillWidth: true + + model: ScriptModel { + id: model + + onValuesChanged: root.currentIndex = 0 + values: Apps.query(search.text) + } + + spacing: 12 + + implicitWidth: 500 + implicitHeight: 500 + highlightMoveDuration: 100 + highlightResizeDuration: 0 + highlight: Rectangle { + radius: 8 + color: Theme.palette.primary + } + + header: StyledText { + text: "Apps" + font.pixelSize: 18 + padding: 8 + } + delegate: appItem + + Component { + id: appItem + + AppItem { + active: ListView.isCurrentItem + } + } +} diff --git a/modules/launcher/Launcher.qml b/modules/launcher/Launcher.qml new file mode 100644 index 0000000..0d7f158 --- /dev/null +++ b/modules/launcher/Launcher.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import "services" +import qs.config +import qs.constants +import qs.services +import qs.widgets +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +StyledWindow { + id: root + name: "launcher" + + visible: Visibility.launcher + implicitWidth: rect.width + implicitHeight: rect.height + + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.keyboardFocus: root.visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + HyprlandFocusGrab { + active: root.visible + windows: [search] + onCleared: { + root.reset(); + } + } + + function reset() { + Visibility.launcher = false; + list.currentIndex = 0; + search.clear(); + } + + WrapperRectangle { + id: rect + + color: Theme.palette.base300 + margin: 18 + radius: 8 + + ColumnLayout { + WrapperRectangle { + id: searchWrapper + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + color: Theme.palette.base200 + margin: 4 + radius: 8 + + RowLayout { + LucideIcon { + id: icon + Layout.leftMargin: 8 + text: Icons.search + } + + StyledTextField { + id: search + + Layout.fillWidth: true + implicitHeight: 40 + cursorVisible: true + focus: root.visible + + placeholderText: "Applications" + placeholderTextColor: "grey" + + Keys.onEscapePressed: event => { + event.accepted = true; + root.reset(); + } + Keys.onReturnPressed: event => { + event.accepted = true; + Apps.launch(list.currentItem.modelData); + root.reset(); + } + Keys.onUpPressed: event => { + event.accepted = true; + list.decrementCurrentIndex(); + } + Keys.onDownPressed: event => { + event.accepted = true; + list.incrementCurrentIndex(); + } + } + } + } + + Item { + implicitWidth: list.width + implicitHeight: list.height + AppList { + id: list + search: search + } + } + } + } +} diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml new file mode 100644 index 0000000..990c50a --- /dev/null +++ b/modules/launcher/items/AppItem.qml @@ -0,0 +1,57 @@ +import qs.config +import qs.widgets +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property DesktopEntry modelData + required property int index + property bool active: false + + anchors.margins: 8 + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: grid.height + + GridLayout { + id: grid + + flow: GridLayout.TopToBottom + columnSpacing: 8 + rowSpacing: 2 + columns: 2 + rows: 2 + + anchors.left: root?.left + anchors.right: root?.right + + IconImage { + Layout.columnSpan: 1 + Layout.rowSpan: 2 + source: Quickshell.iconPath(root.modelData.runInTerminal ? "terminal-app" : root.modelData.icon, "image-missing") + implicitSize: 32 + } + + StyledText { + text: root.modelData.name.trim() + + Layout.fillWidth: true + color: root.active ? Theme.palette.primarycontent : Theme.palette.basecontent + font.pixelSize: 14 + font.bold: true + } + + StyledText { + text: root.modelData.comment != "" ? root.modelData.comment : root.modelData.genericName + + Layout.fillWidth: true + color: root.active ? Theme.palette.primarycontent : Theme.palette.basecontent + font.pixelSize: 10 + elide: Text.ElideRight + } + } +} diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml new file mode 100644 index 0000000..206d058 --- /dev/null +++ b/modules/launcher/services/Apps.qml @@ -0,0 +1,22 @@ +pragma Singleton + +import qs.utils +import Quickshell +import QtQuick + +FuzzySearch { + list: DesktopEntries.applications.values.filter(a => !a.noDisplay).sort((a, b) => a.name.localeCompare(b.name)) + + function launch(entry: DesktopEntry): void { + if (entry.runInTerminal) + Quickshell.execDetached({ + command: ["kitty", "exec", ...entry.command], + workingDirectory: entry.workingDirectory + }); + else + Quickshell.execDetached({ + command: [...entry.command], + workingDirectory: entry.workingDirectory + }); + } +} diff --git a/services/Visibility.qml b/services/Visibility.qml index 1e93a77..c65d00a 100644 --- a/services/Visibility.qml +++ b/services/Visibility.qml @@ -4,6 +4,7 @@ import qs.widgets import Quickshell Singleton { + property bool launcher property bool powermenu property StyledPopupWindow activePopup diff --git a/utils/FuzzySearch.qml b/utils/FuzzySearch.qml new file mode 100644 index 0000000..5d11b2e --- /dev/null +++ b/utils/FuzzySearch.qml @@ -0,0 +1,39 @@ +import Quickshell + +import "scripts/fzf.js" as Fzf +import QtQuick + +Singleton { + required property list list + property string key: "name" + property var extraOpts: ({}) + + // Extra stuff for fuzzy + property list keys: [key] + property list weights: [1] + + readonly property var fzf: new Fzf.Finder(list, Object.assign({ + selector + }, extraOpts)) + + function transformSearch(search: string): string { + return search.trim(); + } + + function selector(item: var): string { + // Only for fzf + return item[key]; + } + + function query(search: string): list { + search = transformSearch(search); + if (!search) + return [...list]; + + return fzf.find(search).sort((a, b) => { + if (a.score === b.score) + return selector(a.item).trim().length - selector(b.item).trim().length; + return b.score - a.score; + }).map(r => r.item); + } +} diff --git a/utils/scripts/fzf.js b/utils/scripts/fzf.js new file mode 100644 index 0000000..995a093 --- /dev/null +++ b/utils/scripts/fzf.js @@ -0,0 +1,1307 @@ +.pragma library + +/* +https://github.com/ajitid/fzf-for-js + +BSD 3-Clause License + +Copyright (c) 2021, Ajit +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const normalized = { + 216: "O", + 223: "s", + 248: "o", + 273: "d", + 295: "h", + 305: "i", + 320: "l", + 322: "l", + 359: "t", + 383: "s", + 384: "b", + 385: "B", + 387: "b", + 390: "O", + 392: "c", + 393: "D", + 394: "D", + 396: "d", + 398: "E", + 400: "E", + 402: "f", + 403: "G", + 407: "I", + 409: "k", + 410: "l", + 412: "M", + 413: "N", + 414: "n", + 415: "O", + 421: "p", + 427: "t", + 429: "t", + 430: "T", + 434: "V", + 436: "y", + 438: "z", + 477: "e", + 485: "g", + 544: "N", + 545: "d", + 549: "z", + 564: "l", + 565: "n", + 566: "t", + 567: "j", + 570: "A", + 571: "C", + 572: "c", + 573: "L", + 574: "T", + 575: "s", + 576: "z", + 579: "B", + 580: "U", + 581: "V", + 582: "E", + 583: "e", + 584: "J", + 585: "j", + 586: "Q", + 587: "q", + 588: "R", + 589: "r", + 590: "Y", + 591: "y", + 592: "a", + 593: "a", + 595: "b", + 596: "o", + 597: "c", + 598: "d", + 599: "d", + 600: "e", + 603: "e", + 604: "e", + 605: "e", + 606: "e", + 607: "j", + 608: "g", + 609: "g", + 610: "G", + 613: "h", + 614: "h", + 616: "i", + 618: "I", + 619: "l", + 620: "l", + 621: "l", + 623: "m", + 624: "m", + 625: "m", + 626: "n", + 627: "n", + 628: "N", + 629: "o", + 633: "r", + 634: "r", + 635: "r", + 636: "r", + 637: "r", + 638: "r", + 639: "r", + 640: "R", + 641: "R", + 642: "s", + 647: "t", + 648: "t", + 649: "u", + 651: "v", + 652: "v", + 653: "w", + 654: "y", + 655: "Y", + 656: "z", + 657: "z", + 663: "c", + 665: "B", + 666: "e", + 667: "G", + 668: "H", + 669: "j", + 670: "k", + 671: "L", + 672: "q", + 686: "h", + 867: "a", + 868: "e", + 869: "i", + 870: "o", + 871: "u", + 872: "c", + 873: "d", + 874: "h", + 875: "m", + 876: "r", + 877: "t", + 878: "v", + 879: "x", + 7424: "A", + 7427: "B", + 7428: "C", + 7429: "D", + 7431: "E", + 7432: "e", + 7433: "i", + 7434: "J", + 7435: "K", + 7436: "L", + 7437: "M", + 7438: "N", + 7439: "O", + 7440: "O", + 7441: "o", + 7442: "o", + 7443: "o", + 7446: "o", + 7447: "o", + 7448: "P", + 7449: "R", + 7450: "R", + 7451: "T", + 7452: "U", + 7453: "u", + 7454: "u", + 7455: "m", + 7456: "V", + 7457: "W", + 7458: "Z", + 7522: "i", + 7523: "r", + 7524: "u", + 7525: "v", + 7834: "a", + 7835: "s", + 8305: "i", + 8341: "h", + 8342: "k", + 8343: "l", + 8344: "m", + 8345: "n", + 8346: "p", + 8347: "s", + 8348: "t", + 8580: "c" +}; +for (let i = "\u0300".codePointAt(0); i <= "\u036F".codePointAt(0); ++i) { + const diacritic = String.fromCodePoint(i); + for (const asciiChar of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") { + const withDiacritic = (asciiChar + diacritic).normalize(); + const withDiacriticCodePoint = withDiacritic.codePointAt(0); + if (withDiacriticCodePoint > 126) { + normalized[withDiacriticCodePoint] = asciiChar; + } + } +} +const ranges = { + a: [7844, 7863], + e: [7870, 7879], + o: [7888, 7907], + u: [7912, 7921] +}; +for (const lowerChar of Object.keys(ranges)) { + const upperChar = lowerChar.toUpperCase(); + for (let i = ranges[lowerChar][0]; i <= ranges[lowerChar][1]; ++i) { + normalized[i] = i % 2 === 0 ? upperChar : lowerChar; + } +} +function normalizeRune(rune) { + if (rune < 192 || rune > 8580) { + return rune; + } + const normalizedChar = normalized[rune]; + if (normalizedChar !== void 0) + return normalizedChar.codePointAt(0); + return rune; +} +function toShort(number) { + return number; +} +function toInt(number) { + return number; +} +function maxInt16(num1, num2) { + return num1 > num2 ? num1 : num2; +} +const strToRunes = (str) => str.split("").map((s) => s.codePointAt(0)); +const runesToStr = (runes) => runes.map((r) => String.fromCodePoint(r)).join(""); +const whitespaceRunes = new Set( + " \f\n\r \v\xA0\u1680\u2028\u2029\u202F\u205F\u3000\uFEFF".split("").map((v) => v.codePointAt(0)) +); +for (let codePoint = "\u2000".codePointAt(0); codePoint <= "\u200A".codePointAt(0); codePoint++) { + whitespaceRunes.add(codePoint); +} +const isWhitespace = (rune) => whitespaceRunes.has(rune); +const whitespacesAtStart = (runes) => { + let whitespaces = 0; + for (const rune of runes) { + if (isWhitespace(rune)) + whitespaces++; + else + break; + } + return whitespaces; +}; +const whitespacesAtEnd = (runes) => { + let whitespaces = 0; + for (let i = runes.length - 1; i >= 0; i--) { + if (isWhitespace(runes[i])) + whitespaces++; + else + break; + } + return whitespaces; +}; +const MAX_ASCII = "\x7F".codePointAt(0); +const CAPITAL_A_RUNE = "A".codePointAt(0); +const CAPITAL_Z_RUNE = "Z".codePointAt(0); +const SMALL_A_RUNE = "a".codePointAt(0); +const SMALL_Z_RUNE = "z".codePointAt(0); +const NUMERAL_ZERO_RUNE = "0".codePointAt(0); +const NUMERAL_NINE_RUNE = "9".codePointAt(0); +function indexAt(index, max, forward) { + if (forward) { + return index; + } + return max - index - 1; +} +const SCORE_MATCH = 16, SCORE_GAP_START = -3, SCORE_GAP_EXTENTION = -1, BONUS_BOUNDARY = SCORE_MATCH / 2, BONUS_NON_WORD = SCORE_MATCH / 2, BONUS_CAMEL_123 = BONUS_BOUNDARY + SCORE_GAP_EXTENTION, BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENTION), BONUS_FIRST_CHAR_MULTIPLIER = 2; +function createPosSet(withPos) { + if (withPos) { + return /* @__PURE__ */ new Set(); + } + return null; +} +function alloc16(offset, slab2, size) { + if (slab2 !== null && slab2.i16.length > offset + size) { + const subarray = slab2.i16.subarray(offset, offset + size); + return [offset + size, subarray]; + } + return [offset, new Int16Array(size)]; +} +function alloc32(offset, slab2, size) { + if (slab2 !== null && slab2.i32.length > offset + size) { + const subarray = slab2.i32.subarray(offset, offset + size); + return [offset + size, subarray]; + } + return [offset, new Int32Array(size)]; +} +function charClassOfAscii(rune) { + if (rune >= SMALL_A_RUNE && rune <= SMALL_Z_RUNE) { + return 1; + } else if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + return 2; + } else if (rune >= NUMERAL_ZERO_RUNE && rune <= NUMERAL_NINE_RUNE) { + return 4; + } else { + return 0; + } +} +function charClassOfNonAscii(rune) { + const char = String.fromCodePoint(rune); + if (char !== char.toUpperCase()) { + return 1; + } else if (char !== char.toLowerCase()) { + return 2; + } else if (char.match(/\p{Number}/gu) !== null) { + return 4; + } else if (char.match(/\p{Letter}/gu) !== null) { + return 3; + } + return 0; +} +function charClassOf(rune) { + if (rune <= MAX_ASCII) { + return charClassOfAscii(rune); + } + return charClassOfNonAscii(rune); +} +function bonusFor(prevClass, currClass) { + if (prevClass === 0 && currClass !== 0) { + return BONUS_BOUNDARY; + } else if (prevClass === 1 && currClass === 2 || prevClass !== 4 && currClass === 4) { + return BONUS_CAMEL_123; + } else if (currClass === 0) { + return BONUS_NON_WORD; + } + return 0; +} +function bonusAt(input, idx) { + if (idx === 0) { + return BONUS_BOUNDARY; + } + return bonusFor(charClassOf(input[idx - 1]), charClassOf(input[idx])); +} +function trySkip(input, caseSensitive, char, from) { + let rest = input.slice(from); + let idx = rest.indexOf(char); + if (idx === 0) { + return from; + } + if (!caseSensitive && char >= SMALL_A_RUNE && char <= SMALL_Z_RUNE) { + if (idx > 0) { + rest = rest.slice(0, idx); + } + const uidx = rest.indexOf(char - 32); + if (uidx >= 0) { + idx = uidx; + } + } + if (idx < 0) { + return -1; + } + return from + idx; +} +function isAscii(runes) { + for (const rune of runes) { + if (rune >= 128) { + return false; + } + } + return true; +} +function asciiFuzzyIndex(input, pattern, caseSensitive) { + if (!isAscii(input)) { + return 0; + } + if (!isAscii(pattern)) { + return -1; + } + let firstIdx = 0, idx = 0; + for (let pidx = 0; pidx < pattern.length; pidx++) { + idx = trySkip(input, caseSensitive, pattern[pidx], idx); + if (idx < 0) { + return -1; + } + if (pidx === 0 && idx > 0) { + firstIdx = idx - 1; + } + idx++; + } + return firstIdx; +} +const fuzzyMatchV2 = (caseSensitive, normalize, forward, input, pattern, withPos, slab2) => { + const M = pattern.length; + if (M === 0) { + return [{ start: 0, end: 0, score: 0 }, createPosSet(withPos)]; + } + const N = input.length; + if (slab2 !== null && N * M > slab2.i16.length) { + return fuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos); + } + const idx = asciiFuzzyIndex(input, pattern, caseSensitive); + if (idx < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let offset16 = 0, offset32 = 0, H0 = null, C0 = null, B = null, F = null; + [offset16, H0] = alloc16(offset16, slab2, N); + [offset16, C0] = alloc16(offset16, slab2, N); + [offset16, B] = alloc16(offset16, slab2, N); + [offset32, F] = alloc32(offset32, slab2, M); + const [, T] = alloc32(offset32, slab2, N); + for (let i = 0; i < T.length; i++) { + T[i] = input[i]; + } + let maxScore = toShort(0), maxScorePos = 0; + let pidx = 0, lastIdx = 0; + const pchar0 = pattern[0]; + let pchar = pattern[0], prevH0 = toShort(0), prevCharClass = 0, inGap = false; + let Tsub = T.subarray(idx); + let H0sub = H0.subarray(idx).subarray(0, Tsub.length), C0sub = C0.subarray(idx).subarray(0, Tsub.length), Bsub = B.subarray(idx).subarray(0, Tsub.length); + for (let [off, char] of Tsub.entries()) { + let charClass = null; + if (char <= MAX_ASCII) { + charClass = charClassOfAscii(char); + if (!caseSensitive && charClass === 2) { + char += 32; + } + } else { + charClass = charClassOfNonAscii(char); + if (!caseSensitive && charClass === 2) { + char = String.fromCodePoint(char).toLowerCase().codePointAt(0); + } + if (normalize) { + char = normalizeRune(char); + } + } + Tsub[off] = char; + const bonus = bonusFor(prevCharClass, charClass); + Bsub[off] = bonus; + prevCharClass = charClass; + if (char === pchar) { + if (pidx < M) { + F[pidx] = toInt(idx + off); + pidx++; + pchar = pattern[Math.min(pidx, M - 1)]; + } + lastIdx = idx + off; + } + if (char === pchar0) { + const score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER; + H0sub[off] = score; + C0sub[off] = 1; + if (M === 1 && (forward && score > maxScore || !forward && score >= maxScore)) { + maxScore = score; + maxScorePos = idx + off; + if (forward && bonus === BONUS_BOUNDARY) { + break; + } + } + inGap = false; + } else { + if (inGap) { + H0sub[off] = maxInt16(prevH0 + SCORE_GAP_EXTENTION, 0); + } else { + H0sub[off] = maxInt16(prevH0 + SCORE_GAP_START, 0); + } + C0sub[off] = 0; + inGap = true; + } + prevH0 = H0sub[off]; + } + if (pidx !== M) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + if (M === 1) { + const result = { + start: maxScorePos, + end: maxScorePos + 1, + score: maxScore + }; + if (!withPos) { + return [result, null]; + } + const pos2 = /* @__PURE__ */ new Set(); + pos2.add(maxScorePos); + return [result, pos2]; + } + const f0 = F[0]; + const width = lastIdx - f0 + 1; + let H = null; + [offset16, H] = alloc16(offset16, slab2, width * M); + { + const toCopy = H0.subarray(f0, lastIdx + 1); + for (const [i, v] of toCopy.entries()) { + H[i] = v; + } + } + let [, C] = alloc16(offset16, slab2, width * M); + { + const toCopy = C0.subarray(f0, lastIdx + 1); + for (const [i, v] of toCopy.entries()) { + C[i] = v; + } + } + const Fsub = F.subarray(1); + const Psub = pattern.slice(1).slice(0, Fsub.length); + for (const [off, f] of Fsub.entries()) { + let inGap2 = false; + const pchar2 = Psub[off], pidx2 = off + 1, row = pidx2 * width, Tsub2 = T.subarray(f, lastIdx + 1), Bsub2 = B.subarray(f).subarray(0, Tsub2.length), Csub = C.subarray(row + f - f0).subarray(0, Tsub2.length), Cdiag = C.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hsub = H.subarray(row + f - f0).subarray(0, Tsub2.length), Hdiag = H.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hleft = H.subarray(row + f - f0 - 1).subarray(0, Tsub2.length); + Hleft[0] = 0; + for (const [off2, char] of Tsub2.entries()) { + const col = off2 + f; + let s1 = 0, s2 = 0, consecutive = 0; + if (inGap2) { + s2 = Hleft[off2] + SCORE_GAP_EXTENTION; + } else { + s2 = Hleft[off2] + SCORE_GAP_START; + } + if (pchar2 === char) { + s1 = Hdiag[off2] + SCORE_MATCH; + let b = Bsub2[off2]; + consecutive = Cdiag[off2] + 1; + if (b === BONUS_BOUNDARY) { + consecutive = 1; + } else if (consecutive > 1) { + b = maxInt16(b, maxInt16(BONUS_CONSECUTIVE, B[col - consecutive + 1])); + } + if (s1 + b < s2) { + s1 += Bsub2[off2]; + consecutive = 0; + } else { + s1 += b; + } + } + Csub[off2] = consecutive; + inGap2 = s1 < s2; + const score = maxInt16(maxInt16(s1, s2), 0); + if (pidx2 === M - 1 && (forward && score > maxScore || !forward && score >= maxScore)) { + maxScore = score; + maxScorePos = col; + } + Hsub[off2] = score; + } + } + const pos = createPosSet(withPos); + let j = f0; + if (withPos && pos !== null) { + let i = M - 1; + j = maxScorePos; + let preferMatch = true; + while (true) { + const I = i * width, j0 = j - f0, s = H[I + j0]; + let s1 = 0, s2 = 0; + if (i > 0 && j >= F[i]) { + s1 = H[I - width + j0 - 1]; + } + if (j > F[i]) { + s2 = H[I + j0 - 1]; + } + if (s > s1 && (s > s2 || s === s2 && preferMatch)) { + pos.add(j); + if (i === 0) { + break; + } + i--; + } + preferMatch = C[I + j0] > 1 || I + width + j0 + 1 < C.length && C[I + width + j0 + 1] > 0; + j--; + } + } + return [{ start: j, end: maxScorePos + 1, score: maxScore }, pos]; +}; +function calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) { + let pidx = 0, score = 0, inGap = false, consecutive = 0, firstBonus = toShort(0); + const pos = createPosSet(withPos); + let prevCharClass = 0; + if (sidx > 0) { + prevCharClass = charClassOf(text[sidx - 1]); + } + for (let idx = sidx; idx < eidx; idx++) { + let rune = text[idx]; + const charClass = charClassOf(rune); + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune === pattern[pidx]) { + if (withPos && pos !== null) { + pos.add(idx); + } + score += SCORE_MATCH; + let bonus = bonusFor(prevCharClass, charClass); + if (consecutive === 0) { + firstBonus = bonus; + } else { + if (bonus === BONUS_BOUNDARY) { + firstBonus = bonus; + } + bonus = maxInt16(maxInt16(bonus, firstBonus), BONUS_CONSECUTIVE); + } + if (pidx === 0) { + score += bonus * BONUS_FIRST_CHAR_MULTIPLIER; + } else { + score += bonus; + } + inGap = false; + consecutive++; + pidx++; + } else { + if (inGap) { + score += SCORE_GAP_EXTENTION; + } else { + score += SCORE_GAP_START; + } + inGap = true; + consecutive = 0; + firstBonus = 0; + } + prevCharClass = charClass; + } + return [score, pos]; +} +function fuzzyMatchV1(caseSensitive, normalize, forward, text, pattern, withPos, slab2) { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let pidx = 0, sidx = -1, eidx = -1; + const lenRunes = text.length; + const lenPattern = pattern.length; + for (let index = 0; index < lenRunes; index++) { + let rune = text[indexAt(index, lenRunes, forward)]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + const pchar = pattern[indexAt(pidx, lenPattern, forward)]; + if (rune === pchar) { + if (sidx < 0) { + sidx = index; + } + pidx++; + if (pidx === lenPattern) { + eidx = index + 1; + break; + } + } + } + if (sidx >= 0 && eidx >= 0) { + pidx--; + for (let index = eidx - 1; index >= sidx; index--) { + const tidx = indexAt(index, lenRunes, forward); + let rune = text[tidx]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + const pidx_ = indexAt(pidx, lenPattern, forward); + const pchar = pattern[pidx_]; + if (rune === pchar) { + pidx--; + if (pidx < 0) { + sidx = index; + break; + } + } + } + if (!forward) { + const sidxTemp = sidx; + sidx = lenRunes - eidx; + eidx = lenRunes - sidxTemp; + } + const [score, pos] = calculateScore( + caseSensitive, + normalize, + text, + pattern, + sidx, + eidx, + withPos + ); + return [{ start: sidx, end: eidx, score }, pos]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const exactMatchNaive = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + const lenRunes = text.length; + const lenPattern = pattern.length; + if (lenRunes < lenPattern) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let pidx = 0; + let bestPos = -1, bonus = toShort(0), bestBonus = toShort(-1); + for (let index = 0; index < lenRunes; index++) { + const index_ = indexAt(index, lenRunes, forward); + let rune = text[index_]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + const pidx_ = indexAt(pidx, lenPattern, forward); + const pchar = pattern[pidx_]; + if (pchar === rune) { + if (pidx_ === 0) { + bonus = bonusAt(text, index_); + } + pidx++; + if (pidx === lenPattern) { + if (bonus > bestBonus) { + bestPos = index; + bestBonus = bonus; + } + if (bonus === BONUS_BOUNDARY) { + break; + } + index -= pidx - 1; + pidx = 0; + bonus = 0; + } + } else { + index -= pidx; + pidx = 0; + bonus = 0; + } + } + if (bestPos >= 0) { + let sidx = 0, eidx = 0; + if (forward) { + sidx = bestPos - lenPattern + 1; + eidx = bestPos + 1; + } else { + sidx = lenRunes - (bestPos + 1); + eidx = lenRunes - (bestPos - lenPattern + 1); + } + const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); + return [{ start: sidx, end: eidx, score }, null]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const prefixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + let trimmedLen = 0; + if (!isWhitespace(pattern[0])) { + trimmedLen = whitespacesAtStart(text); + } + if (text.length - trimmedLen < pattern.length) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + for (const [index, r] of pattern.entries()) { + let rune = text[trimmedLen + index]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune !== r) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + } + const lenPattern = pattern.length; + const [score] = calculateScore( + caseSensitive, + normalize, + text, + pattern, + trimmedLen, + trimmedLen + lenPattern, + false + ); + return [{ start: trimmedLen, end: trimmedLen + lenPattern, score }, null]; +}; +const suffixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + const lenRunes = text.length; + let trimmedLen = lenRunes; + if (pattern.length === 0 || !isWhitespace(pattern[pattern.length - 1])) { + trimmedLen -= whitespacesAtEnd(text); + } + if (pattern.length === 0) { + return [{ start: trimmedLen, end: trimmedLen, score: 0 }, null]; + } + const diff = trimmedLen - pattern.length; + if (diff < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + for (const [index, r] of pattern.entries()) { + let rune = text[index + diff]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune !== r) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + } + const lenPattern = pattern.length; + const sidx = trimmedLen - lenPattern; + const eidx = trimmedLen; + const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); + return [{ start: sidx, end: eidx, score }, null]; +}; +const equalMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + const lenPattern = pattern.length; + if (lenPattern === 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let trimmedLen = 0; + if (!isWhitespace(pattern[0])) { + trimmedLen = whitespacesAtStart(text); + } + let trimmedEndLen = 0; + if (!isWhitespace(pattern[lenPattern - 1])) { + trimmedEndLen = whitespacesAtEnd(text); + } + if (text.length - trimmedLen - trimmedEndLen != lenPattern) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let match = true; + if (normalize) { + const runes = text; + for (const [idx, pchar] of pattern.entries()) { + let rune = runes[trimmedLen + idx]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalizeRune(pchar) !== normalizeRune(rune)) { + match = false; + break; + } + } + } else { + let runesStr = runesToStr(text).substring(trimmedLen, text.length - trimmedEndLen); + if (!caseSensitive) { + runesStr = runesStr.toLowerCase(); + } + match = runesStr === runesToStr(pattern); + } + if (match) { + return [ + { + start: trimmedLen, + end: trimmedLen + lenPattern, + score: (SCORE_MATCH + BONUS_BOUNDARY) * lenPattern + (BONUS_FIRST_CHAR_MULTIPLIER - 1) * BONUS_BOUNDARY + }, + null + ]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const SLAB_16_SIZE = 100 * 1024; +const SLAB_32_SIZE = 2048; +function makeSlab(size16, size32) { + return { + i16: new Int16Array(size16), + i32: new Int32Array(size32) + }; +} +const slab = makeSlab(SLAB_16_SIZE, SLAB_32_SIZE); +var TermType = /* @__PURE__ */ ((TermType2) => { + TermType2[TermType2["Fuzzy"] = 0] = "Fuzzy"; + TermType2[TermType2["Exact"] = 1] = "Exact"; + TermType2[TermType2["Prefix"] = 2] = "Prefix"; + TermType2[TermType2["Suffix"] = 3] = "Suffix"; + TermType2[TermType2["Equal"] = 4] = "Equal"; + return TermType2; +})(TermType || {}); +const termTypeMap = { + [0]: fuzzyMatchV2, + [1]: exactMatchNaive, + [2]: prefixMatch, + [3]: suffixMatch, + [4]: equalMatch +}; +function buildPatternForExtendedMatch(fuzzy, caseMode, normalize, str) { + let cacheable = true; + str = str.trimLeft(); + { + const trimmedAtRightStr = str.trimRight(); + if (trimmedAtRightStr.endsWith("\\") && str[trimmedAtRightStr.length] === " ") { + str = trimmedAtRightStr + " "; + } else { + str = trimmedAtRightStr; + } + } + let sortable = false; + let termSets = []; + termSets = parseTerms(fuzzy, caseMode, normalize, str); + Loop: + for (const termSet of termSets) { + for (const [idx, term] of termSet.entries()) { + if (!term.inv) { + sortable = true; + } + if (!cacheable || idx > 0 || term.inv || fuzzy && term.typ !== 0 || !fuzzy && term.typ !== 1) { + cacheable = false; + if (sortable) { + break Loop; + } + } + } + } + return { + str, + termSets, + sortable, + cacheable, + fuzzy + }; +} +function parseTerms(fuzzy, caseMode, normalize, str) { + str = str.replace(/\\ /g, " "); + const tokens = str.split(/ +/); + const sets = []; + let set = []; + let switchSet = false; + let afterBar = false; + for (const token of tokens) { + let typ = 0, inv = false, text = token.replace(/\t/g, " "); + const lowerText = text.toLowerCase(); + const caseSensitive = caseMode === "case-sensitive" || caseMode === "smart-case" && text !== lowerText; + const normalizeTerm = normalize && lowerText === runesToStr(strToRunes(lowerText).map(normalizeRune)); + if (!caseSensitive) { + text = lowerText; + } + if (!fuzzy) { + typ = 1; + } + if (set.length > 0 && !afterBar && text === "|") { + switchSet = false; + afterBar = true; + continue; + } + afterBar = false; + if (text.startsWith("!")) { + inv = true; + typ = 1; + text = text.substring(1); + } + if (text !== "$" && text.endsWith("$")) { + typ = 3; + text = text.substring(0, text.length - 1); + } + if (text.startsWith("'")) { + if (fuzzy && !inv) { + typ = 1; + } else { + typ = 0; + } + text = text.substring(1); + } else if (text.startsWith("^")) { + if (typ === 3) { + typ = 4; + } else { + typ = 2; + } + text = text.substring(1); + } + if (text.length > 0) { + if (switchSet) { + sets.push(set); + set = []; + } + let textRunes = strToRunes(text); + if (normalizeTerm) { + textRunes = textRunes.map(normalizeRune); + } + set.push({ + typ, + inv, + text: textRunes, + caseSensitive, + normalize: normalizeTerm + }); + switchSet = true; + } + } + if (set.length > 0) { + sets.push(set); + } + return sets; +} +const buildPatternForBasicMatch = (query, casing, normalize) => { + let caseSensitive = false; + switch (casing) { + case "smart-case": + if (query.toLowerCase() !== query) { + caseSensitive = true; + } + break; + case "case-sensitive": + caseSensitive = true; + break; + case "case-insensitive": + query = query.toLowerCase(); + caseSensitive = false; + break; + } + let queryRunes = strToRunes(query); + if (normalize) { + queryRunes = queryRunes.map(normalizeRune); + } + return { + queryRunes, + caseSensitive + }; +}; +function iter(algoFn, tokens, caseSensitive, normalize, forward, pattern, slab2) { + for (const part of tokens) { + const [res, pos] = algoFn(caseSensitive, normalize, forward, part.text, pattern, true, slab2); + if (res.start >= 0) { + const sidx = res.start + part.prefixLength; + const eidx = res.end + part.prefixLength; + if (pos !== null) { + const newPos = /* @__PURE__ */ new Set(); + pos.forEach((v) => newPos.add(part.prefixLength + v)); + return [[sidx, eidx], res.score, newPos]; + } + return [[sidx, eidx], res.score, pos]; + } + } + return [[-1, -1], 0, null]; +} +function computeExtendedMatch(text, pattern, fuzzyAlgo, forward) { + const input = [ + { + text, + prefixLength: 0 + } + ]; + const offsets = []; + let totalScore = 0; + const allPos = /* @__PURE__ */ new Set(); + for (const termSet of pattern.termSets) { + let offset = [0, 0]; + let currentScore = 0; + let matched = false; + for (const term of termSet) { + let algoFn = termTypeMap[term.typ]; + if (term.typ === TermType.Fuzzy) { + algoFn = fuzzyAlgo; + } + const [off, score, pos] = iter( + algoFn, + input, + term.caseSensitive, + term.normalize, + forward, + term.text, + slab + ); + const sidx = off[0]; + if (sidx >= 0) { + if (term.inv) { + continue; + } + offset = off; + currentScore = score; + matched = true; + if (pos !== null) { + pos.forEach((v) => allPos.add(v)); + } else { + for (let idx = off[0]; idx < off[1]; ++idx) { + allPos.add(idx); + } + } + break; + } else if (term.inv) { + offset = [0, 0]; + currentScore = 0; + matched = true; + continue; + } + } + if (matched) { + offsets.push(offset); + totalScore += currentScore; + } + } + return { offsets, totalScore, allPos }; +} +function getResultFromScoreMap(scoreMap, limit) { + const scoresInDesc = Object.keys(scoreMap).map((v) => parseInt(v, 10)).sort((a, b) => b - a); + let result = []; + for (const score of scoresInDesc) { + result = result.concat(scoreMap[score]); + if (result.length >= limit) { + break; + } + } + return result; +} +function getBasicMatchIter(scoreMap, queryRunes, caseSensitive) { + return (idx) => { + const itemRunes = this.runesList[idx]; + if (queryRunes.length > itemRunes.length) + return; + let [match, positions] = this.algoFn( + caseSensitive, + this.opts.normalize, + this.opts.forward, + itemRunes, + queryRunes, + true, + slab + ); + if (match.start === -1) + return; + if (this.opts.fuzzy === false) { + positions = /* @__PURE__ */ new Set(); + for (let position = match.start; position < match.end; ++position) { + positions.add(position); + } + } + const scoreKey = this.opts.sort ? match.score : 0; + if (scoreMap[scoreKey] === void 0) { + scoreMap[scoreKey] = []; + } + scoreMap[scoreKey].push(Object.assign({ + item: this.items[idx], + positions: positions != null ? positions : /* @__PURE__ */ new Set() + }, match)); + }; +} +function getExtendedMatchIter(scoreMap, pattern) { + return (idx) => { + const runes = this.runesList[idx]; + const match = computeExtendedMatch(runes, pattern, this.algoFn, this.opts.forward); + if (match.offsets.length !== pattern.termSets.length) + return; + let sidx = -1, eidx = -1; + if (match.allPos.size > 0) { + sidx = Math.min(...match.allPos); + eidx = Math.max(...match.allPos) + 1; + } + const scoreKey = this.opts.sort ? match.totalScore : 0; + if (scoreMap[scoreKey] === void 0) { + scoreMap[scoreKey] = []; + } + scoreMap[scoreKey].push({ + score: match.totalScore, + item: this.items[idx], + positions: match.allPos, + start: sidx, + end: eidx + }); + }; +} +function basicMatch(query) { + const { queryRunes, caseSensitive } = buildPatternForBasicMatch( + query, + this.opts.casing, + this.opts.normalize + ); + const scoreMap = {}; + const iter2 = getBasicMatchIter.bind(this)( + scoreMap, + queryRunes, + caseSensitive + ); + for (let i = 0, len = this.runesList.length; i < len; ++i) { + iter2(i); + } + return getResultFromScoreMap(scoreMap, this.opts.limit); +} +function extendedMatch(query) { + const pattern = buildPatternForExtendedMatch( + Boolean(this.opts.fuzzy), + this.opts.casing, + this.opts.normalize, + query + ); + const scoreMap = {}; + const iter2 = getExtendedMatchIter.bind(this)(scoreMap, pattern); + for (let i = 0, len = this.runesList.length; i < len; ++i) { + iter2(i); + } + return getResultFromScoreMap(scoreMap, this.opts.limit); +} +const defaultOpts = { + limit: Infinity, + selector: (v) => v, + casing: "smart-case", + normalize: true, + fuzzy: "v2", + tiebreakers: [], + sort: true, + forward: true, + match: basicMatch +}; +class Finder { + constructor(list, ...optionsTuple) { + this.opts = Object.assign(defaultOpts, optionsTuple[0]); + this.items = list; + this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize())); + this.algoFn = exactMatchNaive; + switch (this.opts.fuzzy) { + case "v2": + this.algoFn = fuzzyMatchV2; + break; + case "v1": + this.algoFn = fuzzyMatchV1; + break; + } + } + find(query) { + if (query.length === 0 || this.items.length === 0) + return this.items.slice(0, this.opts.limit).map(createResultItemWithEmptyPos); + query = query.normalize(); + let result = this.opts.match.bind(this)(query); + return postProcessResultItems(result, this.opts); + } +} +function createResultItemWithEmptyPos(item) { + return ({ + item, + start: -1, + end: -1, + score: 0, + positions: /* @__PURE__ */ new Set() + }) +}; +function postProcessResultItems(result, opts) { + if (opts.sort) { + const { selector } = opts; + result.sort((a, b) => { + if (a.score === b.score) { + for (const tiebreaker of opts.tiebreakers) { + const diff = tiebreaker(a, b, selector); + if (diff !== 0) { + return diff; + } + } + } + return 0; + }); + } + if (Number.isFinite(opts.limit)) { + result.splice(opts.limit); + } + return result; +} +function byLengthAsc(a, b, selector) { + return selector(a.item).length - selector(b.item).length; +} +function byStartAsc(a, b) { + return a.start - b.start; +} diff --git a/widgets/LucideIcon.qml b/widgets/LucideIcon.qml new file mode 100644 index 0000000..500fff6 --- /dev/null +++ b/widgets/LucideIcon.qml @@ -0,0 +1,11 @@ +import qs.config +import QtQuick + +Text { + + color: Theme.palette.basecontent + + font.family: Theme.lucide.font.family + font.pixelSize: Dimensions.gpu.iconSize + font.bold: true +} diff --git a/widgets/StyledTextField.qml b/widgets/StyledTextField.qml new file mode 100644 index 0000000..bae90ba --- /dev/null +++ b/widgets/StyledTextField.qml @@ -0,0 +1,8 @@ +import qs.config +import QtQuick.Controls + +TextField { + + color: Theme.palette.basecontent + background: null +}