From dc2a8fc8839d84e70e33ebc4c4649d86b8431539 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 23:29:42 +0200 Subject: [PATCH] feat(quickshell): scratchpad ring in the bar, keyboard layout in the OSD The Super+S scratchpad gets a ring after the workspace pips while it holds windows, filled while it is open; clicking it toggles it. Switching keyboard layout flashes the OSD with the short name (US, Dvorak) and the full keymap underneath. --- quickshell/README.md | 5 ++-- quickshell/bar/Workspaces.qml | 53 +++++++++++++++++++++++++++++++++++ quickshell/overlays/Osd.qml | 40 +++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/quickshell/README.md b/quickshell/README.md index cf69553..32655af 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -39,7 +39,8 @@ cost 4.4% of the height on top of what the bar already takes. - **hull** — `bar/Frame.qml`: one surface round the whole screen, with the desktop as a rounded opening. The bar and the rail sit on it. -- **bar** — identity and workspaces on the left; the focused window, the +- **bar** — identity and workspaces on the left (a ring after them while the + `Super+S` scratchpad holds windows, filled while it is open); the focused window, the player, the CPU/MEM/GPU readout and the clock on the right. - **rail** — network, bluetooth, audio, mic, backlight, battery, tray, notifications, power. Popouts grow leftward out of it, into the screen. @@ -64,7 +65,7 @@ components/ the primitives: panels, sliders, list rows, display text services/ data sources, all singletons bar/ the two surfaces and their modules popouts/ hover panels hanging off rail and bar modules -overlays/ power menu, notification toasts, volume/brightness OSD +overlays/ power menu, notification toasts, the volume/brightness/keyboard OSD, idle dim lock/ session lock overview/ every monitor, workspace and window at once backdrop/ the wallpaper, drawn by the shell diff --git a/quickshell/bar/Workspaces.qml b/quickshell/bar/Workspaces.qml index 15dbe13..d86c442 100644 --- a/quickshell/bar/Workspaces.qml +++ b/quickshell/bar/Workspaces.qml @@ -155,5 +155,58 @@ Item { } } } + + // The Super+S scratchpad, after a gap: a ring while it holds windows, + // filled with the light while it is open on this monitor. Absent when + // it is empty, which is most of the time. + Item { + id: scratch + readonly property var ws: { + for (const w of (Hyprland.workspaces?.values ?? [])) + if (w.name === "special:virtual") return w; + return null; + } + readonly property int count: scratch.ws?.toplevels?.values?.length ?? 0 + readonly property bool shown: (root.monitor?.lastIpcObject?.specialWorkspace?.name ?? "") + === "special:virtual" + + visible: scratch.count > 0 + width: 8 + 10 + height: 14 + + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 10 + height: 10 + radius: 5 + color: scratch.shown ? Theme.accent : "transparent" + border.width: scratch.shown ? 0 : 1 + border.color: scratchHover.hovered ? Theme.text : Theme.subtext + + Behavior on color { + ColorAnimation { duration: Theme.durBase } + } + } + + HoverHandler { + id: scratchHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: Hyprland.dispatch("hl.dsp.workspace.toggle_special(\"virtual\")") + } + } + } + + // The monitor snapshot carries which special workspace is open; it is only + // refreshed on request. + Connections { + target: Hyprland + function onRawEvent(event) { + if (event.name === "activespecial" || event.name === "activespecialv2") + Hyprland.refreshMonitors(); + } } } diff --git a/quickshell/overlays/Osd.qml b/quickshell/overlays/Osd.qml index 8906e90..795e1bd 100644 --- a/quickshell/overlays/Osd.qml +++ b/quickshell/overlays/Osd.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Hyprland import Quickshell.Wayland import "root:/config" import "root:/components" @@ -44,6 +45,7 @@ PanelWindow { switch (root.kind) { case "mic": return Audio.micIcon; case "brightness": return Brightness.icon; + case "layout": return "󰌌"; default: return Audio.icon; } } @@ -52,6 +54,7 @@ PanelWindow { switch (root.kind) { case "mic": return Audio.micMuted ? "Mic muted" : "Microphone"; case "brightness": return "Brightness"; + case "layout": return "Keyboard"; default: return Audio.muted ? "Muted" : "Volume"; } } @@ -71,6 +74,7 @@ PanelWindow { // silenced speaker flashed the same colour as a live one. case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent; case "brightness": return Theme.accent; + case "layout": return Theme.accent; default: return Audio.muted ? Theme.treatmentOffText : Theme.accent; } } @@ -100,6 +104,26 @@ PanelWindow { function onBumped() { root.flash("brightness"); } } + // The keymap Hyprland just switched to, e.g. "English (Dvorak, intl., + // with dead keys)", and a short name for the readout: the variant in the + // brackets if there is one, otherwise the language. + property string keymap: "" + readonly property string keymapShort: { + const m = /\(([^,)]+)/.exec(root.keymap); + return (m ? m[1] : root.keymap).trim(); + } + + Connections { + target: Hyprland + function onRawEvent(event) { + if (event.name !== "activelayout") return; + // "keyboard-name,Layout name" — the layout name may hold commas. + const at = event.data.indexOf(","); + root.keymap = at < 0 ? event.data : event.data.slice(at + 1); + root.flash("layout"); + } + } + P5Panel { id: panel anchors.fill: parent @@ -178,12 +202,26 @@ PanelWindow { // width and right-aligning the text would flatten the // glitch flicker away. The title's left edge is anchored, so a // widening readout only moves its own left edge. - text: Math.round(root.level * 100) + "%" + text: root.kind === "layout" ? root.keymapShort : Math.round(root.level * 100) + "%" pixelSize: Theme.fsBody } } + // The layout has no level; its full name stands where the meter + // would be. + Text { + visible: root.kind === "layout" + width: parent.width + text: root.keymap + color: Theme.muted + elide: Text.ElideRight + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + renderType: Text.NativeRendering + } + MeterBar { + visible: root.kind !== "layout" width: parent.width height: 4 segments: 20