From a901d4a71fef1a30a25cd2b0b6fb29088a73c575 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Wed, 13 May 2026 09:17:01 -0400 Subject: [PATCH] Add 5 widgets inspired by Noctalia: activeWindow, nightLight, keyboardLayout, lockKeys, spacer - activeWindow: shows the focused toplevel title from Quickshell.Wayland ToplevelManager. Left-click activates, middle-click closes, right-click killactive. Hidden in vertical bars to avoid title rotation noise. - nightLight: toggles omarchy-toggle-nightlight; status detected via pgrep hyprsunset. Hides when hyprsunset isn't installed. - keyboardLayout: shows current xkb layout short code, click cycles via hyprctl switchxkblayout. Refreshes on Hyprland activelayout events. - lockKeys: caps/num/scroll lock indicators read from /sys/class/leds. Uses plain A/1/S letters; hides each indicator when off. - spacer: configurable blank space (size) for layout tuning. Defaults add activeWindow to the left section and nightLight to the right section between audioPanel and brightness. --- default/quickshell/bar/bar-defaults.json | 4 +- default/quickshell/bar/shell.qml | 7 +- .../quickshell/bar/widgets/activeWindow.qml | 71 ++++++++++++++ .../quickshell/bar/widgets/keyboardLayout.qml | 84 ++++++++++++++++ default/quickshell/bar/widgets/lockKeys.qml | 96 +++++++++++++++++++ default/quickshell/bar/widgets/nightLight.qml | 71 ++++++++++++++ default/quickshell/bar/widgets/spacer.qml | 16 ++++ 7 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 default/quickshell/bar/widgets/activeWindow.qml create mode 100644 default/quickshell/bar/widgets/keyboardLayout.qml create mode 100644 default/quickshell/bar/widgets/lockKeys.qml create mode 100644 default/quickshell/bar/widgets/nightLight.qml create mode 100644 default/quickshell/bar/widgets/spacer.qml diff --git a/default/quickshell/bar/bar-defaults.json b/default/quickshell/bar/bar-defaults.json index d39a3b0d..742f23d5 100644 --- a/default/quickshell/bar/bar-defaults.json +++ b/default/quickshell/bar/bar-defaults.json @@ -3,9 +3,9 @@ "fontFamily": "JetBrainsMono Nerd Font", "centerAnchor": "calendar", "layout": { - "left": ["omarchy", "workspacesPro"], + "left": ["omarchy", "workspacesPro", "activeWindow"], "center": ["media", "calendar", "weatherFlyout", "update", "voxtype", "screenRecording", "idle", "notifications"], - "right": ["tray", "systemStats", "microphone", "bluetoothPanel", "networkPanel", "audioPanel", "brightness", "powerProfile", "battery", "powerMenu"] + "right": ["tray", "systemStats", "microphone", "bluetoothPanel", "networkPanel", "audioPanel", "nightLight", "brightness", "powerProfile", "battery", "powerMenu"] }, "modules": { "calendar": { diff --git a/default/quickshell/bar/shell.qml b/default/quickshell/bar/shell.qml index 75e6ed64..bac94b2b 100644 --- a/default/quickshell/bar/shell.qml +++ b/default/quickshell/bar/shell.qml @@ -274,7 +274,12 @@ ShellRoot { "powerMenu": true, "idleInhibitor": true, "microphone": true, - "notificationCenter": true + "notificationCenter": true, + "activeWindow": true, + "nightLight": true, + "keyboardLayout": true, + "lockKeys": true, + "spacer": true }) function firstPartyWidgetSource(name) { diff --git a/default/quickshell/bar/widgets/activeWindow.qml b/default/quickshell/bar/widgets/activeWindow.qml new file mode 100644 index 00000000..6a3aad61 --- /dev/null +++ b/default/quickshell/bar/widgets/activeWindow.qml @@ -0,0 +1,71 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import "../common" as Common + +Item { + id: root + + property QtObject bar: null + property string moduleName: "activeWindow" + property var settings: ({}) + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + readonly property var toplevel: ToplevelManager.activeToplevel + readonly property string title: toplevel ? (toplevel.title || toplevel.appId || "") : "" + readonly property int maxLabelWidth: Number(setting("maxWidth", 280)) + + readonly property bool vertical: bar ? bar.vertical : false + + visible: title !== "" && !vertical + implicitWidth: visible ? Math.min(maxLabelWidth, labelText.implicitWidth) + 16 : 0 + implicitHeight: bar ? bar.barSize : 26 + + Behavior on implicitWidth { + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } + } + + Item { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + clip: true + + Text { + id: labelText + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + width: parent.width + text: root.title + color: root.bar ? root.bar.foreground : "#cacccc" + font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font" + font.pixelSize: 12 + elide: Text.ElideRight + opacity: 0.85 + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + + onClicked: function(mouse) { + if (!root.toplevel) return + if (mouse.button === Qt.MiddleButton) { + root.toplevel.close() + } else if (mouse.button === Qt.RightButton) { + if (root.bar) root.bar.run("hyprctl dispatch killactive") + } else { + root.toplevel.activate() + } + } + onEntered: if (root.bar) root.bar.showTooltip(root, root.title) + onExited: if (root.bar) root.bar.hideTooltip(root) + } +} diff --git a/default/quickshell/bar/widgets/keyboardLayout.qml b/default/quickshell/bar/widgets/keyboardLayout.qml new file mode 100644 index 00000000..99a5aa63 --- /dev/null +++ b/default/quickshell/bar/widgets/keyboardLayout.qml @@ -0,0 +1,84 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import "../common" as Common + +Item { + id: root + + property QtObject bar: null + property string moduleName: "keyboardLayout" + property var settings: ({}) + + property string layoutLabel: "" + property string layoutFull: "" + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + function refresh() { + if (!queryProc.running) queryProc.running = true + } + + function cycleLayout() { + Hyprland.dispatch("switchxkblayout current next") + refreshTimer.restart() + } + + Component.onCompleted: refresh() + + Connections { + target: Hyprland + function onRawEvent(event) { + if (!event || !event.name) return + if (String(event.name).indexOf("activelayout") !== -1) root.refresh() + } + } + + Process { + id: queryProc + command: ["bash", "-lc", "hyprctl -j devices 2>/dev/null | sed -n '/keyboards/,$p' | head -200"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + var match = String(text || "").match(/"active_keymap":\s*"([^"]+)"/) + if (!match) return + var full = match[1] + root.layoutFull = full + var token = full.split(/\s+/)[0] + root.layoutLabel = token.substring(0, 3).toUpperCase() + } + } + } + + Timer { + id: refreshTimer + interval: 600 + onTriggered: root.refresh() + } + + Timer { + interval: 10000 + running: true + repeat: true + onTriggered: root.refresh() + } + + visible: layoutLabel !== "" + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + Common.WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.layoutLabel + fontSize: 10 + horizontalMargin: 6 + tooltipText: root.layoutFull + onPressed: function() { root.cycleLayout() } + } +} diff --git a/default/quickshell/bar/widgets/lockKeys.qml b/default/quickshell/bar/widgets/lockKeys.qml new file mode 100644 index 00000000..1a30b50b --- /dev/null +++ b/default/quickshell/bar/widgets/lockKeys.qml @@ -0,0 +1,96 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "../common" as Common + +Item { + id: root + + property QtObject bar: null + property string moduleName: "lockKeys" + property var settings: ({}) + + property bool capsOn: false + property bool numOn: false + property bool scrollOn: false + property bool hideWhenOff: true + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + Component.onCompleted: { + hideWhenOff = setting("hideWhenOff", true) === true + refresh() + } + + function refresh() { + if (!stateProc.running) stateProc.running = true + } + + Process { + id: stateProc + command: ["bash", "-lc", "cat /sys/class/leds/input*::capslock/brightness 2>/dev/null | head -1; cat /sys/class/leds/input*::numlock/brightness 2>/dev/null | head -1; cat /sys/class/leds/input*::scrolllock/brightness 2>/dev/null | head -1"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + var lines = String(text || "").split("\n") + root.capsOn = parseInt(lines[0], 10) > 0 + root.numOn = parseInt(lines[1], 10) > 0 + root.scrollOn = parseInt(lines[2], 10) > 0 + } + } + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.refresh() + } + + readonly property bool anyOn: capsOn || numOn || scrollOn + visible: hideWhenOff ? anyOn : true + + readonly property bool vertical: bar ? bar.vertical : false + + implicitWidth: vertical ? (bar ? bar.barSize : 28) : (lay.item ? lay.item.implicitWidth + 8 : 0) + implicitHeight: vertical ? (lay.item ? lay.item.implicitHeight + 8 : 0) : (bar ? bar.barSize : 26) + + Loader { + id: lay + anchors.centerIn: parent + sourceComponent: root.vertical ? colLayout : rowLayout + } + + Component { + id: rowLayout + Row { + spacing: 4 + LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn } + LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn } + LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn } + } + } + + Component { + id: colLayout + Column { + spacing: 2 + LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn } + LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn } + LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn } + } + } + + component LockGlyph: Text { + property string glyph: "" + property bool active: false + + text: glyph + color: active ? (root.bar ? root.bar.foreground : "#cacccc") : Qt.rgba(0.7, 0.7, 0.7, 0.3) + font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font" + font.pixelSize: 11 + } +} diff --git a/default/quickshell/bar/widgets/nightLight.qml b/default/quickshell/bar/widgets/nightLight.qml new file mode 100644 index 00000000..d3ee2daf --- /dev/null +++ b/default/quickshell/bar/widgets/nightLight.qml @@ -0,0 +1,71 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "../common" as Common + +Item { + id: root + + property QtObject bar: null + property string moduleName: "nightLight" + property var settings: ({}) + + property bool active: false + property bool toolAvailable: false + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + function refresh() { + if (!statusProc.running) statusProc.running = true + } + + function toggle() { + if (root.bar) root.bar.run("omarchy-toggle-nightlight") + refreshTimer.restart() + } + + Component.onCompleted: refresh() + + Process { + id: statusProc + command: ["bash", "-lc", "if command -v hyprsunset >/dev/null && pgrep -x hyprsunset >/dev/null 2>&1; then echo on; elif command -v hyprsunset >/dev/null; then echo off; else echo missing; fi"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + var state = String(text || "").trim() + root.toolAvailable = state !== "missing" + root.active = state === "on" + } + } + } + + Timer { + id: refreshTimer + interval: 1200 + onTriggered: root.refresh() + } + + Timer { + interval: 10000 + running: true + repeat: true + onTriggered: root.refresh() + } + + visible: toolAvailable + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + Common.WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.active ? "󰖔" : "󰖙" + active: root.active + tooltipText: root.active ? "Night light on" : "Night light off" + onPressed: function() { root.toggle() } + } +} diff --git a/default/quickshell/bar/widgets/spacer.qml b/default/quickshell/bar/widgets/spacer.qml new file mode 100644 index 00000000..78a428ab --- /dev/null +++ b/default/quickshell/bar/widgets/spacer.qml @@ -0,0 +1,16 @@ +import QtQuick + +Item { + id: root + + property QtObject bar: null + property string moduleName: "spacer" + property var settings: ({}) + + readonly property bool vertical: bar ? bar.vertical : false + readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12 + + implicitWidth: vertical ? (bar ? bar.barSize : 28) : span + implicitHeight: vertical ? span : (bar ? bar.barSize : 26) + visible: span > 0 +}