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.
This commit is contained in:
nvrl committed 2026-10-04 23:29:42 +02:00
1 parent b69f4bcf0d
commit dc2a8fc883
3 files changed
+95 -3

No files matched your search

+3 -2
View File
@@ -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 - **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. 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. player, the CPU/MEM/GPU readout and the clock on the right.
- **rail** — network, bluetooth, audio, mic, backlight, battery, tray, - **rail** — network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. Popouts grow leftward out of it, into the screen. 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 services/ data sources, all singletons
bar/ the two surfaces and their modules bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar 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 lock/ session lock
overview/ every monitor, workspace and window at once overview/ every monitor, workspace and window at once
backdrop/ the wallpaper, drawn by the shell backdrop/ the wallpaper, drawn by the shell
+53
View File
@@ -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();
}
} }
} }
+39 -1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
@@ -44,6 +45,7 @@ PanelWindow {
switch (root.kind) { switch (root.kind) {
case "mic": return Audio.micIcon; case "mic": return Audio.micIcon;
case "brightness": return Brightness.icon; case "brightness": return Brightness.icon;
case "layout": return "󰌌";
default: return Audio.icon; default: return Audio.icon;
} }
} }
@@ -52,6 +54,7 @@ PanelWindow {
switch (root.kind) { switch (root.kind) {
case "mic": return Audio.micMuted ? "Mic muted" : "Microphone"; case "mic": return Audio.micMuted ? "Mic muted" : "Microphone";
case "brightness": return "Brightness"; case "brightness": return "Brightness";
case "layout": return "Keyboard";
default: return Audio.muted ? "Muted" : "Volume"; default: return Audio.muted ? "Muted" : "Volume";
} }
} }
@@ -71,6 +74,7 @@ PanelWindow {
// silenced speaker flashed the same colour as a live one. // silenced speaker flashed the same colour as a live one.
case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent; case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent;
case "brightness": return Theme.accent; case "brightness": return Theme.accent;
case "layout": return Theme.accent;
default: return Audio.muted ? Theme.treatmentOffText : Theme.accent; default: return Audio.muted ? Theme.treatmentOffText : Theme.accent;
} }
} }
@@ -100,6 +104,26 @@ PanelWindow {
function onBumped() { root.flash("brightness"); } 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 { P5Panel {
id: panel id: panel
anchors.fill: parent anchors.fill: parent
@@ -178,12 +202,26 @@ PanelWindow {
// width and right-aligning the text would flatten the // width and right-aligning the text would flatten the
// glitch flicker away. The title's left edge is anchored, so a // glitch flicker away. The title's left edge is anchored, so a
// widening readout only moves its own left edge. // 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 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 { MeterBar {
visible: root.kind !== "layout"
width: parent.width width: parent.width
height: 4 height: 4
segments: 20 segments: 20