summaryrefslogtreecommitdiff
path: root/modules/bar/popouts/kblayout/KbLayoutModel.qml
diff options
context:
space:
mode:
Diffstat (limited to 'modules/bar/popouts/kblayout/KbLayoutModel.qml')
-rw-r--r--modules/bar/popouts/kblayout/KbLayoutModel.qml200
1 files changed, 200 insertions, 0 deletions
diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml
new file mode 100644
index 0000000..41e45b3
--- /dev/null
+++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml
@@ -0,0 +1,200 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+
+import Quickshell
+import Quickshell.Io
+
+import qs.config
+import Caelestia
+
+Item {
+ id: model
+ visible: false
+
+ ListModel { id: _visibleModel }
+ property alias visibleModel: _visibleModel
+
+ property string activeLabel: ""
+ property int activeIndex: -1
+
+ function start() {
+ _xkbXmlBase.running = true;
+ _getKbLayoutOpt.running = true;
+ }
+
+ function refresh() {
+ _notifiedLimit = false;
+ _getKbLayoutOpt.running = true;
+ }
+
+ function switchTo(idx) {
+ _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)];
+ _switchProc.running = true;
+ }
+
+ ListModel { id: _layoutsModel }
+
+ property var _xkbMap: ({})
+ property bool _notifiedLimit: false
+
+ Process {
+ id: _xkbXmlBase
+ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"]
+ stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) }
+ onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) _xkbXmlEvdev.running = true
+ }
+
+ Process {
+ id: _xkbXmlEvdev
+ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"]
+ stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) }
+ }
+
+ function _buildXmlMap(xml) {
+ const map = {};
+
+ const re = /<name>\s*([^<]+?)\s*<\/name>[\s\S]*?<description>\s*([^<]+?)\s*<\/description>/g;
+
+ let m;
+ while ((m = re.exec(xml)) !== null) {
+ const code = (m[1] || "").trim();
+ const desc = (m[2] || "").trim();
+ if (!code || !desc) continue;
+ map[code] = _short(desc);
+ }
+
+ if (Object.keys(map).length === 0)
+ return;
+
+ _xkbMap = map;
+
+ if (_layoutsModel.count > 0) {
+ const tmp = [];
+ for (let i = 0; i < _layoutsModel.count; i++) {
+ const it = _layoutsModel.get(i);
+ tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) });
+ }
+ _layoutsModel.clear();
+ tmp.forEach(t => _layoutsModel.append(t));
+ _fetchActiveLayouts.running = true;
+ }
+ }
+
+ function _short(desc) {
+ const m = desc.match(/^(.*)\((.*)\)$/);
+ if (!m) return desc;
+ const lang = m[1].trim();
+ const region = m[2].trim();
+ const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase();
+ return `${lang} (${code})`;
+ }
+
+ Process {
+ id: _getKbLayoutOpt
+ command: ["hyprctl", "-j", "getoption", "input:kb_layout"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const j = JSON.parse(text);
+ const raw = (j?.str || j?.value || "").toString().trim();
+ if (raw.length) {
+ _setLayouts(raw);
+ _fetchActiveLayouts.running = true;
+ return;
+ }
+ } catch (e) {}
+ _fetchLayoutsFromDevices.running = true;
+ }
+ }
+ }
+
+ Process {
+ id: _fetchLayoutsFromDevices
+ command: ["hyprctl", "-j", "devices"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const dev = JSON.parse(text);
+ const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
+ const raw = (kb?.layout || "").trim();
+ if (raw.length) _setLayouts(raw);
+ } catch (e) {}
+ _fetchActiveLayouts.running = true;
+ }
+ }
+ }
+
+ Process {
+ id: _fetchActiveLayouts
+ command: ["hyprctl", "-j", "devices"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const dev = JSON.parse(text);
+ const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
+ const idx = kb?.active_layout_index ?? -1;
+
+ activeIndex = idx >= 0 ? idx : -1;
+ activeLabel =
+ (idx >= 0 && idx < _layoutsModel.count)
+ ? _layoutsModel.get(idx).label
+ : "";
+ } catch (e) {
+ activeIndex = -1;
+ activeLabel = "";
+ }
+
+ _rebuildVisible();
+ }
+ }
+ }
+
+ Process {
+ id: _switchProc
+ onRunningChanged: if (!running) _fetchActiveLayouts.running = true
+ }
+
+ function _setLayouts(raw) {
+ const parts = raw.split(",").map(s => s.trim()).filter(Boolean);
+ _layoutsModel.clear();
+
+ const seen = new Set();
+ let idx = 0;
+
+ for (const p of parts) {
+ if (seen.has(p)) continue;
+ seen.add(p);
+ _layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) });
+ idx++;
+ }
+ }
+
+ function _rebuildVisible() {
+ _visibleModel.clear();
+
+ let arr = [];
+ for (let i = 0; i < _layoutsModel.count; i++)
+ arr.push(_layoutsModel.get(i));
+
+ arr = arr.filter(i => i.layoutIndex !== activeIndex);
+ arr.forEach(i => _visibleModel.append(i));
+
+ if (!Config.utilities.toasts.kbLimit)
+ return;
+
+ if (_layoutsModel.count > 4) {
+ Toaster.toast(
+ qsTr("Keyboard layout limit"),
+ qsTr("XKB supports only 4 layouts at a time"),
+ "warning"
+ );
+ }
+ }
+
+ function _pretty(token) {
+ const code = token.replace(/\(.*\)$/, "").trim();
+ if (_xkbMap[code]) return code.toUpperCase() + " - " + _xkbMap[code];
+ return code.toUpperCase() + " - " + code;
+ }
+}