window management library
git clone https://git.lucas.co/cce-window-manager.git
feat: volume/brightness media keys as first-class actions
Six new actions (volume_up/down/mute, mic_mute, brightness_up/down) with
default bindings on the XF86 media keysyms. The policy claims them and
spawns a stock wpctl/brightnessctl command (5% steps, volume capped at
100%); a binding's command= property overrides the command wholesale.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/actions.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
src/api.rs | 21 ++++++++++++++++++++-
src/bindings.rs | 16 ++++++++++++++--
3 files changed, 83 insertions(+), 3 deletions(-)
diff --git a/src/actions.rs b/src/actions.rs
index c29dfae..202712c 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -41,6 +41,14 @@ impl Policy for DefaultPolicy {
Action::OverlayRight => {
vec![Command::SetOverlayPosition(OverlaySide::Right), Command::Relayout]
}
+ Action::VolumeUp
+ | Action::VolumeDown
+ | Action::VolumeMute
+ | Action::MicMute
+ | Action::BrightnessUp
+ | Action::BrightnessDown => {
+ vec![Command::Spawn(arg.unwrap_or(media_command(action)).to_string())]
+ }
_ => Vec::new(),
}
}
@@ -354,6 +362,22 @@ fn mode_next_shared(ctx: &ActionCtx) -> Vec<Command> {
cmds
}
+/// Stock command line for a media-key action (PipeWire's wpctl for audio,
+/// brightnessctl for the backlight). A binding's `command="..."` property
+/// overrides this wholesale — that's where a custom step size or a different
+/// mixer goes.
+fn media_command(action: Action) -> &'static str {
+ match action {
+ Action::VolumeUp => "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+",
+ Action::VolumeDown => "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
+ Action::VolumeMute => "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
+ Action::MicMute => "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle",
+ Action::BrightnessUp => "brightnessctl set 5%+",
+ Action::BrightnessDown => "brightnessctl set 5%-",
+ _ => "",
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -647,4 +671,29 @@ mod tests {
// Virtual point under (960, 540) at zoom 0.5: 100 + 960/0.5 = 2020.
assert_eq!(camera.pan_x, 2020.0 - 960.0);
}
+
+ #[test]
+ fn media_keys_spawn_stock_or_overridden_command() {
+ let c = ctx();
+ // Every media action is claimed and spawns its stock command.
+ for action in [
+ Action::VolumeUp, Action::VolumeDown, Action::VolumeMute,
+ Action::MicMute, Action::BrightnessUp, Action::BrightnessDown,
+ ] {
+ assert_eq!(
+ dispatch(&c, action),
+ vec![Command::Spawn(media_command(action).to_string())],
+ "{}", action.name()
+ );
+ }
+ assert_eq!(
+ dispatch(&c, Action::VolumeMute),
+ vec![Command::Spawn("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle".to_string())]
+ );
+ // A binding's command= property replaces the stock command.
+ assert_eq!(
+ DefaultPolicy.action(&c, Action::BrightnessUp, Some("brightnessctl set 10%+")),
+ vec![Command::Spawn("brightnessctl set 10%+".to_string())]
+ );
+ }
}
diff --git a/src/api.rs b/src/api.rs
index 2c6e488..b5de195 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -89,6 +89,12 @@ pub enum Action {
PanUp,
PanDown,
Screenshot,
+ VolumeUp,
+ VolumeDown,
+ VolumeMute,
+ MicMute,
+ BrightnessUp,
+ BrightnessDown,
}
impl Action {
@@ -136,6 +142,12 @@ impl Action {
Action::PanUp => "pan_up",
Action::PanDown => "pan_down",
Action::Screenshot => "screenshot",
+ Action::VolumeUp => "volume_up",
+ Action::VolumeDown => "volume_down",
+ Action::VolumeMute => "volume_mute",
+ Action::MicMute => "mic_mute",
+ Action::BrightnessUp => "brightness_up",
+ Action::BrightnessDown => "brightness_down",
}
}
@@ -183,6 +195,12 @@ impl Action {
"pan_up" => Action::PanUp,
"pan_down" => Action::PanDown,
"screenshot" => Action::Screenshot,
+ "volume_up" => Action::VolumeUp,
+ "volume_down" => Action::VolumeDown,
+ "volume_mute" | "mute" => Action::VolumeMute,
+ "mic_mute" => Action::MicMute,
+ "brightness_up" => Action::BrightnessUp,
+ "brightness_down" => Action::BrightnessDown,
_ => return None,
})
}
@@ -378,7 +396,8 @@ pub enum Command {
/// Decisions, policy-side. Implemented by `actions::DefaultPolicy`.
pub trait Policy {
/// Decide a user action against the snapshot; `arg` is the binding's
- /// command string (Spawn/Toggle carry one). An empty vec means "not
+ /// command string (Spawn/Toggle carry one; media-key actions may carry
+ /// an override of their stock command). An empty vec means "not
/// mine" — the mechanism falls through to its remaining legacy arms.
fn action(&mut self, ctx: &ActionCtx, action: Action, arg: Option<&str>) -> Vec<Command>;
}
diff --git a/src/bindings.rs b/src/bindings.rs
index 3dd81ec..c4ae278 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -56,7 +56,8 @@ pub fn parse_chord(s: &str) -> Option<Chord> {
}
/// One resolved binding: chord (mods + keysym code) → action, with the
-/// command argument for `Spawn`/`Toggle`.
+/// command argument for `Spawn`/`Toggle` (required) and the media-key
+/// actions (optional override of their stock command).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Binding {
pub mods: u32,
@@ -151,6 +152,15 @@ pub const DEFAULT_BINDINGS: &[DefaultBinding] = &[
DefaultBinding { mods: mods::SUPER | mods::SHIFT, key: "r", action: Action::Reload },
// Reverse companion to the (user-configured) super+tab window switcher.
DefaultBinding { mods: mods::SUPER | mods::SHIFT, key: "Tab", action: Action::WindowSwitcherPrev },
+ // Media keys. Each action spawns a stock wpctl/brightnessctl command
+ // (see `actions::media_command`); an input.kdl binding can rebind the
+ // chord and/or override the command with a `command="..."` property.
+ DefaultBinding { mods: 0, key: "XF86AudioRaiseVolume", action: Action::VolumeUp },
+ DefaultBinding { mods: 0, key: "XF86AudioLowerVolume", action: Action::VolumeDown },
+ DefaultBinding { mods: 0, key: "XF86AudioMute", action: Action::VolumeMute },
+ DefaultBinding { mods: 0, key: "XF86AudioMicMute", action: Action::MicMute },
+ DefaultBinding { mods: 0, key: "XF86MonBrightnessUp", action: Action::BrightnessUp },
+ DefaultBinding { mods: 0, key: "XF86MonBrightnessDown", action: Action::BrightnessDown },
];
#[cfg(test)]
@@ -194,7 +204,9 @@ mod tests {
Action::Expose, Action::Minimize, Action::OverlayLeft,
Action::OverlayRight, Action::ZoomIn, Action::ZoomOut,
Action::ZoomReset, Action::PanLeft, Action::PanRight,
- Action::PanUp, Action::PanDown,
+ Action::PanUp, Action::PanDown, Action::VolumeUp,
+ Action::VolumeDown, Action::VolumeMute, Action::MicMute,
+ Action::BrightnessUp, Action::BrightnessDown,
] {
assert_eq!(Action::from_name(action.name()), Some(action), "{}", action.name());
}