Extract panels out into their own top-level concern

This commit is contained in:
David Heinemeier Hansson
2026-05-20 13:51:39 +02:00
parent 3b9700682c
commit ea1b0e7ced
23 changed files with 154 additions and 152 deletions
+5 -5
View File
@@ -53,11 +53,11 @@ o.bind("SUPER + CTRL + ALT + T", "Show time", "omarchy-notification-time")
o.bind("SUPER + CTRL + ALT + B", "Show battery remaining", "omarchy-notification-battery") o.bind("SUPER + CTRL + ALT + B", "Show battery remaining", "omarchy-notification-battery")
o.bind("SUPER + CTRL + ALT + W", "Show weather", "omarchy-notification-weather") o.bind("SUPER + CTRL + ALT + W", "Show weather", "omarchy-notification-weather")
o.bind("SUPER + CTRL + A", "Audio panel", "omarchy-shell audioPanel toggle") o.bind("SUPER + CTRL + A", "Audio panel", "omarchy-shell panels.audio toggle")
o.bind("SUPER + CTRL + B", "Bluetooth panel", "omarchy-shell bluetoothPanel toggle") o.bind("SUPER + CTRL + B", "Bluetooth panel", "omarchy-shell panels.bluetooth toggle")
o.bind("SUPER + CTRL + D", "Display panel", "omarchy-shell monitorPanel toggle") o.bind("SUPER + CTRL + D", "Display panel", "omarchy-shell panels.monitor toggle")
o.bind("SUPER + CTRL + W", "Network panel", "omarchy-shell networkPanel toggle") o.bind("SUPER + CTRL + W", "Network panel", "omarchy-shell panels.network toggle")
o.bind("SUPER + CTRL + P", "Power panel", "omarchy-shell powerPanel toggle") o.bind("SUPER + CTRL + P", "Power panel", "omarchy-shell panels.power toggle")
o.bind("SUPER + CTRL + T", "Activity", { tui = "btop" }) o.bind("SUPER + CTRL + T", "Activity", { tui = "btop" })
o.bind("SUPER + CTRL + X", "Toggle dictation", "voxtype record toggle") o.bind("SUPER + CTRL + X", "Toggle dictation", "voxtype record toggle")
+1 -1
View File
@@ -7,7 +7,7 @@ sudo sed -i 's/^#\?AutoEnable=.*/AutoEnable=false/' /etc/bluetooth/main.conf
mkdir -p ~/.config/wireplumber/wireplumber.conf.d/ mkdir -p ~/.config/wireplumber/wireplumber.conf.d/
cp "$OMARCHY_PATH/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf" ~/.config/wireplumber/wireplumber.conf.d/ cp "$OMARCHY_PATH/default/wireplumber/wireplumber.conf.d/bluetooth-a2dp-autoconnect.conf" ~/.config/wireplumber/wireplumber.conf.d/
# Quickshell.Bluetooth has no Agent API, so the omarchy-shell bluetoothPanel # Quickshell.Bluetooth has no Agent API, so the omarchy-shell panels.bluetooth
# can't answer the auth prompts bluez issues during pair(). bt-agent registers # can't answer the auth prompts bluez issues during pair(). bt-agent registers
# a NoInputNoOutput agent on the system bus so pair() actually completes. # a NoInputNoOutput agent on the system bus so pair() actually completes.
mkdir -p ~/.config/systemd/user/ mkdir -p ~/.config/systemd/user/
+1 -1
View File
@@ -8,7 +8,7 @@ notify_update() {
notify_wifi() { notify_wifi() {
( (
action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph:󰖩 "Click to Setup Wi-Fi" -A "default=Setup") action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph:󰖩 "Click to Setup Wi-Fi" -A "default=Setup")
[[ $action == "default" ]] && omarchy-shell networkPanel toggle [[ $action == "default" ]] && omarchy-shell panels.network toggle
) >/dev/null 2>&1 & ) >/dev/null 2>&1 &
} }
+3 -3
View File
@@ -59,8 +59,8 @@ PanelWindow {
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property string barPos: bar ? bar.position : "top" readonly property string barPos: bar ? bar.position : "top"
function closePopout() { function close() {
if (owner && "closePopout" in owner) owner.closePopout() if (owner && "close" in owner) owner.close()
else root.open = false else root.open = false
} }
@@ -255,7 +255,7 @@ PanelWindow {
onExited: hoveringBar = false onExited: hoveringBar = false
onClicked: function(mouse) { onClicked: function(mouse) {
if (inBarRegion(mouse.x, mouse.y) && forwardBarClick(mouse.x, mouse.y, mouse.button)) return if (inBarRegion(mouse.x, mouse.y) && forwardBarClick(mouse.x, mouse.y, mouse.button)) return
root.closePopout() root.close()
} }
} }
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import Quickshell.Io
// Base item for shell panels. Panels are not bar widgets, but the bar may host
// or toggle them and injects the same ambient context while doing so. The base
// owns the shared IPC-backed open/close lifecycle; panel implementations own
// their button behavior, keyboard navigation, and content.
Item {
id: root
property QtObject bar: null
property string moduleName: ""
property var settings: ({})
property string ipcTarget: ""
property bool manageIpc: true
property alias controller: panelController
readonly property bool opened: panelController.open
function open() { panelController.show() }
function close() { panelController.hide() }
function toggle() { opened ? close() : open() }
PanelController {
id: panelController
}
property IpcHandler _ipc: manageIpc ? ipcComponent.createObject(root) : null
property Component ipcComponent: Component {
IpcHandler {
target: root.ipcTarget
function open(): void { root.open() }
function close(): void { root.close() }
function show(): void { root.open() }
function hide(): void { root.close() }
function toggle(): void { root.toggle() }
}
}
}
+4 -37
View File
@@ -1,49 +1,16 @@
import QtQuick import QtQuick
import Quickshell // Stores the open state for a shell panel. Panel owns the public lifecycle
import Quickshell.Io // methods and IPC wiring; this object only keeps state separate from the
// panel implementation's own properties.
// Owns the open/close lifecycle for a bar panel widget. Wraps the
// repetitive popupOpen + closePopout + toggle/show/hide IpcHandler triplet
// so each panel just declares one of these and binds its WidgetButton +
// KeyboardPanel to the exposed `open` property.
// //
// Usage: // Usage:
// PanelController { id: ctrl; ipcTarget: "audioPanel" } // PanelController { id: panelController }
//
// WidgetButton { onPressed: ctrl.toggle() }
// KeyboardPanel { open: ctrl.open; owner: ctrl; focusTarget: keyCatcher }
//
// The bar popout coordinator uses `owner` as a registry key, so each
// PanelController instance doubles as that key. KeyboardPanel calls
// `owner.closePopout()` when another panel grabs the slot.
//
// Set `manageIpc: false` when the panel needs to declare its own IpcHandler
// for additional methods on the same target (monitorPanel adds brightness +
// state). Quickshell only honors one IpcHandler per target, so the panel's
// handler must then also delegate toggle/show/hide to this controller.
QtObject { QtObject {
id: root id: root
// IPC target name. The bar pairs this with the bar widget's filename so a
// Hyprland keybind (`omarchy-shell <target> toggle`) summons the panel.
property string ipcTarget: ""
property bool manageIpc: true
property bool open: false property bool open: false
function toggle() { open = !open } function toggle() { open = !open }
function show() { if (!open) open = true } function show() { if (!open) open = true }
function hide() { open = false } function hide() { open = false }
function closePopout() { open = false }
property IpcHandler _ipc: manageIpc ? ipcComponent.createObject(root) : null
property Component ipcComponent: Component {
IpcHandler {
target: root.ipcTarget
function toggle(): void { root.toggle() }
function show(): void { root.show() }
function hide(): void { root.hide() }
}
}
} }
+1 -1
View File
@@ -12,7 +12,7 @@ import QtQuick
// anchors.fill: parent // anchors.fill: parent
// onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) } // onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) }
// onActivateRequested: root.activateCursor() // onActivateRequested: root.activateCursor()
// onCloseRequested: root.closePopout() // onCloseRequested: root.close()
// onDeleteRequested: root.deleteSelected() // onDeleteRequested: root.deleteSelected()
// onTextKey: function(t) { if (t === "r") root.refresh() } // onTextKey: function(t) { if (t === "r") root.refresh() }
// //
+3 -3
View File
@@ -55,8 +55,8 @@ PopupWindow {
return Math.round(Math.min(desired, maxHeight)) return Math.round(Math.min(desired, maxHeight))
} }
function closePopout() { function close() {
if (owner && "closePopout" in owner) owner.closePopout() if (owner && "close" in owner) owner.close()
else root.open = false else root.open = false
} }
@@ -80,7 +80,7 @@ PopupWindow {
HyprlandFocusGrab { HyprlandFocusGrab {
active: root.open && root.triggerMode === "click" active: root.open && root.triggerMode === "click"
windows: root.anchorWindow ? [root, root.anchorWindow] : [root] windows: root.anchorWindow ? [root, root.anchorWindow] : [root]
onCleared: root.closePopout() onCleared: root.close()
} }
anchor { anchor {
+1
View File
@@ -8,6 +8,7 @@ CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml KeyboardPanel 1.0 KeyboardPanel.qml
NumberField 1.0 NumberField.qml NumberField 1.0 NumberField.qml
Panel 1.0 Panel.qml
PanelActionButton 1.0 PanelActionButton.qml PanelActionButton 1.0 PanelActionButton.qml
PanelController 1.0 PanelController.qml PanelController 1.0 PanelController.qml
PanelKeyCatcher 1.0 PanelKeyCatcher.qml PanelKeyCatcher 1.0 PanelKeyCatcher.qml
+13 -11
View File
@@ -95,7 +95,7 @@ Item {
function requestPopout(owner) { function requestPopout(owner) {
if (activePopout === owner) return if (activePopout === owner) return
if (activePopout && "closePopout" in activePopout) activePopout.closePopout() if (activePopout && "close" in activePopout) activePopout.close()
activePopout = owner activePopout = owner
} }
@@ -277,16 +277,16 @@ Item {
return source ? Util.fileUrl(source) : "" return source ? Util.fileUrl(source) : ""
} }
// First-party widgets are registered with the BarWidgetRegistry at startup. // First-party modules are registered with the BarWidgetRegistry at startup.
// Each entry maps a widget id to its display metadata; the QML source lives // Each entry maps a module id to its display metadata; the QML source is
// at widgets/<id>.qml and is loaded asynchronously via Qt.createComponent. // loaded asynchronously via Qt.createComponent.
readonly property var firstPartyWidgetMetadata: ({ readonly property var firstPartyWidgetMetadata: ({
"media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false }, "media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false },
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false }, "audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false, sourceDir: "../panels", sourceName: "Audio" },
"monitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false }, "monitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Monitor" },
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false }, "networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Network" },
"powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false }, "powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" },
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false }, "bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" },
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" }, "calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false }, "notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false }, "systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
@@ -316,8 +316,10 @@ Item {
} }
function registerOneFirstPartyWidget(id) { function registerOneFirstPartyWidget(id) {
var url = Qt.resolvedUrl("widgets/" + id + ".qml")
var meta = firstPartyWidgetMetadata[id] || {} var meta = firstPartyWidgetMetadata[id] || {}
var sourceDir = meta.sourceDir || "widgets"
var sourceName = meta.sourceName || id
var url = Qt.resolvedUrl(sourceDir + "/" + sourceName + ".qml")
var enrichedMeta = { var enrichedMeta = {
displayName: meta.displayName || id, displayName: meta.displayName || id,
description: meta.description || "", description: meta.description || "",
@@ -1285,7 +1287,7 @@ Item {
property bool expanded: false property bool expanded: false
property bool managePopupOpen: false property bool managePopupOpen: false
function closePopout() { managePopupOpen = false } function close() { managePopupOpen = false }
// Re-resolve the tray's own entry settings whenever the bar layout reloads. // Re-resolve the tray's own entry settings whenever the bar layout reloads.
readonly property var trayEntry: { readonly property var trayEntry: {
+19 -10
View File
@@ -7,7 +7,8 @@ the shell for its whole session.
- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`) and points at `Bar.qml` as the entry point. - `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`) and points at `Bar.qml` as the entry point.
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly. - `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy. - `widgets/` holds first-party bar widgets.
- `../panels/` holds first-party panels that the bar can toggle by id.
- The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `shell-defaults.json` when the user has no file). - The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `shell-defaults.json` when the user has no file).
- `omarchy-style-bar-position` updates only the user shell.json file. - `omarchy-style-bar-position` updates only the user shell.json file.
@@ -49,15 +50,11 @@ Example `shell.json` (bar subtree only shown):
## Module catalogue ## Module catalogue
### First-party interactive widgets (in `widgets/`) ### First-party interactive widgets
| Name | What it does | Interactions | | Name | What it does | Interactions |
|---|---|---| |---|---|---|
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | | `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
| `audioPanel` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = popup · scroll = volume |
| `networkPanel` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui |
| `powerPanel` | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup |
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector | | `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | | `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal | | `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
@@ -65,11 +62,21 @@ Example `shell.json` (bar subtree only shown):
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle | | `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume | | `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume |
### First-party panels (in `../panels/`)
| Name | What it does | Interactions |
|---|---|---|
| `panels.audio` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = popup · scroll = volume |
| `panels.network` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui |
| `panels.power` | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup |
| `panels.bluetooth` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
| `panels.monitor` | Brightness and laptop display controls | left = popup |
### Built-in base modules (in `Bar.qml`) ### Built-in base modules (in `Bar.qml`)
`omarchy`, `workspaces`, `clock`, `update`, `indicators`, `tray`. `omarchy`, `workspaces`, `clock`, `update`, `indicators`, `tray`.
The `indicators` module loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `widgets/` above. The `indicators` module loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above.
## Orientation ## Orientation
@@ -162,9 +169,11 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup - `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator - `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
First-party widgets live in `widgets/<name>.qml` and are picked up by the First-party bar widgets live in `widgets/<name>.qml`; first-party panels
shell's `BarWidgetRegistry` at startup; reference one by `id` in any live in `../panels/<name>.qml` and expose IPC targets such as
layout list. `panels.audio`. The compatibility bar layout ids remain `audioPanel`,
`networkPanel`, and so on, and are picked up by the shell's
`BarWidgetRegistry` at startup; reference one by `id` in any layout list.
Third-party widgets ship as separate plugins under Third-party widgets ship as separate plugins under
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json` `~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
+1 -1
View File
@@ -13,7 +13,7 @@ BarWidget {
property date viewMonth: new Date() property date viewMonth: new Date()
property bool popupOpen: false property bool popupOpen: false
function closePopout() { popupOpen = false } function close() { popupOpen = false }
function setting(name, fallback) { function setting(name, fallback) {
var value = settings ? settings[name] : undefined var value = settings ? settings[name] : undefined
+1 -1
View File
@@ -33,7 +33,7 @@ BarWidget {
property bool popupOpen: false property bool popupOpen: false
function closePopout() { popupOpen = false } function close() { popupOpen = false }
property real maxLabelWidth: 180 property real maxLabelWidth: 180
visible: hasMedia visible: hasMedia
+1 -1
View File
@@ -42,7 +42,7 @@ BarWidget {
active: root.inUse active: root.inUse
tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live") tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live")
onPressed: function(b) { onPressed: function(b) {
if (b === Qt.MiddleButton) root.bar.run("omarchy-shell audioPanel toggle") if (b === Qt.MiddleButton) root.bar.run("omarchy-shell panels.audio toggle")
else root.toggleMute() else root.toggleMute()
} }
onWheelMoved: function(delta) { onWheelMoved: function(delta) {
@@ -10,10 +10,10 @@ BarWidget {
property bool popupOpen: false property bool popupOpen: false
function closePopout() { popupOpen = false } function close() { popupOpen = false }
// Always default to the pending tab when there's anything unseen, no // Always default to the pending tab when there's anything unseen, no
// matter how the popup was opened (click, keybind/IPC, or the closePopout // matter how the popup was opened (click, keybind/IPC, or the close
// path). Keeps the spec from drifting based on the user's last manual // path). Keeps the spec from drifting based on the user's last manual
// tab selection. // tab selection.
onPopupOpenChanged: { onPopupOpenChanged: {
+1 -1
View File
@@ -19,7 +19,7 @@ BarWidget {
property bool popupOpen: false property bool popupOpen: false
function closePopout() { popupOpen = false } function close() { popupOpen = false }
readonly property int historyLimit: 30 readonly property int historyLimit: 30
+1 -1
View File
@@ -10,7 +10,7 @@ BarWidget {
property bool popupOpen: false property bool popupOpen: false
function closePopout() { popupOpen = false } function close() { popupOpen = false }
function showPopup() { function showPopup() {
root.popupOpen = !root.popupOpen root.popupOpen = !root.popupOpen
+1 -1
View File
@@ -407,7 +407,7 @@ Item {
color: Qt.darker(root.foreground, 1.4) color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns cursorActive + focusSection + selectedIndex; each element binds hasCursor: root.cursorActive && root.focusSection === 'X' && root.selectedIndex === N, and onHovered flips cursorActive on while updating the same state. No initial highlight, then one highlight on screen once the keyboard or mouse enters. See plugins/bar/widgets/audioPanel.qml for the canonical recipe." text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns cursorActive + focusSection + selectedIndex; each element binds hasCursor: root.cursorActive && root.focusSection === 'X' && root.selectedIndex === N, and onHovered flips cursorActive on while updating the same state. No initial highlight, then one highlight on screen once the keyboard or mouse enters. See plugins/panels/Audio.qml for the canonical recipe."
} }
Text { Text {
width: parent.width width: parent.width
@@ -7,14 +7,10 @@ import Quickshell.Services.Pipewire
import qs.Ui import qs.Ui
import qs.Commons import qs.Commons
BarWidget { Panel {
id: root id: root
moduleName: "audioPanel" moduleName: "audioPanel"
ipcTarget: "panels.audio"
PanelController { id: ctrl; ipcTarget: "audioPanel" }
readonly property bool popupOpen: ctrl.open
function closePopout() { ctrl.hide() }
readonly property var sink: Pipewire.defaultAudioSink readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource readonly property var source: Pipewire.defaultAudioSource
@@ -214,8 +210,8 @@ BarWidget {
} }
} }
onPopupOpenChanged: { onOpenedChanged: {
if (popupOpen) { if (opened) {
focusSection = "output" focusSection = "output"
selectedIndex = -1 // first keyboard cursor reveal starts on the output slider selectedIndex = -1 // first keyboard cursor reveal starts on the output slider
cursorActive = false cursorActive = false
@@ -546,7 +542,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
Timer { Timer {
interval: 5000 interval: 5000
running: root.popupOpen running: root.opened
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true
@@ -560,7 +556,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
fontSize: Style.font.body fontSize: Style.font.body
onPressed: function(b) { onPressed: function(b) {
if (b === Qt.RightButton) root.toggleOutputMute() if (b === Qt.RightButton) root.toggleOutputMute()
else ctrl.toggle() else root.toggle()
} }
onWheelMoved: function(delta) { onWheelMoved: function(delta) {
@@ -574,7 +570,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
anchorItem: button anchorItem: button
owner: ctrl owner: ctrl
bar: root.bar bar: root.bar
open: ctrl.open open: root.opened
focusTarget: keyCatcher focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(370)) contentWidth: panel.fittedContentWidth(Style.space(370))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560)) contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
@@ -588,7 +584,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
else if (dx !== 0) root.adjustVolume(dx * 0.05) else if (dx !== 0) root.adjustVolume(dx * 0.05)
} }
onActivateRequested: if (root.cursorActive) root.activateCursor() onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout() onCloseRequested: root.close()
onTextKey: function(t) { onTextKey: function(t) {
// 'm' mutes whatever the cursor is on: focused section's slider // 'm' mutes whatever the cursor is on: focused section's slider
// for output/input, the focused stream for streams. // for output/input, the focused stream for streams.
@@ -6,13 +6,10 @@ import Quickshell.Bluetooth
import qs.Ui import qs.Ui
import qs.Commons import qs.Commons
BarWidget { Panel {
id: root id: root
moduleName: "bluetoothPanel" moduleName: "bluetoothPanel"
ipcTarget: "panels.bluetooth"
PanelController { id: ctrl; ipcTarget: "bluetoothPanel" }
readonly property bool popupOpen: ctrl.open
// Address -> true while we are waiting for a click-initiated pair to land // Address -> true while we are waiting for a click-initiated pair to land
// so we can chain trust + connect at root scope. Doing this in the row's // so we can chain trust + connect at root scope. Doing this in the row's
@@ -20,8 +17,6 @@ BarWidget {
// moment `paired` flips, before the row's handler reliably fires. // moment `paired` flips, before the row's handler reliably fires.
property var pendingPairAddresses: ({}) property var pendingPairAddresses: ({})
function closePopout() { ctrl.hide() }
readonly property var adapter: Bluetooth.defaultAdapter readonly property var adapter: Bluetooth.defaultAdapter
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : [] readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
@@ -219,8 +214,8 @@ BarWidget {
else if (dev.forget) dev.forget() else if (dev.forget) dev.forget()
} }
onPopupOpenChanged: { onOpenedChanged: {
if (popupOpen) { if (opened) {
if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 } if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
else { focusSection = "header"; selectedIndex = 1 } else { focusSection = "header"; selectedIndex = 1 }
@@ -303,7 +298,7 @@ BarWidget {
Connections { Connections {
target: root.adapter || null target: root.adapter || null
function onEnabledChanged() { function onEnabledChanged() {
if (root.popupOpen && root.adapter && root.adapter.enabled && !root.adapter.discovering) if (root.opened && root.adapter && root.adapter.enabled && !root.adapter.discovering)
root.adapter.discovering = true root.adapter.discovering = true
} }
} }
@@ -341,7 +336,7 @@ BarWidget {
onPressed: function(b) { onPressed: function(b) {
if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth") else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
else ctrl.toggle() else root.toggle()
} }
} }
@@ -350,7 +345,7 @@ BarWidget {
anchorItem: button anchorItem: button
owner: ctrl owner: ctrl
bar: root.bar bar: root.bar
open: ctrl.open open: root.opened
focusTarget: keyCatcher focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320)) contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(column.implicitHeight) contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -364,7 +359,7 @@ BarWidget {
else if (dx !== 0) root.moveCursorH(dx) else if (dx !== 0) root.moveCursorH(dx)
} }
onActivateRequested: if (root.cursorActive) root.activateCursor() onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout() onCloseRequested: root.close()
onDeleteRequested: if (root.cursorActive) root.deleteSelected() onDeleteRequested: if (root.cursorActive) root.deleteSelected()
Column { Column {
@@ -5,15 +5,14 @@ import Quickshell.Io
import qs.Ui import qs.Ui
import qs.Commons import qs.Commons
BarWidget { Panel {
id: root id: root
moduleName: "monitorPanel" moduleName: "monitorPanel"
ipcTarget: "panels.monitor"
manageIpc: false
// manageIpc: false so this panel can own the single IpcHandler the target // manageIpc: false so this panel can own the single IpcHandler the target
// permits needed for the brightness + state methods below. // permits needed for the brightness + state methods below.
PanelController { id: ctrl; ipcTarget: "monitorPanel"; manageIpc: false }
readonly property bool popupOpen: ctrl.open
property int brightnessPercent: 0 property int brightnessPercent: 0
property int pendingBrightnessPercent: 0 property int pendingBrightnessPercent: 0
property bool brightnessSetQueued: false property bool brightnessSetQueued: false
@@ -29,7 +28,7 @@ BarWidget {
// Cursor model shared by keyboard and mouse. Sections: // Cursor model shared by keyboard and mouse. Sections:
// "brightness" - single slider row, selectedIndex = -1 sentinel // "brightness" - single slider row, selectedIndex = -1 sentinel
// (mirrors audioPanel's slider rows). Only present if a // (mirrors Audio's slider rows). Only present if a
// controllable backlight was detected. // controllable backlight was detected.
// "scale" - 6 Button scale presets; treated as a single // "scale" - 6 Button scale presets; treated as a single
// horizontal row from j/k's perspective. h/l moves // horizontal row from j/k's perspective. h/l moves
@@ -170,10 +169,8 @@ BarWidget {
flick.contentY = bottom + margin - flick.height flick.contentY = bottom + margin - flick.height
} }
function closePopout() { ctrl.hide() }
IpcHandler { IpcHandler {
target: "monitorPanel" target: "panels.monitor"
function brightness(percent: string): string { function brightness(percent: string): string {
var value = Number(percent) var value = Number(percent)
@@ -191,9 +188,11 @@ BarWidget {
}) })
} }
function toggle(): void { ctrl.toggle() } function open(): void { root.open() }
function show(): void { ctrl.show() } function close(): void { root.close() }
function hide(): void { ctrl.hide() } function toggle(): void { root.toggle() }
function show(): void { root.open() }
function hide(): void { root.close() }
} }
function refresh() { function refresh() {
@@ -260,8 +259,8 @@ BarWidget {
// KeyboardPanel takes Exclusive focus at map-time, so SUPER-bound IPC // KeyboardPanel takes Exclusive focus at map-time, so SUPER-bound IPC
// summons land with j/k ready to navigate. Keep a default landing point, // summons land with j/k ready to navigate. Keep a default landing point,
// but don't paint the cursor until hover or the first navigation key. // but don't paint the cursor until hover or the first navigation key.
onPopupOpenChanged: { onOpenedChanged: {
if (popupOpen) { if (opened) {
refresh() refresh()
if (brightnessAvailable) { if (brightnessAvailable) {
focusSection = "brightness" focusSection = "brightness"
@@ -343,7 +342,7 @@ BarWidget {
bar: root.bar bar: root.bar
text: root.displays.length > 1 ? "󰍺" : "󰍹" text: root.displays.length > 1 ? "󰍺" : "󰍹"
fontSize: Style.font.subtitle fontSize: Style.font.subtitle
onPressed: function(b) { ctrl.toggle() } onPressed: function(b) { root.toggle() }
onWheelMoved: function(delta) { onWheelMoved: function(delta) {
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5)) if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
} }
@@ -354,7 +353,7 @@ BarWidget {
anchorItem: button anchorItem: button
owner: ctrl owner: ctrl
bar: root.bar bar: root.bar
open: ctrl.open open: root.opened
focusTarget: keyCatcher focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320)) contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560)) contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
@@ -371,7 +370,7 @@ BarWidget {
} }
} }
onActivateRequested: if (root.cursorActive) root.activateCursor() onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout() onCloseRequested: root.close()
ScrollView { ScrollView {
id: scrollArea id: scrollArea
@@ -5,16 +5,14 @@ import Quickshell.Io
import qs.Ui import qs.Ui
import qs.Commons import qs.Commons
BarWidget { Panel {
id: root id: root
moduleName: "networkPanel" moduleName: "networkPanel"
ipcTarget: "panels.network"
PanelController { id: ctrl; ipcTarget: "networkPanel" }
readonly property bool popupOpen: ctrl.open
// Centralized close so callers can't forget to drop the passphrase prompt. // Centralized close so callers can't forget to drop the passphrase prompt.
function closePopout() { function close() {
ctrl.hide() root.controller.hide()
passwordSsid = "" passwordSsid = ""
} }
@@ -96,11 +94,11 @@ BarWidget {
readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent" readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent"
// The panel below is its own layer-shell with Exclusive keyboard focus, // The panel below is its own layer-shell with Exclusive keyboard focus,
// so Hyprland grants focus when the surface is mapped (popupOpen flips // so Hyprland grants focus when the surface is mapped (opened flips
// to true). That's what makes the SUPER+CTRL+W keybind actually work // to true). That's what makes the SUPER+CTRL+W keybind actually work
// OnDemand only grants focus on click/hover. // OnDemand only grants focus on click/hover.
onPopupOpenChanged: { onOpenedChanged: {
if (popupOpen) { if (opened) {
refresh(true) refresh(true)
selectedIndex = wifiNetworks.length > 0 ? 0 : -1 selectedIndex = wifiNetworks.length > 0 ? 0 : -1
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns" focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
@@ -115,7 +113,7 @@ BarWidget {
// The KeyboardPanel's focusTarget covers initial popup-open; this handles // The KeyboardPanel's focusTarget covers initial popup-open; this handles
// the inline-editor case where focus was handed off to a child. // the inline-editor case where focus was handed off to a child.
onPasswordSsidChanged: { onPasswordSsidChanged: {
if (passwordSsid === "" && popupOpen) { if (passwordSsid === "" && opened) {
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() }) Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
} }
} }
@@ -129,7 +127,7 @@ BarWidget {
if (focusSection === "wifi") focusSection = "dns" if (focusSection === "wifi") focusSection = "dns"
} else if (selectedIndex >= wifiNetworks.length) { } else if (selectedIndex >= wifiNetworks.length) {
selectedIndex = wifiNetworks.length - 1 selectedIndex = wifiNetworks.length - 1
} else if (selectedIndex < 0 && popupOpen) { } else if (selectedIndex < 0 && opened) {
selectedIndex = 0 selectedIndex = 0
} }
} }
@@ -385,14 +383,14 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
if (provider === "Custom") { if (provider === "Custom") {
var launcher = Util.shellQuote(root.bar.omarchyPath + "/bin/omarchy-launch-floating-terminal-with-presentation") var launcher = Util.shellQuote(root.bar.omarchyPath + "/bin/omarchy-launch-floating-terminal-with-presentation")
root.bar.run(launcher + " " + Util.shellQuote(root.dnsCommand(provider))) root.bar.run(launcher + " " + Util.shellQuote(root.dnsCommand(provider)))
root.closePopout() root.close()
return return
} }
root.pendingDnsProvider = provider root.pendingDnsProvider = provider
actionProc.command = ["bash", "-lc", root.dnsCommand(provider)] actionProc.command = ["bash", "-lc", root.dnsCommand(provider)]
actionProc.running = true actionProc.running = true
root.closePopout() root.close()
} }
function isProtected(security) { function isProtected(security) {
@@ -597,7 +595,7 @@ fi
id: detailsPoll id: detailsPoll
interval: 1500 interval: 1500
repeat: true repeat: true
running: root.popupOpen running: root.opened
onTriggered: if (!detailsProc.running) detailsProc.running = true onTriggered: if (!detailsProc.running) detailsProc.running = true
} }
@@ -631,8 +629,8 @@ fi
rightExtraMargin: 2 rightExtraMargin: 2
onPressed: function(b) { onPressed: function(b) {
if (ctrl.open) root.closePopout() if (root.opened) root.close()
else { ctrl.show(); root.refresh() } else { root.open(); root.refresh() }
} }
} }
@@ -647,7 +645,7 @@ fi
anchorItem: button anchorItem: button
owner: ctrl owner: ctrl
bar: root.bar bar: root.bar
open: ctrl.open open: root.opened
focusTarget: keyCatcher focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340)) contentWidth: panel.fittedContentWidth(Style.space(340))
contentHeight: panel.fittedContentHeight(column.implicitHeight) contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -699,7 +697,7 @@ fi
else root.activateSelected() else root.activateSelected()
} }
} }
onCloseRequested: root.closePopout() onCloseRequested: root.close()
onDeleteRequested: { onDeleteRequested: {
if (root.cursorActive && root.focusSection === "wifi") root.forgetSelected() if (root.cursorActive && root.focusSection === "wifi") root.forgetSelected()
} }
@@ -5,13 +5,10 @@ import Quickshell.Services.UPower
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
BarWidget { Panel {
id: root id: root
moduleName: "powerPanel" moduleName: "powerPanel"
ipcTarget: "panels.power"
PanelController { id: ctrl; ipcTarget: "powerPanel" }
readonly property bool popupOpen: ctrl.open
property var batteryInfo: ({}) property var batteryInfo: ({})
property var systemInfo: ({}) property var systemInfo: ({})
property var profiles: [] property var profiles: []
@@ -19,8 +16,6 @@ BarWidget {
property int profileIndex: 0 property int profileIndex: 0
property bool cursorActive: false property bool cursorActive: false
function closePopout() { ctrl.hide() }
function selectProfileByDelta(delta) { function selectProfileByDelta(delta) {
if (profiles.length === 0) { profileIndex = 0; return } if (profiles.length === 0) { profileIndex = 0; return }
profileIndex = Math.max(0, Math.min(profiles.length - 1, profileIndex + delta)) profileIndex = Math.max(0, Math.min(profiles.length - 1, profileIndex + delta))
@@ -95,7 +90,7 @@ BarWidget {
profiles = list profiles = list
activeProfile = active activeProfile = active
if (profileIndex >= profiles.length) profileIndex = Math.max(0, profiles.length - 1) if (profileIndex >= profiles.length) profileIndex = Math.max(0, profiles.length - 1)
if (popupOpen && activeProfile !== "") { if (opened && activeProfile !== "") {
var idx = profiles.indexOf(activeProfile) var idx = profiles.indexOf(activeProfile)
if (idx >= 0) profileIndex = idx if (idx >= 0) profileIndex = idx
} }
@@ -107,8 +102,8 @@ BarWidget {
actionProc.running = true actionProc.running = true
} }
onPopupOpenChanged: { onOpenedChanged: {
if (popupOpen) { if (opened) {
refresh() refresh()
var idx = profiles.indexOf(activeProfile) var idx = profiles.indexOf(activeProfile)
profileIndex = idx >= 0 ? idx : 0 profileIndex = idx >= 0 ? idx : 0
@@ -164,7 +159,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
rightExtraMargin: 2 rightExtraMargin: 2
active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery
tooltipText: "" tooltipText: ""
onPressed: function(b) { ctrl.toggle() } onPressed: function(b) { root.toggle() }
} }
KeyboardPanel { KeyboardPanel {
@@ -172,7 +167,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
anchorItem: button anchorItem: button
owner: ctrl owner: ctrl
bar: root.bar bar: root.bar
open: ctrl.open open: root.opened
focusTarget: keyCatcher focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340)) contentWidth: panel.fittedContentWidth(Style.space(340))
contentHeight: panel.fittedContentHeight(column.implicitHeight) contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -186,7 +181,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
else if (dy !== 0) root.selectProfileByDelta(dy) else if (dy !== 0) root.selectProfileByDelta(dy)
} }
onActivateRequested: if (root.cursorActive) root.activateSelectedProfile() onActivateRequested: if (root.cursorActive) root.activateSelectedProfile()
onCloseRequested: root.closePopout() onCloseRequested: root.close()
Column { Column {
id: column id: column