chore: split cmake into multiple files and fix settings searching regex + add enabled option to llm
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Failing after 17s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 1m11s
Rust / fmt (pull_request) Successful in 1m3s
Rust / build (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Failing after 16m40s
Python / verify (pull_request) Failing after 17m52s
C++ / clang-tidy (pull_request) Failing after 18m0s
C++ / build (pull_request) Failing after 18m2s
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Failing after 17s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 1m11s
Rust / fmt (pull_request) Successful in 1m3s
Rust / build (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Failing after 16m40s
Python / verify (pull_request) Failing after 17m52s
C++ / clang-tidy (pull_request) Failing after 18m0s
C++ / build (pull_request) Failing after 18m2s
This commit is contained in:
+14
-16
@@ -26,6 +26,18 @@ if(NOT DEFINED VERSION)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(NOT DEFINED GIT_REVISION)
|
||||||
|
execute_process(COMMAND git rev-parse HEAD
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
OUTPUT_VARIABLE GIT_REVISION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
if("${GIT_REVISION}" STREQUAL "")
|
||||||
|
message(FATAL_ERROR "GIT_REVISION is not set and failed to get from git")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
|
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
|
||||||
|
|
||||||
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
|
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
|
||||||
@@ -36,7 +48,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||||
@@ -54,21 +66,7 @@ add_compile_options(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if("shell" IN_LIST ENABLE_MODULES)
|
if("cli" IN_LIST ENABLE_MODULES)
|
||||||
# Build settings index
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Nuitka compilation
|
# Nuitka compilation
|
||||||
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||||
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ TextEditBase {
|
|||||||
|
|
||||||
color: Colors.palette.m3onSurface
|
color: Colors.palette.m3onSurface
|
||||||
readOnly: true
|
readOnly: true
|
||||||
anchors.margins: Tokens.padding.medium
|
|
||||||
textFormat: Text.MarkdownText
|
textFormat: Text.MarkdownText
|
||||||
font.pointSize: Tokens.font.size.smaller
|
font.pointSize: Tokens.font.size.smaller
|
||||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import ZShell.Llm
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Components
|
import qs.Components
|
||||||
@@ -31,7 +30,7 @@ Item {
|
|||||||
|
|
||||||
Tabs {
|
Tabs {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: ChatState.isWindow ? 0 : implicitHeight
|
Layout.preferredHeight: ChatState.isWindow || !Config.llm.enabled ? 0 : implicitHeight
|
||||||
dashState: root.props
|
dashState: root.props
|
||||||
nonAnimWidth: layout.width
|
nonAnimWidth: layout.width
|
||||||
visible: height > 0
|
visible: height > 0
|
||||||
@@ -53,12 +52,10 @@ Item {
|
|||||||
x: root.props.currentTab === 0 ? 0 : -root.width
|
x: root.props.currentTab === 0 ? 0 : -root.width
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -73,33 +70,38 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Loader {
|
||||||
id: chatPage
|
id: chatLoader
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
implicitWidth: parent.width
|
active: Config.llm.enabled
|
||||||
opacity: root.props.currentTab === 1 ? 1 : 0
|
width: parent.width
|
||||||
visible: opacity > 0
|
|
||||||
x: root.props.currentTab === 0 ? root.width : 0
|
|
||||||
z: 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
sourceComponent: Item {
|
||||||
Anim {
|
id: chatPage
|
||||||
|
|
||||||
|
opacity: root.props.currentTab === 1 ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
x: root.props.currentTab === 0 ? root.width : 0
|
||||||
|
z: 1
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
anchors.fill: parent
|
|
||||||
color: Colors.tPalette.m3surfaceContainerLow
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
ChatPanel {
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
color: Colors.tPalette.m3surfaceContainerLow
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
|
ChatPanel {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ColumnLayout {
|
|||||||
function findAnchor(item: Item, anchor: string): Item {
|
function findAnchor(item: Item, anchor: string): Item {
|
||||||
if (!item)
|
if (!item)
|
||||||
return null;
|
return null;
|
||||||
if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
|
if (item.settingAnchor !== undefined && item.settingAnchor === anchor) // qmllint disable missing-property
|
||||||
return item;
|
return item;
|
||||||
const kids = item.children;
|
const kids = item.children;
|
||||||
for (let i = 0; i < kids.length; i++) {
|
for (let i = 0; i < kids.length; i++) {
|
||||||
@@ -43,8 +43,8 @@ ColumnLayout {
|
|||||||
|
|
||||||
function highlightAnchor(anchor: string): void {
|
function highlightAnchor(anchor: string): void {
|
||||||
const row = findAnchor(contentChild, anchor);
|
const row = findAnchor(contentChild, anchor);
|
||||||
if (row && row.flashHighlight !== undefined)
|
if (row && row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||||
row.flashHighlight();
|
row.flashHighlight(); // qmllint disable missing-property
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToAnchor(anchor: string): bool {
|
function scrollToAnchor(anchor: string): bool {
|
||||||
@@ -61,8 +61,8 @@ ColumnLayout {
|
|||||||
root.animateScroll = true;
|
root.animateScroll = true;
|
||||||
flickable.contentY = target;
|
flickable.contentY = target;
|
||||||
Qt.callLater(() => root.animateScroll = false);
|
Qt.callLater(() => root.animateScroll = false);
|
||||||
if (row.flashHighlight !== undefined)
|
if (row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||||
row.flashHighlight();
|
row.flashHighlight(); // qmllint disable missing-property
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +154,20 @@ ColumnLayout {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: -topMargin
|
Layout.topMargin: -topMargin
|
||||||
|
|
||||||
bottomMargin: Tokens.padding.extraLarge
|
bottomMargin: Tokens.padding.extraLarge
|
||||||
|
topMargin: Tokens.padding.large
|
||||||
|
fadeAmount: 0.1
|
||||||
|
|
||||||
contentHeight: root.contentChild?.implicitHeight ?? 0
|
contentHeight: root.contentChild?.implicitHeight ?? 0
|
||||||
contentItem.children: [root.contentChild]
|
contentItem.children: [root.contentChild]
|
||||||
fadeAmount: 0.1
|
|
||||||
topMargin: Tokens.padding.large
|
rebound: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "x,y"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on contentY {
|
Behavior on contentY {
|
||||||
enabled: root.animateScroll
|
enabled: root.animateScroll
|
||||||
|
|||||||
@@ -81,14 +81,10 @@ VerticalFadeFlickable {
|
|||||||
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
|
||||||
RadiusBehavior on bottomLeftRadius {
|
RadiusBehavior on bottomLeftRadius {}
|
||||||
}
|
RadiusBehavior on bottomRightRadius {}
|
||||||
RadiusBehavior on bottomRightRadius {
|
RadiusBehavior on topLeftRadius {}
|
||||||
}
|
RadiusBehavior on topRightRadius {}
|
||||||
RadiusBehavior on topLeftRadius {
|
|
||||||
}
|
|
||||||
RadiusBehavior on topRightRadius {
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
id: stateLayer
|
id: stateLayer
|
||||||
@@ -262,14 +258,10 @@ VerticalFadeFlickable {
|
|||||||
topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
width: cardList.width
|
width: cardList.width
|
||||||
|
|
||||||
RadiusBehavior on bottomLeftRadius {
|
RadiusBehavior on bottomLeftRadius {}
|
||||||
}
|
RadiusBehavior on bottomRightRadius {}
|
||||||
RadiusBehavior on bottomRightRadius {
|
RadiusBehavior on topLeftRadius {}
|
||||||
}
|
RadiusBehavior on topRightRadius {}
|
||||||
RadiusBehavior on topLeftRadius {
|
|
||||||
}
|
|
||||||
RadiusBehavior on topRightRadius {
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: resultLayout
|
id: resultLayout
|
||||||
@@ -318,9 +310,10 @@ VerticalFadeFlickable {
|
|||||||
|
|
||||||
z: 1
|
z: 1
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.sState.jumpToSetting(result.modelData.pageIdx, result.modelData.subPath, result.modelData.anchor);
|
const target = result.modelData.targetSubPath;
|
||||||
}
|
root.sState.jumpToSetting(result.modelData.pageIdx, target.length > 0 ? target : result.modelData.subPath, target.length > 0 ? "" : result.modelData.anchor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomSwitch {
|
CustomSwitch {
|
||||||
|
|||||||
@@ -67,6 +67,20 @@ PageBase {
|
|||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
first: true
|
first: true
|
||||||
|
text: qsTr("General")
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
settingAnchor: "panels-sidebar-llm-enabled"
|
||||||
|
checked: Config.llm.enabled
|
||||||
|
text: qsTr("Enabled")
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
|
||||||
|
onToggled: Config.llm.enabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
text: qsTr("Appearance")
|
text: qsTr("Appearance")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ PageBase {
|
|||||||
|
|
||||||
NavRow {
|
NavRow {
|
||||||
text: qsTr("AI chat")
|
text: qsTr("AI chat")
|
||||||
|
settingAnchor: "panels-sidebar-llm"
|
||||||
icon: "robot_2"
|
icon: "robot_2"
|
||||||
first: true
|
first: true
|
||||||
last: true
|
last: true
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ PageBase {
|
|||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
IconTextButton {
|
IconTextButton {
|
||||||
|
|
||||||
|
property string settingAnchor: "style-wallpapers"
|
||||||
|
|
||||||
enabled: Config.background.enabled
|
enabled: Config.background.enabled
|
||||||
horizontalPadding: Tokens.padding.extraLarge
|
horizontalPadding: Tokens.padding.extraLarge
|
||||||
icon: "wallpaper"
|
icon: "wallpaper"
|
||||||
@@ -51,6 +54,9 @@ PageBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconTextButton {
|
IconTextButton {
|
||||||
|
|
||||||
|
property string settingAnchor: "style-colors-fonts"
|
||||||
|
|
||||||
enabled: Config.background.enabled
|
enabled: Config.background.enabled
|
||||||
horizontalPadding: Tokens.padding.extraLarge
|
horizontalPadding: Tokens.padding.extraLarge
|
||||||
icon: "palette"
|
icon: "palette"
|
||||||
|
|||||||
@@ -1,66 +1,24 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import "../../scripts/fzf.js" as Fzf
|
import "../../scripts/fzf.js" as Fzf
|
||||||
|
import "../../scripts/settings-indexer.js" as SettingsIndexer
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import ZShell
|
import ZShell
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import qs.Paths
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var fzfFinder: null
|
property var inverted: ({})
|
||||||
|
property var ranking: ({})
|
||||||
readonly property var highlightCache: ({
|
readonly property var highlightCache: ({
|
||||||
"search": "",
|
"search": "",
|
||||||
"pattern": null
|
"pattern": null
|
||||||
})
|
})
|
||||||
property var inverted: ({})
|
property var fzfFinder: null
|
||||||
property var ranking: ({})
|
readonly property string cachePath: Paths.cache + "/settings-index.json"
|
||||||
|
|
||||||
function highlight(text: string, search: string, colour: color): string {
|
|
||||||
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
if (search.length === 0)
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
const cache = root.highlightCache;
|
|
||||||
if (search !== cache.search) {
|
|
||||||
const tokens = tokenize(search);
|
|
||||||
cache.search = search;
|
|
||||||
if (tokens.length === 0)
|
|
||||||
cache.pattern = null;
|
|
||||||
else {
|
|
||||||
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
||||||
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = cache.pattern;
|
|
||||||
if (!pattern)
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
pattern.lastIndex = 0;
|
|
||||||
if (!pattern.test(escaped))
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
pattern.lastIndex = 0;
|
|
||||||
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<QtObject> {
|
function query(search: string): list<QtObject> {
|
||||||
const tokens = root.tokenize(search);
|
const tokens = root.tokenize(search);
|
||||||
@@ -70,7 +28,7 @@ Singleton {
|
|||||||
const scores = ({});
|
const scores = ({});
|
||||||
const hitCounts = ({});
|
const hitCounts = ({});
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
const matches = root.lookup(token);
|
const matches = root.lookup(token); // { id: weight }
|
||||||
for (const id in matches) {
|
for (const id in matches) {
|
||||||
scores[id] = (scores[id] ?? 0) + matches[id];
|
scores[id] = (scores[id] ?? 0) + matches[id];
|
||||||
hitCounts[id] = (hitCounts[id] ?? 0) + 1;
|
hitCounts[id] = (hitCounts[id] ?? 0) + 1;
|
||||||
@@ -103,13 +61,76 @@ Singleton {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 tokenize(text: string): var {
|
function tokenize(text: string): var {
|
||||||
return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
|
return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highlight(text: string, search: string, colour: color): string {
|
||||||
|
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
if (search.length === 0)
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
const cache = root.highlightCache;
|
||||||
|
if (search !== cache.search) {
|
||||||
|
const tokens = root.tokenize(search);
|
||||||
|
cache.search = search;
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
cache.pattern = null;
|
||||||
|
} else {
|
||||||
|
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||||
|
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = cache.pattern;
|
||||||
|
if (!pattern)
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
if (!pattern.test(escaped))
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadIndex(): var {
|
||||||
|
const revision = ZUtils.gitRevision();
|
||||||
|
// const cached = ZUtils.readTextFile(cachePath);
|
||||||
|
// if (cached) {
|
||||||
|
// try {
|
||||||
|
// const parsed = JSON.parse(cached);
|
||||||
|
// if (parsed.version === 3 && revision && parsed.revision === revision && parsed.locale === Qt.locale().name)
|
||||||
|
// return parsed;
|
||||||
|
// } catch (e) {}
|
||||||
|
// }
|
||||||
|
const data = SettingsIndexer.buildIndex(`${Quickshell.shellDir}/Modules/Settings`, p => ZUtils.readTextFile(p), (d, s) => ZUtils.listFiles(d, s), (ctx, text) => qsTranslate(ctx, text));
|
||||||
|
data.revision = revision;
|
||||||
|
data.locale = Qt.locale().name;
|
||||||
|
ZUtils.writeTextFile(cachePath, JSON.stringify(data));
|
||||||
|
console.log(`SettingsSearcher: indexed ${data.entries.length} settings (revision ${revision || "unknown"})`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(ZUtils.settingsIndex());
|
const data = root.loadIndex();
|
||||||
entries.model = data.entries;
|
entries.model = data.entries;
|
||||||
root.inverted = data.inverted ?? {};
|
root.inverted = data.inverted ?? {};
|
||||||
root.ranking = data.ranking ?? {};
|
root.ranking = data.ranking ?? {};
|
||||||
@@ -122,6 +143,7 @@ Singleton {
|
|||||||
limit: 25
|
limit: 25
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.warn("SettingsSearcher: failed to build settings index:", e);
|
||||||
entries.model = [];
|
entries.model = [];
|
||||||
root.inverted = {};
|
root.inverted = {};
|
||||||
root.ranking = {};
|
root.ranking = {};
|
||||||
@@ -132,22 +154,25 @@ Singleton {
|
|||||||
Variants {
|
Variants {
|
||||||
id: entries
|
id: entries
|
||||||
|
|
||||||
SettingEntry {
|
SettingEntry {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component SettingEntry: QtObject {
|
component SettingEntry: QtObject {
|
||||||
readonly property string anchor: modelData.anchor ?? ""
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property int pageIdx: modelData.pageIdx
|
||||||
|
readonly property var subPath: modelData.subPath
|
||||||
|
readonly property var targetSubPath: modelData.targetSubPath ?? []
|
||||||
readonly property var crumbIcons: modelData.crumbIcons
|
readonly property var crumbIcons: modelData.crumbIcons
|
||||||
readonly property var crumbLabels: modelData.crumbLabels
|
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 title: modelData.title
|
||||||
|
readonly property string section: modelData.section ?? ""
|
||||||
|
readonly property string subtext: modelData.subtext ?? ""
|
||||||
|
readonly property string anchor: modelData.anchor ?? ""
|
||||||
|
readonly property string icon: modelData.icon ?? ""
|
||||||
|
|
||||||
readonly property string togglePath: modelData.togglePath ?? ""
|
readonly property string togglePath: modelData.togglePath ?? ""
|
||||||
|
readonly property bool isToggle: togglePath.length > 0
|
||||||
readonly property bool toggleValue: {
|
readonly property bool toggleValue: {
|
||||||
if (!isToggle)
|
if (!isToggle)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1 +1,24 @@
|
|||||||
|
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus CanvasPainter)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||||
|
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
||||||
|
|
||||||
|
if(NOT Cava_FOUND)
|
||||||
|
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(cmake/sensorslib.cmake)
|
||||||
|
|
||||||
|
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||||
|
qt_standard_project_setup(REQUIRES 6.9)
|
||||||
|
|
||||||
|
include(cmake/pch.cmake)
|
||||||
|
include(cmake/qml-module.cmake)
|
||||||
|
|
||||||
|
add_library(zshell-util INTERFACE)
|
||||||
|
target_include_directories(zshell-util INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||||
|
|
||||||
add_subdirectory(ZShell)
|
add_subdirectory(ZShell)
|
||||||
|
|||||||
@@ -1,55 +1,3 @@
|
|||||||
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus CanvasPainter)
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
|
|
||||||
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
|
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
|
||||||
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
|
||||||
|
|
||||||
if(NOT Cava_FOUND)
|
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT TARGET Sensors::Sensors)
|
|
||||||
add_library(Sensors::Sensors UNKNOWN IMPORTED)
|
|
||||||
set_target_properties(Sensors::Sensors PROPERTIES
|
|
||||||
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
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;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}
|
|
||||||
URI module_uri
|
|
||||||
VERSION module_version
|
|
||||||
PLUGIN_TARGET module_plugin_target
|
|
||||||
TARGET_PATH module_target_path
|
|
||||||
QMLDIR module_qmldir
|
|
||||||
TYPEINFO module_typeinfo
|
|
||||||
)
|
|
||||||
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
|
|
||||||
install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
|
||||||
install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
|
||||||
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
|
||||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
|
||||||
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
|
qml_module(ZShell
|
||||||
URI ZShell
|
URI ZShell
|
||||||
SOURCES
|
SOURCES
|
||||||
@@ -60,8 +8,6 @@ qml_module(ZShell
|
|||||||
toaster.hpp toaster.cpp
|
toaster.hpp toaster.cpp
|
||||||
qalculator.hpp qalculator.cpp
|
qalculator.hpp qalculator.cpp
|
||||||
zutils.hpp zutils.cpp
|
zutils.hpp zutils.cpp
|
||||||
RESOURCES
|
|
||||||
"${SETTINGS_INDEX_JSON}"
|
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
@@ -69,9 +15,13 @@ qml_module(ZShell
|
|||||||
Qt::Sql
|
Qt::Sql
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
PkgConfig::Qalculate
|
PkgConfig::Qalculate
|
||||||
|
zshell-util
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(ZShell PRIVATE ZSHELL_VERSION="${VERSION}")
|
target_compile_definitions(ZShell PRIVATE
|
||||||
|
ZSHELL_VERSION="${VERSION}"
|
||||||
|
GIT_REVISION="${GIT_REVISION}"
|
||||||
|
)
|
||||||
|
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Llm : public ConfigObject {
|
|||||||
CFG_PROPERTY(QString, model, "")
|
CFG_PROPERTY(QString, model, "")
|
||||||
CFG_PROPERTY(double, temperature, 0.7)
|
CFG_PROPERTY(double, temperature, 0.7)
|
||||||
CFG_PROPERTY(bool, tools, true)
|
CFG_PROPERTY(bool, tools, true)
|
||||||
|
CFG_PROPERTY(bool, enabled, true)
|
||||||
CONFIG_SUBOBJECT(LlmAppearance, appearance)
|
CONFIG_SUBOBJECT(LlmAppearance, appearance)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QtQuick/qquickwindow.h>
|
#include <QtQuick/qquickwindow.h>
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
|
#include <qdiriterator.h>
|
||||||
#include <qfileinfo.h>
|
#include <qfileinfo.h>
|
||||||
#include <qfuturewatcher.h>
|
#include <qfuturewatcher.h>
|
||||||
#include <qjsprimitivevalue.h>
|
#include <qjsprimitivevalue.h>
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qfile.h>
|
#include <qfile.h>
|
||||||
|
|
||||||
|
#include "util/metaenum.hpp"
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg)
|
Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg)
|
||||||
|
|
||||||
namespace ZShell {
|
namespace ZShell {
|
||||||
@@ -171,13 +174,44 @@ qreal ZUtils::clamp(qreal value, qreal min, qreal max) {
|
|||||||
return qBound(min, value, max);
|
return qBound(min, value, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ZUtils::settingsIndex() {
|
QString ZUtils::enumToString(
|
||||||
QFile file(QStringLiteral(":/qt/qml/ZShell/settings-index.json"));
|
QObject* target, const QString& property, const QVariant& value) {
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (!target) {
|
||||||
qCWarning(lcZUtils) << "Failed to open embedded settings index";
|
qCWarning(lcZUtils) << "enumToString: a target is required";
|
||||||
return QString();
|
return {};
|
||||||
}
|
}
|
||||||
return QString::fromUtf8(file.readAll());
|
|
||||||
|
const auto* meta = target->metaObject();
|
||||||
|
const auto index = meta->indexOfProperty(property.toUtf8().constData());
|
||||||
|
if (index < 0) {
|
||||||
|
qCWarning(lcZUtils)
|
||||||
|
<< "enumToString:" << target << "has no property" << property;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto prop = meta->property(index);
|
||||||
|
const auto metaEnum = prop.isEnumType()
|
||||||
|
? prop.enumerator()
|
||||||
|
: util::metaEnumFor(prop.metaType());
|
||||||
|
if (!metaEnum.isValid() || metaEnum.is64Bit()) {
|
||||||
|
qCWarning(lcZUtils) << "enumToString: property" << property << "of"
|
||||||
|
<< target << "is not a supported enum";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto val = value.isValid() ? value : prop.read(target);
|
||||||
|
const auto* key = util::enumKeyFor(metaEnum, val);
|
||||||
|
if (!key) {
|
||||||
|
qCWarning(
|
||||||
|
lcZUtils,
|
||||||
|
"enumToString: no enumerator of %s::%s has the value %lld",
|
||||||
|
metaEnum.scope(),
|
||||||
|
metaEnum.name(),
|
||||||
|
val.toLongLong());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString::fromUtf8(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef ZSHELL_VERSION
|
#ifndef ZSHELL_VERSION
|
||||||
@@ -192,4 +226,46 @@ QString ZUtils::qtVersion() const {
|
|||||||
return QStringLiteral(QT_VERSION_STR);
|
return QStringLiteral(QT_VERSION_STR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ZUtils::gitRevision() {
|
||||||
|
#ifdef GIT_REVISION
|
||||||
|
return QStringLiteral(GIT_REVISION);
|
||||||
|
#else
|
||||||
|
return QString();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ZUtils::readTextFile(const QString& path) {
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return QString::fromUtf8(file.readAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ZUtils::writeTextFile(const QString& path, const QString& text) {
|
||||||
|
const QFileInfo info(path);
|
||||||
|
if (!QDir().mkpath(info.absolutePath())) {
|
||||||
|
qCWarning(lcZUtils) << "Failed to create directory for" << path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
qCWarning(lcZUtils) << "Failed to open" << path << "for writing";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return file.write(text.toUtf8()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ZUtils::listFiles(const QString& dir, const QString& suffix) {
|
||||||
|
QStringList out;
|
||||||
|
QDirIterator it(dir, QDirIterator::Subdirectories);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QString path = it.next();
|
||||||
|
if (it.fileInfo().isFile() && path.endsWith(suffix)) {
|
||||||
|
out.append(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ZShell
|
} // namespace ZShell
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
namespace ZShell {
|
namespace ZShell {
|
||||||
|
|
||||||
@@ -32,7 +34,24 @@ class ZUtils : public QObject {
|
|||||||
|
|
||||||
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
|
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
|
||||||
|
|
||||||
Q_INVOKABLE static QString settingsIndex();
|
Q_INVOKABLE static QString enumToString(
|
||||||
|
QObject* target,
|
||||||
|
const QString& property,
|
||||||
|
const QVariant& value = QVariant());
|
||||||
|
|
||||||
|
Q_INVOKABLE static QString gitRevision();
|
||||||
|
Q_INVOKABLE static QString readTextFile(const QString& path);
|
||||||
|
Q_INVOKABLE static bool writeTextFile(
|
||||||
|
const QString& path, const QString& text);
|
||||||
|
Q_INVOKABLE static QStringList listFiles(
|
||||||
|
const QString& dir, const QString& suffix);
|
||||||
|
|
||||||
|
Q_INVOKABLE static QQuickItem* findChild(
|
||||||
|
QQuickItem* root, const QString& name);
|
||||||
|
Q_INVOKABLE static QList<QQuickItem*> findChildren(
|
||||||
|
QQuickItem* root, const QString& name);
|
||||||
|
Q_INVOKABLE static QList<QQuickItem*> findChildrenMatching(
|
||||||
|
QQuickItem* root, const QString& pattern);
|
||||||
|
|
||||||
[[nodiscard]] QString version() const;
|
[[nodiscard]] QString version() const;
|
||||||
[[nodiscard]] QString qtVersion() const;
|
[[nodiscard]] QString qtVersion() const;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
add_library(zshell-pch INTERFACE)
|
||||||
|
target_precompile_headers(zshell-pch INTERFACE
|
||||||
|
<qobject.h>
|
||||||
|
<qqmlintegration.h>
|
||||||
|
<qstring.h>
|
||||||
|
<qqmlengine.h>
|
||||||
|
<qloggingcategory.h>
|
||||||
|
<qvariant.h>
|
||||||
|
<qtimer.h>
|
||||||
|
<qdir.h>
|
||||||
|
<qlist.h>
|
||||||
|
<qstringlist.h>
|
||||||
|
<qpointer.h>
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
message(STATUS "QML install dir: ${CMAKE_INSTALL_PREFIX}/${INSTALL_QMLDIR}")
|
||||||
|
|
||||||
|
function(qml_module arg_TARGET)
|
||||||
|
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;QML_SINGLETONS;DEPENDENCIES;IMPORTS;OPTIONAL_IMPORTS;DEFAULT_IMPORTS;LIBRARIES")
|
||||||
|
qt_add_qml_module(${arg_TARGET}
|
||||||
|
URI ${arg_URI}
|
||||||
|
SOURCES ${arg_SOURCES}
|
||||||
|
QML_FILES ${arg_QML_FILES} ${arg_QML_SINGLETONS}
|
||||||
|
DEPENDENCIES ${arg_DEPENDENCIES}
|
||||||
|
IMPORTS ${arg_IMPORTS}
|
||||||
|
OPTIONAL_IMPORTS ${arg_OPTIONAL_IMPORTS}
|
||||||
|
DEFAULT_IMPORTS ${arg_DEFAULT_IMPORTS}
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_query_qml_module(${arg_TARGET}
|
||||||
|
URI module_uri
|
||||||
|
PLUGIN_TARGET module_plugin_target
|
||||||
|
TARGET_PATH module_target_path
|
||||||
|
QMLDIR module_qmldir
|
||||||
|
TYPEINFO module_typeinfo
|
||||||
|
)
|
||||||
|
|
||||||
|
message(STATUS "Created QML module: ${module_uri}")
|
||||||
|
|
||||||
|
string(REPLACE "/" ";" uri_parts "${module_target_path}")
|
||||||
|
list(GET uri_parts 0 top_level)
|
||||||
|
set(backing_lib_dir "${INSTALL_QMLDIR}/${top_level}/lib")
|
||||||
|
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
|
||||||
|
|
||||||
|
install(TARGETS ${arg_TARGET}
|
||||||
|
LIBRARY DESTINATION "${backing_lib_dir}"
|
||||||
|
RUNTIME DESTINATION "${backing_lib_dir}"
|
||||||
|
)
|
||||||
|
install(TARGETS "${module_plugin_target}"
|
||||||
|
LIBRARY DESTINATION "${module_dir}"
|
||||||
|
RUNTIME DESTINATION "${module_dir}"
|
||||||
|
)
|
||||||
|
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
||||||
|
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||||
|
|
||||||
|
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
|
||||||
|
|
||||||
|
file(RELATIVE_PATH plugin_to_lib "/${module_target_path}" "/${top_level}/lib")
|
||||||
|
set_property(TARGET ${module_plugin_target} APPEND PROPERTY INSTALL_RPATH "$ORIGIN/${plugin_to_lib}")
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
|
||||||
|
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
|
||||||
|
if(NOT TARGET Sensors::Sensors)
|
||||||
|
add_library(Sensors::Sensors UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(Sensors::Sensors PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qmetaobject.h>
|
||||||
|
#include <qmetatype.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
inline QMetaEnum metaEnumFor(const QMetaType& type) {
|
||||||
|
const auto* meta = type.metaObject();
|
||||||
|
if (!meta) return {};
|
||||||
|
|
||||||
|
auto name = QByteArray(type.name());
|
||||||
|
if (const auto scope = name.lastIndexOf("::"); scope >= 0)
|
||||||
|
name = name.mid(scope + 2);
|
||||||
|
|
||||||
|
return meta->enumerator(meta->indexOfEnumerator(name.constData()));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool isSupportedEnum(const QMetaType& type) {
|
||||||
|
if (!type.flags().testFlag(QMetaType::IsEnumeration)) return false;
|
||||||
|
|
||||||
|
const auto metaEnum = metaEnumFor(type);
|
||||||
|
return metaEnum.isValid() && !metaEnum.is64Bit();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* enumKeyFor(const QMetaEnum& metaEnum, const QVariant& value) {
|
||||||
|
return metaEnum.valueToKey(static_cast<quint64>(value.toLongLong()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
from functools import cache
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
@cache
|
|
||||||
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, strict=False):
|
|
||||||
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"(?<!^)(?=[A-Z])", " ", label)
|
|
||||||
children[pos] = (main_icon, label, "")
|
|
||||||
for pos, (icon, label, section) in children.items():
|
|
||||||
if pos >= 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())
|
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
const STOPWORDS = [
|
||||||
|
"a",
|
||||||
|
"an",
|
||||||
|
"and",
|
||||||
|
"are",
|
||||||
|
"for",
|
||||||
|
"in",
|
||||||
|
"not",
|
||||||
|
"notification",
|
||||||
|
"of",
|
||||||
|
"on",
|
||||||
|
"or",
|
||||||
|
"out",
|
||||||
|
"the",
|
||||||
|
"to",
|
||||||
|
];
|
||||||
|
|
||||||
|
const FIELD_WEIGHT = {
|
||||||
|
title: 1.0,
|
||||||
|
keywords: 0.4,
|
||||||
|
};
|
||||||
|
const SKIP_LABELS = ["Muted", "None"];
|
||||||
|
|
||||||
|
function cleanLabel(text) {
|
||||||
|
return String(text ?? "")
|
||||||
|
.replace(/\s*\(?%\d+\)?/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_NAME_RE = /^\s*name:\s*qsTr\("([^"]+)"\)/;
|
||||||
|
const ROW_RE =
|
||||||
|
/^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|RowButton|InfoRow|PopupRow|DefaultRow|TextFieldRow|IconTextButton|TimeDialogSelect)\s*\{/;
|
||||||
|
const NAV_ROW_TYPES = ["NavRow", "IconTextButton"];
|
||||||
|
const LABEL_RE = /^\s*(?:label|text):\s*qsTr\("([^"]+)"\)/;
|
||||||
|
const ANCHOR_RE = /^\s*(?:property\s+string\s+)?settingAnchor:\s*"([^"]+)"/;
|
||||||
|
const CHECKED_RE = /^\s*checked:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*$/;
|
||||||
|
const ONTOGGLED_RE =
|
||||||
|
/^\s*onToggled:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*=\s*checked\s*$/;
|
||||||
|
const ICON_RE = /^\s*icon:\s*"([^"]+)"/;
|
||||||
|
const SUBTEXT_RE = /^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)/;
|
||||||
|
const SECTION_RE = /^\s*SectionHeader\s*\{/;
|
||||||
|
|
||||||
|
function tokenize(text) {
|
||||||
|
const toks = [];
|
||||||
|
for (const word of text.toLowerCase().split(/\s+/)) {
|
||||||
|
if (!word) continue;
|
||||||
|
const parts = word.split(/[^a-z0-9]+/).filter((p) => p);
|
||||||
|
for (const p of parts) {
|
||||||
|
if (!STOPWORDS.includes(p) && !toks.includes(p)) toks.push(p);
|
||||||
|
}
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const joined = parts.join("");
|
||||||
|
if (!toks.includes(joined)) toks.push(joined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverFiles(settingsDir, listFiles) {
|
||||||
|
const files = {};
|
||||||
|
for (const p of listFiles(`${settingsDir}/Pages`, ".qml")) {
|
||||||
|
const name = p.slice(p.lastIndexOf("/") + 1).replace(/\.qml$/, "");
|
||||||
|
files[name] = p;
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePageRegistry(settingsDir, readLines) {
|
||||||
|
const lines = readLines(`${settingsDir}/PageRegistry.qml`);
|
||||||
|
const out = [];
|
||||||
|
let inArray = false;
|
||||||
|
let depth = 0;
|
||||||
|
let label = null;
|
||||||
|
let icon = null;
|
||||||
|
for (const line of lines) {
|
||||||
|
const s = line.trim();
|
||||||
|
if (s.includes("pages:") && s.includes("[")) {
|
||||||
|
inArray = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inArray) continue;
|
||||||
|
if (s.startsWith("//")) continue;
|
||||||
|
if (s.startsWith("]")) break;
|
||||||
|
if (s.startsWith("{")) {
|
||||||
|
depth++;
|
||||||
|
label = icon = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (s.startsWith("}")) {
|
||||||
|
if (label !== null) out.push([icon || "tune", label]);
|
||||||
|
depth--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (depth >= 1) {
|
||||||
|
const m = PAGE_NAME_RE.exec(line);
|
||||||
|
if (m && label === null) label = m[1];
|
||||||
|
const mi = ICON_RE.exec(line);
|
||||||
|
if (mi && icon === null) icon = mi[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePageComps(settingsDir, readFile) {
|
||||||
|
const text = readFile(`${settingsDir}/PageCompRegistry.qml`);
|
||||||
|
const start = text.indexOf("pageComps:");
|
||||||
|
if (start === -1) return [];
|
||||||
|
const comps = [];
|
||||||
|
let current = null;
|
||||||
|
let depth = 0;
|
||||||
|
for (const raw of text.slice(start).split("\n")) {
|
||||||
|
const line = raw.split("//")[0];
|
||||||
|
const s = line.trim();
|
||||||
|
if (s.startsWith("]")) break;
|
||||||
|
const atTop = depth === 0;
|
||||||
|
depth +=
|
||||||
|
(line.match(/\{/g) ?? []).length -
|
||||||
|
(line.match(/\}/g) ?? []).length;
|
||||||
|
if (atTop && /^Component\s*\{/.test(s)) {
|
||||||
|
current = [];
|
||||||
|
comps.push(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current !== null) {
|
||||||
|
const m = /(^|\s)([A-Z][A-Za-z0-9]*)\s*\{\s*\}/.exec(s);
|
||||||
|
if (m) current.push(m[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return comps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupCrumbs(labels, icons) {
|
||||||
|
const outLabels = [];
|
||||||
|
const outIcons = [];
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
if (
|
||||||
|
outLabels.length > 0 &&
|
||||||
|
outLabels[outLabels.length - 1] === labels[i]
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
outLabels.push(labels[i]);
|
||||||
|
outIcons.push(icons[i]);
|
||||||
|
}
|
||||||
|
return [outLabels, outIcons];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNavMap(settingsDir, files, readFile, readLines) {
|
||||||
|
const comps = parsePageComps(settingsDir, readFile);
|
||||||
|
const registry = parsePageRegistry(settingsDir, readLines);
|
||||||
|
|
||||||
|
const navChildren = {};
|
||||||
|
for (const names of comps) {
|
||||||
|
for (const name of names) {
|
||||||
|
const pf = files[name];
|
||||||
|
if (!pf) continue;
|
||||||
|
let pendingIcon = null;
|
||||||
|
let pendingLabel = null;
|
||||||
|
let section = "";
|
||||||
|
let expectSection = false;
|
||||||
|
for (const ln of readLines(pf)) {
|
||||||
|
if (SECTION_RE.test(ln)) {
|
||||||
|
expectSection = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ml = LABEL_RE.exec(ln);
|
||||||
|
if (ml) {
|
||||||
|
if (expectSection) {
|
||||||
|
section = ml[1];
|
||||||
|
expectSection = false;
|
||||||
|
} else {
|
||||||
|
pendingLabel = ml[1];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mi = ICON_RE.exec(ln);
|
||||||
|
if (mi) pendingIcon = mi[1];
|
||||||
|
const mo = /openSubPage\((\d+)\)/.exec(ln);
|
||||||
|
if (mo) {
|
||||||
|
const pos = parseInt(mo[1], 10);
|
||||||
|
if (!navChildren[name]) navChildren[name] = {};
|
||||||
|
navChildren[name][pos] = [
|
||||||
|
pendingIcon || "tune",
|
||||||
|
pendingLabel || "",
|
||||||
|
section,
|
||||||
|
];
|
||||||
|
pendingIcon = pendingLabel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nav = {};
|
||||||
|
for (let topIdx = 0; topIdx < comps.length; topIdx++) {
|
||||||
|
const names = comps[topIdx];
|
||||||
|
if (names.length === 0) continue;
|
||||||
|
const main = names[0];
|
||||||
|
const [mainIcon, mainLabel] = registry[topIdx] ?? ["tune", main];
|
||||||
|
nav[main] = {
|
||||||
|
pageIdx: topIdx,
|
||||||
|
subPath: [],
|
||||||
|
crumbIcons: [mainIcon],
|
||||||
|
crumbLabels: [mainLabel],
|
||||||
|
};
|
||||||
|
const children = Object.assign({}, navChildren[main] ?? {});
|
||||||
|
const openedViaSubpage = [];
|
||||||
|
for (const owner in navChildren) {
|
||||||
|
const ownerGroup = comps.find((ns) => ns.includes(owner));
|
||||||
|
if (!ownerGroup) continue;
|
||||||
|
for (const kpos in navChildren[owner]) {
|
||||||
|
const k = parseInt(kpos, 10);
|
||||||
|
if (k < ownerGroup.length) openedViaSubpage.push(ownerGroup[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let pos = 1; pos < names.length; pos++) {
|
||||||
|
if (!(pos in children) && !openedViaSubpage.includes(names[pos])) {
|
||||||
|
let label = names[pos].replace(/(Detail)?Page$/, "");
|
||||||
|
label = label.replace(/(?<!^)(?=[A-Z])/g, " ");
|
||||||
|
children[pos] = [mainIcon, label, ""];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const posKey in children) {
|
||||||
|
const pos = parseInt(posKey, 10);
|
||||||
|
if (pos >= names.length) continue;
|
||||||
|
const [icon, label, section] = children[posKey];
|
||||||
|
const child = names[pos];
|
||||||
|
let labels = [mainLabel]
|
||||||
|
.concat(section ? [section] : [])
|
||||||
|
.concat([label]);
|
||||||
|
let icons = [mainIcon].concat(section ? [icon] : []).concat([icon]);
|
||||||
|
[labels, icons] = dedupCrumbs(labels, icons);
|
||||||
|
nav[child] = {
|
||||||
|
pageIdx: topIdx,
|
||||||
|
subPath: [pos],
|
||||||
|
crumbIcons: icons,
|
||||||
|
crumbLabels: labels,
|
||||||
|
};
|
||||||
|
const grandChildren = navChildren[child] ?? {};
|
||||||
|
for (const gposKey in grandChildren) {
|
||||||
|
const gpos = parseInt(gposKey, 10);
|
||||||
|
if (gpos >= names.length) continue;
|
||||||
|
const [gicon, glabel, gsection] = grandChildren[gposKey];
|
||||||
|
let glabels = labels
|
||||||
|
.concat(gsection ? [gsection] : [])
|
||||||
|
.concat([glabel]);
|
||||||
|
let gicons = icons
|
||||||
|
.concat(gsection ? [gicon] : [])
|
||||||
|
.concat([gicon]);
|
||||||
|
[glabels, gicons] = dedupCrumbs(glabels, gicons);
|
||||||
|
nav[names[gpos]] = {
|
||||||
|
pageIdx: topIdx,
|
||||||
|
subPath: [pos, gpos],
|
||||||
|
crumbIcons: gicons,
|
||||||
|
crumbLabels: glabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBlockEnd(lines, start) {
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = start; i < lines.length; i++) {
|
||||||
|
const line = lines[i].split("//")[0];
|
||||||
|
depth +=
|
||||||
|
(line.match(/\{/g) ?? []).length -
|
||||||
|
(line.match(/\}/g) ?? []).length;
|
||||||
|
if (i > start && depth <= 0) return i;
|
||||||
|
}
|
||||||
|
return lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSettings(files, nav, comps, readLines) {
|
||||||
|
const entries = [];
|
||||||
|
for (const comp in nav) {
|
||||||
|
const meta = nav[comp];
|
||||||
|
const pf = files[comp];
|
||||||
|
if (!pf) continue;
|
||||||
|
const lines = readLines(pf);
|
||||||
|
let section = "";
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (SECTION_RE.test(lines[i])) {
|
||||||
|
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
||||||
|
const m = LABEL_RE.exec(lines[j]);
|
||||||
|
if (m) {
|
||||||
|
section = m[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rowMatch = ROW_RE.exec(lines[i]);
|
||||||
|
if (!rowMatch) continue;
|
||||||
|
const rowType = rowMatch[1];
|
||||||
|
let label = null;
|
||||||
|
let anchor = null;
|
||||||
|
let subtext = null;
|
||||||
|
let checkedPath = null;
|
||||||
|
let toggledPath = null;
|
||||||
|
let targetPos = null;
|
||||||
|
const rowEnd = findBlockEnd(lines, i);
|
||||||
|
for (let j = i + 1; j < rowEnd; j++) {
|
||||||
|
if (label === null) {
|
||||||
|
const m = LABEL_RE.exec(lines[j]);
|
||||||
|
if (m) label = m[1];
|
||||||
|
}
|
||||||
|
if (anchor === null) {
|
||||||
|
const a = ANCHOR_RE.exec(lines[j]);
|
||||||
|
if (a) anchor = a[1];
|
||||||
|
}
|
||||||
|
if (subtext === null) {
|
||||||
|
const st = SUBTEXT_RE.exec(lines[j]);
|
||||||
|
if (st) subtext = st[1];
|
||||||
|
}
|
||||||
|
if (checkedPath === null) {
|
||||||
|
const ch = CHECKED_RE.exec(lines[j]);
|
||||||
|
if (ch) checkedPath = ch[1];
|
||||||
|
}
|
||||||
|
if (toggledPath === null) {
|
||||||
|
const tg = ONTOGGLED_RE.exec(lines[j]);
|
||||||
|
if (tg) toggledPath = tg[1];
|
||||||
|
}
|
||||||
|
if (NAV_ROW_TYPES.includes(rowType) && targetPos === null) {
|
||||||
|
const sp = /openSubPage\((\d+)\)/.exec(lines[j]);
|
||||||
|
if (sp) targetPos = parseInt(sp[1], 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const togglePath =
|
||||||
|
rowType === "ToggleRow" &&
|
||||||
|
checkedPath &&
|
||||||
|
checkedPath === toggledPath
|
||||||
|
? checkedPath
|
||||||
|
: "";
|
||||||
|
if (label && !SKIP_LABELS.includes(label) && anchor) {
|
||||||
|
const extra =
|
||||||
|
meta.crumbLabels.join(" ") +
|
||||||
|
" " +
|
||||||
|
section +
|
||||||
|
" " +
|
||||||
|
(subtext && !/%\d/.test(subtext) ? subtext : "");
|
||||||
|
const group = comps[meta.pageIdx] ?? [];
|
||||||
|
const targetSubPath =
|
||||||
|
targetPos !== null && targetPos < group.length
|
||||||
|
? meta.subPath.concat([targetPos])
|
||||||
|
: [];
|
||||||
|
entries.push({
|
||||||
|
rowType: rowType,
|
||||||
|
pageIdx: meta.pageIdx,
|
||||||
|
subPath: meta.subPath,
|
||||||
|
targetSubPath: targetSubPath,
|
||||||
|
crumbIcons: meta.crumbIcons,
|
||||||
|
crumbLabels: meta.crumbLabels,
|
||||||
|
trailKey: meta.crumbLabels.join("/"),
|
||||||
|
title: cleanLabel(label),
|
||||||
|
anchor: anchor,
|
||||||
|
section: section,
|
||||||
|
subtext: subtext && !/%\d/.test(subtext) ? subtext : "",
|
||||||
|
togglePath: togglePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergeInfoRows(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeInfoRows(entries) {
|
||||||
|
const out = [];
|
||||||
|
const merged = {};
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const isInfo = entry.rowType === "InfoRow";
|
||||||
|
delete entry.rowType;
|
||||||
|
|
||||||
|
if (!isInfo || !entry.section) {
|
||||||
|
out.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key =
|
||||||
|
entry.anchor.split("-")[0] +
|
||||||
|
"/" +
|
||||||
|
entry.trailKey +
|
||||||
|
"/" +
|
||||||
|
entry.section;
|
||||||
|
const existing = merged[key];
|
||||||
|
if (existing === undefined) {
|
||||||
|
entry.keywords = entry.title;
|
||||||
|
entry.title = entry.section;
|
||||||
|
merged[key] = entry;
|
||||||
|
out.push(entry);
|
||||||
|
} else {
|
||||||
|
existing.keywords += " " + entry.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInvertedAndRanking(entries) {
|
||||||
|
const inverted = {};
|
||||||
|
const ranking = {};
|
||||||
|
for (let idx = 0; idx < entries.length; idx++) {
|
||||||
|
const e = entries[idx];
|
||||||
|
const extra =
|
||||||
|
e.crumbLabels.join(" ") +
|
||||||
|
" " +
|
||||||
|
e.section +
|
||||||
|
" " +
|
||||||
|
e.subtext +
|
||||||
|
" " +
|
||||||
|
(e.keywords ?? "");
|
||||||
|
const fields = {
|
||||||
|
title: e.title,
|
||||||
|
keywords: tokenize(e.title + " " + extra)
|
||||||
|
.sort()
|
||||||
|
.join(" "),
|
||||||
|
};
|
||||||
|
for (const field in fields) {
|
||||||
|
const weight = FIELD_WEIGHT[field] ?? 0.2;
|
||||||
|
for (const tok of tokenize(fields[field])) {
|
||||||
|
if (!inverted[tok]) inverted[tok] = [];
|
||||||
|
if (!inverted[tok].includes(idx)) inverted[tok].push(idx);
|
||||||
|
if (!ranking[tok]) ranking[tok] = {};
|
||||||
|
ranking[tok][idx] = Math.max(ranking[tok][idx] ?? 0.0, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const tok in inverted)
|
||||||
|
inverted[tok].sort((a, b) => ranking[tok][b] - ranking[tok][a]);
|
||||||
|
return [inverted, ranking];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndex(settingsDir, readFile, listFiles) {
|
||||||
|
const lineCache = {};
|
||||||
|
const readLines = (path) => {
|
||||||
|
if (!(path in lineCache)) lineCache[path] = readFile(path).split("\n");
|
||||||
|
return lineCache[path];
|
||||||
|
};
|
||||||
|
const files = discoverFiles(settingsDir, listFiles);
|
||||||
|
const nav = buildNavMap(settingsDir, files, readFile, readLines);
|
||||||
|
const comps = parsePageComps(settingsDir, readFile);
|
||||||
|
const entries = extractSettings(files, nav, comps, readLines);
|
||||||
|
const [inverted, ranking] = buildInvertedAndRanking(entries);
|
||||||
|
return {
|
||||||
|
version: 3,
|
||||||
|
entries: entries,
|
||||||
|
inverted: inverted,
|
||||||
|
ranking: ranking,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user