Integrate swaybg and swayosd into the new shell

This commit is contained in:
David Heinemeier Hansson
2026-05-16 20:34:09 +02:00
parent 83e7f41308
commit 1eab0cffc9
48 changed files with 665 additions and 353 deletions
+1 -2
View File
@@ -1,8 +1,7 @@
hl.on("hyprland.start", function()
hl.exec_cmd("uwsm-app -- hypridle")
hl.exec_cmd("omarchy-restart-shell")
hl.exec_cmd("uwsm-app -- hypridle")
hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem")
hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill")
hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1")
hl.exec_cmd("omarchy-first-run")
hl.exec_cmd("omarchy-powerprofiles-init")
+10 -10
View File
@@ -1,8 +1,8 @@
-- Volume, brightness, keyboard backlight, and touchpad controls.
o.bind("XF86AudioRaiseVolume", "Volume up", "omarchy-swayosd-client --output-volume raise", { locked = true, repeating = true })
o.bind("XF86AudioLowerVolume", "Volume down", "omarchy-swayosd-client --output-volume lower", { locked = true, repeating = true })
o.bind("XF86AudioMute", "Mute", "omarchy-swayosd-client --output-volume mute-toggle", { locked = true, repeating = true })
o.bind("XF86AudioMicMute", "Mute microphone", "omarchy-audio-input-mute", { locked = true, repeating = true })
o.bind("XF86AudioRaiseVolume", "Volume up", "omarchy-audio-output-volume raise", { locked = true, repeating = true })
o.bind("XF86AudioLowerVolume", "Volume down", "omarchy-audio-output-volume lower", { locked = true, repeating = true })
o.bind("XF86AudioMute", "Mute", "omarchy-audio-output-volume mute-toggle", { locked = true })
o.bind("XF86AudioMicMute", "Mute microphone", "omarchy-audio-input-mute", { locked = true })
o.bind("XF86MonBrightnessUp", "Brightness up", "omarchy-brightness-display +5%", { locked = true, repeating = true })
o.bind("XF86MonBrightnessDown", "Brightness down", "omarchy-brightness-display 5%-", { locked = true, repeating = true })
o.bind("SHIFT + XF86MonBrightnessUp", "Brightness maximum", "omarchy-brightness-display 100%", { locked = true, repeating = true })
@@ -15,15 +15,15 @@ o.bind("XF86TouchpadOn", "Enable touchpad", "omarchy-toggle-touchpad on", { lock
o.bind("XF86TouchpadOff", "Disable touchpad", "omarchy-toggle-touchpad off", { locked = true })
-- Precise volume and brightness controls.
o.bind("ALT + XF86AudioRaiseVolume", "Volume up precise", "omarchy-swayosd-client --output-volume +1", { locked = true, repeating = true })
o.bind("ALT + XF86AudioLowerVolume", "Volume down precise", "omarchy-swayosd-client --output-volume -1", { locked = true, repeating = true })
o.bind("ALT + XF86AudioRaiseVolume", "Volume up precise", "omarchy-audio-output-volume +1", { locked = true, repeating = true })
o.bind("ALT + XF86AudioLowerVolume", "Volume down precise", "omarchy-audio-output-volume -1", { locked = true, repeating = true })
o.bind("ALT + XF86MonBrightnessUp", "Brightness up precise", "omarchy-brightness-display +1%", { locked = true, repeating = true })
o.bind("ALT + XF86MonBrightnessDown", "Brightness down precise", "omarchy-brightness-display 1%-", { locked = true, repeating = true })
-- Media controls.
o.bind("XF86AudioNext", "Next track", "omarchy-swayosd-client --playerctl next", { locked = true })
o.bind("XF86AudioPause", "Pause", "omarchy-swayosd-client --playerctl play-pause", { locked = true })
o.bind("XF86AudioPlay", "Play", "omarchy-swayosd-client --playerctl play-pause", { locked = true })
o.bind("XF86AudioPrev", "Previous track", "omarchy-swayosd-client --playerctl previous", { locked = true })
o.bind("XF86AudioNext", "Next track", "omarchy-audio-player next", { locked = true })
o.bind("XF86AudioPause", "Pause", "omarchy-audio-player play-pause", { locked = true })
o.bind("XF86AudioPlay", "Play", "omarchy-audio-player play-pause", { locked = true })
o.bind("XF86AudioPrev", "Previous track", "omarchy-audio-player previous", { locked = true })
o.bind("SUPER + XF86AudioMute", "Switch audio output", "omarchy-audio-output-switch", { locked = true })
+4 -4
View File
@@ -79,7 +79,7 @@ Omarchy is built on:
| **Omarchy shell** | Status bar + notifications (Quickshell) | `~/.config/omarchy/shell.json` |
| **Walker** | App launcher | `~/.config/walker/` |
| **Alacritty/Foot/Kitty/Ghostty** | Terminals | `~/.config/<terminal>/` |
| **SwayOSD** | On-screen display | `~/.config/swayosd/` |
| **Omarchy OSD** | On-screen display | Quickshell plugin |
## Command Discovery
@@ -111,7 +111,7 @@ Run `omarchy --help` for the full list. The most common groups:
| Group | Purpose | Example |
|-------|---------|---------|
| `omarchy refresh` | Reset config to defaults (backs up first) | `omarchy refresh shell` |
| `omarchy restart` | Restart a service/app | `omarchy restart shell` |
| `omarchy restart` | Restart a service/app | `omarchy-restart-shell` |
| `omarchy toggle` | Toggle feature on/off | `omarchy toggle nightlight` |
| `omarchy theme` | Theme management | `omarchy theme set <name>` |
| `omarchy install` | Install optional software / packages | `omarchy install docker dbs` |
@@ -160,7 +160,7 @@ inside a single long-running Quickshell process (`omarchy-shell`).
The shell hot-reloads `shell.json` on save — no restart needed for layout
changes. For more invasive changes (new plugin, packaged update):
**Commands:** `omarchy restart shell`, `omarchy refresh shell`, `omarchy launch settings`
**Commands:** `omarchy-restart-shell`, `omarchy refresh shell`, `omarchy launch settings`
### Terminals
@@ -201,7 +201,7 @@ cp ~/.config/hypr/bindings.conf ~/.config/hypr/bindings.conf.bak.$(date +%s)
# 4. Apply changes
# - Hyprland: auto-reloads on save, but MUST validate with `hyprctl reload` and `hyprctl configerrors`
# - Omarchy shell: shell.json hot-reloads; use `omarchy restart shell` for plugin/widget code changes
# - Omarchy shell: shell.json hot-reloads; use `omarchy-restart-shell` for plugin/widget code changes
# - Walker: MUST restart with `omarchy restart walker`
# - Terminals: MUST restart with `omarchy restart terminal`
```
+3 -3
View File
@@ -164,7 +164,7 @@
"setup.config.hypridle": {"icon":"","label":"Hypridle","keywords":"idle","action":"omarchy-launch-config-editor ~/.config/hypr/hypridle.conf && omarchy-restart-hypridle"},
"setup.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"lock","action":"omarchy-launch-config-editor ~/.config/hypr/hyprlock.conf"},
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"night light","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
"setup.config.swayosd": {"icon":"","label":"Swayosd","action":"omarchy-launch-config-editor ~/.config/swayosd/config.toml && omarchy-restart-swayosd"},
"setup.config.osd": {"icon":"","label":"OSD","action":"omarchy launch settings"},
"setup.config.walker": {"icon":"󰌧","label":"Walker","keywords":"launcher","action":"omarchy-launch-config-editor ~/.config/walker/config.toml && omarchy-restart-walker"},
"setup.config.bar": {"icon":"󰍜","label":"Bar","keywords":"quickshell shell config","action":"omarchy-launch-settings bar"},
"setup.config.xcompose": {"icon":"󰞅","label":"XCompose","keywords":"compose key","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
@@ -311,7 +311,7 @@
"update.channel.dev": {"icon":"🔴","label":"Dev","keywords":"development","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set dev'"},
"update.process.hypridle": {"icon":"","label":"Hypridle","keywords":"restart","action":"omarchy-restart-hypridle"},
"update.process.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"restart","action":"omarchy-restart-hyprsunset"},
"update.process.swayosd": {"icon":"","label":"Swayosd","keywords":"restart","action":"omarchy-restart-swayosd"},
"update.process.osd": {"icon":"","label":"OSD","keywords":"restart","action":"omarchy-restart-shell"},
"update.process.walker": {"icon":"󰌧","label":"Walker","keywords":"restart launcher","action":"omarchy-restart-walker"},
"update.process.shell": {"icon":"󰍜","label":"Shell","keywords":"restart bar quickshell omarchy-shell","action":"omarchy-restart-shell"},
"update.config.hyprland": {"icon":"","label":"Hyprland","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprland"},
@@ -319,7 +319,7 @@
"update.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprlock"},
"update.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprsunset"},
"update.config.plymouth": {"icon":"󱣴","label":"Plymouth","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-plymouth"},
"update.config.swayosd": {"icon":"","label":"Swayosd","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-swayosd"},
"update.config.osd": {"icon":"","label":"OSD","keywords":"refresh","action":"omarchy-restart-shell"},
"update.config.tmux": {"icon":"","label":"Tmux","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-tmux"},
"update.config.walker": {"icon":"󰌧","label":"Walker","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-walker"},
"update.config.shell": {"icon":"󰍜","label":"Shell","keywords":"refresh bar quickshell","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-shell"},
+1 -1
View File
@@ -121,7 +121,7 @@ quickshell ipc -p $OMARCHY_PATH/default/quickshell/omarchy-shell call shell ping
```
Hyprland starts the shell through `omarchy-restart-shell` on boot.
Use `omarchy restart shell` to reload the long-running shell process.
Use `omarchy-restart-shell` to reload the long-running shell process.
A convenience wrapper, [`omarchy-shell-ipc`](../../../bin/omarchy-shell-ipc),
starts the shell if it is not already running, then forwards a `call`. It
@@ -0,0 +1,259 @@
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
Item {
id: root
property string omarchyPath: ""
property var shell: null
property var manifest: null
readonly property string home: Quickshell.env("HOME")
readonly property string currentBackgroundLink: home + "/.config/omarchy/current/background"
property string currentBackground: ""
property string displayedBackground: ""
property string incomingBackground: ""
property string oldBackground: ""
property bool finishingTransition: false
property int backgroundVersion: 0
property real revealProgress: 1
function imageUrl(path) {
if (!path) return ""
return "file://" + path
}
function refreshBackground() {
if (!readlinkProc.running) readlinkProc.running = true
}
function setBackground(path) {
path = String(path || "").trim()
if (!path || path === currentBackground) return
currentBackground = path
backgroundVersion += 1
if (!displayedBackground) {
displayedBackground = path
revealProgress = 1
return
}
revealAnimation.stop()
finishingTransition = false
oldBackground = displayedBackground
incomingBackground = path
revealProgress = 0
}
function openSelector() {
if (!bgSwitchProc.running) bgSwitchProc.running = true
}
Process {
id: bgSwitchProc
command: ["bash", "-lc", "background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""]
onExited: root.refreshBackground()
}
Process {
id: readlinkProc
command: ["readlink", "-f", root.currentBackgroundLink]
stdout: StdioCollector {
onStreamFinished: root.setBackground(String(text || "").trim())
}
}
Timer {
interval: 100
running: true
repeat: true
onTriggered: root.refreshBackground()
}
NumberAnimation {
id: revealAnimation
target: root
property: "revealProgress"
from: 0
to: 1
duration: 420
easing.type: Easing.InOutCubic
onFinished: {
if (root.incomingBackground) {
root.displayedBackground = root.incomingBackground
root.finishingTransition = true
}
root.revealProgress = 1
}
}
Component.onCompleted: refreshBackground()
Variants {
model: Quickshell.screens
PanelWindow {
id: panel
required property var modelData
screen: modelData
visible: true
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
property bool maskReady: false
WlrLayershell.namespace: "omarchy-background"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
Image {
id: base
anchors.fill: parent
source: root.imageUrl(root.displayedBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
onStatusChanged: {
if (status === Image.Ready && root.finishingTransition) {
root.incomingBackground = ""
root.oldBackground = ""
root.finishingTransition = false
}
}
}
Image {
id: incomingFrame
anchors.fill: parent
source: root.imageUrl(root.incomingBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
visible: root.incomingBackground !== "" && status === Image.Ready
opacity: root.revealProgress >= 1 ? 1 : 0.001
onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.prepareImage()
}
Image {
id: oldFrame
anchors.fill: parent
source: root.imageUrl(root.oldBackground)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
visible: root.oldBackground !== "" && root.revealProgress < 1
onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.requestPaint()
}
Canvas {
id: revealCanvas
anchors.fill: parent
visible: root.incomingBackground !== "" && root.revealProgress < 1 && incomingFrame.status === Image.Ready && oldFrame.status === Image.Ready
opacity: panel.maskReady ? 1 : 0
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Immediate
readonly property real slant: -0.18
function prepareImage() {
var src = root.imageUrl(root.incomingBackground)
if (!src) return
if (isImageLoaded(src)) {
requestPaint()
} else if (!isImageLoading(src)) {
loadImage(src)
}
}
onImageLoaded: function(url) {
if (url === root.imageUrl(root.incomingBackground)) requestPaint()
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
var src = root.imageUrl(root.incomingBackground)
if (!src || incomingFrame.status !== Image.Ready || root.revealProgress >= 1) return
if (!isImageLoaded(src)) {
prepareImage()
return
}
var iw = incomingFrame.sourceSize.width
var ih = incomingFrame.sourceSize.height
if (iw <= 0 || ih <= 0 || width <= 0 || height <= 0) return
var sx = 0
var sy = 0
var sw = iw
var sh = ih
if (iw / ih > width / height) {
sw = ih * width / height
sx = (iw - sw) / 2
} else {
sh = iw * height / width
sy = (ih - sh) / 2
}
var center = width / 2
var reach = width / 2 + Math.abs(slant) * height + 4
var spread = reach * root.revealProgress
var leftTop = center - spread
var leftBottom = center - spread + slant * height
var rightTop = center + spread
var rightBottom = center + spread + slant * height
ctx.save()
ctx.beginPath()
ctx.moveTo(center, 0)
ctx.lineTo(leftTop, 0)
ctx.lineTo(leftBottom, height)
ctx.lineTo(center, height)
ctx.closePath()
ctx.clip()
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height)
ctx.restore()
ctx.save()
ctx.beginPath()
ctx.moveTo(center, 0)
ctx.lineTo(rightTop, 0)
ctx.lineTo(rightBottom, height)
ctx.lineTo(center, height)
ctx.closePath()
ctx.clip()
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height)
ctx.restore()
if (!panel.maskReady && root.incomingBackground && root.revealProgress === 0) {
Qt.callLater(function() {
panel.maskReady = true
revealAnimation.restart()
})
}
}
Connections {
target: root
function onRevealProgressChanged() { revealCanvas.requestPaint() }
function onIncomingBackgroundChanged() {
panel.maskReady = false
revealCanvas.prepareImage()
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onDoubleClicked: root.openSelector()
}
}
}
}
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "omarchy.background",
"name": "Background",
"version": "1.0.0",
"author": "Omarchy",
"description": "Desktop background renderer with click handling and transitions",
"kinds": ["service"],
"activation": "startup",
"entryPoints": { "service": "Background.qml" }
}
@@ -0,0 +1,140 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
Item {
id: root
property string omarchyPath: ""
property var shell: null
property var manifest: null
property bool opened: false
property string icon: ""
property string message: ""
property int value: 0
property int maxValue: 100
property bool hasProgress: true
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)) }
function iconFor(name, percent) {
var n = String(name || "").toLowerCase()
if (n === "volume-muted" || n === "volume-mute" || n === "muted" || n === "mute") return "󰝟"
if (n === "volume-low") return ""
if (n === "volume-medium") return ""
if (n === "volume-high" || n === "volume") return ""
if (n === "microphone-muted" || n === "microphone-off" || n === "mic-muted" || n === "mic-off") return "󰍭"
if (n === "microphone" || n === "mic") return "󰍬"
if (n === "keyboard") return "󰌌"
if (n === "brightness" || n === "display") return "󰃠"
if (n === "touchpad") return "󰟸"
if (n === "touch" || n === "touchscreen") return "󰜉"
if (n === "media" || n === "player") return "󰝚"
if (percent <= 0) return "󰝟"
if (percent <= 33) return ""
if (percent <= 66) return ""
return ""
}
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText) {
maxValue = Math.max(1, parseInt(rawMax || "100", 10))
var parsed = parseInt(rawValue || "0", 10)
hasProgress = rawValue !== "" && !isNaN(parsed) && rawMessage === ""
value = hasProgress ? clamp(parsed, 0, maxValue) : 0
message = String(rawMessage || (hasProgress ? (rawProgressText || Math.round(value * 100 / maxValue) + "%") : ""))
icon = iconFor(iconName, hasProgress ? Math.round(value * 100 / maxValue) : -1)
opened = true
hideTimer.restart()
}
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 || "")
} catch (e) {}
}
function close() { opened = false }
Timer {
id: hideTimer
interval: 1200
onTriggered: root.opened = false
}
IpcHandler {
target: "osd"
function show(payloadJson: string): string {
root.open(payloadJson)
return "ok"
}
function close(): string { root.close(); return "ok" }
function state(): string { return root.opened ? "open" : "closed" }
function ping(): string { return "ok" }
}
PanelWindow {
id: panel
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "omarchy-osd"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
Rectangle {
id: card
width: 269
height: 68
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 67
color: Color.alpha(Color.background, 0.97)
border.color: Color.foreground
border.width: 2
radius: 0
opacity: root.opened ? 1 : 0
Row {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 16
Text {
width: 28
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
text: root.icon
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 27
color: Color.foreground
}
Rectangle {
visible: root.hasProgress
width: visible ? 142 : 0
height: 6
anchors.verticalCenter: parent.verticalCenter
color: Color.alpha(Color.foreground, 0.45)
Rectangle {
height: parent.height
width: parent.width * (root.hasProgress ? root.value / root.maxValue : 0)
color: Color.accent
}
}
Text {
width: root.hasProgress ? 41 : 190
anchors.verticalCenter: parent.verticalCenter
text: root.message
font.family: "JetBrainsMono Nerd Font"
font.bold: true
font.pixelSize: 14
color: Color.foreground
}
}
}
}
}
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "omarchy.osd",
"name": "On-screen display",
"version": "1.0.0",
"description": "Quickshell volume, brightness, and status overlays.",
"kinds": ["panel"],
"activation": "persistent",
"keepLoaded": true,
"entryPoints": { "panel": "Osd.qml" }
}
@@ -5,28 +5,67 @@
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "workspaces" }
{
"id": "omarchy"
},
{
"id": "workspaces"
}
],
"center": [
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
{ "id": "weatherFlyout" },
{ "id": "update" },
{ "id": "voxtype" },
{ "id": "screenRecording" },
{ "id": "idle" },
{ "id": "notifications" }
{
"id": "calendar",
"format": "dddd HH:mm",
"formatAlt": "dd MMMM 'W'ww yyyy",
"verticalFormat": "HH\n\u2014\nmm"
},
{
"id": "weatherFlyout"
},
{
"id": "update"
},
{
"id": "voxtype"
},
{
"id": "screenRecording"
},
{
"id": "idle"
},
{
"id": "notifications"
}
],
"right": [
{ "id": "tray" },
{ "id": "bluetoothPanel" },
{ "id": "networkPanel" },
{ "id": "audioPanel" },
{ "id": "battery" },
{ "id": "systemStats" },
{ "id": "controlCenter" }
{
"id": "tray"
},
{
"id": "bluetoothPanel"
},
{
"id": "networkPanel"
},
{
"id": "audioPanel"
},
{
"id": "battery"
},
{
"id": "systemStats"
},
{
"id": "controlCenter"
}
]
}
},
"plugins": []
"plugins": [
{
"id": "omarchy.osd"
}
]
}
+2 -1
View File
@@ -55,7 +55,8 @@ ShellRoot {
},
plugins: [
{ id: "omarchy.settings" },
{ id: "omarchy.image-picker" }
{ id: "omarchy.image-picker" },
{ id: "omarchy.osd" }
]
})
-5
View File
@@ -1,5 +0,0 @@
@define-color background-color {{ background }};
@define-color border-color {{ foreground }};
@define-color label {{ foreground }};
@define-color image {{ foreground }};
@define-color progress {{ accent }};