Merge branch 'quattro' into unified-input-method-setup

This commit is contained in:
David Heinemeier Hansson committed 2026-10-10 15:51:52 +02:00
commit 101bb547bb
245 files changed
+19949 -375

No files matched your search

+105 -11
View File
@@ -569,6 +569,61 @@ Item {
readonly property bool vertical: position === "left" || position === "right"
readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal
// Display cutouts (a camera notch) the platform's own package describes in
// the fixed platform root, which no environment variable moves. Most machines
// have none. It is read as the shell starts (blockLoading), so a bar surface
// knows before it maps whether its screen may have one. See
// BarModel.parseCutouts.
readonly property var displayCutouts: displayCutoutsFile.missing ? [] : BarModel.parseCutouts(displayCutoutsFile.text())
FileView {
id: displayCutoutsFile
// A file that went away describes nothing, whatever text() still holds.
property bool missing: false
path: "/usr/share/omarchy-platform/display-cutouts.json"
blockLoading: true
watchChanges: true
printErrors: false
onLoaded: missing = false
onLoadFailed: missing = true
onFileChanged: reload()
}
// The physical mode Hyprland reports for a screen, as hyprctl monitors does.
// Qt's whole-number devicePixelRatio can't rebuild it at a fractional scale.
function panelModeFor(screen) {
var monitor = screen ? Hyprland.monitorFor(screen) : null
return monitor ? ({
width: monitor.width,
height: monitor.height,
transform: monitor.lastIpcObject ? monitor.lastIpcObject.transform : 0
}) : null
}
// How thick the bar is on a screen. A top bar shorter than a panel's camera
// cutout leaves a sliver of every window peeking out beside the camera, so
// the cutout is this panel's minimum sensible top-bar height. An
// intentionally taller bar still wins.
function thicknessFor(screen) {
if (root.vertical || !screen) return root.barSize
return Math.max(root.barSize, BarModel.notchFloor(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, root.panelModeFor(screen), Style.bar.notchHeight))
}
// Each screen's bar thickness, by screen name: barSize, or more where a notch
// floor raises a top bar. Toasts and plugins clear the bar by it; barSize
// stays the configured size the widgets are drawn at.
readonly property var screenBarSizes: {
var sizes = ({})
var screens = Quickshell.screens
for (var i = 0; i < screens.length; i++) sizes[screens[i].name] = root.thicknessFor(screens[i])
return sizes
}
function barSizeFor(screenName) {
return BarModel.barSizeFor(root.screenBarSizes, screenName, root.barSize)
}
function normalizePosition(value) {
return BarModel.normalizePosition(value)
}
@@ -1253,7 +1308,12 @@ Item {
// reveal has to rebuild them — new surface, re-shaped glyphs, re-uploaded
// textures — which measures ~150ms against ~20ms to tear down. Parking
// keeps the surface alive, so showing is only a margin change.
visible: !remapGuard.remapping
//
// A top bar that can't tell its notch floor yet waits for Hyprland's mode
// before it maps (see BarModel.cutoutPending), so it maps at its floor
// instead of growing once the windows are laid out. The wait ends once, for
// good: when the mode settles it, or after two seconds without one.
visible: !remapGuard.remapping && (cutoutWaitOver || !cutoutPending)
exclusionMode: root.barHidden ? ExclusionMode.Ignore : ExclusionMode.Auto
ScreenMoveRemap {
@@ -1261,11 +1321,13 @@ Item {
window: barWindow
}
// Parked by its full thickness, which a notch floor may make more than
// barSize, so no strip of it stays on screen.
margins {
top: root.barHidden && root.position === "top" ? -root.barSize : 0
bottom: root.barHidden && root.position === "bottom" ? -root.barSize : 0
left: root.barHidden && root.position === "left" ? -root.barSize : 0
right: root.barHidden && root.position === "right" ? -root.barSize : 0
top: root.barHidden && root.position === "top" ? -barWindow.thickness : 0
bottom: root.barHidden && root.position === "bottom" ? -barWindow.thickness : 0
left: root.barHidden && root.position === "left" ? -barWindow.thickness : 0
right: root.barHidden && root.position === "right" ? -barWindow.thickness : 0
}
anchors {
@@ -1275,8 +1337,26 @@ Item {
right: root.position === "right" || !root.vertical
}
implicitWidth: root.vertical ? root.barSize : 0
implicitHeight: root.vertical ? 0 : root.barSize
readonly property var panelMode: root.panelModeFor(screen)
readonly property bool centerBesideRight: BarModel.centerBesideRight(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, panelMode)
readonly property bool cutoutPending: BarModel.cutoutPending(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, panelMode)
property bool cutoutWaitOver: false
onCutoutPendingChanged: if (!cutoutPending) cutoutWaitOver = true
Component.onCompleted: if (!cutoutPending) cutoutWaitOver = true
Timer {
interval: 2000
running: barWindow.cutoutPending && !barWindow.cutoutWaitOver
onTriggered: barWindow.cutoutWaitOver = true
}
// The same thickness the bar publishes for its screen (screenBarSizes).
readonly property int thickness: root.thicknessFor(screen)
implicitWidth: root.vertical ? thickness : 0
implicitHeight: root.vertical ? 0 : thickness
color: root.transparent ? "transparent" : root.background
surfaceFormat.opaque: false
WlrLayershell.namespace: "omarchy-bar"
@@ -1367,7 +1447,10 @@ Item {
Item {
anchors.fill: parent
CenterModules { anchors.fill: parent }
CenterModules {
anchors.fill: parent
entries: barWindow.centerBesideRight ? [] : root.layoutEntries("center")
}
LeftModules {
anchors.left: parent.left
@@ -1376,10 +1459,22 @@ Item {
}
RightModules {
id: rightModules
anchors.right: parent.right
anchors.rightMargin: Style.space(8)
anchors.verticalCenter: parent.verticalCenter
}
// Keeps the center region, so settings pushes, drag and drop, and panel
// routing still address it. The gap lets a drop at the seam land in
// the section the pointer is nearer to.
ModuleList {
entries: barWindow.centerBesideRight ? root.layoutEntries("center") : []
region: "center"
anchors.right: rightModules.left
anchors.rightMargin: Style.space(4)
anchors.verticalCenter: parent.verticalCenter
}
}
}
@@ -1529,8 +1624,7 @@ Item {
}
}
function findCenterAnchorEntry() {
var entries = root.layoutEntries("center")
function findCenterAnchorEntry(entries) {
var idx = root.entryIndex(entries, root.centerAnchor)
return idx === -1 ? null : entries[idx]
}
@@ -1550,7 +1644,7 @@ Item {
property var entries: root.layoutEntries("center")
readonly property bool hasAnchor: root.entryIndex(entries, root.centerAnchor) !== -1
readonly property var anchorEntry: root.findCenterAnchorEntry()
readonly property var anchorEntry: root.findCenterAnchorEntry(entries)
Loader {
anchors.fill: parent
+113
View File
@@ -208,9 +208,122 @@ function nearestDropTarget(candidates, point, vertical) {
return best
}
// Display cutouts (a camera notch at the top of a laptop panel) are described
// by the platform's own package, in /usr/share/omarchy-platform/display-cutouts.json:
//
// { "panels": [ { "connector": "eDP", "width": 3024, "height": 1964, "top": 64 } ] }
//
// A panel matches a screen whose connector name starts with `connector` and
// whose mode is width x height physical pixels; `top` is how many physical rows
// at its top the cutout covers. Anything malformed is dropped.
function parseCutouts(text) {
var parsed
try {
parsed = JSON.parse(String(text || ""))
} catch (error) {
return []
}
var panels = parsed && Array.isArray(parsed.panels) ? parsed.panels : []
var cutouts = []
for (var i = 0; i < panels.length; i++) {
var panel = panels[i] || {}
var width = Number(panel.width)
var height = Number(panel.height)
var top = Number(panel.top)
if (typeof panel.connector !== "string" || panel.connector === "") continue
if (!(width > 0) || !(height > 0) || !(top > 0) || top >= height) continue
cutouts.push({ connector: panel.connector, width: width, height: height, top: top })
}
return cutouts
}
// The cutout at the top of this screen, in logical pixels, or 0. `mode` is the
// output's physical mode as Hyprland reports it ({ width, height, transform }).
// Qt's devicePixelRatio is a whole number even at a fractional scale, so the
// logical size times it rebuilds the mode only at whole scales; that stands in
// just until Hyprland's answer arrives. A mode Hyprland reports that matches no
// panel has no cutout.
function cutoutTop(cutouts, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode) {
var name = String(screenName || "")
var logical = Number(logicalWidth)
var logicalTall = Number(logicalHeight)
if (!(logical > 0) || !(logicalTall > 0)) return 0
var width = Number(mode && mode.width)
var height = Number(mode && mode.height)
if (width > 0 && height > 0) {
// Turned a quarter or upside down, the cutout is on another edge.
var transform = Number(mode.transform) || 0
if (transform !== 0 && transform !== 4) return 0
if ((width > height) !== (logical > logicalTall)) return 0
} else {
var scale = Number(devicePixelRatio) > 0 ? Number(devicePixelRatio) : 1
width = Math.round(logical * scale)
height = Math.round(logicalTall * scale)
}
var list = Array.isArray(cutouts) ? cutouts : []
for (var i = 0; i < list.length; i++) {
var panel = list[i]
if (name.indexOf(panel.connector) !== 0) continue
// Logical sizes are rounded, so a rebuilt mode can be a couple of pixels off.
if (Math.abs(width - panel.width) <= 4 && Math.abs(height - panel.height) <= 4)
return Math.ceil(panel.top * logical / width)
}
return 0
}
// The height a top bar on this screen must reach to cover its cutout, or a
// calibrated [bar] notch-height in its place. A screen without a cutout, or a
// bar on another edge, has no floor, so a calibration never reaches an
// external monitor.
function notchFloor(cutouts, position, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode, calibrated) {
if (position !== "top") return 0
var top = cutoutTop(cutouts, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode)
if (!(top > 0)) return 0
return Number(calibrated) > 0 ? Math.round(Number(calibrated)) : top
}
// A cutout covers the middle of a top bar, so that bar draws its center
// section beside the right one.
function centerBesideRight(cutouts, position, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode) {
return position === "top" && cutoutTop(cutouts, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode) > 0
}
// Whether a top bar can't tell its floor yet: Hyprland hasn't reported the
// screen's mode, the logical size times Qt's ratio matched no panel (as at a
// fractional scale), and a described panel's connector matches the screen, so
// the mode may still bring a cutout. Such a bar waits for the mode before it
// maps rather than guess a floor. A screen no entry's connector matches never
// waits.
function cutoutPending(cutouts, position, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode) {
if (position !== "top") return false
if (Number(mode && mode.width) > 0 && Number(mode && mode.height) > 0) return false
var name = String(screenName || "")
var list = Array.isArray(cutouts) ? cutouts : []
var described = false
for (var i = 0; i < list.length; i++) {
if (name.indexOf(list[i].connector) === 0) described = true
}
return described && !(cutoutTop(cutouts, screenName, logicalWidth, logicalHeight, devicePixelRatio, mode) > 0)
}
// The bar's thickness on the screen with this name, from the sizes a bar
// publishes by screen name, or `fallback` (its configured size) for a screen
// it has none for.
function barSizeFor(sizes, screenName, fallback) {
var name = String(screenName || "")
var size = sizes && Object.prototype.hasOwnProperty.call(sizes, name) ? Number(sizes[name]) : 0
return size > 0 ? size : fallback
}
if (typeof module !== "undefined") {
module.exports = {
barSizeFor: barSizeFor,
centerBesideRight: centerBesideRight,
cutoutPending: cutoutPending,
cutoutTop: cutoutTop,
isDrawnSlot: isDrawnSlot,
notchFloor: notchFloor,
parseCutouts: parseCutouts,
pickDrawnSlot: pickDrawnSlot,
pickPanelSlot: pickPanelSlot,
nearestDropTarget: nearestDropTarget,
@@ -0,0 +1,26 @@
import QtQuick
import qs.Commons as Commons
import qs.Ui
BarIndicator {
id: root
readonly property var remoteSessionService: bar?.shell?.firstPartyServiceFor("omarchy.remote-session")
readonly property var peers: remoteSessionService && Array.isArray(remoteSessionService.peers) ? remoteSessionService.peers : []
active: remoteSessionService ? remoteSessionService.active : false
activeText: "󰢹"
inactiveText: "󰢹"
activeTooltipText: peers.length > 0 ? "Remote session from " + peers.join(", ") : "Remote session"
inactiveTooltipText: "Remote Session"
useActiveColor: true
activeColor: Commons.Color.urgent
Connections {
target: root.indicatorHost
ignoreUnknownSignals: true
function onRefreshRequested() {
if (root.remoteSessionService) root.remoteSessionService.refresh()
}
}
}
@@ -15,7 +15,7 @@ BarIndicator {
function refresh() {
if (!root.bar || statusProc.running) return
statusProc.command = ["pgrep", "--quiet", "-f", "^gpu-screen-recorder"]
statusProc.command = ["omarchy-capture-screenrecording", "--status"]
statusProc.running = true
}
@@ -36,6 +36,11 @@
"label": "Screen recording",
"description": "GPU screen recorder status"
},
{
"value": "RemoteSession",
"label": "Remote session",
"description": "Warning while someone is connected through gliff"
},
{
"value": "Reminder",
"label": "Reminder",
+1 -1
View File
@@ -8,7 +8,7 @@ BarWidget {
id: root
moduleName: "omarchy.indicators"
readonly property var defaultIndicatorEntries: [ "PasswordlessSudo", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var defaultIndicatorEntries: [ "PasswordlessSudo", "ScreenRecording", "RemoteSession", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: []
property var indicatorActiveStates: ({})
+26 -5
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import qs.Ui
import qs.Commons
BarWidget {
id: root
@@ -9,15 +10,28 @@ BarWidget {
readonly property var source: Pipewire.defaultAudioSource
readonly property bool muted: source && source.audio ? source.audio.muted : true
readonly property real volume: source && source.audio ? source.audio.volume : 0
// A virtual source is untyped in Quickshell, so it has no PwNode.audio; its
// volume and mute go through wpctl (UntypedInput).
readonly property bool viaWpctl: UntypedInput.active
readonly property bool muted: viaWpctl ? (!UntypedInput.known || UntypedInput.muted) : (source && source.audio ? source.audio.muted : true)
readonly property real volume: viaWpctl ? UntypedInput.volume : (source && source.audio ? source.audio.volume : 0)
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
readonly property var nodeNames: {
var names = []
for (var i = 0; i < nodes.length; i++)
if (nodes[i] && nodes[i].name) names.push(String(nodes[i].name))
return names
}
readonly property var activeStreams: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isStream && node.isSink === false && !node.audio?.muted) list.push(node)
if (!node || !node.isStream || node.isSink !== false || node.audio?.muted) continue
// The shell's own level meters, and a platform's audio processing that
// records all the time, are not an app using the microphone.
if (AudioNodes.isShellLevelMeter(node.name) || AudioNodes.platformHides(node.name, nodeNames)) continue
list.push(node)
}
return list
}
@@ -29,7 +43,10 @@ BarWidget {
implicitHeight: button.implicitHeight
function toggleMute() {
if (source && source.audio) source.audio.muted = !source.audio.muted
if (viaWpctl) {
if (UntypedInput.known) UntypedInput.setMuted(!UntypedInput.muted)
}
else if (source && source.audio) source.audio.muted = !source.audio.muted
}
PwObjectTracker { objects: root.source ? [root.source] : [] }
@@ -46,8 +63,12 @@ BarWidget {
else root.toggleMute()
}
onWheelMoved: function(delta) {
if (!root.source || !root.source.audio) return
var step = 0.05
if (root.viaWpctl) {
if (UntypedInput.known) UntypedInput.setVolume(root.volume + (delta > 0 ? step : -step))
return
}
if (!root.source || !root.source.audio) return
root.source.audio.volume = Math.max(0, Math.min(1, root.volume + (delta > 0 ? step : -step)))
}
}