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 + W", "Show weather", "omarchy-notification-weather")
o.bind("SUPER + CTRL + A", "Audio panel", "omarchy-shell audioPanel toggle")
o.bind("SUPER + CTRL + B", "Bluetooth panel", "omarchy-shell bluetoothPanel toggle")
o.bind("SUPER + CTRL + D", "Display panel", "omarchy-shell monitorPanel toggle")
o.bind("SUPER + CTRL + W", "Network panel", "omarchy-shell networkPanel toggle")
o.bind("SUPER + CTRL + P", "Power panel", "omarchy-shell powerPanel toggle")
o.bind("SUPER + CTRL + A", "Audio panel", "omarchy-shell panels.audio toggle")
o.bind("SUPER + CTRL + B", "Bluetooth panel", "omarchy-shell panels.bluetooth toggle")
o.bind("SUPER + CTRL + D", "Display panel", "omarchy-shell panels.monitor toggle")
o.bind("SUPER + CTRL + W", "Network panel", "omarchy-shell panels.network 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 + 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/
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
# a NoInputNoOutput agent on the system bus so pair() actually completes.
mkdir -p ~/.config/systemd/user/
+1 -1
View File
@@ -8,7 +8,7 @@ notify_update() {
notify_wifi() {
(
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 &
}
+3 -3
View File
@@ -59,8 +59,8 @@ PanelWindow {
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property string barPos: bar ? bar.position : "top"
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
function close() {
if (owner && "close" in owner) owner.close()
else root.open = false
}
@@ -255,7 +255,7 @@ PanelWindow {
onExited: hoveringBar = false
onClicked: function(mouse) {
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 Quickshell
import Quickshell.Io
// 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.
// Stores the open state for a shell panel. Panel owns the public lifecycle
// methods and IPC wiring; this object only keeps state separate from the
// panel implementation's own properties.
//
// Usage:
// PanelController { id: ctrl; ipcTarget: "audioPanel" }
//
// 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.
// PanelController { id: panelController }
QtObject {
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
function toggle() { open = !open }
function show() { if (!open) open = true }
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
// onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) }
// onActivateRequested: root.activateCursor()
// onCloseRequested: root.closePopout()
// onCloseRequested: root.close()
// onDeleteRequested: root.deleteSelected()
// onTextKey: function(t) { if (t === "r") root.refresh() }
//
+3 -3
View File
@@ -55,8 +55,8 @@ PopupWindow {
return Math.round(Math.min(desired, maxHeight))
}
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
function close() {
if (owner && "close" in owner) owner.close()
else root.open = false
}
@@ -80,7 +80,7 @@ PopupWindow {
HyprlandFocusGrab {
active: root.open && root.triggerMode === "click"
windows: root.anchorWindow ? [root, root.anchorWindow] : [root]
onCleared: root.closePopout()
onCleared: root.close()
}
anchor {
+1
View File
@@ -8,6 +8,7 @@ CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml
NumberField 1.0 NumberField.qml
Panel 1.0 Panel.qml
PanelActionButton 1.0 PanelActionButton.qml
PanelController 1.0 PanelController.qml
PanelKeyCatcher 1.0 PanelKeyCatcher.qml
+13 -11
View File
@@ -95,7 +95,7 @@ Item {
function requestPopout(owner) {
if (activePopout === owner) return
if (activePopout && "closePopout" in activePopout) activePopout.closePopout()
if (activePopout && "close" in activePopout) activePopout.close()
activePopout = owner
}
@@ -277,16 +277,16 @@ Item {
return source ? Util.fileUrl(source) : ""
}
// First-party widgets are registered with the BarWidgetRegistry at startup.
// Each entry maps a widget id to its display metadata; the QML source lives
// at widgets/<id>.qml and is loaded asynchronously via Qt.createComponent.
// First-party modules are registered with the BarWidgetRegistry at startup.
// Each entry maps a module id to its display metadata; the QML source is
// loaded asynchronously via Qt.createComponent.
readonly property var firstPartyWidgetMetadata: ({
"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 },
"monitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false },
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
"powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false },
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", 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, sourceDir: "../panels", sourceName: "Monitor" },
"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, sourceDir: "../panels", sourceName: "Power" },
"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" },
"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 },
@@ -316,8 +316,10 @@ Item {
}
function registerOneFirstPartyWidget(id) {
var url = Qt.resolvedUrl("widgets/" + id + ".qml")
var meta = firstPartyWidgetMetadata[id] || {}
var sourceDir = meta.sourceDir || "widgets"
var sourceName = meta.sourceName || id
var url = Qt.resolvedUrl(sourceDir + "/" + sourceName + ".qml")
var enrichedMeta = {
displayName: meta.displayName || id,
description: meta.description || "",
@@ -1285,7 +1287,7 @@ Item {
property bool expanded: 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.
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.
- `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).
- `omarchy-style-bar-position` updates only the user shell.json file.
@@ -49,15 +50,11 @@ Example `shell.json` (bar subtree only shown):
## Module catalogue
### First-party interactive widgets (in `widgets/`)
### First-party interactive widgets
| 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 |
| `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 |
| `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 |
@@ -65,11 +62,21 @@ Example `shell.json` (bar subtree only shown):
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
| `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`)
`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
@@ -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.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
shell's `BarWidgetRegistry` at startup; reference one by `id` in any
layout list.
First-party bar widgets live in `widgets/<name>.qml`; first-party panels
live in `../panels/<name>.qml` and expose IPC targets such as
`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
`~/.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 bool popupOpen: false
function closePopout() { popupOpen = false }
function close() { popupOpen = false }
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
+1 -1
View File
@@ -33,7 +33,7 @@ BarWidget {
property bool popupOpen: false
function closePopout() { popupOpen = false }
function close() { popupOpen = false }
property real maxLabelWidth: 180
visible: hasMedia
+1 -1
View File
@@ -42,7 +42,7 @@ BarWidget {
active: root.inUse
tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live")
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()
}
onWheelMoved: function(delta) {
@@ -10,10 +10,10 @@ BarWidget {
property bool popupOpen: false
function closePopout() { popupOpen = false }
function close() { popupOpen = false }
// 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
// tab selection.
onPopupOpenChanged: {
+1 -1
View File
@@ -19,7 +19,7 @@ BarWidget {
property bool popupOpen: false
function closePopout() { popupOpen = false }
function close() { popupOpen = false }
readonly property int historyLimit: 30
+1 -1
View File
@@ -10,7 +10,7 @@ BarWidget {
property bool popupOpen: false
function closePopout() { popupOpen = false }
function close() { popupOpen = false }
function showPopup() {
root.popupOpen = !root.popupOpen
+1 -1
View File
@@ -407,7 +407,7 @@ Item {
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
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 {
width: parent.width
@@ -7,14 +7,10 @@ import Quickshell.Services.Pipewire
import qs.Ui
import qs.Commons
BarWidget {
Panel {
id: root
moduleName: "audioPanel"
PanelController { id: ctrl; ipcTarget: "audioPanel" }
readonly property bool popupOpen: ctrl.open
function closePopout() { ctrl.hide() }
ipcTarget: "panels.audio"
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
@@ -214,8 +210,8 @@ BarWidget {
}
}
onPopupOpenChanged: {
if (popupOpen) {
onOpenedChanged: {
if (opened) {
focusSection = "output"
selectedIndex = -1 // first keyboard cursor reveal starts on the output slider
cursorActive = false
@@ -546,7 +542,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
Timer {
interval: 5000
running: root.popupOpen
running: root.opened
repeat: true
triggeredOnStart: 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
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleOutputMute()
else ctrl.toggle()
else root.toggle()
}
onWheelMoved: function(delta) {
@@ -574,7 +570,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
anchorItem: button
owner: ctrl
bar: root.bar
open: ctrl.open
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(370))
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)
}
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
onCloseRequested: root.close()
onTextKey: function(t) {
// 'm' mutes whatever the cursor is on: focused section's slider
// for output/input, the focused stream for streams.
@@ -6,13 +6,10 @@ import Quickshell.Bluetooth
import qs.Ui
import qs.Commons
BarWidget {
Panel {
id: root
moduleName: "bluetoothPanel"
PanelController { id: ctrl; ipcTarget: "bluetoothPanel" }
readonly property bool popupOpen: ctrl.open
ipcTarget: "panels.bluetooth"
// 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
@@ -20,8 +17,6 @@ BarWidget {
// moment `paired` flips, before the row's handler reliably fires.
property var pendingPairAddresses: ({})
function closePopout() { ctrl.hide() }
readonly property var adapter: Bluetooth.defaultAdapter
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
@@ -219,8 +214,8 @@ BarWidget {
else if (dev.forget) dev.forget()
}
onPopupOpenChanged: {
if (popupOpen) {
onOpenedChanged: {
if (opened) {
if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
else { focusSection = "header"; selectedIndex = 1 }
@@ -303,7 +298,7 @@ BarWidget {
Connections {
target: root.adapter || null
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
}
}
@@ -341,7 +336,7 @@ BarWidget {
onPressed: function(b) {
if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
else ctrl.toggle()
else root.toggle()
}
}
@@ -350,7 +345,7 @@ BarWidget {
anchorItem: button
owner: ctrl
bar: root.bar
open: ctrl.open
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -364,7 +359,7 @@ BarWidget {
else if (dx !== 0) root.moveCursorH(dx)
}
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
onCloseRequested: root.close()
onDeleteRequested: if (root.cursorActive) root.deleteSelected()
Column {
@@ -5,15 +5,14 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
BarWidget {
Panel {
id: root
moduleName: "monitorPanel"
ipcTarget: "panels.monitor"
manageIpc: false
// manageIpc: false so this panel can own the single IpcHandler the target
// 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 pendingBrightnessPercent: 0
property bool brightnessSetQueued: false
@@ -29,7 +28,7 @@ BarWidget {
// Cursor model shared by keyboard and mouse. Sections:
// "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.
// "scale" - 6 Button scale presets; treated as a single
// horizontal row from j/k's perspective. h/l moves
@@ -170,10 +169,8 @@ BarWidget {
flick.contentY = bottom + margin - flick.height
}
function closePopout() { ctrl.hide() }
IpcHandler {
target: "monitorPanel"
target: "panels.monitor"
function brightness(percent: string): string {
var value = Number(percent)
@@ -191,9 +188,11 @@ BarWidget {
})
}
function toggle(): void { ctrl.toggle() }
function show(): void { ctrl.show() }
function hide(): void { ctrl.hide() }
function open(): void { root.open() }
function close(): void { root.close() }
function toggle(): void { root.toggle() }
function show(): void { root.open() }
function hide(): void { root.close() }
}
function refresh() {
@@ -260,8 +259,8 @@ BarWidget {
// KeyboardPanel takes Exclusive focus at map-time, so SUPER-bound IPC
// 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.
onPopupOpenChanged: {
if (popupOpen) {
onOpenedChanged: {
if (opened) {
refresh()
if (brightnessAvailable) {
focusSection = "brightness"
@@ -343,7 +342,7 @@ BarWidget {
bar: root.bar
text: root.displays.length > 1 ? "󰍺" : "󰍹"
fontSize: Style.font.subtitle
onPressed: function(b) { ctrl.toggle() }
onPressed: function(b) { root.toggle() }
onWheelMoved: function(delta) {
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
}
@@ -354,7 +353,7 @@ BarWidget {
anchorItem: button
owner: ctrl
bar: root.bar
open: ctrl.open
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
@@ -371,7 +370,7 @@ BarWidget {
}
}
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
onCloseRequested: root.close()
ScrollView {
id: scrollArea
@@ -5,16 +5,14 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
BarWidget {
Panel {
id: root
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.
function closePopout() {
ctrl.hide()
function close() {
root.controller.hide()
passwordSsid = ""
}
@@ -96,11 +94,11 @@ BarWidget {
readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent"
// 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
// OnDemand only grants focus on click/hover.
onPopupOpenChanged: {
if (popupOpen) {
onOpenedChanged: {
if (opened) {
refresh(true)
selectedIndex = wifiNetworks.length > 0 ? 0 : -1
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
@@ -115,7 +113,7 @@ BarWidget {
// The KeyboardPanel's focusTarget covers initial popup-open; this handles
// the inline-editor case where focus was handed off to a child.
onPasswordSsidChanged: {
if (passwordSsid === "" && popupOpen) {
if (passwordSsid === "" && opened) {
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -129,7 +127,7 @@ BarWidget {
if (focusSection === "wifi") focusSection = "dns"
} else if (selectedIndex >= wifiNetworks.length) {
selectedIndex = wifiNetworks.length - 1
} else if (selectedIndex < 0 && popupOpen) {
} else if (selectedIndex < 0 && opened) {
selectedIndex = 0
}
}
@@ -385,14 +383,14 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
if (provider === "Custom") {
var launcher = Util.shellQuote(root.bar.omarchyPath + "/bin/omarchy-launch-floating-terminal-with-presentation")
root.bar.run(launcher + " " + Util.shellQuote(root.dnsCommand(provider)))
root.closePopout()
root.close()
return
}
root.pendingDnsProvider = provider
actionProc.command = ["bash", "-lc", root.dnsCommand(provider)]
actionProc.running = true
root.closePopout()
root.close()
}
function isProtected(security) {
@@ -597,7 +595,7 @@ fi
id: detailsPoll
interval: 1500
repeat: true
running: root.popupOpen
running: root.opened
onTriggered: if (!detailsProc.running) detailsProc.running = true
}
@@ -631,8 +629,8 @@ fi
rightExtraMargin: 2
onPressed: function(b) {
if (ctrl.open) root.closePopout()
else { ctrl.show(); root.refresh() }
if (root.opened) root.close()
else { root.open(); root.refresh() }
}
}
@@ -647,7 +645,7 @@ fi
anchorItem: button
owner: ctrl
bar: root.bar
open: ctrl.open
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -699,7 +697,7 @@ fi
else root.activateSelected()
}
}
onCloseRequested: root.closePopout()
onCloseRequested: root.close()
onDeleteRequested: {
if (root.cursorActive && root.focusSection === "wifi") root.forgetSelected()
}
@@ -5,13 +5,10 @@ import Quickshell.Services.UPower
import qs.Commons
import qs.Ui
BarWidget {
Panel {
id: root
moduleName: "powerPanel"
PanelController { id: ctrl; ipcTarget: "powerPanel" }
readonly property bool popupOpen: ctrl.open
ipcTarget: "panels.power"
property var batteryInfo: ({})
property var systemInfo: ({})
property var profiles: []
@@ -19,8 +16,6 @@ BarWidget {
property int profileIndex: 0
property bool cursorActive: false
function closePopout() { ctrl.hide() }
function selectProfileByDelta(delta) {
if (profiles.length === 0) { profileIndex = 0; return }
profileIndex = Math.max(0, Math.min(profiles.length - 1, profileIndex + delta))
@@ -95,7 +90,7 @@ BarWidget {
profiles = list
activeProfile = active
if (profileIndex >= profiles.length) profileIndex = Math.max(0, profiles.length - 1)
if (popupOpen && activeProfile !== "") {
if (opened && activeProfile !== "") {
var idx = profiles.indexOf(activeProfile)
if (idx >= 0) profileIndex = idx
}
@@ -107,8 +102,8 @@ BarWidget {
actionProc.running = true
}
onPopupOpenChanged: {
if (popupOpen) {
onOpenedChanged: {
if (opened) {
refresh()
var idx = profiles.indexOf(activeProfile)
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
active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery
tooltipText: ""
onPressed: function(b) { ctrl.toggle() }
onPressed: function(b) { root.toggle() }
}
KeyboardPanel {
@@ -172,7 +167,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
anchorItem: button
owner: ctrl
bar: root.bar
open: ctrl.open
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340))
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)
}
onActivateRequested: if (root.cursorActive) root.activateSelectedProfile()
onCloseRequested: root.closePopout()
onCloseRequested: root.close()
Column {
id: column