diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2023-09-05 12:58:51 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2023-09-05 12:58:59 +0400 |
| commit | 5225bc9e558cd87ed54271e47dcddaac2d5bcf62 (patch) | |
| tree | 554fa51a59abc2abad1aefac6d1cf2d5e246b5ec /src | |
| parent | bdc86032e44a5c84b4552cd1ad2bbbca07955e23 (diff) | |
| download | niri-5225bc9e558cd87ed54271e47dcddaac2d5bcf62.tar.gz niri-5225bc9e558cd87ed54271e47dcddaac2d5bcf62.tar.bz2 niri-5225bc9e558cd87ed54271e47dcddaac2d5bcf62.zip | |
Add configuration file
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 297 | ||||
| -rw-r--r-- | src/input.rs | 190 | ||||
| -rw-r--r-- | src/main.rs | 22 | ||||
| -rw-r--r-- | src/niri.rs | 27 |
4 files changed, 413 insertions, 123 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..a7d6fd7d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,297 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use bitflags::bitflags; +use directories::ProjectDirs; +use miette::{miette, Context, IntoDiagnostic}; +use smithay::input::keyboard::xkb::{keysym_from_name, KEY_NoSymbol, KEYSYM_CASE_INSENSITIVE}; +use smithay::input::keyboard::Keysym; + +#[derive(knuffel::Decode, Debug, PartialEq)] +pub struct Config { + #[knuffel(child, default)] + pub input: Input, + #[knuffel(child, default)] + pub binds: Binds, +} + +// FIXME: Add other devices. +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Input { + #[knuffel(child, default)] + pub keyboard: Keyboard, + #[knuffel(child, default)] + pub touchpad: Touchpad, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +pub struct Keyboard { + #[knuffel(child, default)] + pub xkb: Xkb, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +pub struct Xkb { + #[knuffel(child, unwrap(argument), default)] + pub rules: String, + #[knuffel(child, unwrap(argument), default)] + pub model: String, + #[knuffel(child, unwrap(argument))] + pub layout: Option<String>, + #[knuffel(child, unwrap(argument), default)] + pub variant: String, + #[knuffel(child, unwrap(argument))] + pub options: Option<String>, +} + +// FIXME: Add the rest of the settings. +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Touchpad { + #[knuffel(child)] + pub tap: bool, + #[knuffel(child)] + pub natural_scroll: bool, + #[knuffel(child, unwrap(argument), default)] + pub accel_speed: f64, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +pub struct Binds(#[knuffel(children)] pub Vec<Bind>); + +#[derive(knuffel::Decode, Debug, PartialEq, Eq)] +pub struct Bind { + #[knuffel(node_name)] + pub key: Key, + #[knuffel(children)] + pub actions: Vec<Action>, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Key { + pub keysym: Keysym, + pub modifiers: Modifiers, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct Modifiers : u8 { + const CTRL = 1; + const SHIFT = 2; + const ALT = 4; + const SUPER = 8; + const COMPOSITOR = 16; + } +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +pub enum Action { + #[knuffel(skip)] + None, + Quit, + #[knuffel(skip)] + ChangeVt(i32), + Suspend, + ToggleDebugTint, + Spawn(#[knuffel(arguments)] Vec<String>), + Screenshot, + CloseWindow, + FullscreenWindow, + FocusColumnLeft, + FocusColumnRight, + FocusWindowDown, + FocusWindowUp, + MoveColumnLeft, + MoveColumnRight, + MoveWindowDown, + MoveWindowUp, + ConsumeWindowIntoColumn, + ExpelWindowFromColumn, + FocusWorkspaceDown, + FocusWorkspaceUp, + MoveWindowToWorkspaceDown, + MoveWindowToWorkspaceUp, + FocusMonitorLeft, + FocusMonitorRight, + FocusMonitorDown, + FocusMonitorUp, + MoveWindowToMonitorLeft, + MoveWindowToMonitorRight, + MoveWindowToMonitorDown, + MoveWindowToMonitorUp, + SwitchPresetColumnWidth, + MaximizeColumn, +} + +impl Config { + pub fn load(path: Option<PathBuf>) -> miette::Result<Self> { + let path = if let Some(path) = path { + path + } else { + let mut path = ProjectDirs::from("", "", "niri") + .ok_or_else(|| miette!("error retrieving home directory"))? + .config_dir() + .to_owned(); + path.push("config.kdl"); + path + }; + + let contents = std::fs::read_to_string(&path) + .into_diagnostic() + .with_context(|| format!("error reading {path:?}"))?; + + let config = Self::parse("config.kdl", &contents).context("error parsing")?; + debug!("loaded config from {path:?}"); + Ok(config) + } + + pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> { + knuffel::parse(filename, text) + } +} + +impl Default for Config { + fn default() -> Self { + Config::parse( + "default-config.kdl", + include_str!("../resources/default-config.kdl"), + ) + .unwrap() + } +} + +impl FromStr for Key { + type Err = miette::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut modifiers = Modifiers::empty(); + + let mut split = s.split('+'); + let key = split.next_back().unwrap(); + + for part in split { + let part = part.trim(); + if part.eq_ignore_ascii_case("mod") { + modifiers |= Modifiers::COMPOSITOR + } else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") { + modifiers |= Modifiers::CTRL; + } else if part.eq_ignore_ascii_case("shift") { + modifiers |= Modifiers::SHIFT; + } else if part.eq_ignore_ascii_case("alt") { + modifiers |= Modifiers::ALT; + } else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") { + modifiers |= Modifiers::SUPER; + } else { + return Err(miette!("invalid modifier: {part}")); + } + } + + let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE); + if keysym == KEY_NoSymbol { + return Err(miette!("invalid key: {key}")); + } + + Ok(Key { keysym, modifiers }) + } +} + +#[cfg(test)] +mod tests { + use smithay::input::keyboard::xkb::keysyms::*; + + use super::*; + + #[track_caller] + fn check(text: &str, expected: Config) { + let parsed = Config::parse("test.kdl", text) + .map_err(miette::Report::new) + .unwrap(); + assert_eq!(parsed, expected); + } + + #[test] + fn parse() { + check( + r#" + input { + keyboard { + xkb { + layout "us,ru" + options "grp:win_space_toggle" + } + } + + touchpad { + tap + accel-speed 0.2 + } + } + + binds { + Mod+T { spawn "alacritty"; } + Mod+Q { close-window; } + Mod+Shift+H { focus-monitor-left; } + Mod+Ctrl+Shift+L { move-window-to-monitor-right; } + Mod+Comma { consume-window-into-column; } + } + "#, + Config { + input: Input { + keyboard: Keyboard { + xkb: Xkb { + layout: Some("us,ru".to_owned()), + options: Some("grp:win_space_toggle".to_owned()), + ..Default::default() + }, + }, + touchpad: Touchpad { + tap: true, + natural_scroll: false, + accel_speed: 0.2, + }, + }, + binds: Binds(vec![ + Bind { + key: Key { + keysym: KEY_t, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::Spawn(vec!["alacritty".to_owned()])], + }, + Bind { + key: Key { + keysym: KEY_q, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::CloseWindow], + }, + Bind { + key: Key { + keysym: KEY_h, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + actions: vec![Action::FocusMonitorLeft], + }, + Bind { + key: Key { + keysym: KEY_l, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, + }, + actions: vec![Action::MoveWindowToMonitorRight], + }, + Bind { + key: Key { + keysym: KEY_comma, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::ConsumeWindowIntoColumn], + }, + ]), + }, + ); + } + + #[test] + fn can_create_default_config() { + let _ = Config::default(); + } +} diff --git a/src/input.rs b/src/input.rs index 514dcd2c..ad83d331 100644 --- a/src/input.rs +++ b/src/input.rs @@ -17,45 +17,10 @@ use smithay::input::pointer::{ use smithay::utils::SERIAL_COUNTER; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; +use crate::config::{Action, Config, Modifiers}; use crate::niri::State; use crate::utils::get_monotonic_time; -enum Action { - None, - Quit, - ChangeVt(i32), - Suspend, - ToggleDebugTint, - Spawn(String), - Screenshot, - CloseWindow, - ToggleFullscreen, - FocusLeft, - FocusRight, - FocusDown, - FocusUp, - MoveLeft, - MoveRight, - MoveDown, - MoveUp, - ConsumeIntoColumn, - ExpelFromColumn, - SwitchWorkspaceDown, - SwitchWorkspaceUp, - MoveToWorkspaceDown, - MoveToWorkspaceUp, - FocusMonitorLeft, - FocusMonitorRight, - FocusMonitorDown, - FocusMonitorUp, - MoveToMonitorLeft, - MoveToMonitorRight, - MoveToMonitorDown, - MoveToMonitorUp, - ToggleWidth, - ToggleFullWidth, -} - pub enum CompositorMod { Super, Alt, @@ -70,13 +35,17 @@ impl From<Action> for FilterResult<Action> { } } -fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) -> Action { +fn action( + config: &Config, + comp_mod: CompositorMod, + keysym: KeysymHandle, + mods: ModifiersState, +) -> Action { use keysyms::*; - let modified = keysym.modified_sym(); - + // Handle hardcoded binds. #[allow(non_upper_case_globals)] // wat - match modified { + match keysym.modified_sym() { modified @ KEY_XF86Switch_VT_1..=KEY_XF86Switch_VT_12 => { let vt = (modified - KEY_XF86Switch_VT_1 + 1) as i32; return Action::ChangeVt(vt); @@ -85,59 +54,45 @@ fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) - _ => (), } - let mod_down = match comp_mod { - CompositorMod::Super => mods.logo, - CompositorMod::Alt => mods.alt, + // Handle configured binds. + let mut modifiers = Modifiers::empty(); + if mods.ctrl { + modifiers |= Modifiers::CTRL; + } + if mods.shift { + modifiers |= Modifiers::SHIFT; + } + if mods.alt { + modifiers |= Modifiers::ALT; + } + if mods.logo { + modifiers |= Modifiers::SUPER; + } + + let (mod_down, mut comp_mod) = match comp_mod { + CompositorMod::Super => (mods.logo, Modifiers::SUPER), + CompositorMod::Alt => (mods.alt, Modifiers::ALT), }; + if mod_down { + modifiers |= Modifiers::COMPOSITOR; + } else { + comp_mod = Modifiers::empty(); + } - if !mod_down { + let Some(&raw) = keysym.raw_syms().first() else { return Action::None; - } + }; + for bind in &config.binds.0 { + if bind.key.keysym != raw { + continue; + } - // FIXME: these don't work in the Russian layout. I guess I'll need to - // find a US keymap, then map keys somehow. - #[allow(non_upper_case_globals)] // wat - match modified { - KEY_E => Action::Quit, - KEY_t => Action::Spawn("alacritty".to_owned()), - KEY_d => Action::Spawn("fuzzel".to_owned()), - KEY_n => Action::Spawn("nautilus".to_owned()), - // Alt + PrtSc = SysRq - KEY_Sys_Req | KEY_Print => Action::Screenshot, - KEY_T if mods.shift && mods.ctrl => Action::ToggleDebugTint, - KEY_q => Action::CloseWindow, - KEY_F => Action::ToggleFullscreen, - KEY_comma => Action::ConsumeIntoColumn, - KEY_period => Action::ExpelFromColumn, - KEY_r => Action::ToggleWidth, - KEY_f => Action::ToggleFullWidth, - // Move to monitor. - KEY_H | KEY_Left if mods.shift && mods.ctrl => Action::MoveToMonitorLeft, - KEY_L | KEY_Right if mods.shift && mods.ctrl => Action::MoveToMonitorRight, - KEY_J | KEY_Down if mods.shift && mods.ctrl => Action::MoveToMonitorDown, - KEY_K | KEY_Up if mods.shift && mods.ctrl => Action::MoveToMonitorUp, - // Focus monitor. - KEY_H | KEY_Left if mods.shift => Action::FocusMonitorLeft, - KEY_L | KEY_Right if mods.shift => Action::FocusMonitorRight, - KEY_J | KEY_Down if mods.shift => Action::FocusMonitorDown, - KEY_K | KEY_Up if mods.shift => Action::FocusMonitorUp, - // Move. - KEY_h | KEY_Left if mods.ctrl => Action::MoveLeft, - KEY_l | KEY_Right if mods.ctrl => Action::MoveRight, - KEY_j | KEY_Down if mods.ctrl => Action::MoveDown, - KEY_k | KEY_Up if mods.ctrl => Action::MoveUp, - // Focus. - KEY_h | KEY_Left => Action::FocusLeft, - KEY_l | KEY_Right => Action::FocusRight, - KEY_j | KEY_Down => Action::FocusDown, - KEY_k | KEY_Up => Action::FocusUp, - // Workspaces. - KEY_u if mods.ctrl => Action::MoveToWorkspaceDown, - KEY_i if mods.ctrl => Action::MoveToWorkspaceUp, - KEY_u => Action::SwitchWorkspaceDown, - KEY_i => Action::SwitchWorkspaceUp, - _ => Action::None, + if bind.key.modifiers | comp_mod == modifiers { + return bind.actions.first().cloned().unwrap_or(Action::None); + } } + + Action::None } impl State { @@ -166,9 +121,9 @@ impl State { event.state(), serial, time, - |_, mods, keysym| { + |self_, mods, keysym| { if event.state() == KeyState::Pressed { - action(comp_mod, keysym, *mods).into() + action(&self_.config, comp_mod, keysym, *mods).into() } else { FilterResult::Forward } @@ -192,8 +147,10 @@ impl State { self.backend.toggle_debug_tint(); } Action::Spawn(command) => { - if let Err(err) = Command::new(command).spawn() { - warn!("error spawning alacritty: {err}"); + if let Some((command, args)) = command.split_first() { + if let Err(err) = Command::new(command).args(args).spawn() { + warn!("error spawning {command}: {err}"); + } } } Action::Screenshot => { @@ -211,78 +168,78 @@ impl State { window.toplevel().send_close(); } } - Action::ToggleFullscreen => { + Action::FullscreenWindow => { let focus = self.niri.monitor_set.focus().cloned(); if let Some(window) = focus { self.niri.monitor_set.toggle_fullscreen(&window); } } - Action::MoveLeft => { + Action::MoveColumnLeft => { self.niri.monitor_set.move_left(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveRight => { + Action::MoveColumnRight => { self.niri.monitor_set.move_right(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveDown => { + Action::MoveWindowDown => { self.niri.monitor_set.move_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveUp => { + Action::MoveWindowUp => { self.niri.monitor_set.move_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::FocusLeft => { + Action::FocusColumnLeft => { self.niri.monitor_set.focus_left(); } - Action::FocusRight => { + Action::FocusColumnRight => { self.niri.monitor_set.focus_right(); } - Action::FocusDown => { + Action::FocusWindowDown => { self.niri.monitor_set.focus_down(); } - Action::FocusUp => { + Action::FocusWindowUp => { self.niri.monitor_set.focus_up(); } - Action::MoveToWorkspaceDown => { + Action::MoveWindowToWorkspaceDown => { self.niri.monitor_set.move_to_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveToWorkspaceUp => { + Action::MoveWindowToWorkspaceUp => { self.niri.monitor_set.move_to_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::SwitchWorkspaceDown => { + Action::FocusWorkspaceDown => { self.niri.monitor_set.switch_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::SwitchWorkspaceUp => { + Action::FocusWorkspaceUp => { self.niri.monitor_set.switch_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ConsumeIntoColumn => { + Action::ConsumeWindowIntoColumn => { self.niri.monitor_set.consume_into_column(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ExpelFromColumn => { + Action::ExpelWindowFromColumn => { self.niri.monitor_set.expel_from_column(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ToggleWidth => { + Action::SwitchPresetColumnWidth => { self.niri.monitor_set.toggle_width(); } - Action::ToggleFullWidth => { + Action::MaximizeColumn => { self.niri.monitor_set.toggle_full_width(); } Action::FocusMonitorLeft => { @@ -309,25 +266,25 @@ impl State { self.move_cursor_to_output(&output); } } - Action::MoveToMonitorLeft => { + Action::MoveWindowToMonitorLeft => { if let Some(output) = self.niri.output_left() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorRight => { + Action::MoveWindowToMonitorRight => { if let Some(output) = self.niri.output_right() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorDown => { + Action::MoveWindowToMonitorDown => { if let Some(output) = self.niri.output_down() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorUp => { + Action::MoveWindowToMonitorUp => { if let Some(output) = self.niri.output_up() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); @@ -740,9 +697,10 @@ impl State { // According to Mutter code, this setting is specific to touchpads. let is_touchpad = device.config_tap_finger_count() > 0; if is_touchpad { - let _ = device.config_tap_set_enabled(true); - let _ = device.config_scroll_set_natural_scroll_enabled(true); - let _ = device.config_accel_set_speed(0.2); + let c = &self.config.input.touchpad; + let _ = device.config_tap_set_enabled(c.tap); + let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll); + let _ = device.config_accel_set_speed(c.accel_speed); } } } diff --git a/src/main.rs b/src/main.rs index 9af30d43..f71a959a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate tracing; mod animation; mod backend; +mod config; mod dbus; mod frame_clock; mod handlers; @@ -13,8 +14,11 @@ mod utils; use std::env; use std::ffi::OsString; +use std::path::PathBuf; use clap::Parser; +use config::Config; +use miette::Context; use niri::{Niri, State}; use smithay::reexports::calloop::EventLoop; use smithay::reexports::wayland_server::Display; @@ -23,6 +27,9 @@ use tracing_subscriber::EnvFilter; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { + /// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`). + #[arg(short, long)] + config: Option<PathBuf>, /// Command to run upon compositor startup. #[arg(last = true)] command: Vec<OsString>, @@ -47,9 +54,22 @@ fn main() { let _client = tracy_client::Client::start(); + let config = match Config::load(cli.config).context("error loading config") { + Ok(config) => config, + Err(err) => { + warn!("{err:?}"); + Config::default() + } + }; + let mut event_loop = EventLoop::try_new().unwrap(); let mut display = Display::new().unwrap(); - let state = State::new(event_loop.handle(), event_loop.get_signal(), &mut display); + let state = State::new( + config, + event_loop.handle(), + event_loop.get_signal(), + &mut display, + ); let mut data = LoopData { display, state }; if let Some((command, args)) = cli.command.split_first() { diff --git a/src/niri.rs b/src/niri.rs index 88a11e8f..23b8f3b1 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -56,6 +56,7 @@ use smithay::wayland::tablet_manager::TabletManagerState; use time::OffsetDateTime; use crate::backend::{Backend, Tty, Winit}; +use crate::config::Config; use crate::dbus::mutter_service_channel::ServiceChannel; use crate::frame_clock::FrameClock; use crate::layout::{MonitorRenderElement, MonitorSet}; @@ -115,12 +116,14 @@ pub struct OutputState { } pub struct State { + pub config: Config, pub backend: Backend, pub niri: Niri, } impl State { pub fn new( + config: Config, event_loop: LoopHandle<'static, LoopData>, stop_signal: LoopSignal, display: &mut Display<State>, @@ -134,10 +137,20 @@ impl State { Backend::Tty(Tty::new(event_loop.clone())) }; - let mut niri = Niri::new(event_loop, stop_signal, display, backend.seat_name()); + let mut niri = Niri::new( + &config, + event_loop, + stop_signal, + display, + backend.seat_name(), + ); backend.init(&mut niri); - Self { backend, niri } + Self { + config, + backend, + niri, + } } pub fn move_cursor(&mut self, location: Point<f64, Logical>) { @@ -178,6 +191,7 @@ impl State { impl Niri { pub fn new( + config: &Config, event_loop: LoopHandle<'static, LoopData>, stop_signal: LoopSignal, display: &mut Display<State>, @@ -202,11 +216,12 @@ impl Niri { PresentationState::new::<State>(&display_handle, CLOCK_MONOTONIC as u32); let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, seat_name); - // FIXME: get Xkb and repeat interval from GNOME dconf. let xkb = XkbConfig { - layout: "us,ru", - options: Some("grp:win_space_toggle,compose:ralt,ctrl:nocaps".to_owned()), - ..Default::default() + rules: &config.input.keyboard.xkb.rules, + model: &config.input.keyboard.xkb.model, + layout: &config.input.keyboard.xkb.layout.as_deref().unwrap_or("us"), + variant: &config.input.keyboard.xkb.variant, + options: config.input.keyboard.xkb.options.clone(), }; seat.add_keyboard(xkb, 400, 30).unwrap(); seat.add_pointer(); |
