Step the internal backlight in the shell instead of a script per keypress (#13427)

Each brightness key ran omarchy-brightness-display: resolving the focused
monitor and backlight device, checking for an Apple display, reading,
writing and reading back through brightnessctl, then omarchy-osd over a
qs IPC client. That is 64-133ms from keypress to OSD depending on load,
on keys that repeat while held.

The brightness up and down keys now dispatch global shortcuts. On an
internal panel the shell reads the level from sysfs, steps it by the
script's rules, writes it with one brightnessctl, and shows the OSD from
the level read back. A press overlapping one still being applied is
dropped, as the script's flock drops it. External and Apple displays
fall back to the script, which drives them over DDC or their own helper.
The absolute and precise brightness keys still run the script.

Across 24 steps from eight starting levels the shell and the script land
on identical levels, and keypress to OSD drops to ~31ms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-09-27 19:34:36 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 25ead28f90
commit d3fb0284a8
8 changed files with 184 additions and 12 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ o.bind("XF86AudioRaiseVolume", "Volume up", { audio = "raise" }, { locked = true
o.bind("XF86AudioLowerVolume", "Volume down", { audio = "lower" }, { locked = true, repeating = true })
o.bind("XF86AudioMute", "Mute", { audio = "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("XF86MonBrightnessUp", "Brightness up", { brightness = "raise" }, { locked = true, repeating = true })
o.bind("XF86MonBrightnessDown", "Brightness down", { brightness = "lower" }, { locked = true, repeating = true })
o.bind("SHIFT + XF86MonBrightnessUp", "Brightness maximum", "omarchy-brightness-display 100%", { locked = true, repeating = true })
o.bind("SHIFT + XF86MonBrightnessDown", "Brightness minimum", "omarchy-brightness-display 1%", { locked = true, repeating = true })
o.bind("XF86KbdBrightnessUp", "Keyboard brightness up", "omarchy-brightness-keyboard up", { locked = true, repeating = true })
+3
View File
@@ -100,6 +100,9 @@ local function command_from(value, description)
return shell_dispatcher("panel", value.panel, "omarchy-shell shell toggle " .. shell_quote(value.panel))
elseif value.audio then
return shell_dispatcher("audio", value.audio, "omarchy-audio-output-volume " .. shell_quote(value.audio))
elseif value.brightness then
local step = value.brightness == "raise" and "+5%" or "5%-"
return shell_dispatcher("brightness", value.brightness, "omarchy-brightness-display " .. step)
elseif value.ipc then
local target, method = value.ipc:match("^([^.]+)%.(.+)$")
return shell_dispatcher("ipc", value.ipc, "omarchy-shell " .. shell_quote(target) .. " " .. shell_quote(method))
+10 -7
View File
@@ -1,11 +1,12 @@
# Hyprland global shortcuts the Omarchy shell registers, one per line: a menu
# route to toggle, a panel plugin to toggle, a volume key, or an argument-free
# IPC call as target.method. A binding written as o.bind(keys, description,
# { menu = "theme" }), { panel = "omarchy.emojis" }, { audio = "raise" } or
# { ipc = "media.next" } dispatches the matching shortcut straight to the shell,
# with no process to spawn per keypress. Anything missing here still binds,
# through the omarchy-menu, omarchy-shell or omarchy-audio-output-volume
# command instead.
# route to toggle, a panel plugin to toggle, a volume or brightness key, or an
# argument-free IPC call as target.method. A binding written as o.bind(keys,
# description, { menu = "theme" }), { panel = "omarchy.emojis" },
# { audio = "raise" }, { brightness = "raise" } or { ipc = "media.next" }
# dispatches the matching shortcut straight to the shell, with no process to
# spawn per keypress. Anything missing here still binds, through the
# omarchy-menu, omarchy-shell, omarchy-audio-output-volume or
# omarchy-brightness-display command instead.
menu root
menu apps
menu capture
@@ -27,6 +28,8 @@ panel omarchy.power
audio raise
audio lower
audio mute-toggle
brightness raise
brightness lower
ipc media.playPause
ipc media.next
ipc media.previous
+101
View File
@@ -0,0 +1,101 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Commons
import "BrightnessModel.js" as BrightnessModel
// The display brightness keys, arriving as global shortcuts. On the internal
// panel they step the backlight here: one brightnessctl write and the OSD in
// process, instead of omarchy-brightness-display resolving the monitor and
// device, reading, writing and reading back, then an IPC client for the OSD.
// They step, clamp and read back the way that script does, so either path
// lands on the same level and OSD. External and Apple displays go through the
// script, which drives them over DDC or their own helper.
Item {
id: root
// The shell host, for summoning the OSD.
property var host: null
// The backlight omarchy-hw-display picks. Devices do not come and go at
// runtime, but each press refreshes it for the next.
property string device: ""
readonly property string devicePath: device ? "/sys/class/backlight/" + device : ""
// Returns false when the focused display is not one to handle here, so the
// caller falls back to the script.
function handle(action) {
var monitor = Hyprland.focusedMonitor
var name = monitor ? String(monitor.name || "") : ""
if (!/^(eDP|LVDS|DSI)-/.test(name) || !device) return false
if (action !== "raise" && action !== "lower") return false
// The script drops a press that overlaps one still being applied, so key
// repeat cannot race the writes.
if (setProc.running) return true
var max = readNumber(maxFile)
if (!(max > 0)) return false
var current = Math.round(100 * readNumber(brightnessFile) / max)
setProc.command = ["brightnessctl", "-q", "-d", device, "set", BrightnessModel.brightnessKeyTarget(action, current) + "%"]
setProc.running = true
return true
}
// reload() reads in the background, so wait for it: text() would otherwise
// still hold the previous reading, and a level changed elsewhere (the
// monitor panel, a script) would step from the wrong place.
function readNumber(file) {
file.reload()
file.waitForJob()
return Number(String(file.text() || "").trim())
}
// The payload omarchy-osd builds, from the level read back after the write.
function showOsd() {
var max = readNumber(maxFile)
if (!host || !(max > 0)) return
var percent = Math.round(100 * readNumber(brightnessFile) / max)
host.summon("omarchy.osd", JSON.stringify({
icon: "brightness",
message: "",
value: String(percent),
progressText: percent + "%",
max: "100",
duration: ""
}))
}
FileView {
id: brightnessFile
path: root.devicePath ? root.devicePath + "/brightness" : ""
blockLoading: true
printErrors: false
}
FileView {
id: maxFile
path: root.devicePath ? root.devicePath + "/max_brightness" : ""
blockLoading: true
printErrors: false
}
Process {
id: setProc
onExited: {
root.showOsd()
if (!deviceProc.running) deviceProc.running = true
}
}
Process {
id: deviceProc
command: ["omarchy-hw-display"]
running: true
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.device = String(text || "").trim()
}
}
}
+12
View File
@@ -0,0 +1,12 @@
// omarchy-brightness-display's steps for the brightness keys: 1% at or below
// 5%, otherwise 5%, kept between 1% and 100%.
function brightnessKeyTarget(action, current) {
if (action === "raise") return Math.min(current < 5 ? current + 1 : current + 5, 100)
return Math.max(current <= 5 ? current - 1 : current - 5, 1)
}
if (typeof module !== "undefined") {
module.exports = {
brightnessKeyTarget: brightnessKeyTarget
}
}
+6 -2
View File
@@ -20,6 +20,7 @@ ShellRoot {
property PluginRegistry pluginRegistry: PluginRegistry { }
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }
property AppLibrary appLibrary: AppLibrary { }
property BrightnessKeys brightnessKeys: BrightnessKeys { host: shell }
property string home: Quickshell.env("HOME")
@@ -1526,7 +1527,7 @@ ShellRoot {
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var match = /^([A-Za-z]+)\s+(\S+)\s*$/.exec(lines[i])
if (match && ["menu", "panel", "audio", "ipc"].indexOf(match[1]) !== -1)
if (match && ["menu", "panel", "audio", "brightness", "ipc"].indexOf(match[1]) !== -1)
entries.push({ kind: match[1], target: match[2], name: match[1] + "." + match[2] })
}
return entries
@@ -1546,6 +1547,9 @@ ShellRoot {
var service = shell.serviceFor(shell.ipcShortcutServices[target] || "")
if (!service || !service.runShortcut(method))
Util.execArgv(["omarchy-shell", target, method])
} else if (entry.kind === "brightness") {
if (!shell.brightnessKeys.handle(entry.target))
Util.execArgv(["omarchy-brightness-display", entry.target === "raise" ? "+5%" : "5%-"])
} else if (entry.kind === "audio") {
var media = shell.serviceFor("omarchy.media")
if (!media || !media.handleVolumeKey(entry.target))
@@ -1570,7 +1574,7 @@ ShellRoot {
appid: "omarchy"
name: modelData.name
description: modelData.kind === "audio" ? "Volume " + modelData.target : (modelData.kind === "ipc" ? "Run " + modelData.target : "Toggle the " + modelData.target + " " + modelData.kind)
description: modelData.kind === "audio" || modelData.kind === "brightness" ? (modelData.kind === "audio" ? "Volume " : "Brightness ") + modelData.target : (modelData.kind === "ipc" ? "Run " + modelData.target : "Toggle the " + modelData.target + " " + modelData.kind)
onPressed: shell.runShortcut(modelData)
}
}
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const model = requireFromRoot('shell/services/BrightnessModel.js')
// The in-shell brightness keys follow omarchy-brightness-display's steps.
const steps = [
['raise', 60, 65], ['lower', 60, 55],
['raise', 4, 5], ['raise', 5, 10], ['lower', 5, 4], ['lower', 6, 1],
['raise', 98, 100], ['raise', 100, 100], ['lower', 1, 1], ['lower', 2, 1],
]
for (const [action, current, target] of steps) {
assertEqual(model.brightnessKeyTarget(action, current), target, `brightness ${action} from ${current}% lands on ${target}%`)
}
const qml = fs.readFileSync(path.join(root, 'shell/services/BrightnessKeys.qml'), 'utf8')
assert(
qml.includes('if (!/^(eDP|LVDS|DSI)-/.test(name) || !device) return false'),
'brightness keys act in the shell only on the internal panel and defer external and Apple displays to the script'
)
assert(
/if \(setProc\.running\) return true/.test(qml),
'brightness keys drop a press that overlaps one still being applied, as the script does'
)
assert(
/file\.reload\(\)\s*file\.waitForJob\(\)/.test(qml),
'brightness keys read the current level fresh, so a level changed elsewhere steps from the right place'
)
assert(
qml.includes('var current = Math.round(100 * readNumber(brightnessFile) / max)') &&
qml.includes('var percent = Math.round(100 * readNumber(brightnessFile) / max)'),
'brightness keys compute percentages as brightnessctl reports them'
)
const shellQml = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
assert(
/if \(!shell\.brightnessKeys\.handle\(entry\.target\)\)\s*Util\.execArgv\(\["omarchy-brightness-display", entry\.target === "raise" \? "\+5%" : "5%-"\]\)/.test(shellQml),
'a brightness key the shell declines runs omarchy-brightness-display'
)
JS
+5 -1
View File
@@ -57,6 +57,7 @@ o.bind("C", "listed panel", { panel = "omarchy.emojis" })
o.bind("D", "unlisted panel", { panel = "omarchy.wifiqr" })
o.bind("E", "listed audio", { audio = "raise" })
o.bind("F", "unlisted audio", { audio = "+1" })
o.bind("I", "listed brightness", { brightness = "raise" })
o.bind("G", "listed ipc", { ipc = "media.next" })
o.bind("H", "unlisted ipc", { ipc = "media.sourceNext" })
@@ -80,6 +81,7 @@ expect_binding $'global\tomarchy:panel.omarchy.emojis\tlisted panel' "a listed p
expect_binding $'exec\tomarchy-shell shell toggle \'omarchy.wifiqr\'\tunlisted panel' "an unlisted panel falls back to the command"
expect_binding $'global\tomarchy:audio.raise\tlisted audio' "a listed volume key binds its global shortcut"
expect_binding $'exec\tomarchy-audio-output-volume \'+1\'\tunlisted audio' "an unlisted volume step falls back to the script"
expect_binding $'global\tomarchy:brightness.raise\tlisted brightness' "a listed brightness key binds its global shortcut"
expect_binding $'global\tomarchy:ipc.media.next\tlisted ipc' "a listed IPC call binds its global shortcut"
expect_binding $'exec\tomarchy-shell \'media\' \'sourceNext\'\tunlisted ipc' "an unlisted IPC call falls back to omarchy-shell"
pass "shell bindings use global shortcuts only for what the shell registers"
@@ -94,6 +96,8 @@ expect_binding $'global\tomarchy:panel.omarchy.clipboard\tClipboard manager' "th
expect_binding $'global\tomarchy:audio.raise\tVolume up' "the volume up key steps the volume in the shell"
expect_binding $'global\tomarchy:audio.lower\tVolume down' "the volume down key steps the volume in the shell"
expect_binding $'global\tomarchy:audio.mute-toggle\tMute' "the mute key toggles mute in the shell"
expect_binding $'global\tomarchy:brightness.raise\tBrightness up' "the brightness up key steps the backlight in the shell"
expect_binding $'global\tomarchy:brightness.lower\tBrightness down' "the brightness down key steps the backlight in the shell"
expect_binding $'global\tomarchy:ipc.media.playPause\tPlay' "the play key reaches the media service directly"
expect_binding $'global\tomarchy:ipc.notifications.dismissOne\tDismiss last notification' "dismissing a notification reaches the service directly"
! grep -E $'^exec\tomarchy-shell (media|notifications) ' <<<"$bindings" ||
@@ -103,7 +107,7 @@ pass "default bindings toggle menus and panels through global shortcuts"
# The shell and the helpers read the same list, in the same format.
while IFS= read -r line; do
[[ -z $line || $line == \#* ]] && continue
[[ $line =~ ^(menu|panel|audio|ipc)\ [^[:space:]]+$ ]] || fail "shortcuts lines are a kind and a target: $line"
[[ $line =~ ^(menu|panel|audio|brightness|ipc)\ [^[:space:]]+$ ]] || fail "shortcuts lines are a kind and a target: $line"
if [[ $line == ipc\ * ]]; then
[[ $line =~ ^ipc\ (media|notifications)\.[A-Za-z]+$ ]] || fail "ipc shortcuts name a mapped target and method: $line"
fi