Handle volume, media and notification keys in the shell (#13424)
* Step the volume in the shell instead of a script per keypress Each volume key ran omarchy-audio-output-volume: several pactl calls in bash, then omarchy-osd delivering the OSD over a qs IPC client, ~165ms from keypress to OSD, on keys that repeat while held. The volume up, down and mute keys now dispatch global shortcuts that the media service handles over PipeWire, stepping, clamping, unmuting and debouncing by the script's rules and showing the same OSD. It acts only when the default sink is an ALSA sink, which is its own physical sink. Any other default, a DSP chain or EasyEffects above all, falls back to the script, which resolves the physical sink from the live routing on every press. The precise +1/-1 keys still run the script. Keypress to OSD drops from ~168ms to ~20ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run media and notification keys in the shell without an IPC client The media keys and the notification dismiss, invoke and history keys ran omarchy-shell, starting a qs client for one argument-free IPC call. A new ipc shortcut kind names such a call as target.method. The shell hands it to the service that owns the target, which runs its own IpcHandler function, so the key behaves exactly as the omarchy-shell call did. A call missing from the list binds through omarchy-shell. Dismissing a notification with SUPER+comma clears the popup in ~11ms instead of ~40ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf44355a98
commit
804749970a
10 files changed
+200
-28
No files matched your search
@@ -854,7 +854,16 @@ Item {
|
||||
|
||||
// ---------------------------------------------------- IPC
|
||||
|
||||
// Keybindings reach these handlers as Hyprland global shortcuts, run here
|
||||
// exactly as the IPC call would run them, with no client to spawn.
|
||||
function runShortcut(method) {
|
||||
if (typeof ipcHandler[method] !== "function") return false
|
||||
ipcHandler[method]()
|
||||
return true
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
id: ipcHandler
|
||||
target: "notifications"
|
||||
|
||||
function dndState(): string {
|
||||
|
||||
@@ -119,6 +119,20 @@ function osdMessage(player, fallback) {
|
||||
return label || fallback
|
||||
}
|
||||
|
||||
// What a volume key does to the output, by omarchy-audio-output-volume's rules:
|
||||
// raise and lower step 5 and clamp to 0..100 (so a boosted sink drops to 100
|
||||
// on raise), unmuting as they go; mute-toggle flips mute and keeps the volume.
|
||||
function volumeKeyStep(action, percent, muted) {
|
||||
if (action === "raise") return { percent: Math.min(percent + 5, 100), muted: false }
|
||||
if (action === "lower") return { percent: Math.max(percent - 5, 0), muted: false }
|
||||
if (action === "mute-toggle") return { percent: percent, muted: !muted }
|
||||
return null
|
||||
}
|
||||
|
||||
function volumeOsdIcon(percent, muted) {
|
||||
return muted || percent === 0 ? "volume-muted" : "volume-high"
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
isProxyPlayer: isProxyPlayer,
|
||||
@@ -137,6 +151,8 @@ if (typeof module !== "undefined") {
|
||||
trackSignature: trackSignature,
|
||||
trackChanged: trackChanged,
|
||||
labelFor: labelFor,
|
||||
osdMessage: osdMessage
|
||||
osdMessage: osdMessage,
|
||||
volumeKeyStep: volumeKeyStep,
|
||||
volumeOsdIcon: volumeOsdIcon
|
||||
}
|
||||
}
|
||||
@@ -455,6 +455,55 @@ Item {
|
||||
|
||||
PwObjectTracker { objects: root.playbackStreams }
|
||||
|
||||
// ------------------------------------------------------------- volume keys
|
||||
//
|
||||
// The volume keys arrive as global shortcuts and change the volume here,
|
||||
// with no process per press, stepping, clamping, unmuting and debouncing the
|
||||
// way omarchy-audio-output-volume does, so either path lands on the same
|
||||
// volume and OSD. That script resolves a DSP sink through to the physical
|
||||
// sink it feeds on every press, from the live routing. An ALSA sink is its
|
||||
// own physical sink, so only then do the keys act here; any other default
|
||||
// sink falls back to the script.
|
||||
readonly property var defaultSink: Pipewire.defaultAudioSink
|
||||
readonly property var volumeSink: defaultSink && String(defaultSink.name).indexOf("alsa_output.") === 0 ? defaultSink : null
|
||||
property double lastMuteToggle: 0
|
||||
|
||||
// Returns false when the default sink is not one to control here, so the
|
||||
// caller falls back to the script.
|
||||
function handleVolumeKey(action) {
|
||||
var audio = volumeSink && volumeSink.audio
|
||||
if (!audio) return false
|
||||
|
||||
var step = MediaModel.volumeKeyStep(action, Math.round(audio.volume * 100), audio.muted)
|
||||
if (!step) return false
|
||||
|
||||
if (action === "mute-toggle") {
|
||||
// Some keyboards bounce the mute key; the script ignores a second
|
||||
// toggle within 250ms too.
|
||||
var now = Date.now()
|
||||
if (now - lastMuteToggle < 250) return true
|
||||
lastMuteToggle = now
|
||||
audio.muted = step.muted
|
||||
} else {
|
||||
audio.muted = false
|
||||
audio.volume = step.percent / 100
|
||||
}
|
||||
|
||||
// The payload omarchy-osd builds, from the values just set: the node may
|
||||
// not report them back before the OSD draws.
|
||||
shell.summon("omarchy.osd", JSON.stringify({
|
||||
icon: MediaModel.volumeOsdIcon(step.percent, step.muted),
|
||||
message: "",
|
||||
value: String(step.percent),
|
||||
progressText: step.percent + "%",
|
||||
max: "100",
|
||||
duration: ""
|
||||
}))
|
||||
return true
|
||||
}
|
||||
|
||||
PwObjectTracker { objects: root.defaultSink ? [root.defaultSink] : [] }
|
||||
|
||||
function statusJson() {
|
||||
var p = activePlayer
|
||||
return JSON.stringify({
|
||||
@@ -473,7 +522,16 @@ Item {
|
||||
})
|
||||
}
|
||||
|
||||
// Keybindings reach these handlers as Hyprland global shortcuts, run here
|
||||
// exactly as the IPC call would run them, with no client to spawn.
|
||||
function runShortcut(method) {
|
||||
if (typeof ipcHandler[method] !== "function") return false
|
||||
ipcHandler[method]()
|
||||
return true
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
id: ipcHandler
|
||||
target: "media"
|
||||
|
||||
function status(): string {
|
||||
|
||||
Reference in new issue
Block a user