diff --git a/CMakeLists.txt b/CMakeLists.txt
index ec4d8e7..1681ef4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -51,6 +51,20 @@ add_compile_options(
-Wunreachable-code
)
+if("shell" IN_LIST ENABLE_MODULES)
+ find_package(Python3 COMPONENTS Interpreter REQUIRED)
+ set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
+ execute_process(
+ COMMAND ${Python3_EXECUTABLE}
+ "${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
+ "${CMAKE_SOURCE_DIR}/Modules/Settings"
+ "${SETTINGS_INDEX_JSON}"
+ RESULT_VARIABLE SETTINGS_INDEX_RESULT
+ )
+ if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
+ message(FATAL_ERROR "Failed to build settings search index")
+ endif()
+endif()
if("plugin" IN_LIST ENABLE_MODULES)
add_subdirectory(Plugins)
diff --git a/Modules/Clipboard/Content.qml b/Modules/Clipboard/Content.qml
index 8df9f84..152c552 100644
--- a/Modules/Clipboard/Content.qml
+++ b/Modules/Clipboard/Content.qml
@@ -34,21 +34,11 @@ Item {
implicitHeight: 50
radius: Appearance.rounding.full
- MaterialIcon {
- id: searchIcon
-
- anchors.left: parent.left
- anchors.margins: Appearance.padding.large
- anchors.verticalCenter: parent.verticalCenter
- text: "search"
- }
-
- CustomTextField {
+ SearchBar {
id: searchField
anchors.bottom: parent.bottom
- anchors.left: searchIcon.right
- anchors.leftMargin: Appearance.spacing.small
+ anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3onSurface
@@ -170,7 +160,6 @@ Item {
id: lineText
color: DynamicColors.palette.m3onSurface
- font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
text: lineRow.modelData.text.trim()
verticalAlignment: Text.AlignVCenter
@@ -336,6 +325,8 @@ Item {
}
model: ScriptModel {
values: ClipHistory.fuzzyQuery(searchField.text)
+
+ onValuesChanged: view.currentIndex = 0
}
onCurrentItemChanged: {
diff --git a/Modules/Launcher/Content.qml b/Modules/Launcher/Content.qml
index 0bc84c4..3fafb77 100644
--- a/Modules/Launcher/Content.qml
+++ b/Modules/Launcher/Content.qml
@@ -52,11 +52,7 @@ Item {
placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix)
topPadding: Appearance.padding.larger
- Component.onCompleted: {
- console.log(search.color);
- console.log(search.placeholderTextColor);
- forceActiveFocus();
- }
+ Component.onCompleted: forceActiveFocus()
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
Keys.onEscapePressed: root.visibilities.launcher = false
Keys.onPressed: event => {
diff --git a/Modules/Settings/Common/ConnectedRect.qml b/Modules/Settings/Common/ConnectedRect.qml
index e273ae4..8bb9829 100644
--- a/Modules/Settings/Common/ConnectedRect.qml
+++ b/Modules/Settings/Common/ConnectedRect.qml
@@ -3,12 +3,64 @@ import qs.Components
import qs.Config
CustomRect {
+ id: root
+
property bool first
property bool last
+ property string settingAnchor
+
+ function flashHighlight(): void {
+ flash.restart();
+ }
bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
color: DynamicColors.tPalette.m3surfaceContainer
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
+
+ CustomRect {
+ id: highlight
+
+ anchors.fill: parent
+ bottomLeftRadius: parent.bottomLeftRadius
+ bottomRightRadius: parent.bottomRightRadius
+ color: DynamicColors.palette.m3primary
+ opacity: 0
+ radius: parent.radius
+ topLeftRadius: parent.topLeftRadius
+ topRightRadius: parent.topRightRadius
+
+ SequentialAnimation {
+ id: flash
+
+ Anim {
+ duration: Appearance.anim.durations.small
+ property: "opacity"
+ target: highlight
+ to: 0.2
+ }
+
+ Anim {
+ duration: Appearance.anim.durations.normal
+ property: "opacity"
+ target: highlight
+ to: 0.08
+ }
+
+ Anim {
+ duration: Appearance.anim.durations.small
+ property: "opacity"
+ target: highlight
+ to: 0.2
+ }
+
+ Anim {
+ duration: Appearance.anim.durations.extraLarge
+ property: "opacity"
+ target: highlight
+ to: 0
+ }
+ }
+ }
}
diff --git a/Modules/Settings/Common/PageBase.qml b/Modules/Settings/Common/PageBase.qml
index 60d9088..4bf164f 100644
--- a/Modules/Settings/Common/PageBase.qml
+++ b/Modules/Settings/Common/PageBase.qml
@@ -9,6 +9,7 @@ import qs.Config
ColumnLayout {
id: root
+ property bool animateScroll: false
readonly property int cappedWidth: Math.min(800, width)
default property Item contentChild
readonly property alias flickable: flickable
@@ -16,11 +17,100 @@ ColumnLayout {
required property SettingsState sState
required property string title
+ function applySearchAnchor(): void {
+ if (!sState.searchAnchor)
+ return;
+ scrollRetry.tries = 0;
+ scrollRetry.lastHeight = -1;
+ scrollRetry.stableFrames = 0;
+ scrollRetry.restart();
+ }
+
+ function findAnchor(item: Item, anchor: string): Item {
+ if (!item)
+ return null;
+ if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
+ return item;
+ const kids = item.children;
+ for (let i = 0; i < kids.length; i++) {
+ const found = findAnchor(kids[i], anchor);
+ if (found)
+ return found;
+ }
+ return null;
+ }
+
+ function highlightAnchor(anchor: string): void {
+ const row = findAnchor(contentChild, anchor);
+ if (row && row.flashHighlight !== undefined)
+ row.flashHighlight();
+ }
+
+ function scrollToAnchor(anchor: string): bool {
+ if (!anchor || !contentChild)
+ return false;
+ const row = findAnchor(contentChild, anchor);
+ if (!row)
+ return false;
+ const pos = row.mapToItem(flickable.contentItem, 0, 0);
+ const inset = flickable.height * flickable.fadeAmount + Appearance.padding.large;
+ const minY = -flickable.topMargin;
+ const maxY = Math.max(minY, flickable.contentHeight + flickable.bottomMargin - flickable.height);
+ const target = Math.max(minY, Math.min(pos.y - inset, maxY));
+ root.animateScroll = true;
+ flickable.contentY = target;
+ Qt.callLater(() => root.animateScroll = false);
+ if (row.flashHighlight !== undefined)
+ row.flashHighlight();
+ return true;
+ }
+
spacing: Appearance.spacing.large
- MouseArea { // Prevent clicks from reaching flickable
- Layout.bottomMargin: -flickable.topMargin // Extra height to block clicks on flickable top margin
+ Component.onCompleted: applySearchAnchor()
+ Timer {
+ id: scrollRetry
+
+ property real lastHeight: -1
+ property int stableFrames: 0
+ property int tries: 0
+
+ interval: 16
+ repeat: true
+
+ onTriggered: {
+ const h = flickable.contentHeight;
+ if (h === lastHeight && h > flickable.height)
+ stableFrames++;
+ else
+ stableFrames = 0;
+ lastHeight = h;
+
+ const ready = stableFrames >= 3 || tries >= 30;
+ if (ready) {
+ if (root.scrollToAnchor(root.sState.searchAnchor))
+ root.sState.searchAnchor = "";
+ stop();
+ }
+ tries++;
+ }
+ }
+
+ Connections {
+ function onHighlightSetting(anchor: string): void {
+ root.highlightAnchor(anchor);
+ }
+
+ function onSearchAnchorChanged(): void {
+ root.applySearchAnchor();
+ }
+
+ target: root.sState
+ }
+
+ MouseArea {
+ Layout.bottomMargin: -flickable.topMargin
implicitHeight: header.implicitHeight - Layout.bottomMargin
implicitWidth: header.implicitWidth
z: 1
@@ -69,6 +159,14 @@ ColumnLayout {
fadeAmount: 0.1
topMargin: Appearance.padding.large
+ Behavior on contentY {
+ enabled: root.animateScroll
+
+ Anim {
+ type: Anim.DefaultSpatial
+ }
+ }
+
TapHandler {
onTapped: flickable.focus = true
}
diff --git a/Modules/Settings/Common/ToggleRow.qml b/Modules/Settings/Common/ToggleRow.qml
index 25e4168..77dd3e8 100644
--- a/Modules/Settings/Common/ToggleRow.qml
+++ b/Modules/Settings/Common/ToggleRow.qml
@@ -9,8 +9,13 @@ CustomSwitch {
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
+ property string settingAnchor
property string subtext
+ function flashHighlight(): void {
+ bg.flashHighlight();
+ }
+
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Appearance.padding.largeIncreased
diff --git a/Modules/Settings/NavPane.qml b/Modules/Settings/NavPane.qml
index b87a45c..ab5ffa6 100644
--- a/Modules/Settings/NavPane.qml
+++ b/Modules/Settings/NavPane.qml
@@ -34,6 +34,12 @@ ColumnLayout {
target: root.sState
value: searchField.text.length > 0
}
+
+ Binding {
+ property: "searchText"
+ target: root.sState
+ value: searchField.text
+ }
}
NavLocations {
diff --git a/Modules/Settings/NavPane/NavLocations.qml b/Modules/Settings/NavPane/NavLocations.qml
index 678c82b..ec0e1fb 100644
--- a/Modules/Settings/NavPane/NavLocations.qml
+++ b/Modules/Settings/NavPane/NavLocations.qml
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
+import Quickshell
import qs.Components
import qs.Config
import qs.Modules.Settings
@@ -9,7 +10,33 @@ import qs.Modules.Settings
VerticalFadeFlickable {
id: root
+ readonly property var groups: {
+ const out = [];
+ const byPage = ({});
+ for (const e of results) {
+ const key = e.pageIdx;
+ if (byPage[key] === undefined) {
+ byPage[key] = {
+ "pageIdx": e.pageIdx,
+ "page": e.crumbLabels[0],
+ "icon": e.crumbIcons[0],
+ "entries": []
+ };
+ out.push(byPage[key]);
+ }
+ byPage[key].entries.push(e);
+ }
+ return out;
+ }
+ readonly property var results: {
+ if (!searching)
+ return [];
+ const all = SettingsSearcher.query(search);
+ return all;
+ }
required property SettingsState sState
+ readonly property string search: sState.searchText
+ readonly property bool searching: search.length > 0
bottomMargin: Appearance.padding.large
contentHeight: content.implicitHeight
@@ -30,14 +57,14 @@ VerticalFadeFlickable {
Repeater {
id: list
- model: PageRegistry.pages
+ model: root.searching ? [] : PageRegistry.pages
CustomRect {
id: item
required property int index
- readonly property bool isCategoryEnd: index === list.model.length - 1 || PageRegistry.pages[index + 1].category !== modelData.category
- readonly property bool isCategoryStart: index === 0 || PageRegistry.pages[index - 1].category !== modelData.category
+ readonly property bool isCategoryEnd: index === list.model.length - 1 || PageRegistry.pages[index + 1]?.category !== modelData.category
+ readonly property bool isCategoryStart: index === 0 || PageRegistry.pages[index - 1]?.category !== modelData.category
readonly property bool isCurrentPage: index === root.sState.currentPageIdx
required property var modelData
@@ -120,6 +147,166 @@ VerticalFadeFlickable {
}
}
}
+
+ ListView {
+ id: resultList
+
+ Layout.fillWidth: true
+ cacheBuffer: 10000
+ implicitHeight: contentHeight
+ interactive: false
+ spacing: Appearance.padding.large
+
+ delegate: ColumnLayout {
+ id: group
+
+ required property int index
+ required property var modelData
+
+ spacing: Appearance.spacing.small
+ width: resultList.width
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.leftMargin: Appearance.padding.small
+ spacing: Appearance.spacing.small
+
+ MaterialIcon {
+ color: DynamicColors.palette.m3primary
+ font.pointSize: Appearance.font.size.large
+ text: group.modelData.icon
+ }
+
+ CustomText {
+ Layout.fillWidth: true
+ color: DynamicColors.palette.m3primary
+ elide: Text.ElideRight
+ font.pointSize: Appearance.font.size.large
+ text: group.modelData.page
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.extraSmall / 2
+
+ Repeater {
+ model: group.modelData.entries
+
+ CustomRect {
+ id: result
+
+ required property int index
+ readonly property bool isFirst: index === 0
+ readonly property bool isLast: index === group.modelData.entries.length - 1
+ required property var modelData
+
+ Layout.fillWidth: true
+ bottomLeftRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
+ bottomRightRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
+ color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
+ implicitHeight: {
+ const h = resultLayout.implicitHeight + resultLayout.anchors.margins * 2;
+ return h % 2 === 0 ? h : h + 1;
+ }
+ topLeftRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
+ topRightRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
+
+ RadiusBehavior on bottomLeftRadius {
+ }
+ RadiusBehavior on bottomRightRadius {
+ }
+ RadiusBehavior on topLeftRadius {
+ }
+ RadiusBehavior on topRightRadius {
+ }
+
+ ColumnLayout {
+ id: resultLayout
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large
+ anchors.rightMargin: result.modelData.togglePath ? toggle.width + Appearance.padding.large * 2 : Appearance.padding.large
+ spacing: Appearance.spacing.small / 2
+
+ CustomText {
+ Layout.fillWidth: true
+ color: DynamicColors.palette.m3onSurfaceVariant
+ elide: Text.ElideRight
+ font.pointSize: Appearance.font.size.small
+ text: {
+ const labels = result.modelData.crumbLabels.slice(1);
+ const section = result.modelData.section;
+ const parts = section && section !== labels[labels.length - 1] ? labels.concat(section) : labels;
+ return parts.join(" \u203a ");
+ }
+ visible: text.length > 0
+ }
+
+ CustomText {
+ Layout.fillWidth: true
+ color: DynamicColors.palette.m3onSurface
+ elide: Text.ElideRight
+ font.pointSize: Appearance.font.size.medium
+ text: SettingsSearcher.highlight(result.modelData.title, root.search, DynamicColors.palette.m3primary)
+ textFormat: Text.StyledText
+ }
+
+ CustomText {
+ Layout.fillWidth: true
+ color: DynamicColors.palette.m3outline
+ elide: Text.ElideRight
+ font.pointSize: Appearance.font.size.small
+ text: SettingsSearcher.highlight(result.modelData.subtext, root.search, DynamicColors.palette.m3primary)
+ textFormat: Text.StyledText
+ visible: result.modelData.subtext.length > 0
+ }
+ }
+
+ StateLayer {
+ id: layer
+
+ z: 1
+
+ onClicked: {
+ root.sState.jumpToSetting(result.modelData.pageIdx, result.modelData.subPath, result.modelData.anchor);
+ }
+ }
+
+ CustomSwitch {
+ id: toggle
+
+ anchors.right: parent.right
+ anchors.rightMargin: Appearance.padding.large
+ anchors.verticalCenter: parent.verticalCenter
+ cLayer: 3
+ checked: result.modelData.toggleValue
+ scale: 0.85
+ transformOrigin: Item.Right
+ visible: result.modelData.togglePath
+ z: 2
+
+ onToggled: result.modelData.setToggle(checked)
+ }
+ }
+ }
+ }
+ }
+ model: ScriptModel {
+ objectProp: "pageIdx"
+ values: root.groups
+ }
+ }
+
+ CustomText {
+ Layout.fillWidth: true
+ Layout.topMargin: Appearance.padding.large
+ color: DynamicColors.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.medium
+ horizontalAlignment: Text.AlignHCenter
+ text: qsTr("No matching settings")
+ visible: root.searching && root.results.length === 0
+ }
}
component RadiusBehavior: Behavior {
diff --git a/Modules/Settings/Pages/AppsPage.qml b/Modules/Settings/Pages/AppsPage.qml
index cd53b40..49be244 100644
--- a/Modules/Settings/Pages/AppsPage.qml
+++ b/Modules/Settings/Pages/AppsPage.qml
@@ -30,6 +30,7 @@ PageBase {
DefaultRow {
first: true
icon: "terminal"
+ settingAnchor: "apps-default-terminal"
status: Config.general.apps.terminal.join(" ")
text: qsTr("Terminal")
@@ -38,6 +39,7 @@ PageBase {
DefaultRow {
icon: "volume_up"
+ settingAnchor: "apps-default-audio"
status: Config.general.apps.audio.join(" ")
text: qsTr("Audio")
@@ -46,6 +48,7 @@ PageBase {
DefaultRow {
icon: "play_circle"
+ settingAnchor: "apps-default-playback"
status: Config.general.apps.playback.join(" ")
text: qsTr("Media playback")
@@ -55,6 +58,7 @@ PageBase {
DefaultRow {
icon: "folder"
last: true
+ settingAnchor: "apps-default-file-manager"
status: Config.general.apps.explorer.join(" ")
text: qsTr("File manager")
@@ -70,6 +74,7 @@ PageBase {
first: true
icon: "apps"
last: true
+ settingAnchor: "apps-all-apps"
status: qsTr("Browse installed apps, set favorites and hidden")
text: qsTr("All apps")
diff --git a/Modules/Settings/Pages/AudioPage.qml b/Modules/Settings/Pages/AudioPage.qml
index f0e1882..050b23b 100644
--- a/Modules/Settings/Pages/AudioPage.qml
+++ b/Modules/Settings/Pages/AudioPage.qml
@@ -25,6 +25,7 @@ PageBase {
first: true
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
label: qsTr("Output")
+ settingAnchor: "audio-output"
value: Audio.volume
valueLabel: Math.round(value * 100) + "%"
@@ -55,6 +56,7 @@ PageBase {
first: true
icon: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
label: qsTr("Input")
+ settingAnchor: "audio-input"
value: Audio.sourceVolume
valueLabel: Math.round(value * 100) + "%"
diff --git a/Modules/Settings/Pages/Panels/Bar/BarClock.qml b/Modules/Settings/Pages/Panels/Bar/BarClock.qml
index 109cf36..6501dec 100644
--- a/Modules/Settings/Pages/Panels/Bar/BarClock.qml
+++ b/Modules/Settings/Pages/Panels/Bar/BarClock.qml
@@ -50,6 +50,7 @@ PageBase {
first: true
last: true
menuItems: root.clockFormats
+ settingAnchor: "bar-clock-format"
subtext: qsTr("Change how time is displayed in the widget")
text: qsTr("Time format")
diff --git a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
index 3c9561a..b2655d5 100644
--- a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
+++ b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
@@ -25,6 +25,7 @@ PageBase {
ToggleRow {
checked: Config.bar.tray.showAudio
first: true
+ settingAnchor: "bar-status-speakers"
text: qsTr("Speakers")
onToggled: Config.bar.tray.showAudio = checked
@@ -32,6 +33,7 @@ PageBase {
ToggleRow {
checked: Config.bar.tray.showMicrophone
+ settingAnchor: "bar-status-mic"
text: qsTr("Microphone")
onToggled: Config.bar.tray.showMicrophone = checked
@@ -62,6 +64,7 @@ PageBase {
ToggleRow {
checked: Config.bar.tray.showPower
last: true
+ settingAnchor: "bar-status-power"
text: qsTr("Battery")
onToggled: Config.bar.tray.showPower = checked
@@ -75,6 +78,7 @@ PageBase {
ToggleRow {
checked: Config.bar.popouts.audio
first: true
+ settingAnchor: "bar-status-audio-popout"
subtext: qsTr("Show a details popout when hovering the audio icons")
text: qsTr("Audio popout on hover")
@@ -84,6 +88,7 @@ PageBase {
ToggleRow {
checked: Config.bar.popouts.upower
last: true
+ settingAnchor: "bar-status-power-popout"
subtext: qsTr("Show a details popout when hovering the power icon")
text: qsTr("Power popout on hover")
diff --git a/Modules/Settings/Pages/Panels/Bar/BarTray.qml b/Modules/Settings/Pages/Panels/Bar/BarTray.qml
index 9f8d096..2fe962c 100644
--- a/Modules/Settings/Pages/Panels/Bar/BarTray.qml
+++ b/Modules/Settings/Pages/Panels/Bar/BarTray.qml
@@ -20,6 +20,7 @@ PageBase {
first: true
from: 12
last: true
+ settingAnchor: "bar-tray-iconsize"
stepSize: 1
text: qsTr("Icon size")
to: 24
diff --git a/Modules/Settings/Pages/Panels/BarPanel.qml b/Modules/Settings/Pages/Panels/BarPanel.qml
index 4bfc80d..9b344eb 100644
--- a/Modules/Settings/Pages/Panels/BarPanel.qml
+++ b/Modules/Settings/Pages/Panels/BarPanel.qml
@@ -26,6 +26,7 @@ PageBase {
checked: Config.bar.autoHide
first: true
last: true
+ settingAnchor: "bar-autohide"
subtext: qsTr("Hide the bar, reveal on hover")
text: qsTr("Auto hide")
@@ -40,6 +41,7 @@ PageBase {
NavRow {
first: true
icon: "widgets"
+ settingAnchor: "bar-tray"
status: qsTr("System tray icons")
text: qsTr("Tray")
@@ -48,6 +50,7 @@ PageBase {
NavRow {
icon: "signal_cellular_alt"
+ settingAnchor: "bar-statusicons"
status: qsTr("Visible indicators")
text: qsTr("Status icons")
@@ -57,6 +60,7 @@ PageBase {
NavRow {
icon: "schedule"
last: true
+ settingAnchor: "bar-clock"
status: qsTr("Date, icon, background")
text: qsTr("Clock")
diff --git a/Modules/Settings/Pages/Panels/DashboardPanel.qml b/Modules/Settings/Pages/Panels/DashboardPanel.qml
index a8e0cef..1aa3f93 100644
--- a/Modules/Settings/Pages/Panels/DashboardPanel.qml
+++ b/Modules/Settings/Pages/Panels/DashboardPanel.qml
@@ -28,6 +28,7 @@ PageBase {
checked: Config.dashboard.enabled
first: true
last: true
+ settingAnchor: "dashboard-enabled"
text: qsTr("Enabled")
onToggled: Config.dashboard.enabled = checked
diff --git a/Modules/Settings/Pages/Panels/LauncherPanel.qml b/Modules/Settings/Pages/Panels/LauncherPanel.qml
index 630896e..8adcd25 100644
--- a/Modules/Settings/Pages/Panels/LauncherPanel.qml
+++ b/Modules/Settings/Pages/Panels/LauncherPanel.qml
@@ -28,6 +28,7 @@ PageBase {
checked: Config.launcher.enabled
first: true
last: true
+ settingAnchor: "launcher-enabled"
text: qsTr("Enabled")
onToggled: Config.launcher.enabled = checked
@@ -41,6 +42,7 @@ PageBase {
SpinRow {
first: true
from: 1
+ settingAnchor: "launcher-max-items-shown"
stepSize: 1
text: qsTr("Max items shown")
to: 20
@@ -51,6 +53,7 @@ PageBase {
SpinRow {
from: 1
+ settingAnchor: "launcher-max-wallpapers"
stepSize: 1
text: qsTr("Max wallpapers")
to: 30
@@ -62,6 +65,7 @@ PageBase {
SpinRow {
from: 0
last: true
+ settingAnchor: "launcher-drag-threshold"
stepSize: 5
subtext: qsTr("Pixels dragged before the launcher opens")
text: qsTr("Drag threshold")
@@ -80,6 +84,7 @@ PageBase {
checked: Config.launcher.enableDangerousActions
first: true
last: true
+ settingAnchor: "launcher-enable-dangerous-actions"
subtext: qsTr("Allow actions that shut down or log out")
text: qsTr("Enable dangerous actions")
@@ -94,6 +99,7 @@ PageBase {
ToggleRow {
checked: Config.launcher.useFuzzy.apps
first: true
+ settingAnchor: "launcher-apps"
text: qsTr("Apps")
onToggled: Config.launcher.useFuzzy.apps = checked
@@ -101,20 +107,15 @@ PageBase {
ToggleRow {
checked: Config.launcher.useFuzzy.actions
+ settingAnchor: "launcher-actions"
text: qsTr("Actions")
onToggled: Config.launcher.useFuzzy.actions = checked
}
- ToggleRow {
- checked: Config.launcher.useFuzzy.schemes
- text: qsTr("Schemes")
-
- onToggled: Config.launcher.useFuzzy.schemes = checked
- }
-
ToggleRow {
checked: Config.launcher.useFuzzy.variants
+ settingAnchor: "launcher-variants"
text: qsTr("Variants")
onToggled: Config.launcher.useFuzzy.variants = checked
@@ -123,6 +124,7 @@ PageBase {
ToggleRow {
checked: Config.launcher.useFuzzy.wallpapers
last: true
+ settingAnchor: "launcher-wallpapers"
text: qsTr("Wallpapers")
onToggled: Config.launcher.useFuzzy.wallpapers = checked
diff --git a/Modules/Settings/Pages/Panels/ResourcesPanel.qml b/Modules/Settings/Pages/Panels/ResourcesPanel.qml
index 3c5cde4..11e9f5b 100644
--- a/Modules/Settings/Pages/Panels/ResourcesPanel.qml
+++ b/Modules/Settings/Pages/Panels/ResourcesPanel.qml
@@ -28,6 +28,7 @@ PageBase {
checked: Config.dashboard.performance.enabled
first: true
last: true
+ settingAnchor: "resources-enabled"
text: qsTr("Enabled")
onToggled: Config.dashboard.performance.enabled = checked
@@ -41,6 +42,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showBattery
first: true
+ settingAnchor: "resources-battery"
text: qsTr("Battery")
onToggled: Config.dashboard.performance.showBattery = checked
@@ -48,6 +50,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showGpu
+ settingAnchor: "resources-gpu"
text: qsTr("GPU")
onToggled: Config.dashboard.performance.showGpu = checked
@@ -55,6 +58,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showCpu
+ settingAnchor: "resources-cpu"
text: qsTr("CPU")
onToggled: Config.dashboard.performance.showCpu = checked
@@ -62,6 +66,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showMemory
+ settingAnchor: "resources-memory"
text: qsTr("Memory")
onToggled: Config.dashboard.performance.showMemory = checked
@@ -69,6 +74,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showStorage
+ settingAnchor: "resources-storage"
text: qsTr("Storage")
onToggled: Config.dashboard.performance.showStorage = checked
@@ -77,6 +83,7 @@ PageBase {
ToggleRow {
checked: Config.dashboard.performance.showNetwork
last: true
+ settingAnchor: "resources-network"
text: qsTr("Network")
onToggled: Config.dashboard.performance.showNetwork = checked
diff --git a/Modules/Settings/Pages/Panels/SidebarPanel.qml b/Modules/Settings/Pages/Panels/SidebarPanel.qml
index e1f02bb..cfa6795 100644
--- a/Modules/Settings/Pages/Panels/SidebarPanel.qml
+++ b/Modules/Settings/Pages/Panels/SidebarPanel.qml
@@ -26,6 +26,7 @@ PageBase {
ToggleRow {
checked: Config.sidebar.enabled
first: true
+ settingAnchor: "sidebar-enabled"
text: qsTr("Enabled")
onToggled: Config.sidebar.enabled = checked
@@ -34,6 +35,7 @@ PageBase {
SpinRow {
from: 0
last: true
+ settingAnchor: "sidebar-drag-threshold"
stepSize: 5
subtext: qsTr("Pixels dragged before the sidebar opens")
text: qsTr("Drag threshold")
diff --git a/Modules/Settings/Pages/PanelsPage.qml b/Modules/Settings/Pages/PanelsPage.qml
index 2200b96..30973d8 100644
--- a/Modules/Settings/Pages/PanelsPage.qml
+++ b/Modules/Settings/Pages/PanelsPage.qml
@@ -16,6 +16,7 @@ PageBase {
NavRow {
first: true
icon: "dock_to_bottom"
+ settingAnchor: "panels-bar"
status: !Config.bar.autoHide ? qsTr("Always visible") : qsTr("Reveal on hover")
text: qsTr("Bar")
@@ -24,6 +25,7 @@ PageBase {
NavRow {
icon: "dashboard"
+ settingAnchor: "panels-dashboard"
status: Config.dashboard.enabled ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Dashboard")
@@ -32,6 +34,7 @@ PageBase {
NavRow {
icon: "insert_chart"
+ settingAnchor: "panels-resources"
status: Config.dashboard.performance.enabled ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Resources")
@@ -40,6 +43,7 @@ PageBase {
NavRow {
icon: "apps"
+ settingAnchor: "panels-launcher"
status: Config.launcher.enabled ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Launcher")
@@ -49,6 +53,7 @@ PageBase {
NavRow {
icon: "dock_to_right"
last: true
+ settingAnchor: "panels-sidebar"
status: Config.sidebar.enabled ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Sidebar")
diff --git a/Modules/Settings/Pages/ScreenshotPage.qml b/Modules/Settings/Pages/ScreenshotPage.qml
index 83b75a1..a97d47c 100644
--- a/Modules/Settings/Pages/ScreenshotPage.qml
+++ b/Modules/Settings/Pages/ScreenshotPage.qml
@@ -28,6 +28,7 @@ PageBase {
ToggleRow {
checked: Config.screenshot.enable_pp
first: true
+ settingAnchor: "screenshot-enable"
text: qsTr("Enable effects")
onToggled: Config.screenshot.enable_pp = checked
@@ -37,6 +38,7 @@ PageBase {
active: Config.screenshot.mode === "manual" ? menuItems[0] : menuItems[1]
enabled: Config.screenshot.enable_pp
last: true
+ settingAnchor: "screenshot-mode"
subtext: qsTr("Automatic or manual effect values")
text: qsTr("Effects mode")
@@ -67,6 +69,7 @@ PageBase {
checked: Config.screenshot.rounding
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual"
first: true
+ settingAnchor: "screenshot-enable-rounded-corners"
text: qsTr("Enable rounded corners")
onToggled: Config.screenshot.rounding = checked
@@ -76,6 +79,7 @@ PageBase {
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.rounding
from: 0
last: true
+ settingAnchor: "screenshot-corner-radius"
stepSize: 1
text: qsTr("Corner radius")
to: 50
@@ -95,6 +99,7 @@ PageBase {
checked: Config.screenshot.shadow
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual"
first: true
+ settingAnchor: "screenshot-enable-shadow"
text: qsTr("Enable shadow")
onToggled: Config.screenshot.shadow = checked
@@ -103,6 +108,7 @@ PageBase {
SpinRow {
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow
from: 0
+ settingAnchor: "screenshot-shadow-blur-amount"
stepSize: 1
text: qsTr("Shadow blur amount")
to: 100
@@ -117,6 +123,7 @@ PageBase {
SpinRow {
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow
from: -100
+ settingAnchor: "screenshot-shadow-horizontal-offset"
stepSize: 10
text: qsTr("Shadow horizontal offset")
to: 100
@@ -132,6 +139,7 @@ PageBase {
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow
from: -100
last: true
+ settingAnchor: "screenshot-shadow-vertical-offset"
stepSize: 10
text: qsTr("Shadow vertical offset")
to: 100
diff --git a/Modules/Settings/Pages/Services/NotificationsPage.qml b/Modules/Settings/Pages/Services/NotificationsPage.qml
index 2b0a90c..e1e04d0 100644
--- a/Modules/Settings/Pages/Services/NotificationsPage.qml
+++ b/Modules/Settings/Pages/Services/NotificationsPage.qml
@@ -25,6 +25,7 @@ PageBase {
ToggleRow {
checked: Config.notifs.showInFullscreen
first: true
+ settingAnchor: "notif-show-in-fullscreen"
subtext: qsTr("Whether notifications appear over fullscreen apps")
text: qsTr("Show in fullscreen")
@@ -33,6 +34,7 @@ PageBase {
ToggleRow {
checked: Config.notifs.expire
+ settingAnchor: "notif-expire-automatically"
subtext: qsTr("Dismiss notifications after their timeout")
text: qsTr("Expire automatically")
@@ -41,6 +43,7 @@ PageBase {
ToggleRow {
checked: Config.notifs.openExpanded
+ settingAnchor: "notif-open-expanded"
subtext: qsTr("Show notifications expanded by default")
text: qsTr("Open expanded")
@@ -49,6 +52,7 @@ PageBase {
SpinRow {
from: 1000
+ settingAnchor: "notif-default-timeout"
stepSize: 500
subtext: qsTr("Time before a notification dismisses (ms)")
text: qsTr("Default timeout")
@@ -61,6 +65,7 @@ PageBase {
SpinRow {
from: 1
last: true
+ settingAnchor: "notif-group-preview-count"
stepSize: 1
subtext: qsTr("Notifications shown per group before collapsing")
text: qsTr("Group preview count")
@@ -78,6 +83,7 @@ PageBase {
SpinRow {
first: true
from: 1
+ settingAnchor: "notif-visible-toasts"
stepSize: 1
subtext: qsTr("Maximum number of toasts shown at once")
text: qsTr("Visible toasts")
@@ -89,6 +95,7 @@ PageBase {
ToggleRow {
checked: Config.utilities.toasts.chargingChanged
+ settingAnchor: "notif-charging-changes"
text: qsTr("Charging changes")
onToggled: Config.utilities.toasts.chargingChanged = checked
@@ -96,6 +103,7 @@ PageBase {
ToggleRow {
checked: Config.utilities.toasts.gameModeChanged
+ settingAnchor: "notif-game-mode-changes"
text: qsTr("Game mode changes")
onToggled: Config.utilities.toasts.gameModeChanged = checked
@@ -110,6 +118,7 @@ PageBase {
ToggleRow {
checked: Config.utilities.toasts.audioOutputChanged
+ settingAnchor: "notif-audio-output-changes"
text: qsTr("Audio output changes")
onToggled: Config.utilities.toasts.audioOutputChanged = checked
@@ -118,6 +127,7 @@ PageBase {
ToggleRow {
checked: Config.utilities.toasts.audioInputChanged
last: true
+ settingAnchor: "notif-audio-input-changes"
text: qsTr("Audio input changes")
onToggled: Config.utilities.toasts.audioInputChanged = checked
diff --git a/Modules/Settings/Pages/ServicesPage.qml b/Modules/Settings/Pages/ServicesPage.qml
index 7241194..8751219 100644
--- a/Modules/Settings/Pages/ServicesPage.qml
+++ b/Modules/Settings/Pages/ServicesPage.qml
@@ -71,6 +71,7 @@ PageBase {
first: true
icon: "notifications"
last: true
+ settingAnchor: "services-notifications"
status: qsTr("Notifications, toasts, timeouts")
text: qsTr("Notifications")
@@ -85,6 +86,7 @@ PageBase {
SpinRow {
first: true
from: 100
+ settingAnchor: "services-media-refresh"
stepSize: 50
subtext: qsTr("How often the media position updates (ms)")
text: qsTr("Media refresh")
@@ -97,6 +99,7 @@ PageBase {
SpinRow {
from: 0.5
last: true
+ settingAnchor: "services-system-stats-refresh"
stepSize: 0.5
subtext: qsTr("CPU, memory and GPU update interval (seconds)")
text: qsTr("System stats refresh")
@@ -118,6 +121,7 @@ PageBase {
first: true
last: true
menuItems: playerVariants.instances
+ settingAnchor: "services-default-player"
subtext: qsTr("Preferred media player when several are open")
text: qsTr("Default player")
@@ -132,6 +136,7 @@ PageBase {
SpinRow {
first: true
from: 1
+ settingAnchor: "services-volume-step"
stepSize: 1
subtext: qsTr("Amount the volume changes per input (%)")
text: qsTr("Volume step")
@@ -143,6 +148,7 @@ PageBase {
SpinRow {
from: 1
+ settingAnchor: "services-brightness-step"
stepSize: 1
subtext: qsTr("Amount the brightness changes per input (%)")
text: qsTr("Brightness step")
@@ -154,6 +160,7 @@ PageBase {
SpinRow {
from: 1
+ settingAnchor: "services-brightness-minimum"
stepSize: 1
subtext: qsTr("Lowest allowed brightness (%)")
text: qsTr("Brightness minimum")
@@ -166,6 +173,7 @@ PageBase {
SpinRow {
from: 50
last: true
+ settingAnchor: "services-max-volume"
stepSize: 5
subtext: qsTr("Upper limit for output volume (%)")
text: qsTr("Max volume")
@@ -183,6 +191,7 @@ PageBase {
SpinRow {
first: true
from: 10
+ settingAnchor: "services-visualizer-resolution"
stepSize: 2
subtext: qsTr("Resolution of the audio visualizer")
text: qsTr("Visualizer resolution")
@@ -194,6 +203,7 @@ PageBase {
ToggleRow {
checked: Config.general.color.smart
+ settingAnchor: "services-smart-color-scheme"
subtext: qsTr("Derive theme mode from the wallpaper")
text: qsTr("Smart color scheme")
@@ -205,6 +215,7 @@ PageBase {
last: true
menuItems: root.gpuItems
menuOnTop: true
+ settingAnchor: "services-gpu"
subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type")
text: qsTr("GPU")
diff --git a/Modules/Settings/Pages/WallpaperPage.qml b/Modules/Settings/Pages/WallpaperPage.qml
index a07c68d..81abe9f 100644
--- a/Modules/Settings/Pages/WallpaperPage.qml
+++ b/Modules/Settings/Pages/WallpaperPage.qml
@@ -49,6 +49,7 @@ PageBase {
ToggleRow {
checked: Config.background.enabled
first: true
+ settingAnchor: "style-display-wallpaper"
text: qsTr("Display wallpaper")
onToggled: Config.background.enabled = checked
@@ -57,6 +58,7 @@ PageBase {
ToggleRow {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
checked: DynamicColors.transparency.enabled
+ settingAnchor: "style-transparency"
subtext: qsTr("Base %1, layers %2").arg(DynamicColors.transparency.base).arg(DynamicColors.transparency.layers)
text: qsTr("Transparency")
@@ -67,6 +69,7 @@ PageBase {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
checked: !DynamicColors.light
last: true
+ settingAnchor: "style-dark-theme"
text: qsTr("Dark theme")
onToggled: DynamicColors.setMode(checked ? "dark" : "light")
@@ -88,6 +91,7 @@ PageBase {
checked: Config.general.color.scheduleDark
first: true
+ settingAnchor: "style-schedule-dark-mode"
subtext: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
text: qsTr("Schedule dark mode")
@@ -129,6 +133,7 @@ PageBase {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
checked: Config.general.color.scheduleHyprsunset
last: true
+ settingAnchor: "style-schedule-hyprsunset"
subtext: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
text: qsTr("Schedule hyprsunset")
diff --git a/Modules/Settings/SettingsSearcher.qml b/Modules/Settings/SettingsSearcher.qml
new file mode 100644
index 0000000..eb61042
--- /dev/null
+++ b/Modules/Settings/SettingsSearcher.qml
@@ -0,0 +1,155 @@
+pragma Singleton
+
+import "../../scripts/fzf.js" as Fzf
+import QtQuick
+import Quickshell
+import ZShell
+import qs.Config
+
+Singleton {
+ id: root
+
+ property var fzfFinder: null
+ property var inverted: ({})
+ property var ranking: ({})
+
+ function highlight(text: string, search: string, colour: color): string {
+ const escaped = text.replace(/&/g, "&").replace(//g, ">");
+ const tokens = tokenize(search);
+ if (tokens.length === 0)
+ return escaped;
+ const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
+ const pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
+ return escaped.replace(pattern, `$1`);
+ }
+
+ function lookup(token: string): var {
+ const result = ({});
+ const exact = root.inverted[token] !== undefined;
+ const keys = exact ? [token] : Object.keys(root.inverted).filter(k => k.startsWith(token));
+ for (const key of keys) {
+ const rank = root.ranking[key] ?? ({});
+ for (const id of root.inverted[key]) {
+ const w = rank[id] ?? 0.1;
+ if (result[id] === undefined || w > result[id])
+ result[id] = w;
+ }
+ }
+
+ return result;
+ }
+
+ function query(search: string): list {
+ const tokens = root.tokenize(search);
+ if (tokens.length === 0)
+ return [];
+
+ const scores = ({});
+ const hitCounts = ({});
+ for (const token of tokens) {
+ const matches = root.lookup(token);
+ for (const id in matches) {
+ scores[id] = (scores[id] ?? 0) + matches[id];
+ hitCounts[id] = (hitCounts[id] ?? 0) + 1;
+ }
+ }
+
+ const ranked = Object.keys(scores).filter(id => hitCounts[id] === tokens.length).sort((a, b) => scores[b] - scores[a] || (parseInt(a) - parseInt(b))).slice(0, 25);
+
+ const all = entries.instances;
+ const out = ranked.map(id => all[parseInt(id)]).filter(e => e !== undefined);
+
+ if (out.length < 5 && root.fzfFinder) {
+ const seen = ({});
+ for (const id of ranked)
+ seen[id] = true;
+ const fuzzy = root.fzfFinder.find(search);
+ for (const r of fuzzy) {
+ const idx = r.item.idx;
+ if (seen[idx])
+ continue;
+ seen[idx] = true;
+ const entry = all[idx];
+ if (entry !== undefined)
+ out.push(entry);
+ if (out.length >= 25)
+ break;
+ }
+ }
+
+ return out;
+ }
+
+ function tokenize(text: string): var {
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
+ }
+
+ Component.onCompleted: {
+ try {
+ const data = JSON.parse(ZUtils.settingsIndex());
+ entries.model = data.entries;
+ root.inverted = data.inverted ?? {};
+ root.ranking = data.ranking ?? {};
+ const docs = data.entries.map((e, i) => ({
+ idx: i,
+ text: e.title
+ }));
+ root.fzfFinder = new Fzf.Finder(docs, {
+ selector: d => d.text,
+ limit: 25
+ });
+ } catch (e) {
+ entries.model = [];
+ root.inverted = {};
+ root.ranking = {};
+ root.fzfFinder = null;
+ }
+ }
+
+ Variants {
+ id: entries
+
+ SettingEntry {
+ }
+ }
+
+ component SettingEntry: QtObject {
+ readonly property string anchor: modelData.anchor ?? ""
+ readonly property var crumbIcons: modelData.crumbIcons
+ readonly property var crumbLabels: modelData.crumbLabels
+ readonly property bool isToggle: togglePath.length > 0
+ required property var modelData
+ readonly property int pageIdx: modelData.pageIdx
+ readonly property string section: modelData.section ?? ""
+ readonly property var subPath: modelData.subPath
+ readonly property string subtext: modelData.subtext ?? ""
+ readonly property string title: modelData.title
+ readonly property string togglePath: modelData.togglePath ?? ""
+ readonly property bool toggleValue: {
+ if (!isToggle)
+ return false;
+ let obj = Config;
+ const parts = togglePath.split(".");
+ for (const part of parts) {
+ if (obj === undefined || obj === null)
+ return false;
+ obj = obj[part];
+ }
+ return obj ?? false;
+ }
+
+ function setToggle(value: bool): void {
+ if (!isToggle)
+ return;
+ const parts = togglePath.split(".");
+ let obj = Config;
+ for (let k = 0; k < parts.length - 1; k++) {
+ if (obj === undefined || obj === null)
+ return;
+ obj = obj[parts[k]];
+ }
+ if (obj !== undefined && obj !== null)
+ obj[parts[parts.length - 1]] = value;
+ }
+ }
+}
diff --git a/Modules/Settings/SettingsState.qml b/Modules/Settings/SettingsState.qml
index 06a753d..79e80d8 100644
--- a/Modules/Settings/SettingsState.qml
+++ b/Modules/Settings/SettingsState.qml
@@ -8,14 +8,19 @@ QtObject {
property bool animatingContainer
property int currentPageIdx
property bool isWindow
+ property string lastAnchor
+ property list pendingSubPath
property ShellScreen screen
+ property string searchAnchor
property bool searchOpen
+ property string searchText
property DesktopEntry selectedApp
property BluetoothDevice selectedBtDevice
property string selectedWallpaperCategory
property list subPageIdxStack
signal close
+ signal highlightSetting(anchor: string)
signal subPageClosed
signal subPageOpened(idx: int)
@@ -24,10 +29,38 @@ QtObject {
subPageIdxStack.pop();
}
+ function jumpToSetting(pageIdx: int, subPath: var, anchor: string): void {
+ const samePage = currentPageIdx === pageIdx;
+ const sameSub = subPageIdxStack.length === subPath.length && subPath.every((v, i) => subPageIdxStack[i] === v);
+ if (samePage && sameSub && anchor === lastAnchor) {
+ highlightSetting(anchor);
+ return;
+ }
+ lastAnchor = anchor;
+ if (samePage && sameSub) {
+ searchAnchor = "";
+ searchAnchor = anchor;
+ return;
+ }
+ searchAnchor = anchor;
+ if (!samePage) {
+ pendingSubPath = subPath.slice();
+ currentPageIdx = pageIdx;
+ } else {
+ while (subPageIdxStack.length > 0)
+ closeSubPage();
+ for (let i = 0; i < subPath.length; i++)
+ openSubPage(subPath[i]);
+ }
+ }
+
function openSubPage(idx: int): void {
subPageIdxStack.push(idx);
subPageOpened(idx);
}
- onCurrentPageIdxChanged: subPageIdxStack.length = 0
+ onCurrentPageIdxChanged: {
+ subPageIdxStack = pendingSubPath;
+ pendingSubPath = [];
+ }
}
diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt
index edf225f..ce7cdd6 100644
--- a/Plugins/ZShell/CMakeLists.txt
+++ b/Plugins/ZShell/CMakeLists.txt
@@ -24,11 +24,12 @@ set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
qt_standard_project_setup(REQUIRES 6.9)
function(qml_module arg_TARGET)
- cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES;RESOURCES")
qt_add_qml_module(${arg_TARGET}
URI ${arg_URI}
VERSION 1.0
SOURCES ${arg_SOURCES}
+ RESOURCES ${arg_RESOURCES}
)
qt_query_qml_module(${arg_TARGET}
@@ -47,6 +48,8 @@ function(qml_module arg_TARGET)
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
endfunction()
+set_source_files_properties("${SETTINGS_INDEX_JSON}" PROPERTIES QT_RESOURCE_ALIAS "settings-index.json")
+
qml_module(ZShell
URI ZShell
SOURCES
@@ -57,6 +60,8 @@ qml_module(ZShell
toaster.hpp toaster.cpp
qalculator.hpp qalculator.cpp
zutils.hpp zutils.cpp
+ RESOURCES
+ "${SETTINGS_INDEX_JSON}"
LIBRARIES
Qt::Gui
Qt::Quick
diff --git a/Plugins/ZShell/zutils.cpp b/Plugins/ZShell/zutils.cpp
index 8265485..4aef1fe 100644
--- a/Plugins/ZShell/zutils.cpp
+++ b/Plugins/ZShell/zutils.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg)
@@ -170,6 +171,15 @@ qreal ZUtils::clamp(qreal value, qreal min, qreal max) {
return qBound(min, value, max);
}
+QString ZUtils::settingsIndex() {
+ QFile file(QStringLiteral(":/qt/qml/ZShell/settings-index.json"));
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCWarning(lcZUtils) << "Failed to open embedded settings index";
+ return QString();
+ }
+ return QString::fromUtf8(file.readAll());
+}
+
#ifndef ZSHELL_VERSION
#define ZSHELL_VERSION ""
#endif
diff --git a/Plugins/ZShell/zutils.hpp b/Plugins/ZShell/zutils.hpp
index 1e037cf..1e9b3f8 100644
--- a/Plugins/ZShell/zutils.hpp
+++ b/Plugins/ZShell/zutils.hpp
@@ -32,6 +32,8 @@ class ZUtils : public QObject {
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
+ Q_INVOKABLE static QString settingsIndex();
+
[[nodiscard]] QString version() const;
[[nodiscard]] QString qtVersion() const;
};
diff --git a/scripts/SettingsIndex.mjs b/scripts/SettingsIndex.mjs
deleted file mode 100644
index 319853d..0000000
--- a/scripts/SettingsIndex.mjs
+++ /dev/null
@@ -1,1312 +0,0 @@
-// Settings search index
-// Each entry contains: name, category, categoryName, section, keywords
-// - name: The display name of the setting (must match exactly)
-// - category: The category key used for navigation
-// - categoryName: Human-readable category name for display
-// - section: The section header within the category
-// - keywords: Additional search terms for better discoverability
-
-export const settingsIndex = [
- // GENERAL CATEGORY
- // General section
- {
- name: "Logo",
- category: "general",
- categoryName: "General",
- section: "General",
- keywords: ["branding", "icon", "image"],
- },
- {
- name: "Wallpaper path",
- category: "general",
- categoryName: "General",
- section: "General",
- keywords: ["background", "image", "path"],
- },
- {
- name: "Desktop icons",
- category: "general",
- categoryName: "General",
- section: "General",
- keywords: ["icons", "desktop", "show"],
- },
- {
- name: "Date format",
- category: "general",
- categoryName: "General",
- section: "General",
- keywords: ["date", "time", "format"],
- },
- // Color section
- {
- name: "Scheme mode",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: ["dark mode", "light mode", "theme", "color scheme"],
- },
- {
- name: "Scheme type",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: [
- "vibrant",
- "expressive",
- "monochrome",
- "neutral",
- "tonal",
- "fidelity",
- "content",
- "rainbow",
- "fruit salad",
- ],
- },
- {
- name: "Automatic color scheme",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: ["auto", "wallpaper", "generate", "color"],
- },
- {
- name: "Smart color scheme",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: ["intelligent", "adaptive", "color"],
- },
- {
- name: "Schedule dark mode",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: ["dark mode", "night", "time", "schedule", "automatic"],
- },
- {
- name: "Schedule Hyprsunset",
- category: "general",
- categoryName: "General",
- section: "Color",
- keywords: [
- "night light",
- "blue light",
- "temperature",
- "warm",
- "schedule",
- ],
- },
- // Default Apps section
- {
- name: "Terminal",
- category: "general",
- categoryName: "General",
- section: "Default Apps",
- keywords: ["console", "command", "shell", "default app"],
- },
- {
- name: "Audio",
- category: "general",
- categoryName: "General",
- section: "Default Apps",
- keywords: ["sound", "volume", "mixer", "default app"],
- },
- {
- name: "Playback",
- category: "general",
- categoryName: "General",
- section: "Default Apps",
- keywords: ["media", "player", "music", "video", "default app"],
- },
- {
- name: "Explorer",
- category: "general",
- categoryName: "General",
- section: "Default Apps",
- keywords: ["file manager", "files", "browser", "default app"],
- },
-
- // WALLPAPER CATEGORY
- {
- name: "Enable wallpaper rendering",
- category: "wallpaper",
- categoryName: "Wallpaper",
- section: "Wallpaper",
- keywords: ["background", "enable", "show"],
- },
- {
- name: "Fade duration",
- category: "wallpaper",
- categoryName: "Wallpaper",
- section: "Wallpaper",
- keywords: ["transition", "animation", "crossfade"],
- },
-
- // BAR CATEGORY
- // Bar section
- {
- name: "Auto hide",
- category: "bar",
- categoryName: "Bar",
- section: "Bar",
- keywords: ["hide", "visibility", "autohide", "panel"],
- },
- {
- name: "Height",
- category: "bar",
- categoryName: "Bar",
- section: "Bar",
- keywords: ["size", "panel", "thickness"],
- },
- {
- name: "Rounding",
- category: "bar",
- categoryName: "Bar",
- section: "Bar",
- keywords: ["corners", "radius", "rounded"],
- },
- {
- name: "Border",
- category: "bar",
- categoryName: "Bar",
- section: "Bar",
- keywords: ["outline", "stroke", "edge"],
- },
-
- {
- name: "Smoothing",
- category: "bar",
- categoryName: "Bar",
- section: "Bar",
- keywords: ["smoothing", "rounding"],
- },
- // System tray section
- {
- name: "Tray icon size",
- category: "bar",
- categoryName: "Bar",
- section: "Tray",
- keywords: ["tray", "icon", "size"],
- },
- // Popouts section
- {
- name: "Tray",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["system tray", "icons", "popout"],
- },
- {
- name: "Audio",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["sound", "volume", "popout"],
- },
- {
- name: "Active window",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["window title", "current", "popout"],
- },
- {
- name: "Resources",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["cpu", "memory", "usage", "popout"],
- },
- {
- name: "Clock",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["time", "date", "popout"],
- },
- {
- name: "Network",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["wifi", "internet", "connection", "popout"],
- },
- {
- name: "Power",
- category: "bar",
- categoryName: "Bar",
- section: "Popouts",
- keywords: ["battery", "upower", "charging", "popout"],
- },
- // Entries section
- {
- name: "Bar entries",
- category: "bar",
- categoryName: "Bar",
- section: "Entries",
- keywords: ["modules", "widgets", "items", "order"],
- },
- // Dock section
- {
- name: "Enable dock",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["taskbar", "show", "visibility"],
- },
- {
- name: "Dock height",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["size", "taskbar", "thickness"],
- },
- {
- name: "Hover to reveal",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["autohide", "mouse", "show"],
- },
- {
- name: "Pin on startup",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["always visible", "pinned", "show"],
- },
- {
- name: "Pinned apps",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["favorites", "applications", "shortcuts"],
- },
- {
- name: "Ignored app regexes",
- category: "bar",
- categoryName: "Bar",
- section: "Dock",
- keywords: ["filter", "exclude", "hide", "regex"],
- },
-
- // LOCKSCREEN CATEGORY
- {
- name: "Recolor logo",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["logo", "color", "tint"],
- },
- {
- name: "Enable fingerprint",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["fprint", "biometric", "unlock"],
- },
- {
- name: "Max fingerprint tries",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["attempts", "limit", "fprint"],
- },
- {
- name: "Show notification details",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["notification", "hide", "privacy"],
- },
- {
- name: "Show notification icon",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["notification", "hide", "icon"],
- },
- {
- name: "Blur amount",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["background", "blur", "effect"],
- },
- {
- name: "Height multiplier",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["size", "scale", "height"],
- },
- {
- name: "Aspect ratio",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["ratio", "width", "height"],
- },
- {
- name: "Center width",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Lockscreen",
- keywords: ["size", "width", "center"],
- },
- {
- name: "Idle Monitors",
- category: "lockscreen",
- categoryName: "Lockscreen",
- section: "Idle",
- keywords: ["timeout", "sleep", "idle", "lock", "suspend"],
- },
-
- // SERVICES CATEGORY
- // Services section
- {
- name: "Weather location",
- category: "services",
- categoryName: "Services",
- section: "Services",
- keywords: ["city", "location", "weather"],
- },
- {
- name: "Use Fahrenheit",
- category: "services",
- categoryName: "Services",
- section: "Services",
- keywords: ["temperature", "celsius", "units"],
- },
- {
- name: "Use twelve hour clock",
- category: "services",
- categoryName: "Services",
- section: "Services",
- keywords: ["time", "format", "12h", "24h", "am pm"],
- },
- {
- name: "Enable ddcutil service",
- category: "services",
- categoryName: "Services",
- section: "Services",
- keywords: ["monitor", "brightness", "ddc"],
- },
- {
- name: "GPU type",
- category: "services",
- categoryName: "Services",
- section: "Services",
- keywords: ["graphics", "nvidia", "amd", "intel"],
- },
- // Media section
- {
- name: "Audio increment",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["volume", "step", "increment"],
- },
- {
- name: "Brightness increment",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["screen", "step", "increment"],
- },
- {
- name: "Minimum brightness",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["brightness", "minimum", "screen"],
- },
- {
- name: "Max volume",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["audio", "limit", "maximum"],
- },
- {
- name: "Default player",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["music", "media", "mpris"],
- },
- {
- name: "Visualizer bars",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["audio", "visualization", "cava"],
- },
- {
- name: "Player aliases",
- category: "services",
- categoryName: "Services",
- section: "Media",
- keywords: ["mpris", "rename", "alias"],
- },
-
- // NOTIFICATIONS CATEGORY
- // Notifications section
- {
- name: "Expire notifications",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["auto dismiss", "timeout", "expire"],
- },
- {
- name: "Default expire timeout",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["duration", "time", "dismiss"],
- },
- {
- name: "App notification cooldown",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["rate limit", "spam", "cooldown"],
- },
- {
- name: "Clear threshold",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["swipe", "dismiss", "threshold"],
- },
- {
- name: "Expand threshold",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["swipe", "expand", "threshold"],
- },
- {
- name: "Action on click",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["click", "action", "default"],
- },
- {
- name: "Group preview count",
- category: "notifications",
- categoryName: "Notifications",
- section: "Notifications",
- keywords: ["group", "stack", "preview"],
- },
- // Sizes section
- {
- name: "Width",
- category: "notifications",
- categoryName: "Notifications",
- section: "Sizes",
- keywords: ["notification", "size", "width"],
- },
- {
- name: "Image size",
- category: "notifications",
- categoryName: "Notifications",
- section: "Sizes",
- keywords: ["notification", "image", "icon"],
- },
- {
- name: "Badge size",
- category: "notifications",
- categoryName: "Notifications",
- section: "Sizes",
- keywords: ["notification", "badge", "app icon"],
- },
-
- // SIDEBAR CATEGORY
- {
- name: "Enable sidebar",
- category: "sidebar",
- categoryName: "Sidebar",
- section: "Sidebar",
- keywords: ["show", "panel", "side"],
- },
- {
- name: "Width",
- category: "sidebar",
- categoryName: "Sidebar",
- section: "Sidebar",
- keywords: ["size", "panel", "width"],
- },
-
- // UTILITIES CATEGORY
- // Utilities section
- {
- name: "Enable utilities",
- category: "utilities",
- categoryName: "Utilities",
- section: "Utilities",
- keywords: ["show", "enable"],
- },
- {
- name: "Max toasts",
- category: "utilities",
- categoryName: "Utilities",
- section: "Utilities",
- keywords: ["notifications", "limit", "maximum"],
- },
- {
- name: "Panel width",
- category: "utilities",
- categoryName: "Utilities",
- section: "Utilities",
- keywords: ["size", "width"],
- },
- {
- name: "Toast width",
- category: "utilities",
- categoryName: "Utilities",
- section: "Utilities",
- keywords: ["notification", "size", "width"],
- },
- // Clipboard section
- {
- name: "Enable clipboard history viewer",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["enable", "clipboard"],
- },
- {
- name: "Max entries visible",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["clipboard", "max"],
- },
- {
- name: "Entry height",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["height", "entry"],
- },
- {
- name: "Entry width",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["width", "entry"],
- },
- {
- name: "Minimum preview width",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["minimum", "preview", "width"],
- },
- {
- name: "Maximum preview width",
- category: "utilities",
- categoryName: "Utilities",
- section: "Clipboard",
- keywords: ["maximum", "preview", "width"],
- },
- // Toasts section
- {
- name: "Config loaded",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "config"],
- },
- {
- name: "Charging changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "battery", "power"],
- },
- {
- name: "Game mode changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "gaming"],
- },
- {
- name: "Do not disturb changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "dnd", "quiet"],
- },
- {
- name: "Audio output changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "speaker", "headphones"],
- },
- {
- name: "Audio input changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "microphone"],
- },
- {
- name: "Caps lock changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "keyboard"],
- },
- {
- name: "Num lock changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "keyboard"],
- },
- {
- name: "Keyboard layout changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "language", "input"],
- },
- {
- name: "VPN changed",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "vpn", "network"],
- },
- {
- name: "Now playing",
- category: "utilities",
- categoryName: "Utilities",
- section: "Toasts",
- keywords: ["toast", "notification", "music", "media"],
- },
- // VPN section
- {
- name: "Enable VPN integration",
- category: "utilities",
- categoryName: "Utilities",
- section: "VPN",
- keywords: ["vpn", "network", "connection"],
- },
- {
- name: "Provider",
- category: "utilities",
- categoryName: "Utilities",
- section: "VPN",
- keywords: ["vpn", "service", "provider"],
- },
-
- // DASHBOARD CATEGORY
- // Dashboard section
- {
- name: "Enable dashboard",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Dashboard",
- keywords: ["show", "enable", "panel"],
- },
- {
- name: "Media update interval",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Dashboard",
- keywords: ["refresh", "interval", "music"],
- },
- {
- name: "Resource update interval",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Dashboard",
- keywords: ["refresh", "interval", "cpu", "memory"],
- },
- {
- name: "Drag threshold",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Dashboard",
- keywords: ["swipe", "gesture", "threshold"],
- },
- // Performance section
- {
- name: "Show battery",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["power", "battery", "visibility"],
- },
- {
- name: "Show GPU",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["graphics", "gpu", "visibility"],
- },
- {
- name: "Show CPU",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["processor", "cpu", "visibility"],
- },
- {
- name: "Show memory",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["ram", "memory", "visibility"],
- },
- {
- name: "Show storage",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["disk", "storage", "visibility"],
- },
- {
- name: "Show network",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Performance",
- keywords: ["internet", "network", "visibility"],
- },
- // Layout Sizes section (read-only)
- {
- name: "Tab indicator height",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "tab", "indicator"],
- },
- {
- name: "Tab indicator spacing",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "tab", "spacing"],
- },
- {
- name: "Info width",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "info", "width"],
- },
- {
- name: "Info icon size",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "icon"],
- },
- {
- name: "Date time width",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "clock", "date"],
- },
- {
- name: "Media width",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "music", "player"],
- },
- {
- name: "Media progress sweep",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "progress", "arc"],
- },
- {
- name: "Media progress thickness",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "progress", "stroke"],
- },
- {
- name: "Resource progress thickness",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "progress", "stroke"],
- },
- {
- name: "Weather width",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "weather"],
- },
- {
- name: "Media cover art size",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "album", "artwork"],
- },
- {
- name: "Media visualiser size",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "visualizer", "cava"],
- },
- {
- name: "Resource size",
- category: "dashboard",
- categoryName: "Dashboard",
- section: "Layout Sizes",
- keywords: ["size", "cpu", "memory"],
- },
-
- // APPEARANCE CATEGORY
- // Scale section
- {
- name: "Rounding scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["corners", "radius", "scale"],
- },
- {
- name: "Spacing scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["gap", "margin", "scale"],
- },
- {
- name: "Padding scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["padding", "inset", "scale"],
- },
- {
- name: "Font size scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["text", "font", "scale"],
- },
- {
- name: "Animation duration scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["animation", "speed", "duration", "scale"],
- },
- {
- name: "Deform animation scale",
- category: "appearance",
- categoryName: "Appearance",
- section: "Scale",
- keywords: ["animation", "deform", "scale"],
- },
- // Fonts section
- {
- name: "Sans family",
- category: "appearance",
- categoryName: "Appearance",
- section: "Fonts",
- keywords: ["font", "typeface", "sans-serif"],
- },
- {
- name: "Monospace family",
- category: "appearance",
- categoryName: "Appearance",
- section: "Fonts",
- keywords: ["font", "typeface", "mono", "code"],
- },
- {
- name: "Material family",
- category: "appearance",
- categoryName: "Appearance",
- section: "Fonts",
- keywords: ["font", "icons", "material"],
- },
- {
- name: "Clock family",
- category: "appearance",
- categoryName: "Appearance",
- section: "Fonts",
- keywords: ["font", "time", "clock"],
- },
- // Animation section
- {
- name: "Media GIF speed adjustment",
- category: "appearance",
- categoryName: "Appearance",
- section: "Animation",
- keywords: ["gif", "speed", "animation"],
- },
- {
- name: "Session GIF speed",
- category: "appearance",
- categoryName: "Appearance",
- section: "Animation",
- keywords: ["gif", "speed", "animation", "login"],
- },
- // Transparency section
- {
- name: "Enable transparency",
- category: "appearance",
- categoryName: "Appearance",
- section: "Transparency",
- keywords: ["opacity", "translucent", "blur"],
- },
- {
- name: "Base opacity",
- category: "appearance",
- categoryName: "Appearance",
- section: "Transparency",
- keywords: ["opacity", "alpha", "transparency"],
- },
- {
- name: "Layer opacity",
- category: "appearance",
- categoryName: "Appearance",
- section: "Transparency",
- keywords: ["opacity", "alpha", "layers"],
- },
-
- // OSD CATEGORY
- // On Screen Display section
- {
- name: "Enable OSD",
- category: "osd",
- categoryName: "On screen display",
- section: "On Screen Display",
- keywords: ["show", "enable", "overlay"],
- },
- {
- name: "Hide delay",
- category: "osd",
- categoryName: "On screen display",
- section: "On Screen Display",
- keywords: ["timeout", "duration", "dismiss"],
- },
- {
- name: "Enable brightness OSD",
- category: "osd",
- categoryName: "On screen display",
- section: "On Screen Display",
- keywords: ["screen", "brightness", "overlay"],
- },
- {
- name: "Enable microphone OSD",
- category: "osd",
- categoryName: "On screen display",
- section: "On Screen Display",
- keywords: ["mic", "audio", "overlay"],
- },
- {
- name: "Brightness on all monitors",
- category: "osd",
- categoryName: "On screen display",
- section: "On Screen Display",
- keywords: ["multi-monitor", "all screens"],
- },
- // Sizes section
- {
- name: "Slider width",
- category: "osd",
- categoryName: "On screen display",
- section: "Sizes",
- keywords: ["size", "osd", "width"],
- },
- {
- name: "Slider height",
- category: "osd",
- categoryName: "On screen display",
- section: "Sizes",
- keywords: ["size", "osd", "height"],
- },
-
- // SCREENSHOT CATEGORY
- // Screenshot section
- {
- name: "Enable effects",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["effects", "shadow", "screenshot"],
- },
- {
- name: "Effects mode",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["effects", "mode"],
- },
- {
- name: "Corner radius",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["corner", "radius"],
- },
- {
- name: "Enable shadow",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["drop", "shadow"],
- },
- {
- name: "Enable rounded corners",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["rounded", "corners"],
- },
- {
- name: "Shadow blur amount",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["blur", "shadow", "radius"],
- },
- // {
- // name: "Shadow color",
- // category: "screenshot",
- // categoryName: "Screenshot",
- // section: "Screenshot",
- // keywords: ["color", "shadow"],
- // },
- {
- name: "Shadow offset X",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["offset", "shadow"],
- },
- {
- name: "Shadow offset Y",
- category: "screenshot",
- categoryName: "Screenshot",
- section: "Screenshot",
- keywords: ["offset", "shadow"],
- },
-
- // LAUNCHER CATEGORY
- // Launcher section
- {
- name: "Max apps shown",
- category: "launcher",
- categoryName: "Launcher",
- section: "Launcher",
- keywords: ["limit", "apps", "search results"],
- },
- {
- name: "Max wallpapers shown",
- category: "launcher",
- categoryName: "Launcher",
- section: "Launcher",
- keywords: ["limit", "wallpapers", "search results"],
- },
- {
- name: "Action prefix",
- category: "launcher",
- categoryName: "Launcher",
- section: "Launcher",
- keywords: ["command", "prefix", "action"],
- },
- {
- name: "Special prefix",
- category: "launcher",
- categoryName: "Launcher",
- section: "Launcher",
- keywords: ["command", "prefix", "special"],
- },
- {
- name: "Use UWSM launch command",
- category: "launcher",
- categoryName: "Launcher",
- section: "Launcher",
- keywords: ["command", "uwsm", "systemd"],
- },
- // Fuzzy Search section
- {
- name: "Apps",
- category: "launcher",
- categoryName: "Launcher",
- section: "Fuzzy Search",
- keywords: ["fuzzy", "search", "applications"],
- },
- {
- name: "Actions",
- category: "launcher",
- categoryName: "Launcher",
- section: "Fuzzy Search",
- keywords: ["fuzzy", "search", "actions"],
- },
- {
- name: "Schemes",
- category: "launcher",
- categoryName: "Launcher",
- section: "Fuzzy Search",
- keywords: ["fuzzy", "search", "color schemes"],
- },
- {
- name: "Variants",
- category: "launcher",
- categoryName: "Launcher",
- section: "Fuzzy Search",
- keywords: ["fuzzy", "search", "variants"],
- },
- {
- name: "Wallpapers",
- category: "launcher",
- categoryName: "Launcher",
- section: "Fuzzy Search",
- keywords: ["fuzzy", "search", "backgrounds"],
- },
- // Sizes section
- {
- name: "Item width",
- category: "launcher",
- categoryName: "Launcher",
- section: "Sizes",
- keywords: ["size", "app", "width"],
- },
- {
- name: "Item height",
- category: "launcher",
- categoryName: "Launcher",
- section: "Sizes",
- keywords: ["size", "app", "height"],
- },
- {
- name: "Wallpaper width",
- category: "launcher",
- categoryName: "Launcher",
- section: "Sizes",
- keywords: ["size", "wallpaper", "width"],
- },
- {
- name: "Wallpaper height",
- category: "launcher",
- categoryName: "Launcher",
- section: "Sizes",
- keywords: ["size", "wallpaper", "height"],
- },
- // Actions section
- {
- name: "Launcher actions",
- category: "launcher",
- categoryName: "Launcher",
- section: "Actions",
- keywords: ["commands", "shortcuts", "actions"],
- },
-];
-
-// Helper function to search with keywords first, then fuzzy fallback
-export function searchSettings(query, fuzzyModule) {
- if (!query || query.trim() === "") {
- return [];
- }
-
- const q = query.toLowerCase().trim();
- const results = [];
- const seen = new Set();
-
- // 1. Exact keyword match (highest priority)
- for (const setting of settingsIndex) {
- const key = `${setting.category}:${setting.name}`;
- if (seen.has(key)) continue;
-
- for (const keyword of setting.keywords) {
- if (keyword.toLowerCase() === q) {
- results.push(
- Object.assign({}, setting, {
- matchType: "exact-keyword",
- score: 1.0,
- })
- );
- seen.add(key);
- break;
- }
- }
- }
-
- // 2. Partial keyword match
- for (const setting of settingsIndex) {
- const key = `${setting.category}:${setting.name}`;
- if (seen.has(key)) continue;
-
- for (const keyword of setting.keywords) {
- if (keyword.toLowerCase().includes(q)) {
- results.push(
- Object.assign({}, setting, {
- matchType: "partial-keyword",
- score: 0.8,
- })
- );
- seen.add(key);
- break;
- }
- }
- }
-
- // 3. Name contains query
- for (const setting of settingsIndex) {
- const key = `${setting.category}:${setting.name}`;
- if (seen.has(key)) continue;
-
- if (setting.name.toLowerCase().includes(q)) {
- results.push(
- Object.assign({}, setting, {
- matchType: "name-contains",
- score: 0.7,
- })
- );
- seen.add(key);
- }
- }
-
- // 4. Fuzzy match on name (fallback for typos)
- if (fuzzyModule && results.length < 10) {
- const fuzzyTargets = settingsIndex
- .filter((s) => !seen.has(`${s.category}:${s.name}`))
- .map((s) => Object.assign({}, s, { _searchTarget: s.name }));
-
- if (fuzzyTargets.length > 0) {
- const fuzzyResults = fuzzyModule.go(query, fuzzyTargets, {
- key: "_searchTarget",
- limit: 10 - results.length,
- threshold: 0.3,
- });
-
- for (const r of fuzzyResults) {
- const setting = r.obj;
- results.push(
- Object.assign({}, setting, {
- matchType: "fuzzy",
- score: r.score * 0.6,
- _searchTarget: undefined,
- })
- );
- }
- }
- }
-
- // Sort by score descending
- results.sort((a, b) => b.score - a.score);
-
- return results;
-}
diff --git a/scripts/build-settings-index.py b/scripts/build-settings-index.py
new file mode 100644
index 0000000..a5315e1
--- /dev/null
+++ b/scripts/build-settings-index.py
@@ -0,0 +1,407 @@
+from __future__ import annotations
+
+import json
+import re
+import sys
+from collections import defaultdict
+from functools import lru_cache
+from pathlib import Path
+
+
+@lru_cache(maxsize=None)
+def read_lines(path: Path) -> tuple[str, ...]:
+ return tuple(path.read_text().splitlines())
+
+
+ROW_RE = re.compile(
+ r'^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|InfoRow|PopupRow|OverlayRow|DefaultRow)\s*\{')
+LABEL_RE = re.compile(r'^\s*(?:label|text):\s*qsTr\("([^"]+)"\)')
+ANCHOR_RE = re.compile(r'^\s*settingAnchor:\s*"([^"]+)"')
+CHECKED_RE = re.compile(r'^\s*checked:\s*(?:Config)\.([\w.]+)\s*$')
+ONTOGGLED_RE = re.compile(
+ r'^\s*onToggled:\s*(?:Config)\.([\w.]+)\s*=\s*checked\s*$')
+ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
+SKIP_LABELS = {"Muted", "None"}
+FIELD_WEIGHT = {"title": 1.0, "keywords": 0.4}
+STOPWORDS = {"the", "a", "an", "of", "and", "or", "to", "on", "in", "for"}
+
+
+def find_pages_dir(settings: Path) -> Path:
+ return settings / "Pages"
+
+
+def discover_files(settings: Path) -> dict[str, Path]:
+ files: dict[str, Path] = {}
+ for p in find_pages_dir(settings).rglob("*.qml"):
+ files[p.stem] = p
+ return files
+
+
+PAGE_NAME_RE = re.compile(r'^\s*name:\s*qsTr\("([^"]+)"\)')
+PAGE_ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
+
+
+def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
+ text = (settings / "PageRegistry.qml").read_text().splitlines()
+
+ start = next(
+ i for i, line in enumerate(text)
+ if re.search(r'\bpages\s*:\s*\[', line)
+ )
+
+ out: list[tuple[str, str]] = []
+ i = start + 1
+
+ while i < len(text):
+ line = text[i].strip()
+
+ if line.startswith("]"):
+ break
+
+ if line.startswith("//") or not line:
+ i += 1
+ continue
+
+ if line.startswith("{"):
+ name = None
+ icon = None
+ i += 1
+
+ while i < len(text):
+ s = text[i].strip()
+
+ if s.startswith("}"):
+ if name is not None:
+ out.append((icon or "tune", name))
+ break
+
+ if name is None:
+ m = PAGE_NAME_RE.match(text[i])
+ if m:
+ name = m.group(1)
+
+ if icon is None:
+ mi = PAGE_ICON_RE.match(text[i])
+ if mi:
+ icon = mi.group(1)
+
+ i += 1
+
+ i += 1
+
+ return out
+
+
+BLOCK_RE = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$')
+
+
+def _strip_comment(line: str) -> str:
+ return line.split("//", 1)[0].rstrip()
+
+
+def parse_block(lines: list[str], i: int) -> tuple[str, list[tuple[str, list]], int]:
+ line = _strip_comment(lines[i]).strip()
+ m = BLOCK_RE.match(line)
+ if not m:
+ raise ValueError(f"Expected block start at line {i + 1}: {lines[i]!r}")
+
+ name = m.group(1)
+ i += 1
+ children: list[tuple[str, list]] = []
+
+ while i < len(lines):
+ s = _strip_comment(lines[i]).strip()
+ if not s:
+ i += 1
+ continue
+
+ if s.startswith("}"):
+ return name, children, i + 1
+
+ if BLOCK_RE.match(s):
+ child_name, child_children, i = parse_block(lines, i)
+ children.append((child_name, child_children))
+ continue
+
+ i += 1
+
+ raise ValueError(f"Unterminated block: {name}")
+
+
+def collect_page_names(block: tuple[str, list[tuple[str, list]]]) -> list[str]:
+ name, children = block
+
+ if name != "Component":
+ return [name]
+
+ for child_name, child_children in children:
+ if child_name == "StackPage":
+ out: list[str] = []
+ for grand_name, grand_children in child_children:
+ if grand_name == "Component":
+ out.extend(collect_page_names((grand_name, grand_children)))
+ return out
+
+ if child_name != "Component":
+ return [child_name]
+
+ return []
+
+
+def parse_page_comps(settings: Path) -> list[list[str]]:
+ text = (settings / "PageCompRegistry.qml").read_text().splitlines()
+
+ start = next(
+ i for i, line in enumerate(text)
+ if re.search(r'\bpageComps\s*:\s*\[', _strip_comment(line))
+ )
+
+ comps: list[list[str]] = []
+ i = start + 1
+
+ while i < len(text):
+ s = _strip_comment(text[i]).strip()
+ if not s:
+ i += 1
+ continue
+ if s.startswith("]"):
+ break
+
+ if BLOCK_RE.match(s) and BLOCK_RE.match(s).group(1) == "Component":
+ block = parse_block(text, i)
+ names = collect_page_names((block[0], block[1]))
+ if names:
+ comps.append(names)
+ i = block[2]
+ continue
+
+ i += 1
+
+ return comps
+
+
+def dedup_crumbs(labels: list[str], icons: list[str]) -> tuple[list[str], list[str]]:
+ out_labels: list[str] = []
+ out_icons: list[str] = []
+ for lbl, ico in zip(labels, icons):
+ if out_labels and out_labels[-1] == lbl:
+ continue
+ out_labels.append(lbl)
+ out_icons.append(ico)
+ return out_labels, out_icons
+
+
+def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
+ comps = parse_page_comps(settings)
+ registry = parse_page_registry(settings)
+
+ top_meta: dict[int, tuple[str, str]] = {}
+ for i, (icon, label) in enumerate(registry):
+ top_meta[i] = (icon, label)
+
+ nav_children: dict[str, dict[int, tuple[str, str, str]]] = {}
+ for names in comps:
+ for name in names:
+ pf = files.get(name)
+ if not pf:
+ continue
+ pending_icon = pending_label = None
+ section = ""
+ expect_section = False
+ for ln in read_lines(pf):
+ if SECTION_RE.match(ln):
+ expect_section = True
+ continue
+ ml = LABEL_RE.match(ln)
+ if ml:
+ if expect_section:
+ section = ml.group(1)
+ expect_section = False
+ else:
+ pending_label = ml.group(1)
+ continue
+ mi = ICON_RE.match(ln)
+ if mi:
+ pending_icon = mi.group(1)
+ mo = re.search(r"openSubPage\((\d+)\)", ln)
+ if mo:
+ pos = int(mo.group(1))
+ nav_children.setdefault(name, {})[pos] = (
+ pending_icon or "tune", pending_label or "", section)
+ pending_icon = pending_label = None
+
+ nav: dict[str, dict] = {}
+ for top_idx, names in enumerate(comps):
+ if not names:
+ continue
+ main = names[0]
+ main_icon, main_label = top_meta.get(top_idx, ("tune", main))
+ nav[main] = {"pageIdx": top_idx, "subPath": [],
+ "crumbIcons": [main_icon], "crumbLabels": [main_label]}
+ children = dict(nav_children.get(main, {}))
+ opened_via_subpage = set()
+ for owner, kids in nav_children.items():
+ owner_group = next((ns for ns in comps if owner in ns), None)
+ if not owner_group:
+ continue
+ for kpos in kids:
+ if kpos < len(owner_group):
+ opened_via_subpage.add(owner_group[kpos])
+ for pos in range(1, len(names)):
+ if pos not in children and names[pos] not in opened_via_subpage:
+ label = re.sub(r"(Detail)?Page$", "", names[pos])
+ label = re.sub(r"(?= len(names):
+ continue
+ child = names[pos]
+ labels = [main_label] + ([section] if section else []) + [label]
+ icons = [main_icon] + ([icon] if section else []) + [icon]
+ labels, icons = dedup_crumbs(labels, icons)
+ nav[child] = {"pageIdx": top_idx, "subPath": [pos],
+ "crumbIcons": icons,
+ "crumbLabels": labels}
+ for gpos, (gicon, glabel, gsection) in nav_children.get(child, {}).items():
+ if gpos >= len(names):
+ continue
+ glabels = labels + ([gsection] if gsection else []) + [glabel]
+ gicons = icons + ([gicon] if gsection else []) + [gicon]
+ glabels, gicons = dedup_crumbs(glabels, gicons)
+ nav[names[gpos]] = {
+ "pageIdx": top_idx, "subPath": [pos, gpos],
+ "crumbIcons": gicons,
+ "crumbLabels": glabels}
+ return nav
+
+
+def tokenize(text: str) -> list[str]:
+ toks: list[str] = []
+ for word in text.lower().split():
+ parts = [p for p in re.split(r"[^a-z0-9]+", word) if p]
+ for p in parts:
+ if p not in STOPWORDS and p not in toks:
+ toks.append(p)
+ if len(parts) > 1:
+ joined = "".join(parts)
+ if joined not in toks:
+ toks.append(joined)
+ return toks
+
+
+SUBTEXT_RE = re.compile(r'^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)')
+SECTION_RE = re.compile(r'^\s*SectionHeader\s*\{')
+
+
+def extract_settings(files: dict[str, Path], nav: dict[str, dict]) -> list[dict]:
+ entries: list[dict] = []
+ for comp, meta in nav.items():
+ pf = files.get(comp)
+ if not pf:
+ continue
+ lines = read_lines(pf)
+ section = ""
+ i = 0
+ while i < len(lines):
+ if SECTION_RE.match(lines[i]):
+ for j in range(i + 1, min(i + 4, len(lines))):
+ m = LABEL_RE.match(lines[j])
+ if m:
+ section = m.group(1)
+ break
+ row_match = ROW_RE.match(lines[i])
+ if row_match:
+ row_type = row_match.group(1)
+ label = anchor = subtext = None
+ checked_path = toggled_path = None
+ for j in range(i + 1, min(i + 12, len(lines))):
+ if label is None:
+ m = LABEL_RE.match(lines[j])
+ if m:
+ label = m.group(1)
+ if anchor is None:
+ a = ANCHOR_RE.match(lines[j])
+ if a:
+ anchor = a.group(1)
+ if subtext is None:
+ st = SUBTEXT_RE.match(lines[j])
+ if st:
+ subtext = st.group(1)
+ if checked_path is None:
+ ch = CHECKED_RE.match(lines[j])
+ if ch:
+ checked_path = ch.group(1)
+ if toggled_path is None:
+ tg = ONTOGGLED_RE.match(lines[j])
+ if tg:
+ toggled_path = tg.group(1)
+ toggle_path = (
+ checked_path
+ if row_type == "ToggleRow" and checked_path and checked_path == toggled_path
+ else ""
+ )
+ if label and label not in SKIP_LABELS and anchor:
+ extra = " ".join(meta["crumbLabels"]) + \
+ " " + section + " " + (subtext or "")
+ entries.append({
+ "pageIdx": meta["pageIdx"], "subPath": meta["subPath"],
+ "crumbIcons": meta["crumbIcons"],
+ "crumbLabels": meta["crumbLabels"],
+ "title": label, "anchor": anchor,
+ "section": section,
+ "subtext": subtext or "",
+ "togglePath": toggle_path,
+ "keywords": " ".join(sorted(set(tokenize(label + " " + extra)))),
+ })
+ i += 1
+ return entries
+
+
+def build_inverted_and_ranking(entries: list[dict]):
+ inverted: dict[str, list[int]] = defaultdict(list)
+ ranking: dict[str, dict[int, float]] = defaultdict(dict)
+ for idx, e in enumerate(entries):
+ fields = {"title": e["title"], "keywords": e["keywords"]}
+ seen: set[str] = set()
+ for field, text in fields.items():
+ weight = FIELD_WEIGHT.get(field, 0.2)
+ for tok in tokenize(text):
+ if idx not in inverted[tok]:
+ inverted[tok].append(idx)
+ ranking[tok][idx] = max(ranking[tok].get(idx, 0.0), weight)
+ seen.add(tok)
+ for tok, ids in inverted.items():
+ ids.sort(key=lambda i: ranking[tok][i], reverse=True)
+ return inverted, {t: {str(k): v for k, v in d.items()} for t, d in ranking.items()}
+
+
+def main() -> int:
+ if len(sys.argv) != 3:
+ print(__doc__)
+ return 1
+ settings = Path(sys.argv[1])
+ out = Path(sys.argv[2])
+ files = discover_files(settings)
+ nav = build_nav_map(settings, files)
+ entries = extract_settings(files, nav)
+ inverted, ranking = build_inverted_and_ranking(entries)
+ for e in entries:
+ e.pop("keywords", None)
+ out.write_text(json.dumps({
+ "version": 2,
+ "entries": entries,
+ "inverted": inverted,
+ "ranking": ranking,
+ }, ensure_ascii=False, indent=2))
+ print(f"settings index: {len(entries)} entries, "
+ f"{len(inverted)} tokens -> {out}")
+ print("files:", len(files))
+ print("comps:", len(parse_page_comps(settings)))
+ print("registry:", len(parse_page_registry(settings)))
+ print("nav:", len(nav))
+ print("entries:", len(entries))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())