Krolyxon
Commit ed7f69d

Branches

Refs →

aurora.configquickshellauroraSetIndex.qml

170 lines · 9.5 KB · QML

1pragma Singleton
2import QtQuick
3import Quickshell
4import Quickshell.Io
5
6// The settings app's sections and its search index. Like Android indexes each preference XML, the rows are parsed out
7// of the page files themselves: Group / SectionLabel (a group: its literal `title:` / `text:`) and Switch / Knob /
8// Choice / Field / Timeout with a literal `label:`, plus their `hint:`, `keywords:` (synonyms, read only here) and a
9// Choice's option labels. IconBtn only with `keywords:` (opt-in). Non-literal labels aren't indexed. `extras` covers rows without a label (wi-fi, bluetooth power).
10// Editing a page hot-reloads the shell, which re-parses it.
11Singleton {
12 id: idx
13
14 // files: parsed in this order (a page's composite rows like BatteryInfo / GpuSwitch count as the page)
15 readonly property var sections: [
16 { id: "network", icon: Theme.iWifi, word: "network", caption: "network · wi-fi, wired, tethering", files: ["NetPage.qml"],
17 keywords: "wifi internet ethernet" },
18 { id: "bluetooth", icon: Theme.iBt, word: "bluetooth", caption: "bluetooth · devices, pairing", files: ["BtPage.qml"], keywords: "bt" },
19 { id: "sound", icon: Theme.iVolHigh, word: "sound", caption: "sound · levels, devices, apps", files: ["AudioPage.qml"],
20 keywords: "audio volume speakers microphone" },
21 { id: "appearance", icon: Theme.iPalette, word: "look", caption: "look · wallpaper, home screen, colours, bar", files: ["SetAppearance.qml"],
22 keywords: "appearance theme style" },
23 { id: "shell", icon: Theme.iDashboard, word: "shell", caption: "shell · home screen, dock, osd, notifications", files: ["SetShell.qml"],
24 keywords: "aurora" },
25 { id: "windows", icon: Theme.iWindows, word: "windows", caption: "windows · hyprland gaps, borders, effects, layout", files: ["SetWindows.qml"],
26 keywords: "hyprland tiling" },
27 { id: "input", icon: Theme.iMouse, word: "input", caption: "input · keyboard, mouse, touchpad", files: ["SetInput.qml"], keywords: "" },
28 { id: "keybinds", icon: Theme.iKeyboard, word: "keys", caption: "keys · hyprland binds", files: [],
29 keywords: "keybinds shortcuts hotkeys bindings" },
30 { id: "display", icon: Theme.iMonitor, word: "display", caption: "display · monitors, layout, night light", files: ["SetDisplay.qml"],
31 keywords: "screen monitor resolution" },
32 { id: "power", icon: Theme.iBolt, word: "power", caption: "power · battery, profiles, idle", files: ["BatteryInfo.qml", "SetPower.qml", "GpuSwitch.qml"],
33 keywords: "energy sleep" },
34 { id: "about", icon: Theme.iArch, word: "about", caption: "about · this system", files: ["SetAbout.qml"], keywords: "info version" }
35 ]
36
37 // rows the parser can't see. group = the SectionLabel to land on ("" = the top of the section)
38 readonly property var extras: [
39 { section: "network", group: "", label: "wi-fi on / off", hint: "rescan for networks",
40 keywords: "wifi wireless wlan radio internet scan" },
41 { section: "network", group: "networks", label: "wi-fi networks", hint: "connect, password, forget",
42 keywords: "wifi ssid connect password forget hotspot" },
43 { section: "bluetooth", group: "", label: "bluetooth on / off", hint: "search for devices",
44 keywords: "bt adapter radio discover scan" }
45 ]
46
47 readonly property string dir: Quickshell.env("HOME") + "/.config/quickshell/aurora/"
48 property var parsed: ({}) // file -> [{kind, group, label, hint, keywords, context}]
49
50 readonly property var files: {
51 var out = []
52 for (var i = 0; i < sections.length; i++)
53 for (var j = 0; j < sections[i].files.length; j++) out.push(sections[i].files[j])
54 return out
55 }
56 Variants {
57 model: idx.files
58 FileView {
59 required property string modelData
60 path: idx.dir + modelData
61 onLoaded: idx.parse(modelData, text())
62 }
63 }
64
65 // every entry: {kind: section|group|row, section, word, group, label, hint, order, l, k, c, h}
66 // l/k/c/h = lowercased label / keywords (own synonyms, options) / context (section, group + its keywords) / hint,
67 // for search()
68 readonly property var entries: {
69 var out = []
70 function add(s, e) {
71 out.push({ kind: e.kind, section: s.id, word: s.word, group: e.group, label: e.label, hint: e.hint,
72 order: out.length, l: norm(e.label), k: norm(e.keywords),
73 c: norm(s.word + " " + s.id + " " + (e.context || "")), h: norm(e.hint) })
74 }
75 for (var i = 0; i < sections.length; i++) {
76 var s = sections[i]
77 add(s, { kind: "section", group: "", label: s.word, hint: s.caption.replace(/^[^·]*·\s*/, ""),
78 keywords: s.id + " " + s.keywords })
79 for (var j = 0; j < s.files.length; j++) {
80 var p = parsed[s.files[j]] || []
81 for (var n = 0; n < p.length; n++) add(s, p[n])
82 }
83 for (var x = 0; x < extras.length; x++)
84 if (extras[x].section === s.id)
85 add(s, { kind: "group", group: extras[x].group,
86 label: extras[x].label, hint: extras[x].hint, keywords: extras[x].keywords })
87 }
88 return out
89 }
90
91 // the text of `key: "literal"` at the block's own level, or null (expressions don't count)
92 function lit(top, key) {
93 var m = new RegExp("(?:^|[\\s;])" + key + ":\\s*\"((?:[^\"\\\\]|\\\\.)*)\"\\s*(?:;|\\n|$)").exec(top)
94 return m ? m[1].replace(/\\(.)/g, "$1") : null
95 }
96 // from just after a `{`: the block's own text (nested {} [] left out) and the nested text, skipping strings/comments
97 function block(t, i) {
98 var depth = 1, top = "", inner = "", q = ""
99 for (; i < t.length && depth > 0; i++) {
100 var ch = t[i]
101 if (q !== "") {
102 if (depth === 1) top += ch; else inner += ch
103 if (ch === "\\") { i++; if (depth === 1) top += t[i]; else inner += t[i] }
104 else if (ch === q) q = ""
105 continue
106 }
107 if (ch === "/" && t[i + 1] === "/") { var nl = t.indexOf("\n", i); i = (nl < 0 ? t.length : nl) - 1; continue }
108 if (ch === '"' || ch === "'" || ch === "`") q = ch
109 if (ch === "{" || ch === "[") { depth++; continue }
110 if (ch === "}" || ch === "]") { depth--; continue }
111 if (depth === 1) top += ch; else inner += ch
112 }
113 return { top: top, inner: inner }
114 }
115 function parse(file, t) {
116 var out = [], group = "", groupKw = "", re = /\b(Group|SectionLabel|Switch|Knob|Choice|Field|Timeout|IconBtn)\s*\{/g, m
117 while ((m = re.exec(t)) !== null) {
118 var b = block(t, re.lastIndex), kw = lit(b.top, "keywords")
119 if (m[1] === "Group" || m[1] === "SectionLabel") {
120 group = lit(b.top, m[1] === "Group" ? "title" : "text") || ""
121 groupKw = group !== "" ? kw || "" : ""
122 if (group !== "") out.push({ kind: "group", group: group, label: group, hint: "", keywords: groupKw, context: "" })
123 continue
124 }
125 var label = lit(b.top, "label")
126 if (!label || (m[1] === "IconBtn" && kw === null)) continue
127 var opts = []
128 if (m[1] === "Choice") {
129 var om, ore = /\blabel:\s*"([^"]*)"/g
130 while ((om = ore.exec(b.inner)) !== null) opts.push(om[1])
131 }
132 out.push({ kind: "row", group: group, label: label, hint: lit(b.top, "hint") || "",
133 keywords: (kw || "") + " " + opts.join(" "), context: group + " " + groupKw })
134 }
135 var p = Object.assign({}, parsed)
136 p[file] = out
137 parsed = p
138 }
139
140 // every word has to match the entry (its own text or its section/group); at least one its own text.
141 // own: label prefix > a label word > keywords > a hint word > inside a label word. Best first, then page order.
142 // A hyphenated word is indexed split and joined ("24-hour" -> "24 hour 24hour"), the query joins them ("wi-fi" = "wifi").
143 function norm(s) {
144 s = s.toLowerCase()
145 return " " + s.replace(/-/g, " ") + (s.indexOf("-") >= 0 ? " " + s.replace(/-/g, "") : "")
146 }
147 function search(q) {
148 var words = q.toLowerCase().replace(/-/g, "").trim().split(/\s+/).filter(function (w) { return w !== "" })
149 if (!words.length) return []
150 var hits = []
151 for (var i = 0; i < entries.length; i++) {
152 var e = entries[i], total = 0, own = false
153 for (var j = 0; j < words.length; j++) {
154 var w = words[j], at = e.l.indexOf(" " + w)
155 var sc = Math.max(at === 0 ? 1000 : at > 0 ? 500 : e.l.indexOf(w) > 0 ? 60 : 0,
156 e.k.indexOf(" " + w) >= 0 ? 250 : e.k.indexOf(w) >= 0 ? 150
157 : e.h.indexOf(" " + w) >= 0 ? 80 : 0)
158 if (sc > 0) own = true
159 else sc = e.c.indexOf(" " + w) >= 0 ? 200 : e.c.indexOf(w) >= 0 ? 100 : 0
160 if (sc <= 0) { total = 0; break }
161 total += sc
162 }
163 if (total > 0 && own) hits.push({ e: e, sc: total })
164 }
165 hits.sort(function (a, b) { return b.sc - a.sc || a.e.order - b.e.order })
166 var res = []
167 for (var n = 0; n < hits.length && n < 60; n++) res.push(hits[n].e)
168 return res
169 }
170}