summaryrefslogtreecommitdiff
path: root/modules/bar
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/bar/components/Clock.qml1
-rw-r--r--modules/bar/components/StatusIcons.qml3
-rw-r--r--modules/bar/components/workspaces/OccupiedBg.qml11
-rw-r--r--modules/bar/popouts/Battery.qml1
-rw-r--r--modules/bar/popouts/Bluetooth.qml1
-rw-r--r--modules/bar/popouts/Content.qml7
-rw-r--r--modules/bar/popouts/KbLayout.qml28
-rw-r--r--modules/bar/popouts/Network.qml20
-rw-r--r--modules/bar/popouts/TrayMenu.qml4
-rw-r--r--modules/bar/popouts/Wrapper.qml6
-rw-r--r--modules/bar/popouts/kblayout/KbLayout.qml158
-rw-r--r--modules/bar/popouts/kblayout/KbLayoutModel.qml200
12 files changed, 381 insertions, 59 deletions
diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml
index be29c0b..801e93d 100644
--- a/modules/bar/components/Clock.qml
+++ b/modules/bar/components/Clock.qml
@@ -17,7 +17,6 @@ Column {
active: Config.bar.clock.showIcon
visible: active
- asynchronous: true
sourceComponent: MaterialIcon {
text: "calendar_month"
diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml
index 2f52596..442bd2c 100644
--- a/modules/bar/components/StatusIcons.qml
+++ b/modules/bar/components/StatusIcons.qml
@@ -143,7 +143,7 @@ StyledRect {
// Network icon
WrappedLoader {
name: "network"
- active: Config.bar.status.showNetwork
+ active: Config.bar.status.showNetwork && (! Nmcli.activeEthernet || Config.bar.status.showWifi)
sourceComponent: MaterialIcon {
animate: true
@@ -265,7 +265,6 @@ StyledRect {
required property string name
Layout.alignment: Qt.AlignHCenter
- asynchronous: true
visible: active
}
}
diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml
index 0364575..da6fa55 100644
--- a/modules/bar/components/workspaces/OccupiedBg.qml
+++ b/modules/bar/components/workspaces/OccupiedBg.qml
@@ -16,12 +16,15 @@ Item {
property list<var> pills: []
onOccupiedChanged: {
+ if (!occupied) return;
let count = 0;
const start = groupOffset;
const end = start + Config.bar.workspaces.shown;
for (const [ws, occ] of Object.entries(occupied)) {
if (ws > start && ws <= end && occ) {
- if (!occupied[ws - 1]) {
+ const isFirstInGroup = Number(ws) === start + 1;
+ const isLastInGroup = Number(ws) === end;
+ if (isFirstInGroup || !occupied[ws - 1]) {
if (pills[count])
pills[count].start = ws;
else
@@ -30,7 +33,7 @@ Item {
}));
count++;
}
- if (!occupied[ws + 1])
+ if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1])
pills[count - 1].end = ws;
}
}
@@ -48,8 +51,8 @@ Item {
required property var modelData
- readonly property Workspace start: root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null
- readonly property Workspace end: root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null
+ readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null
+ readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null
function getWsIdx(ws: int): int {
let i = ws - 1;
diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml
index 35d32c5..ac975e1 100644
--- a/modules/bar/popouts/Battery.qml
+++ b/modules/bar/popouts/Battery.qml
@@ -40,7 +40,6 @@ Column {
anchors.horizontalCenter: parent.horizontalCenter
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
- asynchronous: true
height: active ? (item?.implicitHeight ?? 0) : 0
diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml
index 91ac560..676da82 100644
--- a/modules/bar/popouts/Bluetooth.qml
+++ b/modules/bar/popouts/Bluetooth.qml
@@ -142,7 +142,6 @@ ColumnLayout {
}
Loader {
- asynchronous: true
active: device.modelData.bonded
sourceComponent: Item {
implicitWidth: connectBtn.implicitWidth
diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml
index da993fa..c9a7c5d 100644
--- a/modules/bar/popouts/Content.qml
+++ b/modules/bar/popouts/Content.qml
@@ -6,6 +6,8 @@ import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
+import "./kblayout"
+
Item {
id: root
@@ -114,9 +116,12 @@ Item {
Popout {
name: "kblayout"
- sourceComponent: KbLayout {}
+ sourceComponent: KbLayout {
+ wrapper: root.wrapper
+ }
}
+
Popout {
name: "lockstatus"
sourceComponent: LockStatus {}
diff --git a/modules/bar/popouts/KbLayout.qml b/modules/bar/popouts/KbLayout.qml
deleted file mode 100644
index ace5af2..0000000
--- a/modules/bar/popouts/KbLayout.qml
+++ /dev/null
@@ -1,28 +0,0 @@
-import qs.components
-import qs.components.controls
-import qs.services
-import qs.config
-import Quickshell
-import QtQuick.Layouts
-
-ColumnLayout {
- id: root
-
- spacing: Appearance.spacing.normal
-
- StyledText {
- Layout.topMargin: Appearance.padding.normal
- Layout.rightMargin: Appearance.padding.normal
- text: qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull)
- font.weight: 500
- }
-
- TextButton {
- Layout.bottomMargin: Appearance.padding.normal
- Layout.rightMargin: Appearance.padding.normal
- Layout.fillWidth: true
-
- text: qsTr("Switch layout")
- onClicked: Hypr.extras.message("switchxkblayout all next")
- }
-}
diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml
index 0e99613..5b32e4a 100644
--- a/modules/bar/popouts/Network.qml
+++ b/modules/bar/popouts/Network.qml
@@ -131,17 +131,13 @@ ColumnLayout {
Nmcli.disconnectFromNetwork();
} else {
root.connectingToSsid = networkItem.modelData.ssid;
- NetworkConnection.handleConnect(
- networkItem.modelData,
- null,
- (network) => {
- // Password is required - show password dialog
- root.passwordNetwork = network;
- root.showPasswordDialog = true;
- root.wrapper.currentName = "wirelesspassword";
- }
- );
-
+ NetworkConnection.handleConnect(networkItem.modelData, null, network => {
+ // Password is required - show password dialog
+ root.passwordNetwork = network;
+ root.showPasswordDialog = true;
+ root.wrapper.currentName = "wirelesspassword";
+ });
+
// Clear connecting state if connection succeeds immediately (saved profile)
// This is handled by the onActiveChanged connection below
}
@@ -216,7 +212,7 @@ ColumnLayout {
anchors.centerIn: parent
strokeWidth: Appearance.padding.small / 2
bgColour: "transparent"
- implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2
+ implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2
running: Nmcli.scanning
}
}
diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml
index c87c0e1..9b743db 100644
--- a/modules/bar/popouts/TrayMenu.qml
+++ b/modules/bar/popouts/TrayMenu.qml
@@ -85,7 +85,6 @@ StackView {
anchors.right: parent.right
active: !item.modelData.isSeparator
- asynchronous: true
sourceComponent: Item {
implicitHeight: label.implicitHeight
@@ -118,7 +117,6 @@ StackView {
anchors.left: parent.left
active: item.modelData.icon !== ""
- asynchronous: true
sourceComponent: IconImage {
implicitSize: label.implicitHeight
@@ -155,7 +153,6 @@ StackView {
anchors.right: parent.right
active: item.modelData.hasChildren
- asynchronous: true
sourceComponent: MaterialIcon {
text: "chevron_right"
@@ -169,7 +166,6 @@ StackView {
Loader {
active: menu.isSubMenu
- asynchronous: true
sourceComponent: Item {
implicitWidth: back.implicitWidth
diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml
index 5ef4f9d..fc74222 100644
--- a/modules/bar/popouts/Wrapper.qml
+++ b/modules/bar/popouts/Wrapper.qml
@@ -35,8 +35,8 @@ Item {
if (mode === "winfo") {
detachedMode = mode;
} else {
- detachedMode = "any";
queuedMode = mode;
+ detachedMode = "any";
}
focus = true;
}
@@ -101,7 +101,6 @@ Item {
id: content
shouldBeActive: root.hasCurrent && !root.detachedMode
- asynchronous: true
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -112,7 +111,6 @@ Item {
Comp {
shouldBeActive: root.detachedMode === "winfo"
- asynchronous: true
anchors.centerIn: parent
sourceComponent: WindowInfo {
@@ -123,7 +121,6 @@ Item {
Comp {
shouldBeActive: root.detachedMode === "any"
- asynchronous: true
anchors.centerIn: parent
sourceComponent: ControlCenter {
@@ -173,7 +170,6 @@ Item {
property bool shouldBeActive
- asynchronous: true
active: false
opacity: 0
diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml
new file mode 100644
index 0000000..f612f58
--- /dev/null
+++ b/modules/bar/popouts/kblayout/KbLayout.qml
@@ -0,0 +1,158 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import qs.components
+import qs.components.controls
+import qs.services
+import qs.config
+import qs.utils
+
+import "."
+
+ColumnLayout {
+ id: root
+
+ required property Item wrapper
+
+ spacing: Appearance.spacing.small
+ width: Config.bar.sizes.kbLayoutWidth
+
+ KbLayoutModel { id: kb }
+
+ function refresh() { kb.refresh() }
+ Component.onCompleted: kb.start()
+
+ StyledText {
+ Layout.topMargin: Appearance.padding.normal
+ Layout.rightMargin: Appearance.padding.small
+ text: qsTr("Keyboard Layouts")
+ font.weight: 500
+ }
+
+ ListView {
+ id: list
+ model: kb.visibleModel
+
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ Layout.topMargin: Appearance.spacing.small
+
+ clip: true
+ interactive: true
+ implicitHeight: Math.min(contentHeight, 320)
+ visible: kb.visibleModel.count > 0
+ spacing: Appearance.spacing.small
+
+ add: Transition {
+ NumberAnimation { properties: "opacity"; from: 0; to: 1; duration: 140 }
+ NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic }
+ }
+ remove: Transition { NumberAnimation { properties: "opacity"; to: 0; duration: 100 } }
+ move: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } }
+ displaced: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } }
+
+ delegate: Item {
+ required property int layoutIndex
+ required property string label
+
+ width: list.width
+ height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2)
+
+ readonly property bool isDisabled: layoutIndex > 3
+
+ StateLayer {
+ id: layer
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ implicitHeight: parent.height - 4
+
+ radius: Appearance.rounding.full
+ enabled: !isDisabled
+
+ function onClicked(): void {
+ if (!isDisabled)
+ kb.switchTo(layoutIndex);
+ }
+ }
+
+ StyledText {
+ id: rowText
+ anchors.verticalCenter: layer.verticalCenter
+ anchors.left: layer.left
+ anchors.right: layer.right
+ anchors.leftMargin: Appearance.padding.small
+ anchors.rightMargin: Appearance.padding.small
+ text: label
+ elide: Text.ElideRight
+ opacity: isDisabled ? 0.4 : 1.0
+ }
+
+ ToolTip.visible: isDisabled && layer.containsMouse
+ ToolTip.text: "XKB limitation: maximum 4 layouts allowed"
+ }
+ }
+
+ Rectangle {
+ visible: kb.activeLabel.length > 0
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ Layout.topMargin: Appearance.spacing.small
+
+ height: 1
+ color: Colours.palette.m3onSurfaceVariant
+ opacity: 0.35
+ }
+
+ RowLayout {
+ id: activeRow
+
+ visible: kb.activeLabel.length > 0
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ Layout.topMargin: Appearance.spacing.small
+ spacing: Appearance.spacing.small
+
+ opacity: 1
+ scale: 1
+
+ MaterialIcon {
+ text: "keyboard"
+ color: Colours.palette.m3primary
+ }
+
+ StyledText {
+ Layout.fillWidth: true
+ text: kb.activeLabel
+ elide: Text.ElideRight
+ font.weight: 500
+ color: Colours.palette.m3primary
+ }
+
+ Connections {
+ target: kb
+ function onActiveLabelChanged() {
+ if (!activeRow.visible)
+ return;
+ popIn.restart();
+ }
+ }
+
+ SequentialAnimation {
+ id: popIn
+ running: false
+
+ ParallelAnimation {
+ NumberAnimation { target: activeRow; property: "opacity"; to: 0.0; duration: 70 }
+ NumberAnimation { target: activeRow; property: "scale"; to: 0.92; duration: 70 }
+ }
+
+ ParallelAnimation {
+ NumberAnimation { target: activeRow; property: "opacity"; to: 1.0; duration: 160; easing.type: Easing.OutCubic }
+ NumberAnimation { target: activeRow; property: "scale"; to: 1.0; duration: 220; easing.type: Easing.OutBack }
+ }
+ }
+ }
+}
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;
+ }
+}