Give every OSD card exact padding on all four sides
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LfqzqpU1T6pQRsmpbGxBs5
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e152b0d9e
commit
248659de5a
+3
-6
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Show the Omarchy Quickshell on-screen display
|
# omarchy:summary=Show the Omarchy Quickshell on-screen display
|
||||||
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>] [-d|--duration <ms>] [-f|--fit]
|
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>] [-d|--duration <ms>]
|
||||||
# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello" | omarchy osd -m "Done" --fit
|
# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello"
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ progress=""
|
|||||||
progress_text=""
|
progress_text=""
|
||||||
max="100"
|
max="100"
|
||||||
duration=""
|
duration=""
|
||||||
fit=0
|
|
||||||
|
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case $1 in
|
case $1 in
|
||||||
@@ -20,7 +19,6 @@ while (($#)); do
|
|||||||
-m|--message) message="${2:-}"; shift 2 ;;
|
-m|--message) message="${2:-}"; shift 2 ;;
|
||||||
-p|--progress) progress="${2:-}"; shift 2 ;;
|
-p|--progress) progress="${2:-}"; shift 2 ;;
|
||||||
-d|--duration) duration="${2:-}"; shift 2 ;;
|
-d|--duration) duration="${2:-}"; shift 2 ;;
|
||||||
-f|--fit) fit=1; shift ;;
|
|
||||||
-h|--help) omarchy osd --help; exit 0 ;;
|
-h|--help) omarchy osd --help; exit 0 ;;
|
||||||
*) echo "Unknown OSD option: $1" >&2; exit 1 ;;
|
*) echo "Unknown OSD option: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
@@ -37,7 +35,6 @@ payload=$(jq -cn \
|
|||||||
--arg progressText "$progress_text" \
|
--arg progressText "$progress_text" \
|
||||||
--arg max "$max" \
|
--arg max "$max" \
|
||||||
--arg duration "$duration" \
|
--arg duration "$duration" \
|
||||||
--argjson fit "$fit" \
|
'{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration}')
|
||||||
'{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration,fit:$fit}')
|
|
||||||
|
|
||||||
omarchy-shell -q osd show "$payload"
|
omarchy-shell -q osd show "$payload"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
||||||
|
|
||||||
omarchy-osd -i logout -m "Logging out…" -d 5000 --fit
|
omarchy-osd -i logout -m "Logging out" -d 5000
|
||||||
|
|
||||||
# Now close all windows
|
# Now close all windows
|
||||||
omarchy-hyprland-window-close-all
|
omarchy-hyprland-window-close-all
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# scope cannot terminate it before it runs.
|
# 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
|
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 --fit
|
omarchy-osd -i reboot -m "Rebooting" -d 5000
|
||||||
|
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# scope cannot terminate it before it runs.
|
# 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
|
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 --fit
|
omarchy-osd -i shutdown -m "Shutting down" -d 5000
|
||||||
|
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
|
|
||||||
|
|||||||
+80
-36
@@ -17,24 +17,45 @@ Item {
|
|||||||
property int maxValue: 100
|
property int maxValue: 100
|
||||||
property bool hasProgress: true
|
property bool hasProgress: true
|
||||||
property int duration: 1200
|
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 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)
|
// The card is built out of measured columns instead of fixed widths, so it
|
||||||
readonly property int fitCardWidth: root.message === "" || root.fitMessageWidth === 0
|
// keeps exactly `pad` between border and content on every side whatever
|
||||||
? card.borderLeft + Style.space(16) + Style.space(28) + Style.space(16) + card.borderRight
|
// glyph or message it carries. Messages grow with their text up to
|
||||||
: card.borderLeft + Style.space(16) + Style.space(28) + Style.space(16) + root.fitMessageWidth + Style.space(16) + card.borderRight
|
// `maxMessageWidth` and elide beyond it.
|
||||||
|
readonly property int pad: Style.space(16)
|
||||||
|
readonly property int gap: Style.space(16)
|
||||||
|
// A glyph next to a message reads airier than it measures: the icon outline
|
||||||
|
// and the letterforms both fall away from their ink extremes, so the space
|
||||||
|
// between them opens up well past the nominal gap. Text takes two thirds of
|
||||||
|
// it; the progress bar's hard edge keeps the full gap.
|
||||||
|
readonly property int messageGap: Math.round(root.gap * 2 / 3)
|
||||||
|
readonly property int barWidth: Style.space(142)
|
||||||
|
readonly property int maxMessageWidth: root.mediaOsd ? Style.space(325) : Style.space(190)
|
||||||
|
|
||||||
|
// Nerd Font glyphs draw well outside their monospace cell, so the icon
|
||||||
|
// column is measured by ink rather than by advance width. Progress OSDs pin
|
||||||
|
// it to the widest glyph the model can return, so the bar doesn't shift when
|
||||||
|
// volume crosses an icon threshold.
|
||||||
|
readonly property int iconInkWidth: Math.ceil(iconMetrics.tightBoundingRect.width)
|
||||||
|
readonly property int iconWidth: root.hasProgress
|
||||||
|
? Math.max(root.iconInkWidth, Math.ceil(widestIconMetrics.tightBoundingRect.width))
|
||||||
|
: root.iconInkWidth
|
||||||
|
// Same idea for the readout: it is as wide as the longest percentage so the
|
||||||
|
// digits don't jitter between 9% and 100%.
|
||||||
|
readonly property int valueWidth: Math.ceil(Math.max(valueMetrics.advanceWidth, messageMetrics.advanceWidth))
|
||||||
|
readonly property int messageWidth: Math.min(Math.ceil(messageMetrics.advanceWidth), root.maxMessageWidth)
|
||||||
|
readonly property int contentWidth: root.hasProgress
|
||||||
|
? root.iconWidth + root.gap + root.barWidth + root.gap + root.valueWidth
|
||||||
|
: (root.message === "" ? root.iconWidth : root.iconWidth + root.messageGap + root.messageWidth)
|
||||||
|
|
||||||
function iconFor(name, percent) {
|
function iconFor(name, percent) {
|
||||||
return OsdModel.iconFor(name, percent)
|
return OsdModel.iconFor(name, percent)
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit) {
|
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
|
||||||
var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit)
|
var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration)
|
||||||
iconKey = next.iconKey
|
iconKey = next.iconKey
|
||||||
maxValue = next.maxValue
|
maxValue = next.maxValue
|
||||||
hasProgress = next.hasProgress
|
hasProgress = next.hasProgress
|
||||||
@@ -42,7 +63,6 @@ Item {
|
|||||||
message = next.message
|
message = next.message
|
||||||
icon = next.icon
|
icon = next.icon
|
||||||
duration = next.duration
|
duration = next.duration
|
||||||
fit = next.fit
|
|
||||||
opened = true
|
opened = true
|
||||||
if (duration > 0) hideTimer.restart()
|
if (duration > 0) hideTimer.restart()
|
||||||
else hideTimer.stop()
|
else hideTimer.stop()
|
||||||
@@ -51,7 +71,7 @@ Item {
|
|||||||
function open(payloadJson) {
|
function open(payloadJson) {
|
||||||
try {
|
try {
|
||||||
var p = JSON.parse(payloadJson || "{}")
|
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), p.fit === undefined ? false : p.fit)
|
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))
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +91,25 @@ Item {
|
|||||||
text: root.message
|
text: root.message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: valueMetrics
|
||||||
|
font: messageMetrics.font
|
||||||
|
text: "100%"
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: iconMetrics
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.displayLarge
|
||||||
|
text: root.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: widestIconMetrics
|
||||||
|
font: iconMetrics.font
|
||||||
|
text: OsdModel.widestIcon
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "osd"
|
target: "osd"
|
||||||
function show(payloadJson: string): string {
|
function show(payloadJson: string): string {
|
||||||
@@ -97,8 +136,8 @@ Item {
|
|||||||
|
|
||||||
BorderSurface {
|
BorderSurface {
|
||||||
id: card
|
id: card
|
||||||
width: root.textOnlyOsd ? root.fitCardWidth : (root.mediaOsd ? root.mediaCardWidth : root.cardWidth)
|
width: card.borderLeft + root.pad + root.contentWidth + root.pad + card.borderRight
|
||||||
height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap)
|
height: card.borderTop + root.pad + Style.font.displayLarge + root.pad + card.borderBottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: Style.space(67)
|
anchors.bottomMargin: Style.space(67)
|
||||||
@@ -109,23 +148,27 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: card.borderTop
|
anchors.topMargin: card.borderTop + root.pad
|
||||||
anchors.rightMargin: card.borderRight + Style.space(16)
|
anchors.rightMargin: card.borderRight + root.pad
|
||||||
anchors.bottomMargin: card.borderBottom
|
anchors.bottomMargin: card.borderBottom + root.pad
|
||||||
anchors.leftMargin: card.borderLeft + Style.space(16)
|
anchors.leftMargin: card.borderLeft + root.pad
|
||||||
spacing: Style.space(16)
|
spacing: root.hasProgress ? root.gap : root.messageGap
|
||||||
Text {
|
Item {
|
||||||
width: Style.space(28)
|
width: root.iconWidth
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
height: parent.height
|
||||||
horizontalAlignment: Text.AlignHCenter
|
Text {
|
||||||
text: root.icon
|
// Sit the glyph's ink flush in the column, centered when the
|
||||||
font.family: Style.font.family
|
// column is wider than this particular glyph.
|
||||||
font.pixelSize: Style.font.displayLarge
|
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
||||||
color: Color.popups.text
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.icon
|
||||||
|
font: iconMetrics.font
|
||||||
|
color: Color.popups.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: root.hasProgress
|
visible: root.hasProgress
|
||||||
width: visible ? Style.space(142) : 0
|
width: root.barWidth
|
||||||
height: Math.max(Style.space(6), Style.spacing.sm)
|
height: Math.max(Style.space(6), Style.spacing.sm)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: Util.alpha(Color.popups.text, 0.45)
|
color: Util.alpha(Color.popups.text, 0.45)
|
||||||
@@ -136,16 +179,17 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
width: root.textOnlyOsd ? root.fitMessageWidth : (root.hasProgress ? Style.space(41) : (root.mediaOsd ? root.mediaMessageWidth : root.messageWidth))
|
visible: root.message !== ""
|
||||||
|
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
||||||
|
// The readout hugs the card edge so a short percentage doesn't leave
|
||||||
|
// a hole in the padding; the slack lands in the gap after the bar.
|
||||||
|
horizontalAlignment: root.hasProgress ? Text.AlignRight : Text.AlignLeft
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.message
|
text: root.message
|
||||||
font.family: Style.font.family
|
font: messageMetrics.font
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: Style.font.title
|
|
||||||
color: Color.popups.text
|
color: Color.popups.text
|
||||||
elide: root.textOnlyOsd ? Text.ElideNone : Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
clip: !root.textOnlyOsd
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ function clamp(value, min, max) {
|
|||||||
return Math.max(min, Math.min(max, value))
|
return Math.max(min, Math.min(max, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The widest glyph `iconFor` can return. The progress OSD sizes its icon
|
||||||
|
// column to it so the bar keeps its place as the icon changes.
|
||||||
|
var widestIcon = ""
|
||||||
|
|
||||||
function iconFor(name, percent) {
|
function iconFor(name, percent) {
|
||||||
var n = String(name || "").toLowerCase()
|
var n = String(name || "").toLowerCase()
|
||||||
if (n === "volume-muted" || n === "volume-mute" || n === "muted" || n === "mute") return ""
|
if (n === "volume-muted" || n === "volume-mute" || n === "muted" || n === "mute") return ""
|
||||||
@@ -30,14 +34,13 @@ function iconFor(name, percent) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration, rawFit) {
|
function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
|
||||||
var maxValue = Math.max(1, parseInt(rawMax || "100", 10))
|
var maxValue = Math.max(1, parseInt(rawMax || "100", 10))
|
||||||
var parsedValue = parseInt(rawValue || "0", 10)
|
var parsedValue = parseInt(rawValue || "0", 10)
|
||||||
var hasProgress = rawValue !== "" && !isNaN(parsedValue) && rawMessage === ""
|
var hasProgress = rawValue !== "" && !isNaN(parsedValue) && rawMessage === ""
|
||||||
var value = hasProgress ? clamp(parsedValue, 0, maxValue) : 0
|
var value = hasProgress ? clamp(parsedValue, 0, maxValue) : 0
|
||||||
var percent = hasProgress ? Math.round(value * 100 / maxValue) : -1
|
var percent = hasProgress ? Math.round(value * 100 / maxValue) : -1
|
||||||
var parsedDuration = parseInt(rawDuration || "1200", 10)
|
var parsedDuration = parseInt(rawDuration || "1200", 10)
|
||||||
var fit = rawFit === true || rawFit === 1 || rawFit === "1" || rawFit === "true"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
iconKey: String(iconName || "").toLowerCase(),
|
iconKey: String(iconName || "").toLowerCase(),
|
||||||
@@ -46,13 +49,13 @@ function stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, r
|
|||||||
value: value,
|
value: value,
|
||||||
message: String(rawMessage || (hasProgress ? (rawProgressText || percent + "%") : "")),
|
message: String(rawMessage || (hasProgress ? (rawProgressText || percent + "%") : "")),
|
||||||
icon: iconFor(iconName, percent),
|
icon: iconFor(iconName, percent),
|
||||||
duration: isNaN(parsedDuration) ? 1200 : Math.max(0, parsedDuration),
|
duration: isNaN(parsedDuration) ? 1200 : Math.max(0, parsedDuration)
|
||||||
fit: fit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
widestIcon: widestIcon,
|
||||||
iconFor: iconFor,
|
iconFor: iconFor,
|
||||||
stateForShow: stateForShow
|
stateForShow: stateForShow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ assertEqual(osd.iconFor('', 0), osd.iconFor('muted', 50), 'osd falls back to mut
|
|||||||
assertEqual(osd.iconFor('volume-high', 1), osd.iconFor('', 100), 'osd maps high volume aliases')
|
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('logout', 50), '', 'osd maps logout icon')
|
||||||
assertEqual(osd.iconFor('custom-symbol', 50), 'custom-symbol', 'osd preserves unknown explicit icons')
|
assertEqual(osd.iconFor('custom-symbol', 50), 'custom-symbol', 'osd preserves unknown explicit icons')
|
||||||
|
assertEqual(osd.widestIcon, osd.iconFor('volume-high', 100), 'osd sizes the icon column to a glyph it can show')
|
||||||
|
|
||||||
assertDeepEqual(
|
assertDeepEqual(
|
||||||
osd.stateForShow('volume', '', '75', '100', '', '800'),
|
osd.stateForShow('volume', '', '75', '100', '', '800'),
|
||||||
@@ -21,8 +22,7 @@ assertDeepEqual(
|
|||||||
value: 75,
|
value: 75,
|
||||||
message: '75%',
|
message: '75%',
|
||||||
icon: osd.iconFor('volume', 75),
|
icon: osd.iconFor('volume', 75),
|
||||||
duration: 800,
|
duration: 800
|
||||||
fit: false
|
|
||||||
},
|
},
|
||||||
'osd builds progress state'
|
'osd builds progress state'
|
||||||
)
|
)
|
||||||
@@ -36,24 +36,8 @@ assertDeepEqual(
|
|||||||
value: 0,
|
value: 0,
|
||||||
message: 'Paused',
|
message: 'Paused',
|
||||||
icon: osd.iconFor('media-pause', -1),
|
icon: osd.iconFor('media-pause', -1),
|
||||||
duration: 1200,
|
duration: 1200
|
||||||
fit: false
|
|
||||||
},
|
},
|
||||||
'osd builds message state'
|
'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
|
JS
|
||||||
|
|||||||
Reference in New Issue
Block a user