From b06e51da60505bba42ef310d510f38440b897f9b Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 22 Mar 2024 20:47:40 +0400 Subject: Implement bind cooldown-ms --- niri-config/src/lib.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++--- src/input.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++----- src/niri.rs | 4 +++- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 64904d3c..7a9c0b50 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::time::Duration; use bitflags::bitflags; use knuffel::errors::DecodeError; @@ -720,6 +721,7 @@ pub struct Binds(pub Vec); pub struct Bind { pub key: Key, pub action: Action, + pub cooldown: Option, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -1405,13 +1407,45 @@ where node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { - expect_only_children(node, ctx); + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + for val in node.arguments.iter() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "no arguments expected for this node", + )) + } let key = node .node_name .parse::() .map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?; + let mut cooldown = None; + for (name, val) in &node.properties { + match &***name { + "cooldown-ms" => { + cooldown = Some(Duration::from_millis( + knuffel::traits::DecodeScalar::decode(val, ctx)?, + )); + } + name_str => { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name_str.escape_default()), + )); + } + } + } + let mut children = node.children(); // If the action is invalid but the key is fine, we still want to return something. @@ -1420,6 +1454,7 @@ where let dummy = Self { key, action: Action::Spawn(vec![]), + cooldown: None, }; if let Some(child) = children.next() { @@ -1431,7 +1466,11 @@ where )); } match Action::decode_node(child, ctx) { - Ok(action) => Ok(Self { key, action }), + Ok(action) => Ok(Self { + key, + action, + cooldown, + }), Err(e) => { ctx.emit_error(e); Ok(dummy) @@ -1732,7 +1771,7 @@ mod tests { Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } Mod+Shift+E { quit skip-confirmation=true; } - Mod+WheelDown { focus-workspace-down; } + Mod+WheelDown cooldown-ms=150 { focus-workspace-down; } } debug { @@ -1920,6 +1959,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::Spawn(vec!["alacritty".to_owned()]), + cooldown: None, }, Bind { key: Key { @@ -1927,6 +1967,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::CloseWindow, + cooldown: None, }, Bind { key: Key { @@ -1934,6 +1975,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::FocusMonitorLeft, + cooldown: None, }, Bind { key: Key { @@ -1941,6 +1983,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, }, action: Action::MoveWindowToMonitorRight, + cooldown: None, }, Bind { key: Key { @@ -1948,6 +1991,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::ConsumeWindowIntoColumn, + cooldown: None, }, Bind { key: Key { @@ -1955,6 +1999,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspace(1), + cooldown: None, }, Bind { key: Key { @@ -1962,6 +2007,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::Quit(true), + cooldown: None, }, Bind { key: Key { @@ -1969,6 +2015,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspaceDown, + cooldown: Some(Duration::from_millis(150)), }, ]), debug: DebugConfig { diff --git a/src/input.rs b/src/input.rs index aa08d580..b4e44974 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,9 @@ use std::any::Any; +use std::collections::hash_map::Entry; use std::collections::HashSet; use std::time::Duration; +use calloop::timer::{TimeoutAction, Timer}; use input::event::gesture::GestureEventCoordinates as _; use niri_config::{Action, Bind, Binds, Key, Modifiers, Trigger}; use niri_ipc::LayoutSwitchTarget; @@ -289,7 +291,40 @@ impl State { return; } - self.do_action(bind.action); + self.handle_bind(bind); + } + + pub fn handle_bind(&mut self, bind: Bind) { + let Some(cooldown) = bind.cooldown else { + self.do_action(bind.action); + return; + }; + + // Check this first so that it doesn't trigger the cooldown. + if self.niri.is_locked() && !allowed_when_locked(&bind.action) { + return; + } + + match self.niri.bind_cooldown_timers.entry(bind.key) { + // The bind is on cooldown. + Entry::Occupied(_) => (), + Entry::Vacant(entry) => { + let timer = Timer::from_duration(cooldown); + let token = self + .niri + .event_loop + .insert_source(timer, move |_, _, state| { + if state.niri.bind_cooldown_timers.remove(&bind.key).is_none() { + error!("bind cooldown timer entry disappeared"); + } + TimeoutAction::Drop + }) + .unwrap(); + entry.insert(token); + + self.do_action(bind.action); + } + } } pub fn do_action(&mut self, action: Action) { @@ -1100,12 +1135,12 @@ impl State { let ticks = self.niri.horizontal_wheel_tracker.accumulate(v120); if let Some(right) = bind_right { for _ in 0..ticks { - self.do_action(right.action.clone()); + self.handle_bind(right.clone()); } } if let Some(left) = bind_left { for _ in ticks..0 { - self.do_action(left.action.clone()); + self.handle_bind(left.clone()); } } return; @@ -1125,12 +1160,12 @@ impl State { let ticks = self.niri.vertical_wheel_tracker.accumulate(v120); if let Some(down) = bind_down { for _ in 0..ticks { - self.do_action(down.action.clone()); + self.handle_bind(down.clone()); } } if let Some(up) = bind_up { for _ in ticks..0 { - self.do_action(up.action.clone()); + self.handle_bind(up.clone()); } } return; @@ -1725,6 +1760,7 @@ fn should_intercept_key( modifiers: Modifiers::empty(), }, action, + cooldown: None, }); } } @@ -1772,6 +1808,7 @@ fn find_bind( modifiers: Modifiers::empty(), }, action, + cooldown: None, }); } @@ -1991,6 +2028,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL, }, action: Action::CloseWindow, + cooldown: None, }]); let comp_mod = CompositorMod::Super; @@ -2122,6 +2160,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::CloseWindow, + cooldown: None, }, Bind { key: Key { @@ -2129,6 +2168,7 @@ mod tests { modifiers: Modifiers::SUPER, }, action: Action::FocusColumnLeft, + cooldown: None, }, Bind { key: Key { @@ -2136,6 +2176,7 @@ mod tests { modifiers: Modifiers::empty(), }, action: Action::FocusWindowDown, + cooldown: None, }, Bind { key: Key { @@ -2143,6 +2184,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER, }, action: Action::FocusWindowUp, + cooldown: None, }, Bind { key: Key { @@ -2150,6 +2192,7 @@ mod tests { modifiers: Modifiers::SUPER | Modifiers::ALT, }, action: Action::FocusColumnRight, + cooldown: None, }, ]); diff --git a/src/niri.rs b/src/niri.rs index babd961c..49592cf6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -11,7 +11,7 @@ use std::{env, mem, thread}; use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode; use anyhow::{ensure, Context}; use calloop::futures::Scheduler; -use niri_config::{Config, TrackLayout}; +use niri_config::{Config, Key, TrackLayout}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; @@ -194,6 +194,7 @@ pub struct Niri { pub seat: Seat, /// Scancodes of the keys to suppress. pub suppressed_keys: HashSet, + pub bind_cooldown_timers: HashMap, pub keyboard_focus: KeyboardFocus, pub idle_inhibiting_surfaces: HashSet, pub is_fdo_idle_inhibited: Arc, @@ -1251,6 +1252,7 @@ impl Niri { popups: PopupManager::default(), popup_grab: None, suppressed_keys: HashSet::new(), + bind_cooldown_timers: HashMap::new(), presentation_state, security_context_state, gamma_control_manager_state, -- cgit