forked from Shinonome/dots-hyprland
search: add levelshtein distance based search
This commit is contained in:
@@ -60,6 +60,7 @@ Singleton {
|
|||||||
property int nonAppResultDelay: 30 // This prevents lagging when typing
|
property int nonAppResultDelay: 30 // This prevents lagging when typing
|
||||||
property string engineBaseUrl: "https://www.google.com/search?q="
|
property string engineBaseUrl: "https://www.google.com/search?q="
|
||||||
property list<string> excludedSites: [ "quora.com" ]
|
property list<string> excludedSites: [ "quora.com" ]
|
||||||
|
property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird.
|
||||||
property QtObject prefix: QtObject {
|
property QtObject prefix: QtObject {
|
||||||
property string action: "/"
|
property string action: "/"
|
||||||
property string clipboard: ":"
|
property string clipboard: ":"
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ RippleButton {
|
|||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
id: nameText
|
id: nameText
|
||||||
textFormat: Text.PlainText // TODO: make cliphist entry highlighting working
|
textFormat: Text.PlainText // TODO: make cliphist entry highlighting work
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
font.family: Appearance.font.family[root.fontType]
|
font.family: Appearance.font.family[root.fontType]
|
||||||
color: Appearance.m3colors.m3onSurface
|
color: Appearance.m3colors.m3onSurface
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
|
import "root:/modules/common"
|
||||||
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
|
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
|
||||||
|
import "root:/modules/common/functions/levendist.js" as Levendist
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
|
||||||
@@ -9,6 +11,8 @@ import Quickshell.Io
|
|||||||
*/
|
*/
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
property bool sloppySearch: ConfigOptions?.search.sloppy ?? false
|
||||||
|
property real scoreThreshold: 0.2
|
||||||
|
|
||||||
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
|
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
@@ -19,6 +23,16 @@ Singleton {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
|
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
|
||||||
|
if (root.sloppySearch) {
|
||||||
|
const 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)
|
||||||
|
return results
|
||||||
|
.map(item => item.entry)
|
||||||
|
}
|
||||||
|
|
||||||
return Fuzzy.go(search, preppedNames, {
|
return Fuzzy.go(search, preppedNames, {
|
||||||
all: true,
|
all: true,
|
||||||
key: "name"
|
key: "name"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ pragma Singleton
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
|
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
|
||||||
|
import "root:/modules/common/functions/levendist.js" as Levendist
|
||||||
|
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||||
import "root:/modules/common"
|
import "root:/modules/common"
|
||||||
import "root:/"
|
import "root:/"
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -10,21 +12,29 @@ import Quickshell.Io
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
property bool sloppySearch: ConfigOptions?.search.sloppy ?? false
|
||||||
|
property real scoreThreshold: 0.2
|
||||||
property list<string> entries: []
|
property list<string> entries: []
|
||||||
property string highlightPrefix: `<b><font color="${Appearance.m3colors.m3primary}">`
|
|
||||||
property string highlightSuffix: `</font></b>`
|
|
||||||
readonly property var preparedEntries: entries.map(a => ({
|
readonly property var preparedEntries: entries.map(a => ({
|
||||||
name: Fuzzy.prepare(`${a}`),
|
name: Fuzzy.prepare(`${a}`),
|
||||||
entry: a
|
entry: a
|
||||||
}))
|
}))
|
||||||
function fuzzyQuery(search: string): var {
|
function fuzzyQuery(search: string): var {
|
||||||
|
if (root.sloppySearch) {
|
||||||
|
const results = entries.slice(0, 100).map(str => ({
|
||||||
|
entry: str,
|
||||||
|
score: Levendist.computeTextMatchScore(str.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, preparedEntries, {
|
return Fuzzy.go(search, preparedEntries, {
|
||||||
all: true,
|
all: true,
|
||||||
key: "name"
|
key: "name"
|
||||||
}).map(r => {
|
}).map(r => {
|
||||||
return r.obj.entry
|
return r.obj.entry
|
||||||
// console.log(JSON.stringify(r))
|
|
||||||
// return r.highlight(highlightPrefix, highlightSuffix);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user