diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index d2343ff..6354969 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -48,8 +48,8 @@ pub struct Config { pub environment: Environment, #[knuffel(children(name = "window-rule"))] pub window_rules: Vec, - #[knuffel(child, default)] - pub binds: Binds, + #[knuffel(children(name = "binds"))] + pub binds: Vec, #[knuffel(child, default)] pub debug: DebugConfig, #[knuffel(children(name = "workspace"))] @@ -906,8 +906,8 @@ pub struct BorderRule { pub inactive_gradient: Option, } -#[derive(Debug, Default, PartialEq)] -pub struct Binds(pub Vec); +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Binds(pub String, pub Vec); #[derive(Debug, Clone, PartialEq)] pub struct Bind { @@ -1041,6 +1041,7 @@ pub enum Action { MoveWorkspaceToMonitorRight, MoveWorkspaceToMonitorDown, MoveWorkspaceToMonitorUp, + BindingMode(#[knuffel(argument, str)] String), } impl From for Action { @@ -2193,7 +2194,11 @@ where node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { - expect_only_children(node, ctx); + let mode_name = node + .arguments + .first() + .map(|e| knuffel::DecodeScalar::decode(e, ctx)) + .unwrap_or_else(|| Ok(String::from("default")))?; let mut seen_keys = HashSet::new(); @@ -2243,7 +2248,7 @@ where } } - Ok(Self(binds)) + Ok(Self(mode_name, binds)) } } @@ -2921,100 +2926,105 @@ mod tests { open_on_output: None, }, ], - binds: Binds(vec![ - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::t), - modifiers: Modifiers::COMPOSITOR, + binds: vec![Binds( + "default".to_string(), + vec![ + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::t), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::Spawn(vec!["alacritty".to_owned()]), + repeat: true, + cooldown: None, + allow_when_locked: true, }, - action: Action::Spawn(vec!["alacritty".to_owned()]), - repeat: true, - cooldown: None, - allow_when_locked: true, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::q), - modifiers: Modifiers::COMPOSITOR, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::q), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::CloseWindow, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::CloseWindow, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::h), - modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::h), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::FocusMonitorLeft, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusMonitorLeft, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::l), - modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::l), + modifiers: Modifiers::COMPOSITOR + | Modifiers::SHIFT + | Modifiers::CTRL, + }, + action: Action::MoveWindowToMonitorRight, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::MoveWindowToMonitorRight, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::comma), - modifiers: Modifiers::COMPOSITOR, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::comma), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::ConsumeWindowIntoColumn, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::ConsumeWindowIntoColumn, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::_1), - modifiers: Modifiers::COMPOSITOR, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::_1), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::FocusWorkspace(WorkspaceReference::Index(1)), + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusWorkspace(WorkspaceReference::Index(1)), - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::_1), - modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::_1), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::FocusWorkspace(WorkspaceReference::Name( + "workspace-1".to_string(), + )), + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusWorkspace(WorkspaceReference::Name( - "workspace-1".to_string(), - )), - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::e), - modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::e), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::Quit(true), + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::Quit(true), - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::WheelScrollDown, - modifiers: Modifiers::COMPOSITOR, + Bind { + key: Key { + trigger: Trigger::WheelScrollDown, + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::FocusWorkspaceDown, + repeat: true, + cooldown: Some(Duration::from_millis(150)), + allow_when_locked: false, }, - action: Action::FocusWorkspaceDown, - repeat: true, - cooldown: Some(Duration::from_millis(150)), - allow_when_locked: false, - }, - ]), + ], + )], debug: DebugConfig { render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")), ..Default::default() @@ -3109,4 +3119,20 @@ mod tests { assert_eq!(config.input.keyboard.repeat_delay, 600); assert_eq!(config.input.keyboard.repeat_rate, 25); } + + #[test] + fn binding_modes() { + Config::parse( + "", + r##" + binds { + A { binding-mode "test"; } + } + binds "test" { + B { binding-mode "default"; } + } + "##, + ) + .unwrap(); + } } diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 16817df..66d1c79 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -37,6 +37,8 @@ pub enum Request { FocusedOutput, /// Respond with an error (for testing error handling). ReturnError, + /// Request name of the current binding mode. + BindingMode, } /// Reply from niri to client. @@ -68,6 +70,8 @@ pub enum Response { Workspaces(Vec), /// Information about the focused output. FocusedOutput(Option), + /// Name of the current binding mode. + BindingMode(String), } /// Actions that niri can perform. diff --git a/src/cli.rs b/src/cli.rs index 4004a33..3b38302 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -90,4 +90,6 @@ pub enum Msg { Version, /// Request an error from the running niri instance. RequestError, + /// Get the binding mode name. + BindingMode, } diff --git a/src/input/mod.rs b/src/input/mod.rs index 148129c..8d1e271 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -314,7 +314,7 @@ impl State { serial, time, |this, mods, keysym| { - let bindings = &this.niri.config.borrow().binds; + let bindings = &this.niri.binding_mode; let key_code = event.key_code(); let modified = keysym.modified_sym(); let raw = keysym.raw_latin_sym_or_raw_current_sym(); @@ -1077,6 +1077,11 @@ impl State { } } } + Action::BindingMode(name) => { + if let Some(mode) = self.niri.config.borrow().binds.iter().find(|e| e.0 == name) { + self.niri.binding_mode = mode.clone(); + } + } } } @@ -1500,13 +1505,11 @@ impl State { let horizontal = horizontal_amount_v120.unwrap_or(0.); let ticks = self.niri.horizontal_wheel_tracker.accumulate(horizontal); if ticks != 0 { - let config = self.niri.config.borrow(); - let bindings = &config.binds; + let bindings = &self.niri.binding_mode; let bind_left = find_configured_bind(bindings, comp_mod, Trigger::WheelScrollLeft, mods); let bind_right = find_configured_bind(bindings, comp_mod, Trigger::WheelScrollRight, mods); - drop(config); if let Some(right) = bind_right { for _ in 0..ticks { @@ -1523,13 +1526,11 @@ impl State { let vertical = vertical_amount_v120.unwrap_or(0.); let ticks = self.niri.vertical_wheel_tracker.accumulate(vertical); if ticks != 0 { - let config = self.niri.config.borrow(); - let bindings = &config.binds; + let bindings = &self.niri.binding_mode; let bind_up = find_configured_bind(bindings, comp_mod, Trigger::WheelScrollUp, mods); let bind_down = find_configured_bind(bindings, comp_mod, Trigger::WheelScrollDown, mods); - drop(config); if let Some(down) = bind_down { for _ in 0..ticks { @@ -1566,8 +1567,7 @@ impl State { .horizontal_finger_scroll_tracker .accumulate(horizontal); if ticks != 0 { - let config = self.niri.config.borrow(); - let bindings = &config.binds; + let bindings = &self.niri.binding_mode; let bind_left = find_configured_bind(bindings, comp_mod, Trigger::TouchpadScrollLeft, mods); let bind_right = find_configured_bind( @@ -1576,7 +1576,6 @@ impl State { Trigger::TouchpadScrollRight, mods, ); - drop(config); if let Some(right) = bind_right { for _ in 0..ticks { @@ -1596,13 +1595,11 @@ impl State { .vertical_finger_scroll_tracker .accumulate(vertical); if ticks != 0 { - let config = self.niri.config.borrow(); - let bindings = &config.binds; + let bindings = &self.niri.binding_mode; let bind_up = find_configured_bind(bindings, comp_mod, Trigger::TouchpadScrollUp, mods); let bind_down = find_configured_bind(bindings, comp_mod, Trigger::TouchpadScrollDown, mods); - drop(config); if let Some(down) = bind_down { for _ in 0..ticks { @@ -2290,7 +2287,7 @@ fn find_configured_bind( modifiers |= Modifiers::COMPOSITOR; } - for bind in &bindings.0 { + for bind in &bindings.1 { if bind.key.trigger != trigger { continue; } @@ -2539,7 +2536,7 @@ pub fn mods_with_binds( }; let mut rv = HashSet::new(); - for bind in &binds.0 { + for bind in &binds.1 { if !triggers.iter().any(|trigger| bind.key.trigger == *trigger) { continue; } @@ -2589,16 +2586,19 @@ mod tests { #[test] fn bindings_suppress_keys() { let close_keysym = Keysym::q; - let bindings = Binds(vec![Bind { - key: Key { - trigger: Trigger::Keysym(close_keysym), - modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL, - }, - action: Action::CloseWindow, - repeat: true, - cooldown: None, - allow_when_locked: false, - }]); + let bindings = Binds( + String::from("default"), + vec![Bind { + key: Key { + trigger: Trigger::Keysym(close_keysym), + modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL, + }, + action: Action::CloseWindow, + repeat: true, + cooldown: None, + allow_when_locked: false, + }], + ); let comp_mod = CompositorMod::Super; let mut suppressed_keys = HashSet::new(); @@ -2722,58 +2722,61 @@ mod tests { #[test] fn comp_mod_handling() { - let bindings = Binds(vec![ - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::q), - modifiers: Modifiers::COMPOSITOR, + let bindings = Binds( + String::from("default"), + vec![ + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::q), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::CloseWindow, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::CloseWindow, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::h), - modifiers: Modifiers::SUPER, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::h), + modifiers: Modifiers::SUPER, + }, + action: Action::FocusColumnLeft, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusColumnLeft, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::j), - modifiers: Modifiers::empty(), + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::j), + modifiers: Modifiers::empty(), + }, + action: Action::FocusWindowDown, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusWindowDown, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::k), - modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::k), + modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER, + }, + action: Action::FocusWindowUp, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusWindowUp, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - Bind { - key: Key { - trigger: Trigger::Keysym(Keysym::l), - modifiers: Modifiers::SUPER | Modifiers::ALT, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::l), + modifiers: Modifiers::SUPER | Modifiers::ALT, + }, + action: Action::FocusColumnRight, + repeat: true, + cooldown: None, + allow_when_locked: false, }, - action: Action::FocusColumnRight, - repeat: true, - cooldown: None, - allow_when_locked: false, - }, - ]); + ], + ); assert_eq!( find_configured_bind( @@ -2786,7 +2789,7 @@ mod tests { } ) .as_ref(), - Some(&bindings.0[0]) + Some(&bindings.1[0]) ); assert_eq!( find_configured_bind( @@ -2809,7 +2812,7 @@ mod tests { } ) .as_ref(), - Some(&bindings.0[1]) + Some(&bindings.1[1]) ); assert_eq!( find_configured_bind( @@ -2841,7 +2844,7 @@ mod tests { ModifiersState::default(), ) .as_ref(), - Some(&bindings.0[2]) + Some(&bindings.1[2]) ); assert_eq!( @@ -2855,7 +2858,7 @@ mod tests { } ) .as_ref(), - Some(&bindings.0[3]) + Some(&bindings.1[3]) ); assert_eq!( find_configured_bind( @@ -2879,7 +2882,7 @@ mod tests { } ) .as_ref(), - Some(&bindings.0[4]) + Some(&bindings.1[4]) ); assert_eq!( find_configured_bind( diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 3192925..0ca54df 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -20,6 +20,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { }, Msg::Workspaces => Request::Workspaces, Msg::RequestError => Request::ReturnError, + Msg::BindingMode => Request::BindingMode, }; let socket = Socket::connect().context("error connecting to the niri socket")?; @@ -238,6 +239,19 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { println!("{is_active}{idx}{name}"); } } + Msg::BindingMode => { + let Response::BindingMode(response) = response else { + bail!("unexpected response: expected BindingMode, got {response:?}"); + }; + + if json { + let response = + serde_json::to_string(&response).context("error formatting response")?; + println!("{response}"); + return Ok(()); + } + println!("{response}"); + } } Ok(()) diff --git a/src/ipc/server.rs b/src/ipc/server.rs index d05131a..6f21c2e 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -235,6 +235,16 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { let output = result.map_err(|_| String::from("error getting active output info"))?; Response::FocusedOutput(output) } + Request::BindingMode => { + let (tx, rx) = async_channel::bounded(1); + ctx.event_loop.insert_idle(move |state| { + let workspaces = state.niri.binding_mode.0.clone(); + let _ = tx.send_blocking(workspaces); + }); + let result = rx.recv().await; + let mode = result.map_err(|_| String::from("error getting workspace info"))?; + Response::BindingMode(mode.clone()) + } }; Ok(response) diff --git a/src/niri.rs b/src/niri.rs index 664aea6..2a76274 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -152,6 +152,8 @@ const FRAME_CALLBACK_THROTTLE: Option = Some(Duration::from_millis(995 pub struct Niri { pub config: Rc>, + pub binding_mode: niri_config::Binds, + /// Output config from the config file. /// /// This does not include transient output config changes done via IPC. It is only used when @@ -992,9 +994,9 @@ impl State { if config.binds != old_config.binds { self.niri.hotkey_overlay.on_hotkey_config_updated(); self.niri.mods_with_wheel_binds = - mods_with_wheel_binds(self.backend.mod_key(), &config.binds); + mods_with_wheel_binds(self.backend.mod_key(), &self.niri.binding_mode); self.niri.mods_with_finger_scroll_binds = - mods_with_finger_scroll_binds(self.backend.mod_key(), &config.binds); + mods_with_finger_scroll_binds(self.backend.mod_key(), &self.niri.binding_mode); } if config.window_rules != old_config.window_rules { @@ -1477,6 +1479,13 @@ impl Niri { let config_ = config.borrow(); let config_file_output_config = config_.outputs.clone(); + let binding_mode = config_ + .binds + .iter() + .find(|e| e.0 == "default") + .cloned() + .unwrap_or_else(|| niri_config::Binds(String::from("default"), vec![])); + let layout = Layout::new(&config_); let compositor_state = CompositorState::new_v6::(&display_handle); @@ -1585,14 +1594,14 @@ impl Niri { let cursor_manager = CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size); - let mods_with_wheel_binds = mods_with_wheel_binds(backend.mod_key(), &config_.binds); + let mods_with_wheel_binds = mods_with_wheel_binds(backend.mod_key(), &binding_mode); let mods_with_finger_scroll_binds = - mods_with_finger_scroll_binds(backend.mod_key(), &config_.binds); + mods_with_finger_scroll_binds(backend.mod_key(), &binding_mode); let screenshot_ui = ScreenshotUi::new(config.clone()); let config_error_notification = ConfigErrorNotification::new(config.clone()); - let mut hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key()); + let mut hotkey_overlay = HotkeyOverlay::new(backend.mod_key()); if !config_.hotkey_overlay.skip_at_startup { hotkey_overlay.show(); } @@ -1674,6 +1683,7 @@ impl Niri { drop(config_); Self { config, + binding_mode, config_file_output_config, event_loop, @@ -2880,7 +2890,10 @@ impl Niri { } // Draw the hotkey overlay on top. - if let Some(element) = self.hotkey_overlay.render(renderer, output) { + if let Some(element) = self + .hotkey_overlay + .render(renderer, output, &self.binding_mode.1) + { elements.push(element.into()); } diff --git a/src/ui/hotkey_overlay.rs b/src/ui/hotkey_overlay.rs index 255658b..7039c8b 100644 --- a/src/ui/hotkey_overlay.rs +++ b/src/ui/hotkey_overlay.rs @@ -2,9 +2,8 @@ use std::cell::RefCell; use std::cmp::max; use std::collections::HashMap; use std::iter::zip; -use std::rc::Rc; -use niri_config::{Action, Config, Key, Modifiers, Trigger}; +use niri_config::{Action, Bind, Key, Modifiers, Trigger}; use pangocairo::cairo::{self, ImageSurface}; use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight}; use smithay::backend::renderer::element::Kind; @@ -29,7 +28,6 @@ const TITLE: &str = "Important Hotkeys"; pub struct HotkeyOverlay { is_open: bool, - config: Rc>, comp_mod: CompositorMod, buffers: RefCell>, } @@ -39,10 +37,9 @@ pub struct RenderedOverlay { } impl HotkeyOverlay { - pub fn new(config: Rc>, comp_mod: CompositorMod) -> Self { + pub fn new(comp_mod: CompositorMod) -> Self { Self { is_open: false, - config, comp_mod, buffers: RefCell::new(HashMap::new()), } @@ -78,6 +75,7 @@ impl HotkeyOverlay { &self, renderer: &mut R, output: &Output, + binds: &[Bind], ) -> Option { if !self.is_open { return None; @@ -101,7 +99,7 @@ impl HotkeyOverlay { let rendered = buffers.entry(weak).or_insert_with(|| { let renderer = renderer.as_gles_renderer(); - render(renderer, &self.config.borrow(), self.comp_mod, scale) + render(renderer, binds, self.comp_mod, scale) .unwrap_or_else(|_| RenderedOverlay { buffer: None }) }); let buffer = rendered.buffer.as_ref()?; @@ -127,7 +125,7 @@ impl HotkeyOverlay { fn render( renderer: &mut GlesRenderer, - config: &Config, + binds: &[niri_config::Bind], comp_mod: CompositorMod, scale: f64, ) -> anyhow::Result { @@ -143,8 +141,6 @@ fn render( // target_size.h -= margin * 2; // anyhow::ensure!(target_size.w > 0 && target_size.h > 0); - let binds = &config.binds.0; - // Collect actions that we want to show. let mut actions = vec![&Action::ShowHotkeyOverlay]; @@ -232,9 +228,7 @@ fn render( let strings = actions .into_iter() .map(|action| { - let key = config - .binds - .0 + let key = binds .iter() .find(|bind| bind.action == *action) .map(|bind| key_name(comp_mod, &bind.key))