summaryrefslogtreecommitdiff
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
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.
-rw-r--r--bar/ActiveWindow.qml4
-rw-r--r--bar/Mpris.qml4
-rw-r--r--bar/Workspace.qml2
-rw-r--r--components/IRect.qml20
-rw-r--r--components/IText.qml2
-rw-r--r--components/IWindow.qml1
-rw-r--r--config/Bar.qml2
-rw-r--r--config/Colors.qml1
-rw-r--r--config/General.qml6
-rw-r--r--config/Launcher.qml44
-rw-r--r--config/Style.qml4
-rw-r--r--launcher/ILauncher.qml29
-rw-r--r--launcher/Launcher.qml371
-rw-r--r--launcher/components/App.qml143
-rw-r--r--launcher/components/Calc.qml80
-rw-r--r--launcher/components/Clip.qml0
-rw-r--r--launcher/components/Finder.qml0
-rw-r--r--launcher/components/Flight.qml0
-rw-r--r--launcher/components/IComponent.qml72
-rw-r--r--launcher/components/IInnerComponent.qml34
-rw-r--r--launcher/components/SelectionState.qml27
-rw-r--r--launcher/components/Spell.qml224
-rw-r--r--launcher/components/Time.qml0
-rw-r--r--launcher/components/Translate.qml0
-rw-r--r--launcher/components/Weather.qml0
-rw-r--r--launcher/components/Web.qml0
-rw-r--r--launcher/todo.md33
-rw-r--r--services/AppSearch.qml96
-rw-r--r--services/Clip.qml149
-rw-r--r--shell.qml14
-rw-r--r--utils/cliphist-img.sh15
-rw-r--r--utils/fuzzysort.js692
-rw-r--r--utils/levendist.js325
33 files changed, 1539 insertions, 855 deletions
diff --git a/bar/ActiveWindow.qml b/bar/ActiveWindow.qml
index db69e3e..fe239ed 100644
--- a/bar/ActiveWindow.qml
+++ b/bar/ActiveWindow.qml
@@ -45,8 +45,8 @@ IRect {
IconImage {
id: icon
source: root.icon
- implicitWidth: Bar.appIconSize
- implicitHeight: Bar.appIconSize
+ implicitWidth: General.appIconSize
+ implicitHeight: General.appIconSize
// sync animation
opacity: root.activated ? 1 : 0
diff --git a/bar/Mpris.qml b/bar/Mpris.qml
index 88f5b16..092e459 100644
--- a/bar/Mpris.qml
+++ b/bar/Mpris.qml
@@ -99,8 +99,8 @@ Item {
Item {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
- width: Bar.appIconSize
- height: Bar.appIconSize
+ width: General.appIconSize
+ height: General.appIconSize
opacity: activePlayer?.playbackState === MprisPlaybackState.Playing ? 1 : Bar.mediaPausedOpacity
diff --git a/bar/Workspace.qml b/bar/Workspace.qml
index e04ad75..ebeeb6c 100644
--- a/bar/Workspace.qml
+++ b/bar/Workspace.qml
@@ -79,7 +79,7 @@ Item {
easing.type: Easing.InOutQuad
}
}
- Behavior on implicitWidth {
+ Behavior on width {
NumberAnimation {
duration: Bar.wsAnimationDuration
easing.type: Easing.InOutQuad
diff --git a/components/IRect.qml b/components/IRect.qml
index e5733bf..383affa 100644
--- a/components/IRect.qml
+++ b/components/IRect.qml
@@ -2,10 +2,18 @@ import QtQuick
import qs.config
Rectangle {
- Behavior on color {
- ColorAnimation {
- duration: General.animationDuration
- easing.type: Easing.InOutQuad
- }
- }
+ Behavior on color {
+ NumberAnimation {
+ duration: General.animationDuration
+ easing.type: Easing.InOutQuad
+ }
+ }
+
+ // TODO: use visible instead of opacity for perf, but we need to animate the opacity then set visible
+ Behavior on opacity {
+ NumberAnimation {
+ duration: General.animationDuration
+ easing.type: Easing.InOutQuad
+ }
+ }
}
diff --git a/components/IText.qml b/components/IText.qml
index 31e0423..b6a01b0 100644
--- a/components/IText.qml
+++ b/components/IText.qml
@@ -11,7 +11,7 @@ Text {
property real animateFrom: 0
property real animateTo: 1
- // renderType: Text.NativeRendering
+ renderType: Text.NativeRendering // or Text.CurveRenderer
textFormat: Text.PlainText
elide: Text.ElideRight
color: Colors.foreground1
diff --git a/components/IWindow.qml b/components/IWindow.qml
index 73302fa..2276c8d 100644
--- a/components/IWindow.qml
+++ b/components/IWindow.qml
@@ -1,5 +1,6 @@
import Quickshell
import Quickshell.Wayland
+import QtQuick
PanelWindow {
required property string name
diff --git a/config/Bar.qml b/config/Bar.qml
index b460604..2ae9ab7 100644
--- a/config/Bar.qml
+++ b/config/Bar.qml
@@ -32,8 +32,6 @@ Singleton {
readonly property int wsAnimationDuration: 200
readonly property int wsExtraMouseArea: wsHorizontalSpacing / 2
- readonly property int appIconSize: 25
-
readonly property int resourceIconTextSpacing: Style.spacing.small
readonly property int moduleRadius: Style.rounding.small
diff --git a/config/Colors.qml b/config/Colors.qml
index 6b510f7..259ec73 100644
--- a/config/Colors.qml
+++ b/config/Colors.qml
@@ -22,6 +22,7 @@ Singleton {
readonly property color foreground1: WallustColors.foreground
readonly property color foreground2: WallustColors.color15
readonly property color foreground3: WallustColors.color7
+ readonly property color cursor: WallustColors.cursor
readonly property color accent: background2
readonly property color accentAlt: background1
diff --git a/config/General.qml b/config/General.qml
index 2df675b..75f0e5b 100644
--- a/config/General.qml
+++ b/config/General.qml
@@ -5,8 +5,6 @@ import Quickshell
Singleton {
id: root
- readonly property bool sloopySearch: false
-
readonly property int resourceUpdateInterval: 1
readonly property real iconSize: Style.font.size.larger ?? 16
@@ -15,6 +13,10 @@ Singleton {
readonly property int rectMargin: Style.spacing.small
+ readonly property int appIconSize: 25
+
+ readonly property int appSearchFuzzySearchThreshold: 5
+
readonly property int fontSize: Style.font.size.larger
readonly property int animationDuration: Style.anim.durations.normal
diff --git a/config/Launcher.qml b/config/Launcher.qml
new file mode 100644
index 0000000..ca0748d
--- /dev/null
+++ b/config/Launcher.qml
@@ -0,0 +1,44 @@
+pragma Singleton
+
+import Quickshell
+import QtQuick
+import qs.launcher.components
+
+Singleton {
+ id: root
+
+ readonly property int defaultWidth: 500
+
+ readonly property int widgetWidth: 700
+ readonly property int widgetHeight: 200
+
+ readonly property int innerMargin: 5
+
+ readonly property int borderRadius: Style.rounding.large
+ readonly property int borderWidth: 1
+
+ readonly property int widgetRadius: Style.rounding.small
+ readonly property int widgetFontSize: Style.font.size.large
+
+ readonly property double bgTransparency: 0.4
+ readonly property double widgetBgTransparency: 0.3
+ readonly property double widgetTitleBgTransparency: 0.2 // as it's on top of it
+
+ readonly property bool showWidgetTitle: true
+
+ readonly property int topMargin: 400
+
+ readonly property int predictiveCompletionRadius: 6
+
+ readonly property list<Component> widgets: [
+ Component {
+ Spell {}
+ },
+ Component {
+ App {}
+ },
+ Component {
+ Calc {}
+ }
+ ]
+}
diff --git a/config/Style.qml b/config/Style.qml
index e2c47a0..c6b82a6 100644
--- a/config/Style.qml
+++ b/config/Style.qml
@@ -25,6 +25,7 @@ Singleton {
readonly property int smaller: 10
readonly property int normal: 12
readonly property int larger: 15
+ readonly property int largerr: 17
readonly property int large: 20
}
@@ -37,6 +38,7 @@ Singleton {
}
component FontFamily: QtObject {
+ readonly property string sans: "SF Compact"
readonly property string symbols: "Symbols Nerd Font Mono"
readonly property string iosevka: "Iosevka Nerd Font"
readonly property string mono: "JetBrains Mono"
@@ -47,7 +49,7 @@ Singleton {
readonly property int smaller: 12
readonly property int normal: 13
readonly property int larger: 15
- readonly property int largerr: 16
+ readonly property int largerr: 17
readonly property int large: 18
readonly property int extraLarge: 28
}
diff --git a/launcher/ILauncher.qml b/launcher/ILauncher.qml
new file mode 100644
index 0000000..8793b97
--- /dev/null
+++ b/launcher/ILauncher.qml
@@ -0,0 +1,29 @@
+import Quickshell
+import Quickshell.Hyprland
+import QtQuick.Controls
+import QtQuick
+
+import qs.components
+import qs.config
+
+IWindow {
+ id: root
+ required property var parentLoader
+ // TODO: optional only display on focused screen
+ focusable: true
+
+ anchors {
+ top: false
+ bottom: false
+ left: false
+ right: false
+ }
+
+ HyprlandFocusGrab {
+ active: true
+ windows: [root]
+ onCleared: {
+ parentLoader.active = false; // or active = true again
+ }
+ }
+}
diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml
new file mode 100644
index 0000000..e32dae5
--- /dev/null
+++ b/launcher/Launcher.qml
@@ -0,0 +1,371 @@
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+import "components"
+import qs.components
+import qs.config
+
+// TODO: currently, we're relying on the compositor to provide the animation which ig is fine, but we should use our own in the future, however it might look weird combined with the compositors's
+ILauncher {
+ id: launcher
+ name: "quickshell::launcher::launcher"
+
+ anchors.top: true
+ margins.top: Launcher.topMargin
+ aboveWindows: true
+ exclusionMode: ExclusionMode.Ignore
+
+ IRect {
+ id: container
+ property var selection: SelectionState?.selected || null
+ property var mappedSelection: selection?.mapToItem(null, 0, 0)
+ onMappedSelectionChanged: {
+ // SelectionState?.selected may be slow.. smh qt
+ // my guess is mapToItem is called when its not finished doing whatever it needs to do,
+ // so it doesnt get the right vars, but it doesn't update after since it already triggered it
+ selectionSync.running = true;
+ }
+ Timer {
+ id: selectionSync
+ running: false
+ repeat: false
+ // sync w/ IComponent y animation duration
+ interval: General.animationDuration / 4
+ onTriggered: {
+ parent.mappedSelection = SelectionState?.selected?.mapToItem(null, 0, 0);
+ }
+ }
+
+ IRect {
+ id: activeRect
+ visible: parent.mappedSelection || false
+ y: parent.mappedSelection?.y || 0
+ x: parent.mappedSelection?.x || 0
+ implicitWidth: SelectionState?.selected?.width || 0
+ implicitHeight: SelectionState?.selected?.height || 0
+ radius: Launcher.borderRadius
+ color: Qt.rgba(Colors.foreground2.r, Colors.foreground2.g, Colors.foreground2.b, Launcher.bgTransparency)
+ Behavior on x {
+ ISpringAnimation {}
+ }
+ Behavior on y {
+ ISpringAnimation {}
+ }
+ Behavior on implicitWidth {
+ ISpringAnimation {}
+ }
+ Behavior on height {
+ ISpringAnimation {}
+ }
+ }
+ color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency)
+ radius: Launcher.borderRadius
+
+ width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin
+ height: root.height
+
+ IRect {
+ id: root
+ property alias input: textInput.text
+
+ height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5
+ width: Math.max(widgets.implicitWidth, Launcher.defaultWidth)
+
+ anchors.centerIn: parent
+
+ color: "transparent"
+
+ RowLayout {
+ id: searchBar
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ leftMargin: Launcher.innerMargin
+ rightMargin: Launcher.innerMargin
+ // try to center it when no widgets are present
+ topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this
+
+ Behavior on topMargin {
+ ISpringAnimation {}
+ // NumberAnimation {
+ // duration: General.animationDuration / 2
+ // }
+ }
+ }
+
+ spacing: Launcher.innerMargin * 2
+
+ Icon {
+ text: ""
+ }
+
+ RowLayout {
+ spacing: 0
+ TextInput {
+ id: textInput
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.maximumWidth: implicitWidth
+ color: Colors.foreground1
+ selectionColor: Colors.background3
+ text: SelectionState?.input || ""
+ clip: true
+ focus: true
+ renderType: Text.CurveRendering
+ antialiasing: true
+ smooth: true
+ font {
+ pixelSize: Style.font.size.large
+ family: Style.font.family.sans
+ wordSpacing: 5
+ }
+ // move to different file if we have more textinput
+ cursorDelegate: IRect {
+ id: cursor
+ color: Colors.cursor
+ // width: 1.5
+ width: 2
+ height: 1
+ // blinking animation
+ SequentialAnimation {
+ running: true
+ loops: Animation.Infinite
+ PauseAnimation {
+ duration: 500
+ }
+ ParallelAnimation {
+ PropertyAnimation {
+ target: cursor
+ property: "opacity"
+ to: 0
+ duration: General.animationDuration / 5
+ }
+ PropertyAnimation {
+ target: cursor
+ property: "scale"
+ to: 0
+ duration: General.animationDuration / 5
+ }
+ }
+ PauseAnimation {
+ duration: 500
+ }
+ ParallelAnimation {
+ PropertyAnimation {
+ target: cursor
+ property: "opacity"
+ to: 1
+ duration: General.animationDuration / 5
+ }
+ PropertyAnimation {
+ target: cursor
+ property: "scale"
+ to: 1
+ duration: General.animationDuration / 5
+ }
+ }
+ }
+ }
+ onTextChanged: {
+ if (SelectionState.input !== textInput.text)
+ SelectionState.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias
+ }
+ onCursorPositionChanged: {
+ SelectionState.cursorPosition = textInput.cursorPosition;
+ }
+ // onActiveFocusChanged: {
+ // parentLoader.active = activeFocus;
+ // }
+ onAccepted: {
+ SelectionState?.priorities[0]?._exec(); // TODO: kde like waiting animation for app to launch
+ parentLoader.active = false;
+ textInput.text = "";
+ }
+ Component.onCompleted: {
+ selectAll();
+ }
+ }
+ IRect {
+ opacity: SelectionState?.priorities[0]?.predictiveCompletion ? 1 : 0
+ Layout.fillHeight: true
+ Layout.preferredWidth: text.width
+ radius: Launcher.predictiveCompletionRadius
+ color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency)
+ IText {
+ id: text
+ // animate: true // too jumpy
+ color: Colors.foreground3
+ renderType: Text.CurveRendering
+ antialiasing: true
+ smooth: true
+ font {
+ pixelSize: Style.font.size.large
+ family: Style.font.family.sans
+ wordSpacing: 5
+ }
+ text: SelectionState?.priorities[0]?.predictiveCompletion || ""
+ }
+ Behavior on Layout.preferredWidth {
+ ISpringAnimation {}
+ // NumberAnimation {
+ // duration: General.animationDuration / 5
+ // }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: General.animationDuration / 2
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ Item {
+ Layout.fillWidth: true
+ }
+ Loader {
+ active: SelectionState?.priorities[0]?.priority || false // .priority should always be true if it's in priorities
+ sourceComponent: SelectionState?.priorities[0]?.preview
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: widgets
+ anchors {
+ top: searchBar.bottom
+ left: parent.left
+ right: parent.right
+ topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME
+ }
+
+ Repeater {
+ model: Launcher.widgets
+ delegate: Loader {
+ active: true
+ sourceComponent: modelData
+ Component.onCompleted: {
+ SelectionState.widgets[index] = item;
+ // FIXME: prev predictiveCompletion still exists here
+ // workaround atm: just clear it or preserve the text manually
+ }
+ Connections {
+ target: item
+ function onClose() {
+ parentLoader.active = false;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // not Keys.onPressed because textinput will steal it
+ Shortcut {
+ sequence: "Ctrl+H"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ if (!item)
+ return;
+ item.prev();
+ }
+ }
+
+ Shortcut {
+ sequence: "Ctrl+J"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ if (!item)
+ return;
+ item.down();
+ }
+ }
+
+ Shortcut {
+ sequence: "Ctrl+K"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ if (!item)
+ return;
+ item.up();
+ }
+ }
+
+ Shortcut {
+ sequence: "Ctrl+L"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ if (!item)
+ return;
+ item.next();
+ }
+ }
+
+ Shortcut {
+ sequence: "Escape"
+ context: Qt.ApplicationShortcut
+ onActivated: parentLoader.active = false
+ }
+
+ Shortcut {
+ sequence: "Down"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.down();
+ }
+ }
+
+ Shortcut {
+ sequence: "Up"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.up();
+ }
+ }
+
+ Shortcut {
+ sequence: "PageDown"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.pageDown();
+ }
+ }
+
+ Shortcut {
+ sequence: "PageUp"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.pageUp();
+ }
+ }
+
+ Shortcut {
+ sequence: "Home"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.home();
+ }
+ }
+
+ Shortcut {
+ sequence: "End"
+ context: Qt.ApplicationShortcut
+ onActivated: {
+ const item = SelectionState?.priorities[0];
+ item?.end();
+ }
+ }
+ }
+
+ implicitWidth: container.width
+ implicitHeight: container.height
+}
diff --git a/launcher/components/App.qml b/launcher/components/App.qml
new file mode 100644
index 0000000..6f0e4ce
--- /dev/null
+++ b/launcher/components/App.qml
@@ -0,0 +1,143 @@
+import Quickshell
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+import qs.components
+import qs.services
+import qs.config
+
+IComponent {
+ id: root
+
+ property list<DesktopEntry> entries: inputCleaned ? AppSearch.fuzzyQuery(inputCleaned) : []
+ property int selectedIndex: -1
+
+ name: "Applications"
+
+ implicitHeight: valid ? layout.height : 0
+
+ preview: Component {
+ IconImage {
+ source: Quickshell.iconPath(answer, "image-missing")
+ implicitWidth: General.appIconSize
+ implicitHeight: General.appIconSize
+ }
+ }
+
+ property string predictiveCompletion: valid ? entries[selectedIndex].name.slice(input.length) : ""
+
+ process: function () {
+ const isValid = entries.length > 0;
+ const selected = isValid ? entries[selectedIndex] : '';
+
+ return {
+ valid: isValid,
+ priority: isValid,
+ answer: selected ? selected.icon : ""
+ };
+ }
+
+ exec: function () {
+ SelectionState.selected.modelData.execute();
+ }
+
+ up: function () {
+ if (selectedIndex <= 0)
+ return true;
+ selectedIndex--;
+ }
+ down: function () {
+ if (selectedIndex + 1 > repeater.count - 1)
+ return true;
+ selectedIndex++;
+ }
+
+ onEntriesChanged: {
+ selectedIndex = 0;
+ flickable.contentY = 0;
+ // selectedIndexChanged dont get called somehow
+ syncSelectionState();
+ }
+
+ onSelectedIndexChanged: {
+ syncSelectionState();
+ if (selectedIndex < 0 || selectedIndex >= repeater.count)
+ return;
+ Qt.callLater(() => {
+ const i = repeater.itemAt(selectedIndex), t = i.y, b = i.y + i.height, v = flickable.contentY, vb = v + flickable.height, maxY = Math.max(0, flickable.contentHeight - flickable.height);
+ const y = t < v ? t : b > vb ? b - flickable.height : v;
+ flickable.contentY = Math.max(0, Math.min(maxY, y));
+ });
+ }
+
+ 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: entries
+
+ Item {
+ required property DesktopEntry modelData
+ required property int index
+ Layout.margins: Launcher.innerMargin * 2
+ implicitWidth: item.width
+ implicitHeight: item.height
+
+ RowLayout {
+ id: item
+ spacing: Launcher.innerMargin * 2
+
+ IconImage {
+ source: Quickshell.iconPath(modelData.icon, "image-missing")
+ implicitWidth: General.appIconSize
+ implicitHeight: General.appIconSize
+ }
+ IText {
+ text: modelData.name
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onPositionChanged: {
+ root.selectedIndex = index;
+ }
+ onExited: {
+ root.selectedIndex = 0;
+ }
+ onPressed: {
+ root._exec();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/launcher/components/Calc.qml b/launcher/components/Calc.qml
new file mode 100644
index 0000000..ec5b45f
--- /dev/null
+++ b/launcher/components/Calc.qml
@@ -0,0 +1,80 @@
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+import qs.components
+import qs.services
+import qs.config
+
+IComponent {
+ property int cursorPosition: SelectionState.cursorPosition
+ name: "Calculator"
+ preview: Component {
+ Icon {
+ text: ""
+ }
+ }
+
+ process: function () {
+ const isValid = /^(?=.*\d)(?=.*[+\-*\/\^])[0-9+\-*\/\^().\s]+$/.test(input);
+ const answer = isValid ? String(eval(input.replace(/\^/g, '**').replace(/([\d)])\(/g, '$1*(').replace(/\)([\d])/g, ')*$1'))) : null;
+ return {
+ valid: isValid,
+ priority: isValid,
+ answer: answer,
+ predictiveCompletion: isValid ? ' = ' + answer : ''
+ };
+ }
+ exec: function () {
+ Clip.copy(answer);
+ }
+
+ IInnerComponent {
+ RowLayout {
+ spacing: 0
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.preferredHeight: parent.height
+
+ Item {
+ Layout.fillWidth: true
+ IText {
+ anchors.centerIn: parent
+ elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight
+ width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2)
+ clip: true
+ renderType: Text.CurveRendering
+ visible: valid
+ text: input.replace(/ /g, "").replace(/\+/g, " + ").replace(/-/g, " - ").replace(/\*/g, " × ").replace(/\//g, " ÷ ").replace(/%/g, " % ").replace(/\(/g, " ( ").replace(/\)/g, " ) ")
+ font {
+ pixelSize: Launcher.widgetFontSize
+ bold: true
+ }
+ }
+ }
+
+ IText {
+ visible: valid
+ text: "→"
+ font.pixelSize: Launcher.widgetFontSize
+ font.bold: true
+ }
+
+ Item {
+ Layout.fillWidth: true
+ IText {
+ animate: true
+ anchors.centerIn: parent
+ width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2)
+ renderType: Text.CurveRendering
+ visible: valid
+ text: valid ? answer : ''
+ font {
+ pixelSize: Launcher.widgetFontSize
+ bold: true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/launcher/components/Clip.qml b/launcher/components/Clip.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Clip.qml
diff --git a/launcher/components/Finder.qml b/launcher/components/Finder.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Finder.qml
diff --git a/launcher/components/Flight.qml b/launcher/components/Flight.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Flight.qml
diff --git a/launcher/components/IComponent.qml b/launcher/components/IComponent.qml
new file mode 100644
index 0000000..b8963d2
--- /dev/null
+++ b/launcher/components/IComponent.qml
@@ -0,0 +1,72 @@
+import Quickshell
+import QtQuick
+
+import qs.components
+import qs.config
+
+IRect {
+ id: root
+ // virtual properties
+ property string name // Component/File name
+ readonly property string input: SelectionState.input
+ readonly property string inputCleaned: input.toLowerCase().trim()
+ property string prefix
+ property bool valid: processed?.valid || false
+ property bool priority: valid && processed?.priority || false // please set priority to false if it's invalid
+ property string answer: processed?.answer || ""
+ property Component preview: Component {
+ IText {
+ animate: true
+ text: answer
+ }
+ }
+ property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input
+ property var processed: input && input.startsWith(prefix) ? process() : {} // cached process, thought qml would do that automatically :/
+ property var process: () => ({
+ valid: valid,
+ priority: priority,
+ answer: answer,
+ preview: preview,
+ predictiveCompletion: predictiveCompletion
+ })// use the current IRect if valid is set
+ // actions
+ property var up: () => ({
+ top: false
+ })
+ property var down: () => ({
+ bottom: false
+ })
+ property var prev: () => {}
+ property var next: () => {}
+ property var exec: () => {}
+
+ signal close
+
+ function _exec() {
+ exec();
+ close();
+ }
+
+ opacity: valid ? 1 : 0
+ y: valid ? 0 : -Launcher.widgetHeight
+
+ implicitWidth: valid ? Launcher.widgetWidth : 0
+ implicitHeight: valid ? Launcher.widgetHeight : 0
+
+ // implicitWidth: Launcher.widgetWidth
+ // implicitHeight: Launcher.widgetHeight
+
+ radius: Launcher.widgetRadius
+ color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight
+
+ onPriorityChanged: {
+ SelectionState.priorities = [...[...SelectionState.priorities, root].reduce((s, x) => (s[(s.has(x) && 'delete') || 'add'](x), s), new Set())]; // sync with SelectionState.priorities
+ }
+
+ Behavior on y {
+ NumberAnimation {
+ duration: General.animationDuration / 4
+ easing.type: Easing.InOutQuad
+ }
+ }
+}
diff --git a/launcher/components/IInnerComponent.qml b/launcher/components/IInnerComponent.qml
new file mode 100644
index 0000000..7bbc9f0
--- /dev/null
+++ b/launcher/components/IInnerComponent.qml
@@ -0,0 +1,34 @@
+import QtQuick
+import QtQuick.Layouts
+
+import qs.components
+import qs.config
+
+ColumnLayout {
+ property bool fromParent: true
+ property alias titleBar: titleBar
+ anchors.fill: fromParent ? parent : undefined
+ spacing: 0
+ IRect {
+ id: titleBar
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ visible: Launcher.showWidgetTitle && valid
+ height: childrenRect.height
+ width: parent.width
+ topLeftRadius: Launcher.widgetRadius
+ topRightRadius: Launcher.widgetRadius
+ color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency)
+
+ IText {
+ anchors {
+ top: parent.top
+ left: parent.left
+ leftMargin: Launcher.innerMargin
+ }
+ visible: valid
+ font.pixelSize: Launcher.widgetFontSize
+ text: name
+ }
+ }
+}
diff --git a/launcher/components/SelectionState.qml b/launcher/components/SelectionState.qml
new file mode 100644
index 0000000..00d2420
--- /dev/null
+++ b/launcher/components/SelectionState.qml
@@ -0,0 +1,27 @@
+pragma Singleton
+import Quickshell
+import QtQuick
+
+Singleton {
+ property Item selected: null
+ property string input: ""
+ property int cursorPosition: 0
+ property var priorities: []
+ property var widgets: []
+
+ // order them based on widgets(which came from order from config)
+ onPrioritiesChanged: {
+ // priorities = priorities.filter(item => item.priority === true); // bind loop
+ // dunno y ts works w/o triggering a binding loop but okay..
+ for (let i = 0; i < priorities.length; i++)
+ if (priorities[i].priority !== true)
+ priorities.splice(i, 1);
+
+ priorities.sort((a, b) => widgets.indexOf(a) - widgets.indexOf(b));
+ priorities.forEach((item, index) => {});
+ }
+
+ function clear() {
+ selected = null;
+ }
+}
diff --git a/launcher/components/Spell.qml b/launcher/components/Spell.qml
new file mode 100644
index 0000000..4501132
--- /dev/null
+++ b/launcher/components/Spell.qml
@@ -0,0 +1,224 @@
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import QtQuick.Layouts
+
+import qs.components
+import qs.services
+import qs.config
+
+IComponent {
+ id: root
+
+ property list<string> aspell
+ property string word: input.slice(prefix.length)
+ property int selectedIndex: -1
+
+ name: "Spell"
+ prefix: name.toLowerCase() + " "
+
+ onValidChanged: {
+ if (valid)
+ return;
+
+ aspell = [];
+ proc.running = false;
+ selectedIndex = -1;
+ return;
+ }
+
+ onAspellChanged: {
+ selectedIndex = aspell.length > 0 ? 0 : -1;
+ // selectedIndexChanged dont get called somehow
+ syncSelectionState();
+ if (loader.item && loader.item.flickable) {
+ loader.item.flickable.contentY = 0;
+ }
+ }
+
+ onSelectedIndexChanged: {
+ syncSelectionState();
+ if (!loader.repeater || !loader.item || !loader.item.flickable)
+ return;
+ if (selectedIndex < 0 || selectedIndex >= loader.repeater.count)
+ return;
+ Qt.callLater(() => {
+ const flickable = loader.item.flickable;
+ const i = loader.repeater.itemAt(selectedIndex);
+ if (!i)
+ return;
+ const t = i.y;
+ const b = i.y + i.height;
+ const v = flickable.contentY;
+ const vb = v + flickable.height;
+ const maxY = Math.max(0, flickable.contentHeight - flickable.height);
+ const y = t < v ? t : b > vb ? b - flickable.height : v;
+ flickable.contentY = Math.max(0, Math.min(maxY, y));
+ });
+ }
+
+ process: function () {
+ proc.running = true;
+
+ const isValid = selectedIndex >= 0 && aspell.length > 0;
+ const selected = isValid ? aspell[selectedIndex] : "n";
+ const answer = selected === "*" ? word : selected;
+
+ return {
+ valid: isValid,
+ priority: isValid,
+ answer: isValid ? answer : "",
+ predictiveCompletion: isValid ? answer.slice(word.length) : ""
+ };
+ }
+
+ prev: function () {
+ if (selectedIndex <= 0)
+ return true;
+ selectedIndex--;
+ }
+
+ next: function () {
+ if (selectedIndex + 1 >= aspell.length)
+ return true;
+ selectedIndex++;
+ }
+
+ up: function () {
+ const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6))));
+ if (selectedIndex - cols < 0)
+ return true;
+ selectedIndex -= cols;
+ }
+
+ down: function () {
+ const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6))));
+ if (selectedIndex + cols >= aspell.length)
+ return true;
+ selectedIndex += cols;
+ }
+
+ exec: function () {
+ if (selectedIndex >= 0)
+ if (aspell[selectedIndex] !== "*")
+ Clip.copy(aspell[selectedIndex]);
+ else
+ Clip.copy(word);
+ }
+
+ function syncSelectionState() {
+ if (!loader.repeater)
+ return;
+
+ Qt.callLater(() => {
+ SelectionState.selected = loader.repeater.itemAt(selectedIndex);
+ SelectionState.exec = root.exec;
+ });
+ }
+
+ IInnerComponent {
+ id: innerLoader
+ Loader {
+ id: loader
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.preferredHeight: parent.height
+ sourceComponent: aspell[0] === "*" ? correct : show
+ property var repeater: null
+
+ readonly property Component show: Component {
+ Item {
+ property alias flickable: flickable
+
+ Flickable {
+ id: flickable
+ anchors.fill: parent
+ clip: true
+ contentWidth: grid.width
+ contentHeight: grid.height
+
+ GridLayout {
+ id: grid
+ width: flickable.width
+ columns: Math.max(1, Math.floor(width / Math.max(1, Math.round(Launcher.widgetFontSize * 6))))
+ rowSpacing: Launcher.innerMargin
+ columnSpacing: Launcher.innerMargin
+
+ Repeater {
+ id: repeater
+ model: aspell
+
+ IRect {
+ Layout.fillWidth: true
+ Layout.preferredWidth: Math.max(chipContent.implicitWidth + Launcher.innerMargin * 2, Math.floor(grid.width / grid.columns) - grid.columnSpacing)
+ Layout.preferredHeight: chipContent.implicitHeight + Launcher.innerMargin * 2
+ color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency)
+ radius: Launcher.widgetRadius
+
+ IText {
+ id: chipContent
+ anchors.centerIn: parent
+ text: modelData
+ font.pixelSize: Launcher.widgetFontSize
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onPositionChanged: {
+ root.selectedIndex = index;
+ }
+ onPressed: {
+ root._exec();
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ loader.repeater = repeater;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ readonly property Component correct: Component {
+ IRect {
+ Layout.fillWidth: true
+ color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4)
+ radius: Launcher.widgetRadius
+ IText {
+ anchors.centerIn: parent
+ text: "Correct"
+ font.pixelSize: Launcher.widgetFontSize
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+ }
+
+ Process {
+ id: proc
+ command: ["sh", "-c", "echo " + word + " | aspell -a"]
+ property bool found: false
+ stdout: SplitParser {
+ onRead: data => {
+ if (data.startsWith("&")) {
+ proc.found = true;
+ aspell = data.split(/\W+/).filter(word => /^[A-Za-z]+$/.test(word)).slice(1);
+ } else if (data.startsWith("*"))
+ aspell = ["*"];
+ }
+ }
+ onStarted: {
+ found = false;
+ }
+ onExited: {
+ if (!found)
+ aspell = [];
+ }
+ }
+}
diff --git a/launcher/components/Time.qml b/launcher/components/Time.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Time.qml
diff --git a/launcher/components/Translate.qml b/launcher/components/Translate.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Translate.qml
diff --git a/launcher/components/Weather.qml b/launcher/components/Weather.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Weather.qml
diff --git a/launcher/components/Web.qml b/launcher/components/Web.qml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/launcher/components/Web.qml
diff --git a/launcher/todo.md b/launcher/todo.md
new file mode 100644
index 0000000..bfc2103
--- /dev/null
+++ b/launcher/todo.md
@@ -0,0 +1,33 @@
+when finished:
+- [x] reloads dynamically while typing in
+- [x] prefix: ... for certain ambiguous queries such as "pi" or "e" for math
+- [ ] bang (like ddg's)
+- [ ] web search catch all
+- [x] height dynamically shrink/grow according to content
+
+- [ ] web:
+ - [ ] show multiple options:
+ - [ ] first - search w/ search engine and open in browser
+ - [ ] second - fallback/second option
+ - [ ] custom quick search/fetch
+ - [ ] e.g. wikipedia: (also stuff like spotify, youtube)
+ - [ ] match title
+ - [ ] match first paragraph/description
+
+- [ ] files:
+ - [ ] latest modified date
+ - [ ] file size
+ - [ ] where it belongs
+ - [ ] icon/preview/quick look
+
+- [ ] math:
+ - [x] expression
+ - [ ] natural language (i.e. "5 of 3")
+ - [ ] unit conversion
+ - [ ] temperature
+ - [ ] length
+ - [ ] weight
+ - [ ] currency
+ - [ ] color
+ - [ ] time
+
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();
+ }
+ }
+ }
+}
diff --git a/shell.qml b/shell.qml
index 9717548..0c5e13e 100644
--- a/shell.qml
+++ b/shell.qml
@@ -1,12 +1,19 @@
import Quickshell
import Quickshell.Io
import "bar"
+import "launcher"
import QtQuick
import QtQuick.Layouts
Scope {
Bar {}
+ LazyLoader {
+ id: launcherLoader
+ component: Launcher {
+ parentLoader: launcherLoader
+ }
+ }
IpcHandler {
target: "qs"
@@ -15,6 +22,13 @@ Scope {
Quickshell.reload(hard);
}
}
+ IpcHandler {
+ target: "launcher"
+
+ function toggle() {
+ launcherLoader.active = !launcherLoader.active;
+ }
+ }
Connections {
target: Quickshell
diff --git a/utils/cliphist-img.sh b/utils/cliphist-img.sh
new file mode 100644
index 0000000..8fe8c2c
--- /dev/null
+++ b/utils/cliphist-img.sh
@@ -0,0 +1,15 @@
+tmp_dir="/tmp/cliphist-img"
+
+mkdir -p "$tmp_dir"
+
+read -r -d '' prog <<EOF
+/^[0-9]+\s<meta http-equiv=/ { next }
+match(\$0, /^([0-9]+)\s(\[\[\s)?binary.*(jpg|jpeg|png|bmp)/, grp) {
+ path="$tmp_dir/"grp[1]"."grp[3]
+ system("echo " grp[1] "\\\\\t | cliphist decode >" path)
+ print path
+ next
+}
+EOF
+
+cliphist list | gawk "$prog"
diff --git a/utils/fuzzysort.js b/utils/fuzzysort.js
deleted file mode 100644
index 0e373ce..0000000
--- a/utils/fuzzysort.js
+++ /dev/null
@@ -1,692 +0,0 @@
-.pragma library
-
-// https://github.com/farzher/fuzzysort
-// License: MIT | Copyright (c) 2018 Stephen Kamenar
-// A copy of the license is available in the `licenses` folder of this repository
-
-var single = (search, target) => {
- if (!search || !target) return NULL
-
- var preparedSearch = getPreparedSearch(search)
- if (!isPrepared(target)) target = getPrepared(target)
-
- var searchBitflags = preparedSearch.bitflags
- if ((searchBitflags & target._bitflags) !== searchBitflags) return NULL
-
- return algorithm(preparedSearch, target)
-}
-
-var go = (search, targets, options) => {
- if (!search) return options?.all ? all(targets, options) : noResults
-
- var preparedSearch = getPreparedSearch(search)
- var searchBitflags = preparedSearch.bitflags
- var containsSpace = preparedSearch.containsSpace
-
- var threshold = denormalizeScore(options?.threshold || 0)
- var limit = options?.limit || INFINITY
-
- var resultsLen = 0; var limitedCount = 0
- var targetsLen = targets.length
-
- function push_result(result) {
- if (resultsLen < limit) { q.add(result); ++resultsLen }
- else {
- ++limitedCount
- if (result._score > q.peek()._score) q.replaceTop(result)
- }
- }
-
- // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
-
- // options.key
- if (options?.key) {
- var key = options.key
- for (var i = 0; i < targetsLen; ++i) {
- var obj = targets[i]
- var target = getValue(obj, key)
- if (!target) continue
- if (!isPrepared(target)) target = getPrepared(target)
-
- if ((searchBitflags & target._bitflags) !== searchBitflags) continue
- var result = algorithm(preparedSearch, target)
- if (result === NULL) continue
- if (result._score < threshold) continue
-
- result.obj = obj
- push_result(result)
- }
-
- // options.keys
- } else if (options?.keys) {
- var keys = options.keys
- var keysLen = keys.length
-
- outer: for (var i = 0; i < targetsLen; ++i) {
- var obj = targets[i]
-
- { // early out based on bitflags
- var keysBitflags = 0
- for (var keyI = 0; keyI < keysLen; ++keyI) {
- var key = keys[keyI]
- var target = getValue(obj, key)
- if (!target) { tmpTargets[keyI] = noTarget; continue }
- if (!isPrepared(target)) target = getPrepared(target)
- tmpTargets[keyI] = target
-
- keysBitflags |= target._bitflags
- }
-
- if ((searchBitflags & keysBitflags) !== searchBitflags) continue
- }
-
- if (containsSpace) for (let i = 0; i < preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
-
- for (var keyI = 0; keyI < keysLen; ++keyI) {
- target = tmpTargets[keyI]
- if (target === noTarget) { tmpResults[keyI] = noTarget; continue }
-
- tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
- if (tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
-
- // todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
- // if our second match isn't good we ignore it instead of averaging with it
- if (containsSpace) for (let i = 0; i < preparedSearch.spaceSearches.length; i++) {
- if (allowPartialMatchScores[i] > -1000) {
- if (keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
- var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
- if (tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
- }
- }
- if (allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
- }
- }
-
- if (containsSpace) {
- for (let i = 0; i < preparedSearch.spaceSearches.length; i++) { if (keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
- } else {
- var hasAtLeast1Match = false
- for (let i = 0; i < keysLen; i++) { if (tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
- if (!hasAtLeast1Match) continue
- }
-
- var objResults = new KeysResult(keysLen)
- for (let i = 0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
-
- if (containsSpace) {
- var score = 0
- for (let i = 0; i < preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
- } else {
- // todo could rewrite this scoring to be more similar to when there's spaces
- // if we match multiple keys give us bonus points
- var score = NEGATIVE_INFINITY
- for (let i = 0; i < keysLen; i++) {
- var result = objResults[i]
- if (result._score > -1000) {
- if (score > NEGATIVE_INFINITY) {
- var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
- if (tmp > score) score = tmp
- }
- }
- if (result._score > score) score = result._score
- }
- }
-
- objResults.obj = obj
- objResults._score = score
- if (options?.scoreFn) {
- score = options.scoreFn(objResults)
- if (!score) continue
- score = denormalizeScore(score)
- objResults._score = score
- }
-
- if (score < threshold) continue
- push_result(objResults)
- }
-
- // no keys
- } else {
- for (var i = 0; i < targetsLen; ++i) {
- var target = targets[i]
- if (!target) continue
- if (!isPrepared(target)) target = getPrepared(target)
-
- if ((searchBitflags & target._bitflags) !== searchBitflags) continue
- var result = algorithm(preparedSearch, target)
- if (result === NULL) continue
- if (result._score < threshold) continue
-
- push_result(result)
- }
- }
-
- if (resultsLen === 0) return noResults
- var results = new Array(resultsLen)
- for (var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
- results.total = resultsLen + limitedCount
- return results
-}
-
-
-// this is written as 1 function instead of 2 for minification. perf seems fine ...
-// except when minified. the perf is very slow
-var highlight = (result, open = '<b>', close = '</b>') => {
- var callback = typeof open === 'function' ? open : undefined
-
- var target = result.target
- var targetLen = target.length
- var indexes = result.indexes
- var highlighted = ''
- var matchI = 0
- var indexesI = 0
- var opened = false
- var parts = []
-
- for (var i = 0; i < targetLen; ++i) {
- var char = target[i]
- if (indexes[indexesI] === i) {
- ++indexesI
- if (!opened) {
- opened = true
- if (callback) {
- parts.push(highlighted); highlighted = ''
- } else {
- highlighted += open
- }
- }
-
- if (indexesI === indexes.length) {
- if (callback) {
- highlighted += char
- parts.push(callback(highlighted, matchI++)); highlighted = ''
- parts.push(target.substr(i + 1))
- } else {
- highlighted += char + close + target.substr(i + 1)
- }
- break
- }
- } else {
- if (opened) {
- opened = false
- if (callback) {
- parts.push(callback(highlighted, matchI++)); highlighted = ''
- } else {
- highlighted += close
- }
- }
- }
- highlighted += char
- }
-
- return callback ? parts : highlighted
-}
-
-
-var prepare = (target) => {
- if (typeof target === 'number') target = '' + target
- else if (typeof target !== 'string') target = ''
- var info = prepareLowerInfo(target)
- return new_result(target, { _targetLower: info._lower, _targetLowerCodes: info.lowerCodes, _bitflags: info.bitflags })
-}
-
-var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
-
-
-// Below this point is only internal code
-// Below this point is only internal code
-// Below this point is only internal code
-// Below this point is only internal code
-
-
-class Result {
- get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a, b) => a - b) }
- set ['indexes'](indexes) { return this._indexes = indexes }
- ['highlight'](open, close) { return highlight(this, open, close) }
- get ['score']() { return normalizeScore(this._score) }
- set ['score'](score) { this._score = denormalizeScore(score) }
-}
-
-class KeysResult extends Array {
- get ['score']() { return normalizeScore(this._score) }
- set ['score'](score) { this._score = denormalizeScore(score) }
-}
-
-var new_result = (target, options) => {
- const result = new Result()
- result['target'] = target
- result['obj'] = options.obj ?? NULL
- result._score = options._score ?? NEGATIVE_INFINITY
- result._indexes = options._indexes ?? []
- result._targetLower = options._targetLower ?? ''
- result._targetLowerCodes = options._targetLowerCodes ?? NULL
- result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
- result._bitflags = options._bitflags ?? 0
- return result
-}
-
-
-var normalizeScore = score => {
- if (score === NEGATIVE_INFINITY) return 0
- if (score > 1) return score
- return Math.E ** (((-score + 1) ** .04307 - 1) * -2)
-}
-var denormalizeScore = normalizedScore => {
- if (normalizedScore === 0) return NEGATIVE_INFINITY
- if (normalizedScore > 1) return normalizedScore
- return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
-}
-
-
-var prepareSearch = (search) => {
- if (typeof search === 'number') search = '' + search
- else if (typeof search !== 'string') search = ''
- search = search.trim()
- var info = prepareLowerInfo(search)
-
- var spaceSearches = []
- if (info.containsSpace) {
- var searches = search.split(/\s+/)
- searches = [...new Set(searches)] // distinct
- for (var i = 0; i < searches.length; i++) {
- if (searches[i] === '') continue
- var _info = prepareLowerInfo(searches[i])
- spaceSearches.push({ lowerCodes: _info.lowerCodes, _lower: searches[i].toLowerCase(), containsSpace: false })
- }
- }
-
- return { lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches }
-}
-
-
-
-var getPrepared = (target) => {
- if (target.length > 999) return prepare(target) // don't cache huge targets
- var targetPrepared = preparedCache.get(target)
- if (targetPrepared !== undefined) return targetPrepared
- targetPrepared = prepare(target)
- preparedCache.set(target, targetPrepared)
- return targetPrepared
-}
-var getPreparedSearch = (search) => {
- if (search.length > 999) return prepareSearch(search) // don't cache huge searches
- var searchPrepared = preparedSearchCache.get(search)
- if (searchPrepared !== undefined) return searchPrepared
- searchPrepared = prepareSearch(search)
- preparedSearchCache.set(search, searchPrepared)
- return searchPrepared
-}
-
-
-var all = (targets, options) => {
- var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
-
- var limit = options?.limit || INFINITY
-
- if (options?.key) {
- for (var i = 0; i < targets.length; i++) {
- var obj = targets[i]
- var target = getValue(obj, options.key)
- if (target == NULL) continue
- if (!isPrepared(target)) target = getPrepared(target)
- var result = new_result(target.target, { _score: target._score, obj: obj })
- results.push(result); if (results.length >= limit) return results
- }
- } else if (options?.keys) {
- for (var i = 0; i < targets.length; i++) {
- var obj = targets[i]
- var objResults = new KeysResult(options.keys.length)
- for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
- var target = getValue(obj, options.keys[keyI])
- if (!target) { objResults[keyI] = noTarget; continue }
- if (!isPrepared(target)) target = getPrepared(target)
- target._score = NEGATIVE_INFINITY
- target._indexes.len = 0
- objResults[keyI] = target
- }
- objResults.obj = obj
- objResults._score = NEGATIVE_INFINITY
- results.push(objResults); if (results.length >= limit) return results
- }
- } else {
- for (var i = 0; i < targets.length; i++) {
- var target = targets[i]
- if (target == NULL) continue
- if (!isPrepared(target)) target = getPrepared(target)
- target._score = NEGATIVE_INFINITY
- target._indexes.len = 0
- results.push(target); if (results.length >= limit) return results
- }
- }
-
- return results
-}
-
-
-var algorithm = (preparedSearch, prepared, allowSpaces = false, allowPartialMatch = false) => {
- if (allowSpaces === false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
-
- var searchLower = preparedSearch._lower
- var searchLowerCodes = preparedSearch.lowerCodes
- var searchLowerCode = searchLowerCodes[0]
- var targetLowerCodes = prepared._targetLowerCodes
- var searchLen = searchLowerCodes.length
- var targetLen = targetLowerCodes.length
- var searchI = 0 // where we at
- var targetI = 0 // where you at
- var matchesSimpleLen = 0
-
- // very basic fuzzy match; to remove non-matching targets ASAP!
- // walk through target. find sequential matches.
- // if all chars aren't found then exit
- for (; ;) {
- var isMatch = searchLowerCode === targetLowerCodes[targetI]
- if (isMatch) {
- matchesSimple[matchesSimpleLen++] = targetI
- ++searchI; if (searchI === searchLen) break
- searchLowerCode = searchLowerCodes[searchI]
- }
- ++targetI; if (targetI >= targetLen) return NULL // Failed to find searchI
- }
-
- var searchI = 0
- var successStrict = false
- var matchesStrictLen = 0
-
- var nextBeginningIndexes = prepared._nextBeginningIndexes
- if (nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
- targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]
-
- // Our target string successfully matched all characters in sequence!
- // Let's try a more advanced and strict test to improve the score
- // only count it as a match if it's consecutive or a beginning character!
- var backtrackCount = 0
- if (targetI !== targetLen) for (; ;) {
- if (targetI >= targetLen) {
- // We failed to find a good spot for this search char, go back to the previous search char and force it forward
- if (searchI <= 0) break // We failed to push chars forward for a better match
-
- ++backtrackCount; if (backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
-
- --searchI
- var lastMatch = matchesStrict[--matchesStrictLen]
- targetI = nextBeginningIndexes[lastMatch]
-
- } else {
- var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
- if (isMatch) {
- matchesStrict[matchesStrictLen++] = targetI
- ++searchI; if (searchI === searchLen) { successStrict = true; break }
- ++targetI
- } else {
- targetI = nextBeginningIndexes[targetI]
- }
- }
- }
-
- // check if it's a substring match
- var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
- var isSubstring = !!~substringIndex
- var isSubstringBeginning = !isSubstring ? false : substringIndex === 0 || prepared._nextBeginningIndexes[substringIndex - 1] === substringIndex
-
- // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
- if (isSubstring && !isSubstringBeginning) {
- for (var i = 0; i < nextBeginningIndexes.length; i = nextBeginningIndexes[i]) {
- if (i <= substringIndex) continue
-
- for (var s = 0; s < searchLen; s++) if (searchLowerCodes[s] !== prepared._targetLowerCodes[i + s]) break
- if (s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
- }
- }
-
- // tally up the score & keep track of matches for highlighting later
- // if it's a simple match, we'll switch to a substring match if a substring exists
- // if it's a strict match, we'll switch to a substring match only if that's a better score
-
- var calculateScore = matches => {
- var score = 0
-
- var extraMatchGroupCount = 0
- for (var i = 1; i < searchLen; ++i) {
- if (matches[i] - matches[i - 1] !== 1) { score -= matches[i]; ++extraMatchGroupCount }
- }
- var unmatchedDistance = matches[searchLen - 1] - matches[0] - (searchLen - 1)
-
- score -= (12 + unmatchedDistance) * extraMatchGroupCount // penality for more groups
-
- if (matches[0] !== 0) score -= matches[0] * matches[0] * .2 // penality for not starting near the beginning
-
- if (!successStrict) {
- score *= 1000
- } else {
- // successStrict on a target with too many beginning indexes loses points for being a bad target
- var uniqueBeginningIndexes = 1
- for (var i = nextBeginningIndexes[0]; i < targetLen; i = nextBeginningIndexes[i]) ++uniqueBeginningIndexes
-
- if (uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes - 24) * 10 // quite arbitrary numbers here ...
- }
-
- score -= (targetLen - searchLen) / 2 // penality for longer targets
-
- if (isSubstring) score /= 1 + searchLen * searchLen * 1 // bonus for being a full substring
- if (isSubstringBeginning) score /= 1 + searchLen * searchLen * 1 // bonus for substring starting on a beginningIndex
-
- score -= (targetLen - searchLen) / 2 // penality for longer targets
-
- return score
- }
-
- if (!successStrict) {
- if (isSubstring) for (var i = 0; i < searchLen; ++i) matchesSimple[i] = substringIndex + i // at this point it's safe to overwrite matchehsSimple with substr matches
- var matchesBest = matchesSimple
- var score = calculateScore(matchesBest)
- } else {
- if (isSubstringBeginning) {
- for (var i = 0; i < searchLen; ++i) matchesSimple[i] = substringIndex + i // at this point it's safe to overwrite matchehsSimple with substr matches
- var matchesBest = matchesSimple
- var score = calculateScore(matchesSimple)
- } else {
- var matchesBest = matchesStrict
- var score = calculateScore(matchesStrict)
- }
- }
-
- prepared._score = score
-
- for (var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
- prepared._indexes.len = searchLen
-
- const result = new Result()
- result.target = prepared.target
- result._score = prepared._score
- result._indexes = prepared._indexes
- return result
-}
-var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
- var seen_indexes = new Set()
- var score = 0
- var result = NULL
-
- var first_seen_index_last_search = 0
- var searches = preparedSearch.spaceSearches
- var searchesLen = searches.length
- var changeslen = 0
-
- // Return _nextBeginningIndexes back to its normal state
- var resetNextBeginningIndexes = () => {
- for (let i = changeslen - 1; i >= 0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i * 2 + 0]] = nextBeginningIndexesChanges[i * 2 + 1]
- }
-
- var hasAtLeast1Match = false
- for (var i = 0; i < searchesLen; ++i) {
- allowPartialMatchScores[i] = NEGATIVE_INFINITY
- var search = searches[i]
-
- result = algorithm(search, target)
- if (allowPartialMatch) {
- if (result === NULL) continue
- hasAtLeast1Match = true
- } else {
- if (result === NULL) { resetNextBeginningIndexes(); return NULL }
- }
-
- // if not the last search, we need to mutate _nextBeginningIndexes for the next search
- var isTheLastSearch = i === searchesLen - 1
- if (!isTheLastSearch) {
- var indexes = result._indexes
-
- var indexesIsConsecutiveSubstring = true
- for (let i = 0; i < indexes.len - 1; i++) {
- if (indexes[i + 1] - indexes[i] !== 1) {
- indexesIsConsecutiveSubstring = false; break;
- }
- }
-
- if (indexesIsConsecutiveSubstring) {
- var newBeginningIndex = indexes[indexes.len - 1] + 1
- var toReplace = target._nextBeginningIndexes[newBeginningIndex - 1]
- for (let i = newBeginningIndex - 1; i >= 0; i--) {
- if (toReplace !== target._nextBeginningIndexes[i]) break
- target._nextBeginningIndexes[i] = newBeginningIndex
- nextBeginningIndexesChanges[changeslen * 2 + 0] = i
- nextBeginningIndexesChanges[changeslen * 2 + 1] = toReplace
- changeslen++
- }
- }
- }
-
- score += result._score / searchesLen
- allowPartialMatchScores[i] = result._score / searchesLen
-
- // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
- if (result._indexes[0] < first_seen_index_last_search) {
- score -= (first_seen_index_last_search - result._indexes[0]) * 2
- }
- first_seen_index_last_search = result._indexes[0]
-
- for (var j = 0; j < result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
- }
-
- if (allowPartialMatch && !hasAtLeast1Match) return NULL
-
- resetNextBeginningIndexes()
-
- // allows a search with spaces that's an exact substring to score well
- var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
- if (allowSpacesResult !== NULL && allowSpacesResult._score > score) {
- if (allowPartialMatch) {
- for (var i = 0; i < searchesLen; ++i) {
- allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
- }
- }
- return allowSpacesResult
- }
-
- if (allowPartialMatch) result = target
- result._score = score
-
- var i = 0
- for (let index of seen_indexes) result._indexes[i++] = index
- result._indexes.len = i
-
- return result
-}
-
-// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
-var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
-
-var prepareLowerInfo = (str) => {
- str = remove_accents(str)
- var strLen = str.length
- var lower = str.toLowerCase()
- var lowerCodes = [] // new Array(strLen) sparse array is too slow
- var bitflags = 0
- var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
-
- for (var i = 0; i < strLen; ++i) {
- var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
-
- if (lowerCode === 32) {
- containsSpace = true
- continue // it's important that we don't set any bitflags for space
- }
-
- var bit = lowerCode >= 97 && lowerCode <= 122 ? lowerCode - 97 // alphabet
- : lowerCode >= 48 && lowerCode <= 57 ? 26 // numbers
- // 3 bits available
- : lowerCode <= 127 ? 30 // other ascii
- : 31 // other utf8
- bitflags |= 1 << bit
- }
-
- return { lowerCodes: lowerCodes, bitflags: bitflags, containsSpace: containsSpace, _lower: lower }
-}
-var prepareBeginningIndexes = (target) => {
- var targetLen = target.length
- var beginningIndexes = []; var beginningIndexesLen = 0
- var wasUpper = false
- var wasAlphanum = false
- for (var i = 0; i < targetLen; ++i) {
- var targetCode = target.charCodeAt(i)
- var isUpper = targetCode >= 65 && targetCode <= 90
- var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57
- var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
- wasUpper = isUpper
- wasAlphanum = isAlphanum
- if (isBeginning) beginningIndexes[beginningIndexesLen++] = i
- }
- return beginningIndexes
-}
-var prepareNextBeginningIndexes = (target) => {
- target = remove_accents(target)
- var targetLen = target.length
- var beginningIndexes = prepareBeginningIndexes(target)
- var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
- var lastIsBeginning = beginningIndexes[0]
- var lastIsBeginningI = 0
- for (var i = 0; i < targetLen; ++i) {
- if (lastIsBeginning > i) {
- nextBeginningIndexes[i] = lastIsBeginning
- } else {
- lastIsBeginning = beginningIndexes[++lastIsBeginningI]
- nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning
- }
- }
- return nextBeginningIndexes
-}
-
-var preparedCache = new Map()
-var preparedSearchCache = new Map()
-
-// the theory behind these being globals is to reduce garbage collection by not making new arrays
-var matchesSimple = []; var matchesStrict = []
-var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
-var keysSpacesBestScores = []; var allowPartialMatchScores = []
-var tmpTargets = []; var tmpResults = []
-
-// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
-// prop = 'key1.key2' 10ms
-// prop = ['key1', 'key2'] 27ms
-// prop = obj => obj.tags.join() ??ms
-var getValue = (obj, prop) => {
- var tmp = obj[prop]; if (tmp !== undefined) return tmp
- if (typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
- var segs = prop
- if (!Array.isArray(prop)) segs = prop.split('.')
- var len = segs.length
- var i = -1
- while (obj && (++i < len)) obj = obj[segs[i]]
- return obj
-}
-
-var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
-var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
-var noResults = []; noResults.total = 0
-var NULL = null
-
-var noTarget = prepare('')
-
-// Hacked version of https://github.com/lemire/FastPriorityQueue.js
-var fastpriorityqueue = r => { var e = [], o = 0, a = {}, v = r => { for (var a = 0, v = e[a], c = 1; c < o;) { var s = c + 1; a = c, s < o && e[s]._score < e[c]._score && (a = s), e[a - 1 >> 1] = e[a], c = 1 + (a << 1) } for (var f = a - 1 >> 1; a > 0 && v._score < e[f]._score; f = (a = f) - 1 >> 1)e[a] = e[f]; e[a] = v }; return a.add = (r => { var a = o; e[o++] = r; for (var v = a - 1 >> 1; a > 0 && r._score < e[v]._score; v = (a = v) - 1 >> 1)e[a] = e[v]; e[a] = r }), a.poll = (r => { if (0 !== o) { var a = e[0]; return e[0] = e[--o], v(), a } }), a.peek = (r => { if (0 !== o) return e[0] }), a.replaceTop = (r => { e[0] = r, v() }), a }
-var q = fastpriorityqueue() // reuse this
-
diff --git a/utils/levendist.js b/utils/levendist.js
index ed8a5d8..6565e0a 100644
--- a/utils/levendist.js
+++ b/utils/levendist.js
@@ -1,142 +1,273 @@
-// Original code from https://github.com/koeqaife/hyprland-material-you
-// Original code license: GPLv3
-// Translated to Js from Cython with an LLM and reviewed
+var peq = new Uint32Array(0x10000);
-function min3(a, b, c) {
- return a < b && a < c ? a : b < c ? b : c;
-}
+var myers_32 = function(a, b) {
-function max3(a, b, c) {
- return a > b && a > c ? a : b > c ? b : c;
-}
+ var n = a.length;
-function min2(a, b) {
- return a < b ? a : b;
-}
+ var m = b.length;
-function max2(a, b) {
- return a > b ? a : b;
-}
+ var lst = 1 << (n - 1);
-function levenshteinDistance(s1, s2) {
- let len1 = s1.length;
- let len2 = s2.length;
+ var pv = -1;
- if (len1 === 0) return len2;
- if (len2 === 0) return len1;
+ var mv = 0;
- if (len2 > len1) {
- [s1, s2] = [s2, s1];
- [len1, len2] = [len2, len1];
- }
+ var sc = n;
+
+ var i = n;
- let prev = new Array(len2 + 1);
- let curr = new Array(len2 + 1);
+ while (i--) {
+
+ peq[a.charCodeAt(i)] |= 1 << i;
- for (let j = 0; j <= len2; j++) {
- prev[j] = j;
}
- for (let i = 1; i <= len1; i++) {
- curr[0] = i;
- for (let j = 1; j <= len2; j++) {
- let cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
- curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
+ for (i = 0; i < m; i++) {
+
+ var eq = peq[b.charCodeAt(i)];
+
+ var xv = eq | mv;
+
+ eq |= ((eq & pv) + pv) ^ pv;
+
+ mv |= ~(eq | pv);
+
+ pv &= eq;
+
+ if (mv & lst) {
+
+ sc++;
+
+ }
+
+ if (pv & lst) {
+
+ sc--;
+
}
- [prev, curr] = [curr, prev];
+
+ mv = (mv << 1) | 1;
+
+ pv = (pv << 1) | ~(xv | mv);
+
+ mv &= xv;
+
}
- return prev[len2];
-}
+ i = n;
-function partialRatio(shortS, longS) {
- let lenS = shortS.length;
- let lenL = longS.length;
- let best = 0.0;
+ while (i--) {
- if (lenS === 0) return 1.0;
+ peq[a.charCodeAt(i)] = 0;
- for (let i = 0; i <= lenL - lenS; i++) {
- let sub = longS.slice(i, i + lenS);
- let dist = levenshteinDistance(shortS, sub);
- let score = 1.0 - (dist / lenS);
- if (score > best) best = score;
}
- return best;
-}
+ return sc;
+
+};
+
+var myers_x = function(b, a) {
+
+ var n = a.length;
-function computeScore(s1, s2) {
- if (s1 === s2) return 1.0;
+ var m = b.length;
- let dist = levenshteinDistance(s1, s2);
- let maxLen = max2(s1.length, s2.length);
- if (maxLen === 0) return 1.0;
+ var mhc = [];
- let full = 1.0 - (dist / maxLen);
- let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
+ var phc = [];
- let score = 0.85 * full + 0.15 * part;
+ var hsize = Math.ceil(n / 32);
+
+ var vsize = Math.ceil(m / 32);
+
+ for (var i = 0; i < hsize; i++) {
+
+ phc[i] = -1;
+
+ mhc[i] = 0;
- if (s1 && s2 && s1[0] !== s2[0]) {
- score -= 0.05;
}
- let lenDiff = Math.abs(s1.length - s2.length);
- if (lenDiff >= 3) {
- score -= 0.05 * lenDiff / maxLen;
+ var j = 0;
+
+ for (; j < vsize - 1; j++) {
+
+ var mv_1 = 0;
+
+ var pv_1 = -1;
+
+ var start_1 = j * 32;
+
+ var vlen_1 = Math.min(32, m) + start_1;
+
+ for (var k = start_1; k < vlen_1; k++) {
+
+ peq[b.charCodeAt(k)] |= 1 << k;
+
+ }
+
+ for (var i = 0; i < n; i++) {
+
+ var eq = peq[a.charCodeAt(i)];
+
+ var pb = (phc[(i / 32) | 0] >>> i) & 1;
+
+ var mb = (mhc[(i / 32) | 0] >>> i) & 1;
+
+ var xv = eq | mv_1;
+
+ var xh = ((((eq | mb) & pv_1) + pv_1) ^ pv_1) | eq | mb;
+
+ var ph = mv_1 | ~(xh | pv_1);
+
+ var mh = pv_1 & xh;
+
+ if ((ph >>> 31) ^ pb) {
+
+ phc[(i / 32) | 0] ^= 1 << i;
+
+ }
+
+ if ((mh >>> 31) ^ mb) {
+
+ mhc[(i / 32) | 0] ^= 1 << i;
+
+ }
+
+ ph = (ph << 1) | pb;
+
+ mh = (mh << 1) | mb;
+
+ pv_1 = mh | ~(xv | ph);
+
+ mv_1 = ph & xv;
+
+ }
+
+ for (var k = start_1; k < vlen_1; k++) {
+
+ peq[b.charCodeAt(k)] = 0;
+
+ }
+
+ }
+
+ var mv = 0;
+
+ var pv = -1;
+
+ var start = j * 32;
+
+ var vlen = Math.min(32, m - start) + start;
+
+ for (var k = start; k < vlen; k++) {
+
+ peq[b.charCodeAt(k)] |= 1 << k;
+
}
- let commonPrefixLen = 0;
- let minLen = min2(s1.length, s2.length);
- for (let i = 0; i < minLen; i++) {
- if (s1[i] === s2[i]) {
- commonPrefixLen++;
- } else {
- break;
+ var score = m;
+
+ for (var i = 0; i < n; i++) {
+
+ var eq = peq[a.charCodeAt(i)];
+
+ var pb = (phc[(i / 32) | 0] >>> i) & 1;
+
+ var mb = (mhc[(i / 32) | 0] >>> i) & 1;
+
+ var xv = eq | mv;
+
+ var xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
+
+ var ph = mv | ~(xh | pv);
+
+ var mh = pv & xh;
+
+ score += (ph >>> (m - 1)) & 1;
+
+ score -= (mh >>> (m - 1)) & 1;
+
+ if ((ph >>> 31) ^ pb) {
+
+ phc[(i / 32) | 0] ^= 1 << i;
+
+ }
+
+ if ((mh >>> 31) ^ mb) {
+
+ mhc[(i / 32) | 0] ^= 1 << i;
+
}
+
+ ph = (ph << 1) | pb;
+
+ mh = (mh << 1) | mb;
+
+ pv = mh | ~(xv | ph);
+
+ mv = ph & xv;
+
}
- score += 0.02 * commonPrefixLen;
- if (s1.includes(s2) || s2.includes(s1)) {
- score += 0.06;
+ for (var k = start; k < vlen; k++) {
+
+ peq[b.charCodeAt(k)] = 0;
+
}
- return Math.max(0.0, Math.min(1.0, score));
-}
+ return score;
-function computeTextMatchScore(s1, s2) {
- if (s1 === s2) return 1.0;
+};
- let dist = levenshteinDistance(s1, s2);
- let maxLen = max2(s1.length, s2.length);
- if (maxLen === 0) return 1.0;
+var distance = function(a, b) {
- let full = 1.0 - (dist / maxLen);
- let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
+ if (a.length < b.length) {
- let score = 0.4 * full + 0.6 * part;
+ var tmp = b;
+
+ b = a;
+
+ a = tmp;
- let lenDiff = Math.abs(s1.length - s2.length);
- if (lenDiff >= 10) {
- score -= 0.02 * lenDiff / maxLen;
}
- let commonPrefixLen = 0;
- let minLen = min2(s1.length, s2.length);
- for (let i = 0; i < minLen; i++) {
- if (s1[i] === s2[i]) {
- commonPrefixLen++;
- } else {
- break;
- }
+ if (b.length === 0) {
+
+ return a.length;
+
+ }
+
+ if (a.length <= 32) {
+
+ return myers_32(a, b);
+
}
- score += 0.01 * commonPrefixLen;
- if (s1.includes(s2) || s2.includes(s1)) {
- score += 0.2;
+ return myers_x(a, b);
+
+};
+
+var closest = function(str, arr) {
+
+ var min_distance = Infinity;
+
+ var min_index = 0;
+
+ for (var i = 0; i < arr.length; i++) {
+
+ var dist = distance(str, arr[i]);
+
+ if (dist < min_distance) {
+
+ min_distance = dist;
+
+ min_index = i;
+
+ }
+
}
- return Math.max(0.0, Math.min(1.0, score));
-}
+ return arr[min_index];
+};