mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
Fix launcher filtering and add tests for it
This commit is contained in:
@@ -4,6 +4,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import "LauncherSearch.js" as LauncherSearch
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -88,15 +89,15 @@ Item {
|
||||
}
|
||||
|
||||
function entryName(entry) {
|
||||
return String((entry && entry.name) || (entry && entry.id) || "")
|
||||
return LauncherSearch.entryName(entry)
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return String((entry && entry.genericName) || "")
|
||||
return LauncherSearch.entrySubtext(entry)
|
||||
}
|
||||
|
||||
function entrySortKey(entry) {
|
||||
return root.entryName(entry).toLowerCase()
|
||||
return LauncherSearch.entrySortKey(entry)
|
||||
}
|
||||
|
||||
function toplevelCount() {
|
||||
@@ -104,10 +105,7 @@ Item {
|
||||
}
|
||||
|
||||
function entrySearchText(entry) {
|
||||
if (!entry) return ""
|
||||
var keywords = ""
|
||||
try { keywords = (entry.keywords || []).join(" ") } catch (e) { keywords = "" }
|
||||
return [entry.name, entry.genericName, entry.comment, keywords, entry.id].join(" ").toLowerCase()
|
||||
return LauncherSearch.entrySearchText(entry)
|
||||
}
|
||||
|
||||
function isHiddenEntry(entry) {
|
||||
@@ -150,60 +148,12 @@ Item {
|
||||
}
|
||||
|
||||
function fuzzyScore(entry, query) {
|
||||
var q = String(query || "").trim().toLowerCase()
|
||||
if (!q) return 0
|
||||
|
||||
var name = root.entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = root.entrySearchText(entry)
|
||||
var directName = name.indexOf(q)
|
||||
var directId = id.indexOf(q)
|
||||
if (directName === 0) return 10000 - name.length
|
||||
if (directId === 0) return 9500 - id.length
|
||||
if (directName > 0) return 8000 - directName * 10 - name.length
|
||||
if (directId > 0) return 7600 - directId * 10 - id.length
|
||||
|
||||
var hayIndex = haystack.indexOf(q)
|
||||
if (hayIndex >= 0) return 6000 - hayIndex
|
||||
|
||||
var pos = 0
|
||||
var first = -1
|
||||
var last = -1
|
||||
for (var i = 0; i < haystack.length && pos < q.length; i++) {
|
||||
if (haystack.charAt(i) !== q.charAt(pos)) continue
|
||||
if (first < 0) first = i
|
||||
last = i
|
||||
pos++
|
||||
}
|
||||
if (pos !== q.length) return -1
|
||||
return 3000 - (last - first) - haystack.length * 0.01
|
||||
return LauncherSearch.fuzzyScore(entry, query)
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
var values = DesktopEntries.applications.values || []
|
||||
var q = String(query || "").trim()
|
||||
var rows = []
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var entry = values[i]
|
||||
if (!entry || entry.noDisplay || root.isHiddenEntry(entry)) continue
|
||||
var name = root.entryName(entry)
|
||||
if (!name) continue
|
||||
var score = root.fuzzyScore(entry, q)
|
||||
if (score < 0) continue
|
||||
rows.push({ entry: entry, score: score, key: root.entrySortKey(entry), name: name.toLowerCase() })
|
||||
}
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
if (q && a.score !== b.score) return b.score - a.score
|
||||
if (a.key < b.key) return -1
|
||||
if (a.key > b.key) return 1
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return rows
|
||||
return LauncherSearch.sortedEntries(values, query, function(entry) { return root.isHiddenEntry(entry) })
|
||||
}
|
||||
|
||||
function rebuildDisplay() {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
function entryName(entry) {
|
||||
return String((entry && entry.name) || (entry && entry.id) || "")
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return String((entry && entry.genericName) || "")
|
||||
}
|
||||
|
||||
function entrySortKey(entry) {
|
||||
return entryName(entry).toLowerCase()
|
||||
}
|
||||
|
||||
function keywordText(entry) {
|
||||
try {
|
||||
if (entry && entry.keywords && typeof entry.keywords.join === "function") return entry.keywords.join(" ")
|
||||
} catch (e) {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function entrySearchText(entry) {
|
||||
if (!entry) return ""
|
||||
return [entry.name, entry.genericName, entry.comment, keywordText(entry), entry.id].join(" ").toLowerCase()
|
||||
}
|
||||
|
||||
function wordText(value) {
|
||||
return String(value || "")
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/[._:/\\-]+/g, " ")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function words(value) {
|
||||
var values = wordText(value).split(/[^a-z0-9]+/)
|
||||
var result = []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
if (values[i]) result.push(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function entryAcronym(entry) {
|
||||
var values = words([entry && entry.name, entry && entry.genericName, keywordText(entry), entry && entry.id].join(" "))
|
||||
var result = ""
|
||||
for (var i = 0; i < values.length; i++) result += values[i].charAt(0)
|
||||
return result
|
||||
}
|
||||
|
||||
function termMatches(entry, term) {
|
||||
if (!term) return true
|
||||
|
||||
var name = entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = entrySearchText(entry)
|
||||
|
||||
if (name.indexOf(term) >= 0) return true
|
||||
if (id.indexOf(term) >= 0) return true
|
||||
if (haystack.indexOf(term) >= 0) return true
|
||||
|
||||
return term.length <= 5 && entryAcronym(entry).indexOf(term) >= 0
|
||||
}
|
||||
|
||||
function allTermsMatch(entry, query) {
|
||||
var terms = String(query || "").toLowerCase().trim().split(/\s+/)
|
||||
for (var i = 0; i < terms.length; i++) {
|
||||
if (terms[i] && !termMatches(entry, terms[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function fuzzyScore(entry, query) {
|
||||
var q = String(query || "").trim().toLowerCase()
|
||||
if (!q) return 0
|
||||
if (!allTermsMatch(entry, q)) return -1
|
||||
|
||||
var name = entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = entrySearchText(entry)
|
||||
var directName = name.indexOf(q)
|
||||
var directId = id.indexOf(q)
|
||||
if (directName === 0) return 10000 - name.length
|
||||
if (directId === 0) return 9500 - id.length
|
||||
if (directName > 0) return 8000 - directName * 10 - name.length
|
||||
if (directId > 0) return 7600 - directId * 10 - id.length
|
||||
|
||||
var hayIndex = haystack.indexOf(q)
|
||||
if (hayIndex >= 0) return 6000 - hayIndex
|
||||
|
||||
var acronym = entryAcronym(entry)
|
||||
var acronymIndex = acronym.indexOf(q)
|
||||
if (acronymIndex === 0) return 5000 - acronym.length
|
||||
if (acronymIndex > 0) return 4600 - acronymIndex * 10 - acronym.length
|
||||
|
||||
return 4000 - name.length
|
||||
}
|
||||
|
||||
function sortedEntries(values, query, hiddenCallback) {
|
||||
var q = String(query || "").trim()
|
||||
var rows = []
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var entry = values[i]
|
||||
if (!entry || entry.noDisplay) continue
|
||||
if (hiddenCallback && hiddenCallback(entry)) continue
|
||||
var name = entryName(entry)
|
||||
if (!name) continue
|
||||
var score = fuzzyScore(entry, q)
|
||||
if (score < 0) continue
|
||||
rows.push({ entry: entry, score: score, key: entrySortKey(entry), name: name.toLowerCase() })
|
||||
}
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
if (q && a.score !== b.score) return b.score - a.score
|
||||
if (a.key < b.key) return -1
|
||||
if (a.key > b.key) return 1
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
entryName: entryName,
|
||||
entrySubtext: entrySubtext,
|
||||
entrySortKey: entrySortKey,
|
||||
entrySearchText: entrySearchText,
|
||||
entryAcronym: entryAcronym,
|
||||
fuzzyScore: fuzzyScore,
|
||||
sortedEntries: sortedEntries
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
export ROOT
|
||||
|
||||
node <<'JS'
|
||||
const search = require(`${process.env.ROOT}/shell/plugins/launcher/LauncherSearch.js`)
|
||||
|
||||
function fail(description, detail) {
|
||||
if (detail) console.error(detail)
|
||||
console.error(`not ok - ${description}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function pass(description) {
|
||||
console.log(`ok - ${description}`)
|
||||
}
|
||||
|
||||
function assert(condition, description, detail) {
|
||||
if (!condition) fail(description, detail)
|
||||
pass(description)
|
||||
}
|
||||
|
||||
const entries = [
|
||||
{
|
||||
name: 'Google Contacts',
|
||||
genericName: 'Address Book',
|
||||
comment: 'Manage contacts',
|
||||
keywords: ['contacts', 'address book', 'people'],
|
||||
id: 'google-contacts.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Calculator',
|
||||
genericName: 'Calculator',
|
||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||
id: 'org.gnome.Calculator.desktop'
|
||||
},
|
||||
{
|
||||
name: 'OBS Studio',
|
||||
genericName: 'Streaming/Recording Software',
|
||||
comment: 'Free and Open Source Streaming/Recording Software',
|
||||
keywords: ['streaming', 'recording', 'capture'],
|
||||
id: 'com.obsproject.Studio.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Aether',
|
||||
genericName: '',
|
||||
comment: 'Minimal internet radio player',
|
||||
keywords: ['audio', 'music', 'radio'],
|
||||
id: 'io.github.taqi.aether.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Xournal++',
|
||||
genericName: 'Notetaking',
|
||||
comment: 'Take handwritten notes',
|
||||
keywords: ['notes', 'pdf', 'annotation'],
|
||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||
},
|
||||
{
|
||||
name: 'RustDesk',
|
||||
genericName: 'Remote Desktop',
|
||||
comment: 'Remote desktop control',
|
||||
keywords: ['remote', 'desktop', 'control'],
|
||||
id: 'com.rustdesk.RustDesk.desktop'
|
||||
}
|
||||
]
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assert(
|
||||
contactMatches.length === 1 && contactMatches[0] === 'Google Contacts',
|
||||
'contact search only returns direct contact matches',
|
||||
`matches: ${contactMatches.join(', ')}`
|
||||
)
|
||||
|
||||
assert(
|
||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||
'calculator does not match contact as a loose subsequence'
|
||||
)
|
||||
|
||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||
assert(
|
||||
acronymMatches[0] === 'Google Contacts',
|
||||
'short acronym matching still works'
|
||||
)
|
||||
|
||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||
assert(
|
||||
directMatches[0] === 'OBS Studio',
|
||||
'direct app-name matching still works'
|
||||
)
|
||||
JS
|
||||
Reference in New Issue
Block a user