import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Io import qs.Ui import qs.Commons import "Model.js" as Model Panel { id: root moduleName: "omarchy.monitor" ipcTarget: "omarchy.monitor" manageIpc: false // manageIpc: false so this panel can own the single IpcHandler the target // permits — needed for the brightness + state methods below. property int brightnessPercent: 0 property int pendingBrightnessPercent: 0 property bool brightnessSetQueued: false property bool brightnessAvailable: false property string internalMonitor: "" property string externalMonitor: "" property string focusedMonitor: "" property bool internalEnabled: false property bool mirrorEnabled: false property string monitorScale: "" property var displays: [] property int enabledDisplayCount: 0 // Cursor model shared by keyboard and mouse. Sections: // "brightness" - single slider row, selectedIndex = -1 sentinel // (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 // between presets, identical to bluetooth's header. // "monitors" - vertical display row list for enabling/disabling displays; // j/k walks each row. // Mouse hover on a target updates root state via the components' `hovered` // signal so keyboard cursor and pointer share one highlight. readonly property var scaleValues: ["1", "1.25", "1.6", "2", "3", "4"] property string focusSection: "scale" property int selectedIndex: 0 property bool cursorActive: false readonly property var visibleSections: { var list = [] if (brightnessAvailable) list.push("brightness") list.push("scale") if (displays.length > 1) list.push("monitors") return list } function sectionCount(section) { if (section === "brightness") return 0 // only the slider sentinel at -1 if (section === "scale") return scaleValues.length if (section === "monitors") return displays.length return 0 } function sectionIsSingleRow(section) { // brightness has only the slider; scale presets sit horizontally. return section === "brightness" || section === "scale" } function sectionFirstIndex(section) { if (section === "brightness") return -1 return 0 } function moveCursor(delta) { var sections = visibleSections if (!sections || sections.length === 0) return var sIdx = sections.indexOf(focusSection) if (sIdx < 0) { focusSection = sections[0] selectedIndex = sectionFirstIndex(focusSection) return } var inSingleRow = sectionIsSingleRow(focusSection) var max = inSingleRow ? 0 : sectionCount(focusSection) - 1 if (delta > 0) { if (!inSingleRow && selectedIndex < max) { selectedIndex = selectedIndex + 1; return } if (sIdx < sections.length - 1) { focusSection = sections[sIdx + 1] selectedIndex = sectionFirstIndex(focusSection) } } else { if (!inSingleRow && selectedIndex > 0) { selectedIndex = selectedIndex - 1; return } if (sIdx > 0) { var prev = sections[sIdx - 1] focusSection = prev // Coming up from below — land on the last navigable row of the prev // section, or its sentinel for single-row sections. selectedIndex = sectionIsSingleRow(prev) ? sectionFirstIndex(prev) : sectionCount(prev) - 1 } } } // h/l: in scale section, walks the preset row; everywhere else, no-op // because adjustBrightness handles horizontal motion on the brightness // slider. function moveCursorH(delta) { if (focusSection !== "scale") return var next = selectedIndex + delta if (next < 0) next = 0 if (next > scaleValues.length - 1) next = scaleValues.length - 1 selectedIndex = next } function adjustBrightness(delta) { if (focusSection !== "brightness") return if (!brightnessAvailable) return setBrightness(root.brightnessPercent + delta) } function activateCursor() { if (focusSection === "scale" && selectedIndex >= 0 && selectedIndex < scaleValues.length) { setScale(scaleValues[selectedIndex]) return } if (focusSection === "monitors" && selectedIndex >= 0 && selectedIndex < displays.length) { var d = displays[selectedIndex] if (d) toggleDisplay(d.name, d.enabled) } // brightness: no separate action; the slider value is the action. } function clampCursor() { var sections = visibleSections if (!sections || !sections.length) return if (sections.indexOf(focusSection) < 0) { focusSection = sections[0] selectedIndex = sectionFirstIndex(focusSection) return } var count = sectionCount(focusSection) if (sectionIsSingleRow(focusSection)) { // brightness uses -1 sentinel; scale clamps into the preset range. if (focusSection === "brightness") selectedIndex = -1 else if (selectedIndex < 0 || selectedIndex >= count) selectedIndex = 0 return } if (count === 0) { var sIdx = sections.indexOf(focusSection) focusSection = sIdx > 0 ? sections[sIdx - 1] : sections[0] selectedIndex = sectionFirstIndex(focusSection) return } if (selectedIndex > count - 1) selectedIndex = count - 1 if (selectedIndex < 0) selectedIndex = 0 } // Keep the keyboard-focused row inside the viewport when the panel grows // taller than its allotted height (lots of displays). Mirrors audio's // ensureCursorVisible helper. function ensureCursorVisible(item) { if (!item || !scrollArea) return var flick = scrollArea.contentItem if (!flick || flick.contentY === undefined) return var pt = item.mapToItem(flick.contentItem || flick, 0, 0) var top = pt.y var bottom = top + (item.height || 0) var viewTop = flick.contentY var viewBottom = viewTop + flick.height var margin = 6 if (top < viewTop + margin) flick.contentY = Math.max(0, top - margin) else if (bottom > viewBottom - margin) flick.contentY = bottom + margin - flick.height } function brightnessIpc(percent) { var value = Number(percent) root.setBrightness(value) return "got " + root.pendingBrightnessPercent } function stateIpc() { return JSON.stringify({ brightness: root.brightnessPercent, brightnessAvailable: root.brightnessAvailable, focusedMonitor: root.focusedMonitor, scale: root.monitorScale, displays: root.displays }) } IpcHandler { target: "omarchy.monitor" function brightness(percent: string): string { return root.brightnessIpc(percent) } function state(): string { return root.stateIpc() } function open() { root.open() } function close() { root.close() } function toggle() { root.toggle() } function show() { root.open() } function hide() { root.close() } } function refresh() { if (!stateProc.running) stateProc.running = true } function setBrightness(value) { var percent = Model.clampBrightness(value) root.brightnessPercent = percent root.pendingBrightnessPercent = percent if (setBrightnessProc.running) { root.brightnessSetQueued = true return } root.brightnessSetQueued = false setBrightnessProc.command = ["bash", "-lc", "omarchy-brightness-display --no-osd " + percent + "%"] setBrightnessProc.running = true } function previewBrightness(value) { root.brightnessPercent = Model.clampBrightness(value) brightnessDebounce.restart() } function normalizeScale(scale) { return Model.normalizeScale(scale) } // Playful mood-name for a given brightness percent. Bands intentionally // span ~10–20 points so casual tweaks change the label, while small // nudges within one band don't. function brightnessName(percent) { return Model.brightnessName(percent) } function updateDisplays(displaysJson) { var parsed = Model.parseDisplays(displaysJson) root.displays = parsed.displays root.enabledDisplayCount = parsed.enabledDisplayCount } function toggleDisplay(name, enabled) { if (!name) return if (enabled && root.enabledDisplayCount <= 1) return actionProc.command = ["hyprctl", "keyword", "monitor", name + (enabled ? ",disable" : ",preferred,auto,auto")] if (!actionProc.running) actionProc.running = true } function setScale(scale) { actionProc.command = ["bash", "-lc", "omarchy-hyprland-monitor-scaling " + scale] if (!actionProc.running) actionProc.running = true } implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight Component.onCompleted: refresh() // 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. onOpenedChanged: { if (opened) { refresh() if (brightnessAvailable) { focusSection = "brightness" selectedIndex = -1 } else { focusSection = "scale" selectedIndex = 0 } cursorActive = false } } onBrightnessAvailableChanged: clampCursor() onDisplaysChanged: clampCursor() onVisibleSectionsChanged: clampCursor() Timer { interval: 5000 running: true repeat: true onTriggered: root.refresh() } Process { id: stateProc command: [root.bar ? root.bar.omarchyPath + "/bin/omarchy-monitor-state" : "omarchy-monitor-state"] stdout: StdioCollector { waitForEnd: true onStreamFinished: { var lines = String(text || "").split("\n") var brightness = String(lines[0] || "").trim() root.brightnessAvailable = brightness !== "unavailable" && brightness !== "" root.brightnessPercent = root.brightnessAvailable ? Math.max(0, Math.min(100, parseInt(brightness, 10))) : 0 root.internalMonitor = String(lines[1] || "").trim() root.externalMonitor = String(lines[2] || "").trim() root.internalEnabled = String(lines[3] || "").trim() !== "" root.mirrorEnabled = String(lines[4] || "").trim() === root.externalMonitor && root.externalMonitor !== "" root.focusedMonitor = String(lines[5] || "").trim() root.monitorScale = root.normalizeScale(String(lines[6] || "").trim()) root.updateDisplays(String(lines[7] || "[]").trim()) } } } Timer { id: brightnessDebounce interval: 180 repeat: false onTriggered: root.setBrightness(root.brightnessPercent) } Process { id: setBrightnessProc stdout: StdioCollector { waitForEnd: true } // Do NOT call refresh() after a brightness set completes. The local // brightnessPercent we just wrote is authoritative; re-reading via // `omarchy-brightness-display` races the hardware/driver and can // return an empty string, which the parser then coerces to 0 — // visible as a "bounce to zero" after h/l keypresses. External // brightness changes are still picked up by the 5s periodic refresh, // the open-time refresh, and Component.onCompleted. onRunningChanged: { if (running) return if (root.brightnessSetQueued) { root.setBrightness(root.pendingBrightnessPercent) } } } Process { id: actionProc stdout: StdioCollector { waitForEnd: true } onRunningChanged: if (!running) root.refresh() } WidgetButton { id: button anchors.fill: parent bar: root.bar text: root.displays.length > 1 ? "󰍺" : "󰍹" fontSize: Style.font.subtitle fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 rightExtraMargin: 4 onPressed: function(b) { root.toggle() } onWheelMoved: function(delta) { if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5)) } } KeyboardPanel { id: panel anchorItem: button owner: root bar: root.bar open: root.opened focusTarget: keyCatcher contentWidth: panel.fittedContentWidth(Style.space(380)) contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560)) PanelKeyCatcher { id: keyCatcher anchors.fill: parent onMoveRequested: function(dx, dy) { if (!root.cursorActive) { root.cursorActive = true; return } if (dy !== 0) root.moveCursor(dy) else if (dx !== 0) { if (root.focusSection === "brightness") root.adjustBrightness(dx * 5) else if (root.focusSection === "scale") root.moveCursorH(dx) } } onActivateRequested: if (root.cursorActive) root.activateCursor() onCloseRequested: root.close() onTabRequested: function(direction) { root.switchPanel(direction) } ScrollView { id: scrollArea anchors.fill: parent clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded Column { id: panelColumn width: scrollArea.availableWidth spacing: Style.space(14) // ---------- Hero: display icon · title/status ---------- Item { width: parent.width visible: root.brightnessAvailable implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) Text { id: heroIcon text: root.displays.length > 1 ? "󰍺" : "󰍹" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.display anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } Column { id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: Style.space(2) Text { text: "Display" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title font.bold: true elide: Text.ElideRight width: parent.width } Text { id: heroLabel text: root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase() color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: Style.font.caption font.bold: true font.letterSpacing: 1.2 elide: Text.ElideRight width: parent.width } } } // ---------- Brightness ---------- PanelSeparator { visible: root.brightnessAvailable foreground: root.bar.foreground } Column { visible: root.brightnessAvailable width: parent.width spacing: Style.space(6) Item { width: parent.width implicitHeight: Math.max(brightnessHeader.implicitHeight, brightnessPercent.implicitHeight) PanelSectionHeader { id: brightnessHeader text: "BRIGHTNESS" foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } Text { id: brightnessPercent text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: Style.font.caption font.bold: true anchors.right: parent.right anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter } } CursorSurface { id: brightnessRow width: parent.width height: brightnessSlider.implicitHeight + Style.spacing.controlGap hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1 onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow) foreground: root.bar.foreground outline: true PanelSlider { id: brightnessSlider bar: root.bar anchors.fill: parent anchors.leftMargin: Style.space(6) anchors.rightMargin: Style.space(6) minimum: 1 maximum: 100 step: 1 value: root.brightnessPercent integer: true onMoved: function(v) { root.previewBrightness(v) } onReleased: function(v) { brightnessDebounce.stop() root.setBrightness(v) } } HoverHandler { onHoveredChanged: if (hovered) { root.cursorActive = true root.focusSection = "brightness" root.selectedIndex = -1 } } } } Text { visible: !root.brightnessAvailable text: "No controllable backlight found" color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall } // ---------- Scale ---------- PanelSeparator { foreground: root.bar.foreground } Column { width: parent.width spacing: Style.space(10) PanelSectionHeader { text: "SCALE" foreground: root.bar.foreground fontFamily: root.bar.fontFamily } Grid { id: scaleRow width: parent.width columns: root.scaleValues.length spacing: Style.spacing.xs readonly property real cellWidth: root.scaleValues.length > 0 ? (width - spacing * (columns - 1)) / columns : 0 Repeater { model: root.scaleValues ScalePill { required property string modelData required property int index scaleValue: modelData scaleIndex: index width: scaleRow.cellWidth } } } } // ---------- Monitors ---------- PanelSeparator { visible: root.displays.length > 1 foreground: root.bar.foreground } Column { width: parent.width spacing: Style.space(10) visible: root.displays.length > 1 PanelSectionHeader { text: "DISPLAYS" foreground: root.bar.foreground fontFamily: root.bar.fontFamily } Repeater { model: root.displays MonitorRow { required property var modelData required property int index width: panelColumn.width display: modelData rowIndex: index } } } Item { width: parent.width height: Style.space(4) } } } } } component ScalePill: Button { id: pill required property string scaleValue required property int scaleIndex text: scaleValue + "x" fontSize: Style.font.caption foreground: root.bar.foreground fontFamily: root.bar.fontFamily horizontalPadding: Style.spacing.sm verticalPadding: Style.spacing.controlPaddingY bordered: true active: root.normalizeScale(root.monitorScale) === root.normalizeScale(scaleValue) hasCursor: root.cursorActive && root.focusSection === "scale" && root.selectedIndex === scaleIndex onClicked: root.setScale(scaleValue) onHovered: function(isHovered) { if (!isHovered) return root.cursorActive = true root.focusSection = "scale" root.selectedIndex = pill.scaleIndex } } component MonitorRow: CursorSurface { id: monitorRow required property var display required property int rowIndex readonly property bool isFocused: display && display.focused readonly property bool canToggle: display && (!display.enabled || root.enabledDisplayCount > 1) hasCursor: root.cursorActive && root.focusSection === "monitors" && root.selectedIndex === rowIndex onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(monitorRow) current: isFocused foreground: root.bar.foreground fill: Style.hoverFillFor(root.bar.foreground, Color.accent) currentFill: Style.selectedFillFor(root.bar.foreground, Color.accent) implicitHeight: monitorInner.implicitHeight + Style.spacing.xl opacity: canToggle ? 1.0 : 0.45 Row { id: monitorInner anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Style.space(6) anchors.rightMargin: Style.space(6) spacing: Style.space(8) Text { text: "󰍹" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title width: Style.space(22) horizontalAlignment: Text.AlignHCenter anchors.verticalCenter: parent.verticalCenter } Text { text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.body elide: Text.ElideRight width: parent.width - Style.space(22) - Style.space(14) - Style.space(16) anchors.verticalCenter: parent.verticalCenter } Text { text: monitorRow.display.enabled ? "󰄬" : "" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.subtitle width: Style.space(14) horizontalAlignment: Text.AlignRight anchors.verticalCenter: parent.verticalCenter } } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: monitorRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor onContainsMouseChanged: if (containsMouse) { root.cursorActive = true root.focusSection = "monitors" root.selectedIndex = monitorRow.rowIndex } onClicked: if (monitorRow.canToggle) root.toggleDisplay(monitorRow.display.name, monitorRow.display.enabled) } } }