diff options
Diffstat (limited to 'niri-config')
| -rw-r--r-- | niri-config/src/animations.rs | 36 | ||||
| -rw-r--r-- | niri-config/src/binds.rs | 21 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 156 | ||||
| -rw-r--r-- | niri-config/src/recent_windows.rs | 401 |
4 files changed, 613 insertions, 1 deletions
diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index a3be59c9..346b6251 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -18,6 +18,7 @@ pub struct Animations { pub exit_confirmation_open_close: ExitConfirmationOpenCloseAnim, pub screenshot_ui_open: ScreenshotUiOpenAnim, pub overview_open_close: OverviewOpenCloseAnim, + pub recent_windows_close: RecentWindowsCloseAnim, } impl Default for Animations { @@ -35,6 +36,7 @@ impl Default for Animations { exit_confirmation_open_close: Default::default(), screenshot_ui_open: Default::default(), overview_open_close: Default::default(), + recent_windows_close: Default::default(), } } } @@ -67,6 +69,8 @@ pub struct AnimationsPart { pub screenshot_ui_open: Option<ScreenshotUiOpenAnim>, #[knuffel(child)] pub overview_open_close: Option<OverviewOpenCloseAnim>, + #[knuffel(child)] + pub recent_windows_close: Option<RecentWindowsCloseAnim>, } impl MergeWith<AnimationsPart> for Animations { @@ -92,6 +96,7 @@ impl MergeWith<AnimationsPart> for Animations { exit_confirmation_open_close, screenshot_ui_open, overview_open_close, + recent_windows_close, ); } } @@ -305,6 +310,22 @@ impl Default for OverviewOpenCloseAnim { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RecentWindowsCloseAnim(pub Animation); + +impl Default for RecentWindowsCloseAnim { + fn default() -> Self { + Self(Animation { + off: false, + kind: Kind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 800, + epsilon: 0.001, + }), + }) + } +} + impl<S> knuffel::Decode<S> for WorkspaceSwitchAnim where S: knuffel::traits::ErrorSpan, @@ -488,6 +509,21 @@ where } } +impl<S> knuffel::Decode<S> for RecentWindowsCloseAnim +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + let default = Self::default().0; + Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { + Ok(false) + })?)) + } +} + impl Animation { pub fn new_off() -> Self { Self { diff --git a/niri-config/src/binds.rs b/niri-config/src/binds.rs index 1356d40f..378ae8ed 100644 --- a/niri-config/src/binds.rs +++ b/niri-config/src/binds.rs @@ -12,6 +12,7 @@ use smithay::input::keyboard::keysyms::KEY_NoSymbol; use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE, KEYSYM_NO_FLAGS}; use smithay::input::keyboard::Keysym; +use crate::recent_windows::{MruDirection, MruFilter, MruScope}; use crate::utils::{expect_only_children, MergeWith}; #[derive(Debug, Default, PartialEq)] @@ -364,6 +365,26 @@ pub enum Action { UnsetWindowUrgent(u64), #[knuffel(skip)] LoadConfigFile, + #[knuffel(skip)] + MruAdvance { + direction: MruDirection, + scope: Option<MruScope>, + filter: Option<MruFilter>, + }, + #[knuffel(skip)] + MruConfirm, + #[knuffel(skip)] + MruCancel, + #[knuffel(skip)] + MruCloseCurrentWindow, + #[knuffel(skip)] + MruFirst, + #[knuffel(skip)] + MruLast, + #[knuffel(skip)] + MruSetScope(MruScope), + #[knuffel(skip)] + MruCycleScope, } impl From<niri_ipc::Action> for Action { diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index dda7dfd6..e458da39 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -13,7 +13,7 @@ #[macro_use] extern crate tracing; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::HashSet; use std::ffi::OsStr; use std::fs::{self, File}; @@ -39,6 +39,7 @@ pub mod layer_rule; pub mod layout; pub mod misc; pub mod output; +pub mod recent_windows; pub mod utils; pub mod window_rule; pub mod workspace; @@ -54,6 +55,10 @@ pub use crate::layer_rule::LayerRule; pub use crate::layout::*; pub use crate::misc::*; pub use crate::output::{Output, OutputName, Outputs, Position, Vrr}; +use crate::recent_windows::RecentWindowsPart; +pub use crate::recent_windows::{ + MruDirection, MruFilter, MruPreviews, MruScope, RecentWindows, DEFAULT_MRU_COMMIT_MS, +}; pub use crate::utils::FloatOrInt; use crate::utils::{Flag, MergeWith as _}; pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule}; @@ -85,6 +90,7 @@ pub struct Config { pub switch_events: SwitchBinds, pub debug: Debug, pub workspaces: Vec<Workspace>, + pub recent_windows: RecentWindows, } #[derive(Debug, Clone)] @@ -118,6 +124,7 @@ struct IncludeErrors(Vec<knuffel::Error>); // // We don't *need* it because we have a recursion limit, but it makes for nicer error messages. struct IncludeStack(HashSet<PathBuf>); +struct SawMruBinds(Rc<Cell<bool>>); // Rather than listing all fields and deriving knuffel::Decode, we implement // knuffel::DecodeChildren by hand, since we need custom logic for every field anyway: we want to @@ -140,6 +147,7 @@ where let includes = ctx.get::<Rc<RefCell<Includes>>>().unwrap().clone(); let include_errors = ctx.get::<Rc<RefCell<IncludeErrors>>>().unwrap().clone(); let recursion = ctx.get::<Recursion>().unwrap().0; + let saw_mru_binds = ctx.get::<SawMruBinds>().unwrap().0.clone(); let mut seen = HashSet::new(); @@ -269,6 +277,21 @@ where config.borrow_mut().layout.merge_with(&part); } + "recent-windows" => { + let part = RecentWindowsPart::decode_node(node, ctx)?; + + let mut config = config.borrow_mut(); + + // When an MRU binds section is encountered for the first time, clear out the + // default MRU binds. + if !saw_mru_binds.get() && part.binds.is_some() { + saw_mru_binds.set(true); + config.recent_windows.binds.clear(); + } + + config.recent_windows.merge_with(&part); + } + "include" => { let path: PathBuf = utils::parse_arg_node("include", node, ctx)?; let base = ctx.get::<BasePath>().unwrap(); @@ -331,6 +354,7 @@ where ctx.set(includes.clone()); ctx.set(include_errors.clone()); ctx.set(IncludeStack(include_stack)); + ctx.set(SawMruBinds(saw_mru_binds.clone())); ctx.set(config.clone()); }); @@ -424,6 +448,7 @@ impl Config { ctx.set(includes.clone()); ctx.set(include_errors.clone()); ctx.set(IncludeStack(include_stack)); + ctx.set(SawMruBinds(Rc::new(Cell::new(false)))); ctx.set(config.clone()); }, ); @@ -766,6 +791,10 @@ mod tests { window-close { curve "cubic-bezier" 0.05 0.7 0.1 1 } + + recent-windows-close { + off + } } gestures { @@ -848,6 +877,25 @@ mod tests { } workspace "workspace-2" workspace "workspace-3" + + recent-windows { + off + + highlight { + padding 15 + active-color "#00ff00" + } + + previews { + max-height 960 + } + + binds { + Alt+Tab { next-window; } + Alt+grave { next-window filter="app-id"; } + Super+Tab { next-window scope="output"; } + } + } "##, ); @@ -1507,6 +1555,18 @@ mod tests { ), }, ), + recent_windows_close: RecentWindowsCloseAnim( + Animation { + off: true, + kind: Spring( + SpringParams { + damping_ratio: 1.0, + stiffness: 800, + epsilon: 0.001, + }, + ), + }, + ), }, gestures: Gestures { dnd_edge_view_scroll: DndEdgeViewScroll { @@ -2119,6 +2179,100 @@ mod tests { layout: None, }, ], + recent_windows: RecentWindows { + on: false, + open_delay_ms: 150, + highlight: MruHighlight { + active_color: Color { + r: 0.0, + g: 1.0, + b: 0.0, + a: 1.0, + }, + urgent_color: Color { + r: 1.0, + g: 0.6, + b: 0.6, + a: 1.0, + }, + padding: 15.0, + corner_radius: 0.0, + }, + previews: MruPreviews { + max_height: 960.0, + max_scale: 0.5, + }, + binds: [ + Bind { + key: Key { + trigger: Keysym( + XK_Tab, + ), + modifiers: Modifiers( + ALT, + ), + }, + action: MruAdvance { + direction: Forward, + scope: None, + filter: Some( + All, + ), + }, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: true, + hotkey_overlay_title: None, + }, + Bind { + key: Key { + trigger: Keysym( + XK_grave, + ), + modifiers: Modifiers( + ALT, + ), + }, + action: MruAdvance { + direction: Forward, + scope: None, + filter: Some( + AppId, + ), + }, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: true, + hotkey_overlay_title: None, + }, + Bind { + key: Key { + trigger: Keysym( + XK_Tab, + ), + modifiers: Modifiers( + SUPER, + ), + }, + action: MruAdvance { + direction: Forward, + scope: Some( + Output, + ), + filter: Some( + All, + ), + }, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: true, + hotkey_overlay_title: None, + }, + ], + }, } "#); } diff --git a/niri-config/src/recent_windows.rs b/niri-config/src/recent_windows.rs new file mode 100644 index 00000000..0d293ba1 --- /dev/null +++ b/niri-config/src/recent_windows.rs @@ -0,0 +1,401 @@ +use std::collections::HashSet; + +use knuffel::errors::DecodeError; +use smithay::input::keyboard::Keysym; + +use crate::utils::{expect_only_children, MergeWith}; +use crate::{Action, Bind, Color, FloatOrInt, Key, Modifiers, Trigger}; + +/// Delay before the window focus is considered to be locked-in for Window +/// MRU ordering. For now the delay is not configurable. +pub const DEFAULT_MRU_COMMIT_MS: u64 = 750; + +#[derive(Debug, PartialEq)] +pub struct RecentWindows { + pub on: bool, + pub open_delay_ms: u16, + pub highlight: MruHighlight, + pub previews: MruPreviews, + pub binds: Vec<Bind>, +} + +impl Default for RecentWindows { + fn default() -> Self { + RecentWindows { + on: true, + open_delay_ms: 150, + highlight: MruHighlight::default(), + previews: MruPreviews::default(), + binds: default_binds(), + } + } +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct RecentWindowsPart { + #[knuffel(child)] + pub on: bool, + #[knuffel(child)] + pub off: bool, + #[knuffel(child, unwrap(argument))] + pub open_delay_ms: Option<u16>, + #[knuffel(child)] + pub highlight: Option<MruHighlightPart>, + #[knuffel(child)] + pub previews: Option<MruPreviewsPart>, + #[knuffel(child)] + pub binds: Option<MruBinds>, +} + +impl MergeWith<RecentWindowsPart> for RecentWindows { + fn merge_with(&mut self, part: &RecentWindowsPart) { + self.on |= part.on; + if part.off { + self.on = false; + } + + merge_clone!((self, part), open_delay_ms); + merge!((self, part), highlight, previews); + + if let Some(part) = &part.binds { + // Remove existing binds matching any new bind. + self.binds + .retain(|bind| !part.0.iter().any(|new| new.key == bind.key)); + // Add all new binds. + self.binds.extend(part.0.iter().cloned().map(Bind::from)); + } + } +} + +#[derive(Debug, PartialEq)] +pub struct MruHighlight { + pub active_color: Color, + pub urgent_color: Color, + pub padding: f64, + pub corner_radius: f64, +} + +impl Default for MruHighlight { + fn default() -> Self { + Self { + active_color: Color::new_unpremul(0.6, 0.6, 0.6, 1.), + urgent_color: Color::new_unpremul(1., 0.6, 0.6, 1.), + padding: 30., + corner_radius: 0., + } + } +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct MruHighlightPart { + #[knuffel(child)] + pub active_color: Option<Color>, + #[knuffel(child)] + pub urgent_color: Option<Color>, + #[knuffel(child, unwrap(argument))] + pub padding: Option<FloatOrInt<0, 65535>>, + #[knuffel(child, unwrap(argument))] + pub corner_radius: Option<FloatOrInt<0, 65535>>, +} + +impl MergeWith<MruHighlightPart> for MruHighlight { + fn merge_with(&mut self, part: &MruHighlightPart) { + merge_clone!((self, part), active_color, urgent_color); + merge!((self, part), padding, corner_radius); + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MruPreviews { + pub max_height: f64, + pub max_scale: f64, +} + +impl Default for MruPreviews { + fn default() -> Self { + Self { + max_height: 480., + max_scale: 0.5, + } + } +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct MruPreviewsPart { + #[knuffel(child, unwrap(argument))] + pub max_height: Option<FloatOrInt<1, 65535>>, + #[knuffel(child, unwrap(argument))] + pub max_scale: Option<FloatOrInt<0, 1>>, +} + +impl MergeWith<MruPreviewsPart> for MruPreviews { + fn merge_with(&mut self, part: &MruPreviewsPart) { + merge!((self, part), max_height, max_scale); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MruBind { + // MRU bind keys must have a modifier, this is enforced during parsing. The switcher will close + // once all modifiers are released. + pub key: Key, + pub action: MruAction, + pub allow_inhibiting: bool, + pub hotkey_overlay_title: Option<Option<String>>, +} + +impl From<MruBind> for Bind { + fn from(x: MruBind) -> Self { + Self { + key: x.key, + action: Action::from(x.action), + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: x.allow_inhibiting, + hotkey_overlay_title: x.hotkey_overlay_title, + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum MruDirection { + /// Most recently used to least. + #[default] + Forward, + /// Least recently used to most. + Backward, +} + +#[derive(knuffel::DecodeScalar, Clone, Copy, Debug, Default, PartialEq)] +pub enum MruScope { + /// All windows. + #[default] + All, + /// Windows on the active output. + Output, + /// Windows on the active workspace. + Workspace, +} + +#[derive(knuffel::DecodeScalar, Clone, Copy, Debug, Default, PartialEq)] +pub enum MruFilter { + /// All windows. + #[default] + #[knuffel(skip)] + All, + /// Windows with the same app id as the active window. + AppId, +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +pub enum MruAction { + NextWindow( + #[knuffel(property(name = "scope"))] Option<MruScope>, + #[knuffel(property(name = "filter"), default)] MruFilter, + ), + PreviousWindow( + #[knuffel(property(name = "scope"))] Option<MruScope>, + #[knuffel(property(name = "filter"), default)] MruFilter, + ), +} + +impl From<MruAction> for Action { + fn from(x: MruAction) -> Self { + match x { + MruAction::NextWindow(scope, filter) => Self::MruAdvance { + direction: MruDirection::Forward, + scope, + filter: Some(filter), + }, + MruAction::PreviousWindow(scope, filter) => Self::MruAdvance { + direction: MruDirection::Backward, + scope, + filter: Some(filter), + }, + } + } +} + +#[derive(Debug, Default, PartialEq)] +pub struct MruBinds(pub Vec<MruBind>); + +fn default_binds() -> Vec<Bind> { + let mut rv = Vec::new(); + + let mut push = |trigger, base_mod, filter| { + rv.push(Bind::from(MruBind { + key: Key { + trigger: Trigger::Keysym(trigger), + modifiers: base_mod, + }, + action: MruAction::NextWindow(None, filter), + allow_inhibiting: true, + hotkey_overlay_title: None, + })); + rv.push(Bind::from(MruBind { + key: Key { + trigger: Trigger::Keysym(trigger), + modifiers: base_mod | Modifiers::SHIFT, + }, + action: MruAction::PreviousWindow(None, filter), + allow_inhibiting: true, + hotkey_overlay_title: None, + })); + }; + + for base_mod in [Modifiers::ALT, Modifiers::COMPOSITOR] { + push(Keysym::Tab, base_mod, MruFilter::All); + push(Keysym::grave, base_mod, MruFilter::AppId); + } + + rv +} + +impl<S> knuffel::Decode<S> for MruBinds +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + expect_only_children(node, ctx); + + let mut seen_keys = HashSet::new(); + + let mut binds = Vec::new(); + + for child in node.children() { + match MruBind::decode_node(child, ctx) { + Ok(bind) => { + if !seen_keys.insert(bind.key) { + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "keybind", + "duplicate keybind", + )); + continue; + } + + binds.push(bind); + } + Err(e) => { + ctx.emit_error(e); + } + } + } + + Ok(Self(binds)) + } +} + +impl<S> knuffel::Decode<S> for MruBind +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + 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::<Key>() + .map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?; + + // A modifier is required because MRU remains on screen as long as any modifier is held. + if key.modifiers.is_empty() { + ctx.emit_error(DecodeError::unexpected( + &node.node_name, + "keybind", + "keybind must have a modifier key", + )); + } + + // FIXME: To support this, all the mods_with_mouse_binds()/mods_with_wheel_binds()/etc. + // will need to learn about recent-windows bindings. + if !matches!(key.trigger, Trigger::Keysym(_)) { + ctx.emit_error(DecodeError::unexpected( + &node.node_name, + "key", + "key must be a keyboard key (others are unsupported here for now)", + )); + } + + let mut allow_inhibiting = true; + let mut hotkey_overlay_title = None; + for (name, val) in &node.properties { + match &***name { + "allow-inhibiting" => { + allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } + "hotkey-overlay-title" => { + hotkey_overlay_title = Some(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. + // That way, the parent can handle the existence of duplicate keybinds, + // even if their contents are not valid. + let dummy = Self { + key, + action: MruAction::NextWindow(None, MruFilter::All), + allow_inhibiting: true, + hotkey_overlay_title: None, + }; + + if let Some(child) = children.next() { + for unwanted_child in children { + ctx.emit_error(DecodeError::unexpected( + unwanted_child, + "node", + "only one action is allowed per keybind", + )); + } + match MruAction::decode_node(child, ctx) { + Ok(action) => Ok(Self { + key, + action, + allow_inhibiting, + hotkey_overlay_title, + }), + Err(e) => { + ctx.emit_error(e); + Ok(dummy) + } + } + } else { + ctx.emit_error(DecodeError::missing( + node, + "expected an action for this keybind", + )); + Ok(dummy) + } + } +} |
