feat(settings): unified the settings search in a new singleton

This commit is contained in:
Lemmy
2026-03-11 21:31:43 -04:00
parent 039be7d851
commit 49db78a22b
3 changed files with 172 additions and 184 deletions
@@ -1,6 +1,4 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
@@ -14,23 +12,6 @@ Item {
property string supportedLayouts: "list"
property string iconMode: Settings.data.appLauncher.iconMode
property var searchIndex: []
FileView {
id: searchIndexFile
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
watchChanges: false
printErrors: false
onLoaded: {
try {
root.searchIndex = JSON.parse(text());
} catch (e) {
root.searchIndex = [];
}
}
}
function init() {
Logger.d("SettingsProvider", "Initialized");
}
@@ -57,7 +38,7 @@ Item {
}
function getResults(query) {
if (!query || searchIndex.length === 0)
if (!query || SettingsSearchService.searchIndex.length === 0)
return [];
var trimmed = query.trim();
@@ -78,10 +59,12 @@ Item {
return [];
}
// Build searchable items with resolved translations
// Build searchable items with resolved translations, filtering out invisible entries
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) {
const entry = SettingsSearchService.searchIndex[j];
if (!SettingsSearchService.isEntryVisible(entry))
continue;
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
@@ -133,8 +116,10 @@ Item {
function getAllSettings() {
var launcherItems = [];
for (var j = 0; j < searchIndex.length; j++) {
var entry = searchIndex[j];
for (var j = 0; j < SettingsSearchService.searchIndex.length; j++) {
var entry = SettingsSearchService.searchIndex[j];
if (!SettingsSearchService.isEntryVisible(entry))
continue;
var label = I18n.tr(entry.labelKey);
var tabName = I18n.tr(entry.tabLabel);
var subTabName = entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "";
+4 -159
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Panels.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs.About
@@ -53,7 +52,6 @@ Item {
// Search state
property string searchText: ""
property var searchIndex: []
property var searchResults: []
property int searchSelectedIndex: 0
property string highlightLabelKey: ""
@@ -76,159 +74,6 @@ Item {
// Signal when close button is clicked
signal closeRequested
// Load search index
FileView {
id: searchIndexFile
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
watchChanges: false
printErrors: false
onLoaded: {
try {
root.searchIndex = JSON.parse(text());
} catch (e) {
root.searchIndex = [];
}
}
}
// Visibility condition evaluation for search filtering.
// Resolves a dotted property path on a known root object.
function _resolveValue(path) {
const roots = {
"CompositorService": CompositorService,
"Settings": Settings,
"Quickshell": Quickshell,
"IdleService": IdleService,
"SystemStatService": SystemStatService,
"SoundService": SoundService
};
const parts = path.split(".");
const rootObj = roots[parts[0]];
if (rootObj === undefined)
return undefined;
let obj = rootObj;
for (let i = 1; i < parts.length; i++) {
if (obj === undefined || obj === null)
return undefined;
// Strip optional chaining marker (e.g. "sounds?" -> "sounds")
let key = parts[i];
if (key.endsWith("?"))
key = key.slice(0, -1);
obj = obj[key];
}
return obj;
}
// Split an expression on top-level && (respecting parentheses).
function _splitAnd(expr) {
const parts = [];
let depth = 0;
let current = "";
for (let i = 0; i < expr.length; i++) {
const ch = expr[i];
if (ch === "(")
depth++;
else if (ch === ")")
depth--;
else if (depth === 0 && ch === "&" && i + 1 < expr.length && expr[i + 1] === "&") {
parts.push(current);
current = "";
i++; // skip second &
continue;
}
current += ch;
}
parts.push(current);
return parts;
}
// Evaluate a single visibility condition expression.
// Returns true if the condition is met (item should be visible).
// Falls back to true for unrecognized expressions.
function _evalCondition(expr) {
expr = expr.trim();
// Strip outer parentheses
if (expr.startsWith("(") && expr.endsWith(")")) {
let depth = 0;
let allWrapped = true;
for (let i = 0; i < expr.length - 1; i++) {
if (expr[i] === "(")
depth++;
else if (expr[i] === ")")
depth--;
if (depth === 0) {
allWrapped = false;
break;
}
}
if (allWrapped)
return _evalCondition(expr.slice(1, -1));
}
// AND: all parts must be true
if (expr.includes("&&")) {
const parts = _splitAnd(expr);
if (parts.length > 1) {
for (let i = 0; i < parts.length; i++) {
if (!_evalCondition(parts[i]))
return false;
}
return true;
}
}
// Negation
if (expr.startsWith("!"))
return !_evalCondition(expr.slice(1).trim());
// Literal false
if (expr === "false")
return false;
// Strip nullish coalescing fallback (e.g. "expr ?? false")
const nullishMatch = expr.match(/^(.+?)\s*\?\?\s*(?:false|true)\s*$/);
if (nullishMatch)
return _evalCondition(nullishMatch[1]);
// === comparison
let m = expr.match(/^(.+?)\s*===\s*"([^"]*)"\s*$/);
if (m)
return _resolveValue(m[1].trim()) === m[2];
// !== comparison
m = expr.match(/^(.+?)\s*!==\s*"([^"]*)"\s*$/);
if (m)
return _resolveValue(m[1].trim()) !== m[2];
// > comparison
m = expr.match(/^(.+?)\s*>\s*(\d+)\s*$/);
if (m)
return _resolveValue(m[1].trim()) > parseInt(m[2]);
// Simple property path — resolve and return truthiness
const val = _resolveValue(expr);
if (val !== undefined)
return !!val;
// Unrecognized expression — assume visible
return true;
}
// Check if a search index entry is currently visible.
function _isEntryVisible(entry) {
if (!entry.visibleWhen || entry.visibleWhen.length === 0)
return true;
for (let i = 0; i < entry.visibleWhen.length; i++) {
if (!_evalCondition(entry.visibleWhen[i]))
return false;
}
return true;
}
// Search function
onSearchTextChanged: {
if (searchText.trim() === "") {
@@ -249,14 +94,14 @@ Item {
root.sidebarExpanded = true;
}
if (searchIndex.length === 0)
if (SettingsSearchService.searchIndex.length === 0)
return;
// Build searchable items with resolved translations, filtering out invisible entries
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
if (!_isEntryVisible(entry))
for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) {
const entry = SettingsSearchService.searchIndex[j];
if (!SettingsSearchService.isEntryVisible(entry))
continue;
items.push({
"labelKey": entry.labelKey,