aboutsummaryrefslogtreecommitdiff
path: root/niri-config
diff options
context:
space:
mode:
Diffstat (limited to 'niri-config')
-rw-r--r--niri-config/src/animations.rs36
-rw-r--r--niri-config/src/binds.rs21
-rw-r--r--niri-config/src/lib.rs156
-rw-r--r--niri-config/src/recent_windows.rs401
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)
+ }
+ }
+}