feat(settings): improved search index to support visibility conditions, prevent showing results that would be invisible to the user. Fix #2113

This commit is contained in:
Lemmy
2026-03-11 10:07:30 -04:00
parent 8005958b8e
commit d35f399f53
3 changed files with 451 additions and 61 deletions
+220 -55
View File
@@ -195,7 +195,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.useSeparateOpacity"
]
},
{
"labelKey": "panels.bar.appearance-font-scale-label",
@@ -249,7 +252,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.showCapsule"
]
},
{
"labelKey": "panels.bar.appearance-capsule-opacity-label",
@@ -258,7 +264,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.showCapsule"
]
},
{
"labelKey": "panels.bar.appearance-enable-exclusion-zone-inset-label",
@@ -276,7 +285,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"CompositorService.isNiri"
]
},
{
"labelKey": "panels.bar.appearance-outer-corners-label",
@@ -285,7 +297,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"simple\""
]
},
{
"labelKey": "panels.bar.appearance-frame-settings-label",
@@ -294,7 +309,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"framed\""
]
},
{
"labelKey": "panels.bar.appearance-frame-thickness",
@@ -303,7 +321,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"framed\""
]
},
{
"labelKey": "panels.bar.appearance-frame-radius",
@@ -312,7 +333,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"framed\""
]
},
{
"labelKey": "panels.bar.appearance-margins-vertical",
@@ -321,7 +345,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"floating\""
]
},
{
"labelKey": "panels.bar.appearance-margins-horizontal",
@@ -330,7 +357,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.barType === \"floating\""
]
},
{
"labelKey": "panels.bar.appearance-auto-hide-delay-label",
@@ -339,7 +369,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.displayMode === \"auto_hide\""
]
},
{
"labelKey": "panels.bar.appearance-auto-show-delay-label",
@@ -348,7 +381,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.displayMode === \"auto_hide\""
]
},
{
"labelKey": "panels.bar.appearance-show-on-workspace-switch-label",
@@ -357,7 +393,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.bar.displayMode === \"auto_hide\""
]
},
{
"labelKey": "panels.bar.behavior-workspace-scroll-label",
@@ -375,7 +414,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.mouseWheelAction !== \"none\""
]
},
{
"labelKey": "panels.bar.behavior-wheel-wrap-label",
@@ -384,7 +426,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.mouseWheelAction === \"workspace\""
]
},
{
"labelKey": "panels.bar.behavior-middle-click-label",
@@ -402,7 +447,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.middleClickAction === \"command\""
]
},
{
"labelKey": "panels.bar.behavior-middle-click-follow-mouse-label",
@@ -411,7 +459,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.middleClickAction !== \"none\" && Settings.data.bar.middleClickAction !== \"command\" && !(Settings.data.bar.middleClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")"
]
},
{
"labelKey": "panels.bar.behavior-right-click-label",
@@ -429,7 +480,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.rightClickAction === \"command\""
]
},
{
"labelKey": "panels.bar.behavior-right-click-follow-mouse-label",
@@ -438,7 +492,10 @@
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 2,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.bar.rightClickAction !== \"none\" && Settings.data.bar.rightClickAction !== \"command\" && !(Settings.data.bar.rightClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")"
]
},
{
"labelKey": "panels.bar.monitor-override-settings",
@@ -474,7 +531,10 @@
"tab": 2,
"tabLabel": "panels.color-scheme.title",
"subTab": 0,
"subTabLabel": "common.colors"
"subTabLabel": "common.colors",
"visibleWhen": [
"Settings.data.colorSchemes.schedulingMode === \"manual\""
]
},
{
"labelKey": "panels.color-scheme.color-source-use-wallpaper-colors-label",
@@ -591,7 +651,10 @@
"tab": 16,
"tabLabel": "panels.connections.title",
"subTab": 1,
"subTabLabel": "common.bluetooth"
"subTabLabel": "common.bluetooth",
"visibleWhen": [
"Settings.data.network.bluetoothRssiPollingEnabled"
]
},
{
"labelKey": "toast.airplane-mode.title",
@@ -760,7 +823,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.dockType === \"floating\""
]
},
{
"labelKey": "panels.dock.appearance-sit-on-frame-label",
@@ -769,7 +835,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.dockType === \"static\" && Settings.data.bar.barType === \"framed\""
]
},
{
"labelKey": "panels.dock.appearance-dock-indicator-label",
@@ -787,7 +856,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.showDockIndicator"
]
},
{
"labelKey": "panels.dock.appearance-indicator-color-label",
@@ -796,7 +868,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.showDockIndicator"
]
},
{
"labelKey": "panels.dock.appearance-indicator-opacity-label",
@@ -805,7 +880,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.showDockIndicator"
]
},
{
"labelKey": "panels.osd.background-opacity-label",
@@ -832,7 +910,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.dockType === \"floating\""
]
},
{
"labelKey": "panels.dock.appearance-icon-size-label",
@@ -850,7 +931,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.dockType === \"floating\" && Settings.data.dock.displayMode === \"auto_hide\""
]
},
{
"labelKey": "panels.dock.appearance-inactive-indicators-label",
@@ -886,7 +970,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.groupApps"
]
},
{
"labelKey": "panels.dock.appearance-group-context-menu-mode-label",
@@ -895,7 +982,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.groupApps"
]
},
{
"labelKey": "panels.dock.appearance-group-indicator-style-label",
@@ -904,7 +994,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.groupApps"
]
},
{
"labelKey": "panels.dock.monitors-only-same-monitor-label",
@@ -940,7 +1033,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.showLauncherIcon"
]
},
{
"labelKey": "common.select-icon-color",
@@ -949,7 +1045,10 @@
"tab": 5,
"tabLabel": "panels.dock.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.dock.showLauncherIcon"
]
},
{
"labelKey": "panels.general.profile-picture-label",
@@ -1105,7 +1204,10 @@
"tab": 13,
"tabLabel": "panels.idle.title",
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
"subTabLabel": "panels.idle.tab-behavior",
"visibleWhen": [
"IdleService.nativeIdleMonitorAvailable"
]
},
{
"labelKey": "panels.idle.timeouts-label",
@@ -1177,7 +1279,10 @@
"tab": 8,
"tabLabel": "panels.launcher.title",
"subTab": 1,
"subTabLabel": "common.clipboard"
"subTabLabel": "common.clipboard",
"visibleWhen": [
"Settings.data.appLauncher.enableClipboardHistory"
]
},
{
"labelKey": "panels.launcher.settings-clipboard-watch-image-label",
@@ -1186,7 +1291,10 @@
"tab": 8,
"tabLabel": "panels.launcher.title",
"subTab": 1,
"subTabLabel": "common.clipboard"
"subTabLabel": "common.clipboard",
"visibleWhen": [
"Settings.data.appLauncher.enableClipboardHistory"
]
},
{
"labelKey": "panels.launcher.settings-use-app2unit-label",
@@ -1222,7 +1330,10 @@
"tab": 8,
"tabLabel": "panels.launcher.title",
"subTab": 2,
"subTabLabel": "common.execute"
"subTabLabel": "common.execute",
"visibleWhen": [
"Settings.data.appLauncher.customLaunchPrefixEnabled"
]
},
{
"labelKey": "panels.launcher.settings-annotation-tool-label",
@@ -1348,7 +1459,10 @@
"tab": 11,
"tabLabel": "panels.lock-screen.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.general.clockStyle === \"custom\""
]
},
{
"labelKey": "panels.lock-screen.password-chars-label",
@@ -1375,7 +1489,10 @@
"tab": 11,
"tabLabel": "panels.lock-screen.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"!Settings.data.general.compactLockScreen"
]
},
{
"labelKey": "panels.lock-screen.lock-screen-animations-label",
@@ -1447,7 +1564,10 @@
"tab": 11,
"tabLabel": "panels.lock-screen.title",
"subTab": 1,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.general.showSessionButtonsOnLockScreen"
]
},
{
"labelKey": "panels.session-menu.enable-countdown-label",
@@ -1456,7 +1576,10 @@
"tab": 11,
"tabLabel": "panels.lock-screen.title",
"subTab": 1,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.general.showSessionButtonsOnLockScreen"
]
},
{
"labelKey": "panels.session-menu.countdown-duration-label",
@@ -1465,7 +1588,10 @@
"tab": 11,
"tabLabel": "panels.lock-screen.title",
"subTab": 1,
"subTabLabel": "common.behavior"
"subTabLabel": "common.behavior",
"visibleWhen": [
"Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown"
]
},
{
"labelKey": "panels.notifications.duration-respect-expire-label",
@@ -1591,7 +1717,10 @@
"tab": 9,
"tabLabel": "common.notifications",
"subTab": 3,
"subTabLabel": "common.sound"
"subTabLabel": "common.sound",
"visibleWhen": [
"!SoundService.multimediaAvailable"
]
},
{
"labelKey": "panels.notifications.sounds-enabled-label",
@@ -1627,7 +1756,10 @@
"tab": 9,
"tabLabel": "common.notifications",
"subTab": 3,
"subTabLabel": "common.sound"
"subTabLabel": "common.sound",
"visibleWhen": [
"!(Settings.data.notifications?.sounds?.separateSounds ?? false)"
]
},
{
"labelKey": "panels.notifications.sounds-files-low-label",
@@ -1636,7 +1768,10 @@
"tab": 9,
"tabLabel": "common.notifications",
"subTab": 3,
"subTabLabel": "common.sound"
"subTabLabel": "common.sound",
"visibleWhen": [
"SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)"
]
},
{
"labelKey": "panels.notifications.sounds-files-normal-label",
@@ -1645,7 +1780,10 @@
"tab": 9,
"tabLabel": "common.notifications",
"subTab": 3,
"subTabLabel": "common.sound"
"subTabLabel": "common.sound",
"visibleWhen": [
"SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)"
]
},
{
"labelKey": "panels.notifications.sounds-files-critical-label",
@@ -1654,7 +1792,10 @@
"tab": 9,
"tabLabel": "common.notifications",
"subTab": 3,
"subTabLabel": "common.sound"
"subTabLabel": "common.sound",
"visibleWhen": [
"SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)"
]
},
{
"labelKey": "panels.notifications.sounds-excluded-apps-label",
@@ -1879,7 +2020,10 @@
"tab": 12,
"tabLabel": "session-menu.title",
"subTab": 0,
"subTabLabel": "common.general"
"subTabLabel": "common.general",
"visibleWhen": [
"Settings.data.sessionMenu.largeButtonsStyle"
]
},
{
"labelKey": "panels.session-menu.show-header-label",
@@ -1888,7 +2032,10 @@
"tab": 12,
"tabLabel": "session-menu.title",
"subTab": 0,
"subTabLabel": "common.general"
"subTabLabel": "common.general",
"visibleWhen": [
"!Settings.data.sessionMenu.largeButtonsStyle"
]
},
{
"labelKey": "panels.session-menu.show-keybinds-label",
@@ -2005,7 +2152,10 @@
"tab": 1,
"tabLabel": "panels.user-interface.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"Settings.data.general.enableShadows"
]
},
{
"labelKey": "panels.user-interface.scaling-label",
@@ -2050,7 +2200,10 @@
"tab": 1,
"tabLabel": "panels.user-interface.title",
"subTab": 0,
"subTabLabel": "common.appearance"
"subTabLabel": "common.appearance",
"visibleWhen": [
"!Settings.data.general.animationDisabled"
]
},
{
"labelKey": "panels.user-interface.panels-attached-to-bar-label",
@@ -2068,7 +2221,10 @@
"tab": 1,
"tabLabel": "panels.user-interface.title",
"subTab": 1,
"subTabLabel": "common.panels"
"subTabLabel": "common.panels",
"visibleWhen": [
"(Quickshell.screens.length > 1)"
]
},
{
"labelKey": "panels.user-interface.panel-background-opacity-label",
@@ -2221,7 +2377,10 @@
"tab": 3,
"tabLabel": "common.wallpaper",
"subTab": 0,
"subTabLabel": "common.general"
"subTabLabel": "common.general",
"visibleWhen": [
"CompositorService.isNiri"
]
},
{
"labelKey": "panels.wallpaper.settings-overview-blur-strength-label",
@@ -2230,7 +2389,10 @@
"tab": 3,
"tabLabel": "common.wallpaper",
"subTab": 0,
"subTabLabel": "common.general"
"subTabLabel": "common.general",
"visibleWhen": [
"CompositorService.isNiri"
]
},
{
"labelKey": "panels.wallpaper.settings-overview-tint-label",
@@ -2239,7 +2401,10 @@
"tab": 3,
"tabLabel": "common.wallpaper",
"subTab": 0,
"subTabLabel": "common.general"
"subTabLabel": "common.general",
"visibleWhen": [
"CompositorService.isNiri"
]
},
{
"labelKey": "panels.wallpaper.look-feel-fill-mode-label",
+142 -1
View File
@@ -25,6 +25,8 @@ import qs.Modules.Panels.Settings.Tabs.SessionMenu
import qs.Modules.Panels.Settings.Tabs.SystemMonitor
import qs.Modules.Panels.Settings.Tabs.UserInterface
import qs.Modules.Panels.Settings.Tabs.Wallpaper
import qs.Services.Compositor
import qs.Services.Power
import qs.Services.System
import qs.Services.UI
import qs.Widgets
@@ -90,6 +92,143 @@ Item {
}
}
// 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() === "") {
@@ -113,10 +252,12 @@ Item {
if (searchIndex.length === 0)
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];
if (!_isEntryVisible(entry))
continue;
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
+89 -5
View File
@@ -3,7 +3,7 @@
Build settings search index from QML source files.
Parses settings tab QML files to extract searchable metadata
(i18n keys, widget types, tab/sub-tab locations).
(i18n keys, widget types, tab/sub-tab locations, visibility conditions).
Output: Assets/settings-search-index.json
@@ -42,6 +42,21 @@ RE_WIDGET_OPEN = re.compile(
)
RE_LABEL = re.compile(r'label:\s*I18n\.tr\("([^"]+)"')
RE_DESCRIPTION = re.compile(r'description:\s*I18n\.tr\("([^"]+)"')
RE_VISIBLE = re.compile(r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$')
# Prefixes that indicate externally-resolvable conditions (singleton services or globals).
# Conditions referencing local variables (root., parent., model, index, etc.) are skipped.
RESOLVABLE_PREFIXES = (
"CompositorService.",
"Settings.data.",
"Quickshell.",
"IdleService.",
"SystemStatService.",
"SoundService.",
"BluetoothService.",
"LocationService.",
"false",
)
def parse_component_declarations(content: str) -> dict[str, str]:
@@ -234,9 +249,61 @@ def resolve_tab_info(
return tab_index, tab_label, None, None
def extract_widget_blocks(content: str) -> list[tuple[str, str]]:
def is_resolvable_condition(cond: str) -> bool:
"""Check if a visibility condition can be resolved at runtime by the shell."""
# Strip negation for prefix checking
check = cond.lstrip("!").lstrip(" ").lstrip("(").lstrip(" ")
return any(check.startswith(p) for p in RESOLVABLE_PREFIXES)
def build_scope_visibility(content: str) -> dict[int, list[str]]:
"""
Extract (widget_type, block_text) pairs from QML content.
For each line number, compute the list of inherited visibility conditions
from all enclosing QML scopes.
Tracks brace-depth to maintain a scope stack. When a ``visible:`` property
is found, it is associated with the innermost scope. The conditions are
inherited by all lines within that scope.
Returns: line_number -> list of condition strings
"""
lines = content.splitlines()
# Each entry: visibility condition string or None
scope_stack: list[str | None] = []
result: dict[int, list[str]] = {}
for line_num, raw_line in enumerate(lines):
stripped = raw_line.strip()
# Record inherited conditions BEFORE processing this line's braces.
# This means a widget opening on this line inherits from parent scopes,
# not from its own scope (which hasn't been populated yet).
result[line_num] = [c for c in scope_stack if c is not None]
# Process opening braces — push new scopes
n_opens = stripped.count("{")
for _ in range(n_opens):
scope_stack.append(None)
# Check for visible: property — assign to the innermost scope
vis_match = RE_VISIBLE.match(stripped)
if vis_match and scope_stack:
cond = vis_match.group(1).strip()
if cond != "true":
scope_stack[-1] = cond
# Process closing braces — pop scopes
n_closes = stripped.count("}")
for _ in range(n_closes):
if scope_stack:
scope_stack.pop()
return result
def extract_widget_blocks(content: str) -> list[tuple[str, str, int]]:
"""
Extract (widget_type, block_text, start_line) tuples from QML content.
Uses brace-depth tracking to capture the full widget block.
"""
@@ -250,6 +317,7 @@ def extract_widget_blocks(content: str) -> list[tuple[str, str]]:
widget_type = m.group(1)
depth = 0
block_lines = []
start_line = i
j = i
while j < len(lines):
@@ -261,7 +329,7 @@ def extract_widget_blocks(content: str) -> list[tuple[str, str]]:
j += 1
block_text = "\n".join(block_lines)
results.append((widget_type, block_text))
results.append((widget_type, block_text, start_line))
i = j + 1
else:
i += 1
@@ -282,9 +350,10 @@ def extract_entries(
return []
content = qml_file.read_text()
scope_vis = build_scope_visibility(content)
entries = []
for widget_type, block in extract_widget_blocks(content):
for widget_type, block, start_line in extract_widget_blocks(content):
label_match = RE_LABEL.search(block)
if not label_match:
continue
@@ -293,6 +362,19 @@ def extract_entries(
desc_match = RE_DESCRIPTION.search(block)
desc_key = desc_match.group(1) if desc_match else None
# Collect visibility conditions: ancestor scopes + widget's own visible:
conditions = list(scope_vis.get(start_line, []))
widget_vis = re.search(
r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$', block, re.MULTILINE
)
if widget_vis:
cond = widget_vis.group(1).strip()
if cond != "true" and cond not in conditions:
conditions.append(cond)
# Keep only externally-resolvable conditions
conditions = [c for c in conditions if is_resolvable_condition(c)]
entry = {
"labelKey": label_key,
"descriptionKey": desc_key,
@@ -303,6 +385,8 @@ def extract_entries(
}
if sub_tab_label is not None:
entry["subTabLabel"] = sub_tab_label
if conditions:
entry["visibleWhen"] = conditions
entries.append(entry)