From 60a550a64613a9748ff0e8f681d357848dd18eb9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 22 Jul 2026 16:33:54 -0700 Subject: [PATCH] Add --fit OSD option and use it for system shutdown/reboot/logout --- bin/omarchy-osd | 9 ++++++--- bin/omarchy-system-logout | 2 ++ bin/omarchy-system-reboot | 2 +- bin/omarchy-system-shutdown | 2 +- shell/plugins/osd/Osd.qml | 29 ++++++++++++++++++++++------- shell/plugins/osd/OsdModel.js | 7 +++++-- test/shell.d/osd-test.sh | 22 ++++++++++++++++++++-- 7 files changed, 57 insertions(+), 16 deletions(-) diff --git a/bin/omarchy-osd b/bin/omarchy-osd index 99442989..08debdff 100755 --- a/bin/omarchy-osd +++ b/bin/omarchy-osd @@ -1,8 +1,8 @@ #!/bin/bash # omarchy:summary=Show the Omarchy Quickshell on-screen display -# omarchy:args=[-i|--icon ] [-m|--message ] [-p|--progress <0-100>] [-d|--duration ] -# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello" +# omarchy:args=[-i|--icon ] [-m|--message ] [-p|--progress <0-100>] [-d|--duration ] [-f|--fit] +# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello" | omarchy osd -m "Done" --fit set -euo pipefail @@ -12,6 +12,7 @@ progress="" progress_text="" max="100" duration="" +fit=0 while (($#)); do case $1 in @@ -19,6 +20,7 @@ while (($#)); do -m|--message) message="${2:-}"; shift 2 ;; -p|--progress) progress="${2:-}"; shift 2 ;; -d|--duration) duration="${2:-}"; shift 2 ;; + -f|--fit) fit=1; shift ;; -h|--help) omarchy osd --help; exit 0 ;; *) echo "Unknown OSD option: $1" >&2; exit 1 ;; esac @@ -35,6 +37,7 @@ payload=$(jq -cn \ --arg progressText "$progress_text" \ --arg max "$max" \ --arg duration "$duration" \ - '{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration}') + --argjson fit "$fit" \ + '{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration,fit:$fit}') omarchy-shell -q osd show "$payload" diff --git a/bin/omarchy-system-logout b/bin/omarchy-system-logout index c78f7485..4451db9f 100755 --- a/bin/omarchy-system-logout +++ b/bin/omarchy-system-logout @@ -6,6 +6,8 @@ nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 & +omarchy-osd -i logout -m "Logging out…" -d 5000 --fit + # Now close all windows omarchy-hyprland-window-close-all sleep 1 # Allow apps like Chrome to shutdown correctly diff --git a/bin/omarchy-system-reboot b/bin/omarchy-system-reboot index c2dfd130..c01c3cb2 100755 --- a/bin/omarchy-system-reboot +++ b/bin/omarchy-system-reboot @@ -8,7 +8,7 @@ # scope cannot terminate it before it runs. systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1 -omarchy-osd -i reboot -m "Rebooting…" -d 5000 +omarchy-osd -i reboot -m "Rebooting…" -d 5000 --fit omarchy-state clear re*-required diff --git a/bin/omarchy-system-shutdown b/bin/omarchy-system-shutdown index d1fa77ca..e4021e60 100755 --- a/bin/omarchy-system-shutdown +++ b/bin/omarchy-system-shutdown @@ -8,7 +8,7 @@ # scope cannot terminate it before it runs. systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl poweroff --no-wall || exit 1 -omarchy-osd -i shutdown -m "Shutting down…" -d 5000 +omarchy-osd -i shutdown -m "Shutting down…" -d 5000 --fit omarchy-state clear re*-required diff --git a/shell/plugins/osd/Osd.qml b/shell/plugins/osd/Osd.qml index 3a89ada6..2cd33bef 100644 --- a/shell/plugins/osd/Osd.qml +++ b/shell/plugins/osd/Osd.qml @@ -17,18 +17,24 @@ Item { property int maxValue: 100 property bool hasProgress: true property int duration: 1200 + property bool fit: false readonly property int cardWidth: Style.space(269) readonly property int mediaCardWidth: Math.round(cardWidth * 1.5) readonly property int messageWidth: Style.space(190) readonly property int mediaMessageWidth: messageWidth + mediaCardWidth - cardWidth readonly property bool mediaOsd: iconKey.indexOf("media") === 0 || iconKey.indexOf("player") === 0 + readonly property bool textOnlyOsd: root.fit && !root.hasProgress && !root.mediaOsd + readonly property int fitMessageWidth: root.message === "" ? 0 : Math.round(messageMetrics.boundingRect.width) + Style.space(4) + readonly property int fitCardWidth: root.message === "" || root.fitMessageWidth === 0 + ? card.borderLeft + Style.space(16) + Style.space(28) + Style.space(16) + card.borderRight + : card.borderLeft + Style.space(16) + Style.space(28) + Style.space(16) + root.fitMessageWidth + Style.space(16) + card.borderRight function iconFor(name, percent) { return OsdModel.iconFor(name, percent) } - function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) { - var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) + function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit) { + var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit) iconKey = next.iconKey maxValue = next.maxValue hasProgress = next.hasProgress @@ -36,6 +42,7 @@ Item { message = next.message icon = next.icon duration = next.duration + fit = next.fit opened = true if (duration > 0) hideTimer.restart() else hideTimer.stop() @@ -44,7 +51,7 @@ Item { function open(payloadJson) { try { var p = JSON.parse(payloadJson || "{}") - show(p.icon || "", p.message || "", p.value === undefined ? "" : String(p.value), p.max === undefined ? "100" : String(p.max), p.progressText || "", p.duration === undefined ? "1200" : String(p.duration)) + show(p.icon || "", p.message || "", p.value === undefined ? "" : String(p.value), p.max === undefined ? "100" : String(p.max), p.progressText || "", p.duration === undefined ? "1200" : String(p.duration), p.fit === undefined ? false : p.fit) } catch (e) {} } @@ -56,6 +63,14 @@ Item { onTriggered: root.opened = false } + TextMetrics { + id: messageMetrics + font.family: Style.font.family + font.bold: true + font.pixelSize: Style.font.title + text: root.message + } + IpcHandler { target: "osd" function show(payloadJson: string): string { @@ -82,7 +97,7 @@ Item { BorderSurface { id: card - width: root.mediaOsd ? root.mediaCardWidth : root.cardWidth + width: root.textOnlyOsd ? root.fitCardWidth : (root.mediaOsd ? root.mediaCardWidth : root.cardWidth) height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap) anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -121,16 +136,16 @@ Item { } } Text { - width: root.hasProgress ? Style.space(41) : (root.mediaOsd ? root.mediaMessageWidth : root.messageWidth) + width: root.textOnlyOsd ? root.fitMessageWidth : (root.hasProgress ? Style.space(41) : (root.mediaOsd ? root.mediaMessageWidth : root.messageWidth)) anchors.verticalCenter: parent.verticalCenter text: root.message font.family: Style.font.family font.bold: true font.pixelSize: Style.font.title color: Color.popups.text - elide: Text.ElideRight + elide: root.textOnlyOsd ? Text.ElideNone : Text.ElideRight maximumLineCount: 1 - clip: true + clip: !root.textOnlyOsd } } } diff --git a/shell/plugins/osd/OsdModel.js b/shell/plugins/osd/OsdModel.js index 8154c13a..fda2eede 100644 --- a/shell/plugins/osd/OsdModel.js +++ b/shell/plugins/osd/OsdModel.js @@ -16,6 +16,7 @@ function iconFor(name, percent) { if (n === "touch" || n === "touchscreen") return "󰝁" if (n === "reboot" || n === "restart") return "󰜉" if (n === "shutdown" || n === "power" || n === "poweroff") return "󰐥" + if (n === "logout" || n === "sign-out" || n === "leave") return "󰍃" if (n === "media" || n === "player") return "󰝚" if (n === "media-source" || n === "player-source") return "󰝚" if (n === "media-play" || n === "player-play") return "󰐊" @@ -29,13 +30,14 @@ function iconFor(name, percent) { return "" } -function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) { +function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit) { var maxValue = Math.max(1, parseInt(rawMax || "100", 10)) var parsedValue = parseInt(rawValue || "0", 10) var hasProgress = rawValue !== "" && !isNaN(parsedValue) && rawMessage === "" var value = hasProgress ? clamp(parsedValue, 0, maxValue) : 0 var percent = hasProgress ? Math.round(value * 100 / maxValue) : -1 var parsedDuration = parseInt(rawDuration || "1200", 10) + var fit = rawFit === true || rawFit === 1 || rawFit === "1" || rawFit === "true" return { iconKey: String(iconName || "").toLowerCase(), @@ -44,7 +46,8 @@ function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, r value: value, message: String(rawMessage || (hasProgress ? (rawProgressText || percent + "%") : "")), icon: iconFor(iconName, percent), - duration: isNaN(parsedDuration) ? 1200 : Math.max(0, parsedDuration) + duration: isNaN(parsedDuration) ? 1200 : Math.max(0, parsedDuration), + fit: fit } } diff --git a/test/shell.d/osd-test.sh b/test/shell.d/osd-test.sh index e791d626..d8d1587d 100644 --- a/test/shell.d/osd-test.sh +++ b/test/shell.d/osd-test.sh @@ -9,6 +9,7 @@ const osd = requireFromRoot('shell/plugins/osd/OsdModel.js') assertEqual(osd.iconFor('', 0), osd.iconFor('muted', 50), 'osd falls back to muted icon at zero percent') assertEqual(osd.iconFor('volume-high', 1), osd.iconFor('', 100), 'osd maps high volume aliases') +assertEqual(osd.iconFor('logout', 50), '󰍃', 'osd maps logout icon') assertEqual(osd.iconFor('custom-symbol', 50), 'custom-symbol', 'osd preserves unknown explicit icons') assertDeepEqual( @@ -20,7 +21,8 @@ assertDeepEqual( value: 75, message: '75%', icon: osd.iconFor('volume', 75), - duration: 800 + duration: 800, + fit: false }, 'osd builds progress state' ) @@ -34,8 +36,24 @@ assertDeepEqual( value: 0, message: 'Paused', icon: osd.iconFor('media-pause', -1), - duration: 1200 + duration: 1200, + fit: false }, 'osd builds message state' ) + +assertDeepEqual( + osd.stateForShow('shutdown', 'Shutting down…', '', '100', '', '5000', '1'), + { + iconKey: 'shutdown', + maxValue: 100, + hasProgress: false, + value: 0, + message: 'Shutting down…', + icon: osd.iconFor('shutdown', -1), + duration: 5000, + fit: true + }, + 'osd parses fit flag' +) JS