From 9e9708ed12a90cea06db26c3ad715620e1cc77f8 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 14:45:20 +0100 Subject: [PATCH] desktop icons --- Components/CustomTextInput.qml | 15 ++ Config/Config.qml | 1 + Config/General.qml | 1 + Helpers/AppSearch.qml | 122 +++++++++++ .../DesktopIcons/BackgroundContextMenu.qml | 158 ++++++++------ .../DesktopIcons/DesktopIconContextMenu.qml | 193 +++++++++++------- Modules/DesktopIcons/DesktopIconDelegate.qml | 3 +- Modules/DesktopIcons/DesktopIcons.qml | 70 +++++-- Modules/Wallpaper/Wallpaper.qml | 9 +- Paths/Paths.qml | 2 +- scripts/levendist.js | 143 +++++++++++++ 11 files changed, 561 insertions(+), 156 deletions(-) create mode 100644 Components/CustomTextInput.qml create mode 100644 Helpers/AppSearch.qml create mode 100644 scripts/levendist.js diff --git a/Components/CustomTextInput.qml b/Components/CustomTextInput.qml new file mode 100644 index 0000000..25b1eed --- /dev/null +++ b/Components/CustomTextInput.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Controls +import qs.Config + +TextInput { + renderType: Text.NativeRendering + selectedTextColor: DynamicColors.palette.m3onSecondaryContainer + selectionColor: DynamicColors.tPalette.colSecondaryContainer + + font { + family: Appearance?.font.family.sans ?? "sans-serif" + hintingPreference: Font.PreferFullHinting + pixelSize: Appearance?.font.size.normal ?? 15 + } +} diff --git a/Config/Config.qml b/Config/Config.qml index 4fc63dc..ef7b115 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -168,6 +168,7 @@ Singleton { return { logo: general.logo, wallpaperPath: general.wallpaperPath, + desktopIcons: general.desktopIcons, color: { wallust: general.color.wallust, mode: general.color.mode, diff --git a/Config/General.qml b/Config/General.qml index 9169d31..aac4cb1 100644 --- a/Config/General.qml +++ b/Config/General.qml @@ -6,6 +6,7 @@ JsonObject { } property Color color: Color { } + property bool desktopIcons: false property Idle idle: Idle { } property string logo: "" diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml new file mode 100644 index 0000000..f5391bb --- /dev/null +++ b/Helpers/AppSearch.qml @@ -0,0 +1,122 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import "../scripts/levendist.js" as Levendist +import "../scripts/fuzzysort.js" as Fuzzy +import qs.Config + +Singleton { + id: root + + readonly property list 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 + })) + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + property real scoreThreshold: 0.2 + property bool sloppySearch: Config.options?.search.sloppy ?? false + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot", + "zen": "zen-browser" + }) + + signal reload + + function computeScore(...args) { + return Levendist.computeScore(...args); + } + + function computeTextMatchScore(...args) { + return Levendist.computeTextMatchScore(...args); + } + + function fuzzyQuery(search: string): var { // Idk why list doesn't work + if (root.sloppySearch) { + const results = list.map(obj => ({ + entry: obj, + score: computeScore(obj.name.toLowerCase(), search.toLowerCase()) + })).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score); + return results.map(item => item.entry); + } + + return Fuzzy.go(search, preppedNames, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry; + }); + } + + function guessIcon(str) { + if (!str || str.length == 0) + return "image-missing"; + + // Normal substitutions + if (substitutions[str]) + return substitutions[str]; + + // Regex substitutions + for (let i = 0; i < regexSubstitutions.length; i++) { + const substitution = regexSubstitutions[i]; + const replacedName = str.replace(substitution.regex, substitution.replace); + if (replacedName != str) + return replacedName; + } + + // If it gets detected normally, no need to guess + if (iconExists(str)) + return str; + + let guessStr = str; + // Guess: Take only app name of reverse domain name notation + guessStr = str.split('.').slice(-1)[0].toLowerCase(); + if (iconExists(guessStr)) + return guessStr; + // Guess: normalize to kebab case + guessStr = str.toLowerCase().replace(/\s+/g, "-"); + if (iconExists(guessStr)) + return guessStr; + // Guess: First fuzze desktop entry match + const searchResults = root.fuzzyQuery(str); + if (searchResults.length > 0) { + const firstEntry = searchResults[0]; + guessStr = firstEntry.icon; + if (iconExists(guessStr)) + return guessStr; + } + + // Give up + return str; + } + + function iconExists(iconName) { + if (!iconName || iconName.length == 0) + return false; + return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing"); + } +} diff --git a/Modules/DesktopIcons/BackgroundContextMenu.qml b/Modules/DesktopIcons/BackgroundContextMenu.qml index 0b96270..9d95d54 100644 --- a/Modules/DesktopIcons/BackgroundContextMenu.qml +++ b/Modules/DesktopIcons/BackgroundContextMenu.qml @@ -6,17 +6,18 @@ import Quickshell.Hyprland import qs.Components import qs.Config import qs.Paths +import qs.Helpers Item { id: root - + anchors.fill: parent z: 998 visible: false - + property real menuX: 0 property real menuY: 0 - + MouseArea { anchors.fill: parent onClicked: root.close() @@ -25,39 +26,46 @@ Item { CustomClippingRect { id: popupBackground readonly property real padding: 4 - + x: root.menuX y: root.menuY - + color: DynamicColors.tPalette.m3surface radius: Appearance.rounding.normal - + implicitWidth: menuLayout.implicitWidth + padding * 2 implicitHeight: menuLayout.implicitHeight + padding * 2 - + Behavior on opacity { Anim {} } opacity: root.visible ? 1 : 0 - + ColumnLayout { id: menuLayout - anchors.fill: parent - anchors.margins: popupBackground.padding + anchors.centerIn: parent spacing: 0 - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { + CustomRect { + Layout.preferredWidth: 200 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openTerminalRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openTerminalRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "terminal"; font.pointSize: 20 } CustomText { text: "Open terminal"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) - root.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) + root.close() + } } } @@ -69,81 +77,105 @@ Item { Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: settingsRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: settingsRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "settings"; font.pointSize: 20 } - CustomText { text: "Sleex settings"; Layout.fillWidth: true } + CustomText { text: "ZShell settings"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) - root.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) + root.close() + } } } CustomRect { Layout.fillWidth: true implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant + color: DynamicColors.palette.m3outlineVariant Layout.topMargin: 4 Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: logoutRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: logoutRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "logout"; font.pointSize: 20 } CustomText { text: "Logout"; Layout.fillWidth: true } } - - onClicked: { - Hyprland.dispatch("global quickshell:sessionOpen") - root.close() - } - } - CustomRect { - Layout.fillWidth: true - implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant - Layout.topMargin: 4 - Layout.bottomMargin: 4 - } - - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { - spacing: 8 + StateLayer { anchors.fill: parent - anchors.margins: 12 - MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } - CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } - } - onClicked: { - Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons - root.close() + onClicked: { + Hyprland.dispatch("global quickshell:sessionOpen") + root.close() + } } } + + // CustomRect { + // Layout.fillWidth: true + // implicitHeight: 1 + // color: DynamicColors.palette.m3outlineVariant + // Layout.topMargin: 4 + // Layout.bottomMargin: 4 + // } + // + // CustomRect { + // Layout.fillWidth: true + // radius: popupBackground.radius - popupBackground.padding + // implicitHeight: desktopIconsRow.implicitHeight + Appearance.padding.small * 2 + // + // RowLayout { + // id: desktopIconsRow + // spacing: 8 + // anchors.fill: parent + // anchors.leftMargin: Appearance.padding.smaller + // + // MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } + // CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } + // } + // + // StateLayer { + // anchors.fill: parent + // + // onClicked: { + // Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons + // root.close() + // } + // } + // } } } - + function openAt(mouseX, mouseY, parentW, parentH) { - menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) - menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) visible = true } - + function close() { visible = false } diff --git a/Modules/DesktopIcons/DesktopIconContextMenu.qml b/Modules/DesktopIcons/DesktopIconContextMenu.qml index 238246f..e32cf9e 100644 --- a/Modules/DesktopIcons/DesktopIconContextMenu.qml +++ b/Modules/DesktopIcons/DesktopIconContextMenu.qml @@ -23,73 +23,87 @@ Item { property real menuX: 0 property real menuY: 0 - - MouseArea { - anchors.fill: parent - onClicked: contextMenu.close() - } CustomClippingRect { id: popupBackground - readonly property real padding: 4 - + readonly property real padding: Appearance.padding.small + x: contextMenu.menuX y: contextMenu.menuY - + color: DynamicColors.tPalette.m3surface radius: Appearance.rounding.normal - + implicitWidth: menuLayout.implicitWidth + padding * 2 implicitHeight: menuLayout.implicitHeight + padding * 2 - + Behavior on opacity { Anim {} } opacity: contextMenu.visible ? 1 : 0 - + ColumnLayout { id: menuLayout - anchors.fill: parent - anchors.margins: popupBackground.padding + anchors.centerIn: parent spacing: 0 - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { + CustomRect { + Layout.preferredWidth: 160 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "open_in_new"; font.pointSize: 20 } CustomText { text: "Open"; Layout.fillWidth: true } } - - onClicked: { - for (let i = 0; i < contextMenu.targetPaths.length; i++) { - let p = contextMenu.targetPaths[i]; - if (p === contextMenu.targetFilePath) { - if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() - else contextMenu.openFileRequested(p, contextMenu.targetIsDir) - } else { - Quickshell.execDetached(["xdg-open", p]) + + StateLayer { + anchors.fill: parent + + onClicked: { + for (let i = 0; i < contextMenu.targetPaths.length; i++) { + let p = contextMenu.targetPaths[i]; + if (p === contextMenu.targetFilePath) { + if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() + else contextMenu.openFileRequested(p, contextMenu.targetIsDir) + } else { + Quickshell.execDetached(["xdg-open", p]) + } } + contextMenu.close() } - contextMenu.close() } } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openWithRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + if (contextMenu.targetIsDir) { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]) + } else { + Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) + } + contextMenu.close() + } } } @@ -101,92 +115,117 @@ Item { Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: copyPathRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "content_copy"; font.pointSize: 20 } CustomText { text: "Copy path"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) + contextMenu.close() + } } } - StateLayer { + CustomRect { Layout.fillWidth: true visible: contextMenu.targetPaths.length === 1 - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: renameRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "edit"; font.pointSize: 20 } CustomText { text: "Rename"; Layout.fillWidth: true } } - - onClicked: { - contextMenu.renameRequested(contextMenu.targetFilePath) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + contextMenu.renameRequested(contextMenu.targetFilePath) + contextMenu.close() + } } } Rectangle { Layout.fillWidth: true implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant + color: DynamicColors.palette.m3outlineVariant Layout.topMargin: 4 Layout.bottomMargin: 4 } - StateLayer { - id: deleteButton + CustomRect { Layout.fillWidth: true - colBackgroundHover: Appearance.colors.colError - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: deleteRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { - text: "delete"; - font.pointSize: 20; - color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + text: "delete" + font.pointSize: 20 + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error } + CustomText { - text: "Move to trash"; - Layout.fillWidth: true; - color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + text: "Move to trash" + Layout.fillWidth: true + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error } } - - onClicked: { - let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) - Quickshell.execDetached(cmd) - contextMenu.close() + + StateLayer { + id: deleteButton + anchors.fill: parent + color: DynamicColors.tPalette.m3error + + onClicked: { + let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) + Quickshell.execDetached(cmd) + contextMenu.close() + } } } } } - + function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { targetFilePath = path targetIsDir = isDir targetAppEntry = appEnt - + targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path] - - menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) - menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) - + + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) + visible = true } - + function close() { visible = false } diff --git a/Modules/DesktopIcons/DesktopIconDelegate.qml b/Modules/DesktopIcons/DesktopIconDelegate.qml index 2ec681c..1d77c9b 100644 --- a/Modules/DesktopIcons/DesktopIconDelegate.qml +++ b/Modules/DesktopIcons/DesktopIconDelegate.qml @@ -15,6 +15,7 @@ Item { property int gridX: model.gridX property int gridY: model.gridY property bool isSnapping: snapAnimX.running || snapAnimY.running + property bool lassoActive property string resolvedIcon: { if (fileName.endsWith(".desktop")) { if (appEntry && appEntry.icon && appEntry.icon !== "") @@ -214,7 +215,7 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent - cursorShape: Qt.PointingHandCursor + cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor drag.target: dragContainer hoverEnabled: true diff --git a/Modules/DesktopIcons/DesktopIcons.qml b/Modules/DesktopIcons/DesktopIcons.qml index 03774b9..c06ba69 100644 --- a/Modules/DesktopIcons/DesktopIcons.qml +++ b/Modules/DesktopIcons/DesktopIcons.qml @@ -2,6 +2,8 @@ import QtQuick import Quickshell import qs.Modules import qs.Helpers +import qs.Config +import qs.Components import qs.Paths import ZShell.Services @@ -15,6 +17,7 @@ Item { property string editingFilePath: "" property real groupDragX: 0 property real groupDragY: 0 + property bool lassoActive: false property var selectedIcons: [] property real startX: 0 property real startY: 0 @@ -54,7 +57,6 @@ Item { root.groupDragY = 0; } - anchors.fill: parent focus: true Keys.onPressed: event => { @@ -68,15 +70,55 @@ Item { Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) } - Rectangle { + CustomRect { id: lasso - border.color: Appearance.colors.colPrimary + function hideLasso() { + fadeIn.stop(); + fadeOut.start(); + root.lassoActive = false; + } + + function showLasso() { + root.lassoActive = true; + fadeOut.stop(); + visible = true; + fadeIn.start(); + } + + border.color: DynamicColors.palette.m3primary border.width: 1 color: DynamicColors.tPalette.m3primary + opacity: 0 radius: Appearance.rounding.small visible: false z: 99 + + NumberAnimation { + id: fadeIn + + duration: 120 + from: 0 + property: "opacity" + target: lasso + to: 1 + } + + SequentialAnimation { + id: fadeOut + + NumberAnimation { + duration: 120 + from: lasso.opacity + property: "opacity" + target: lasso + to: 0 + } + + ScriptAction { + script: lasso.visible = false + } + } } MouseArea { @@ -85,10 +127,10 @@ Item { onPositionChanged: mouse => { if (lasso.visible) { - lasso.x = Math.min(mouse.x, root.startX); - lasso.y = Math.min(mouse.y, root.startY); - lasso.width = Math.abs(mouse.x - root.startX); - lasso.height = Math.abs(mouse.y - root.startY); + lasso.x = Math.floor(Math.min(mouse.x, root.startX)); + lasso.y = Math.floor(Math.min(mouse.y, root.startY)); + lasso.width = Math.floor(Math.abs(mouse.x - root.startX)); + lasso.height = Math.floor(Math.abs(mouse.y - root.startY)); let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth); let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth); @@ -115,17 +157,17 @@ Item { } else { bgContextMenu.close(); root.selectedIcons = []; - root.startX = mouse.x; - root.startY = mouse.y; - lasso.x = mouse.x; - lasso.y = mouse.y; + root.startX = Math.floor(mouse.x); + root.startY = Math.floor(mouse.y); + lasso.x = Math.floor(mouse.x); + lasso.y = Math.floor(mouse.y); lasso.width = 0; lasso.height = 0; - lasso.visible = true; + lasso.showLasso(); } } onReleased: { - lasso.visible = false; + lasso.hideLasso(); } } @@ -142,6 +184,8 @@ Item { delegate: DesktopIconDelegate { property int itemIndex: index + + lassoActive: root.lassoActive } } } diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index 06f940d..44c44d2 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -32,7 +32,14 @@ Loader { WallBackground { } - DesktopIcons { + Loader { + id: loader + + active: Config.general.desktopIcons + anchors.fill: parent + + sourceComponent: DesktopIcons { + } } } } diff --git a/Paths/Paths.qml b/Paths/Paths.qml index eacf7a7..27131f1 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -10,7 +10,7 @@ Singleton { readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` - readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}` + readonly property string desktop: `${Quickshell.env("HOME")}/Desktop` readonly property string home: Quickshell.env("HOME") readonly property string imagecache: `${cache}/imagecache` readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" diff --git a/scripts/levendist.js b/scripts/levendist.js new file mode 100644 index 0000000..332ed09 --- /dev/null +++ b/scripts/levendist.js @@ -0,0 +1,143 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + 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); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.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; +} + +function computeScore(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; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + 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; + } + + 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; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, 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; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + 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; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +}