Merge branch 'quattro' into unified-input-method-setup
This commit is contained in:
commit
101bb547bb
245 files changed
+19949
-375
No files matched your search
+105
-11
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: ({})
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user