diff --git a/Cargo.lock b/Cargo.lock index 226eab0c..5a69c53b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.3.2" @@ -291,6 +297,7 @@ dependencies = [ "egui", "id_tree", "indexmap", + "ron", "serde", "serde_json", "slog", @@ -303,6 +310,8 @@ dependencies = [ "thiserror", "wayland-scanner", "xcursor", + "xdg", + "xkbcommon", ] [[package]] @@ -366,6 +375,15 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -376,6 +394,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1134,6 +1163,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "ron" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678" +dependencies = [ + "base64", + "bitflags", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -1175,6 +1215,20 @@ name = "serde" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -1734,6 +1788,15 @@ dependencies = [ "nom", ] +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + [[package]] name = "xkbcommon" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 25d9e165..ff656cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,17 @@ slog-term = "2.8" slog-async = "2.7" slog-scope = "4.4" slog-stdlog = "4.1" -serde = { version = "1", optional = true } +serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } egui = { version = "0.16", optional = true } edid-rs = { version = "0.1" } thiserror = "1.0.26" xcursor = "0.3.3" id_tree = "1.8.0" +xkbcommon = "0.4" indexmap = "1.8.0" +xdg = "^2.1" +ron = "0.7" [dependencies.smithay] version = "0.3" @@ -39,7 +42,7 @@ wayland-scanner = "0.29" [features] default = [] -debug = ["egui", "smithay-egui", "serde", "serde_json"] +debug = ["egui", "smithay-egui", "serde_json"] [profile.dev] lto = "thin" diff --git a/config.ron b/config.ron new file mode 100644 index 00000000..1fcdea5d --- /dev/null +++ b/config.ron @@ -0,0 +1,32 @@ +( + key_bindings: { + (modifiers: [Logo, Shift], key: "Escape"): Terminate, + (modifiers: [Logo], key: "Escape"): Debug, + (modifiers: [Logo, Shift], key: "Q"): Close, + (modifiers: [Logo], key: "1"): Workspace(1), + (modifiers: [Logo], key: "2"): Workspace(2), + (modifiers: [Logo], key: "3"): Workspace(3), + (modifiers: [Logo], key: "4"): Workspace(4), + (modifiers: [Logo], key: "5"): Workspace(5), + (modifiers: [Logo], key: "6"): Workspace(6), + (modifiers: [Logo], key: "7"): Workspace(7), + (modifiers: [Logo], key: "8"): Workspace(8), + (modifiers: [Logo], key: "9"): Workspace(9), + (modifiers: [Logo], key: "0"): Workspace(0), + (modifiers: [Logo, Shift], key: "1"): MoveToWorkspace(1), + (modifiers: [Logo, Shift], key: "2"): MoveToWorkspace(2), + (modifiers: [Logo, Shift], key: "3"): MoveToWorkspace(3), + (modifiers: [Logo, Shift], key: "4"): MoveToWorkspace(4), + (modifiers: [Logo, Shift], key: "5"): MoveToWorkspace(5), + (modifiers: [Logo, Shift], key: "6"): MoveToWorkspace(6), + (modifiers: [Logo, Shift], key: "7"): MoveToWorkspace(7), + (modifiers: [Logo, Shift], key: "8"): MoveToWorkspace(8), + (modifiers: [Logo, Shift], key: "9"): MoveToWorkspace(9), + (modifiers: [Logo, Shift], key: "0"): MoveToWorkspace(0), + (modifiers: [Logo], key: "Left"): Focus(Left), + (modifiers: [Logo], key: "Right"): Focus(Right), + (modifiers: [Logo], key: "Up"): Focus(Up), + (modifiers: [Logo], key: "Down"): Focus(Down), + (modifiers: [Ctrl], key: "Return"): Spawn("gnome-terminal"), + } +) \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..62005091 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use serde::Deserialize; +use smithay::wayland::seat::Keysym; +pub use smithay::{ + backend::input::KeyState, + wayland::seat::{keysyms as KeySyms, ModifiersState as KeyModifiers}, +}; +use std::{collections::HashMap, fs::OpenOptions, path::PathBuf}; +use xkbcommon::xkb; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub key_bindings: HashMap, +} + +impl Config { + pub fn load() -> Config { + let mut locations = if let Ok(base) = xdg::BaseDirectories::new() { + base.list_config_files_once("cosmic-comp.ron") + } else { + Vec::with_capacity(3) + }; + if cfg!(debug_assertions) { + if let Ok(mut cwd) = std::env::current_dir() { + cwd.push("config.ron"); + locations.push(cwd); + } + } + locations.push(PathBuf::from("/etc/cosmic-comp/config.ron")); + locations.push(PathBuf::from("/etc/cosmic-comp.ron")); + + for path in locations { + if path.exists() { + return ron::de::from_reader(OpenOptions::new().read(true).open(path).unwrap()) + .expect("Malformed config file"); + } + } + + Config { + key_bindings: HashMap::new(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub enum KeyModifier { + Ctrl, + Alt, + Shift, + Logo, + CapsLock, + NumLock, +} + +impl std::ops::AddAssign for KeyModifiers { + fn add_assign(&mut self, rhs: KeyModifier) { + match rhs { + KeyModifier::Ctrl => self.ctrl = true, + KeyModifier::Alt => self.alt = true, + KeyModifier::Shift => self.shift = true, + KeyModifier::Logo => self.logo = true, + KeyModifier::CapsLock => self.caps_lock = true, + KeyModifier::NumLock => self.num_lock = true, + }; + } +} + +impl std::ops::BitOr for KeyModifier { + type Output = KeyModifiers; + + fn bitor(self, rhs: KeyModifier) -> Self::Output { + let mut modifiers = self.into(); + modifiers += rhs; + modifiers + } +} + +impl Into for KeyModifier { + fn into(self) -> KeyModifiers { + let mut modifiers = KeyModifiers { + ctrl: false, + alt: false, + shift: false, + caps_lock: false, + logo: false, + num_lock: false, + }; + modifiers += self; + modifiers + } +} + +#[derive(Deserialize)] +#[serde(transparent)] +struct KeyModifiersDef(Vec); + +impl From for KeyModifiers { + fn from(src: KeyModifiersDef) -> Self { + src.0.into_iter().fold( + KeyModifiers { + ctrl: false, + alt: false, + shift: false, + caps_lock: false, + logo: false, + num_lock: false, + }, + |mut modis, modi| { + modis += modi; + modis + }, + ) + } +} + +#[allow(non_snake_case)] +fn deserialize_KeyModifiers<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + KeyModifiersDef::deserialize(deserializer).map(Into::into) +} + +#[allow(non_snake_case)] +fn deserialize_Keysym<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::{Error, Unexpected}; + + let name = String::deserialize(deserializer)?; + //let name = format!("KEY_{}", code); + match xkb::keysym_from_name(&name, xkb::KEYSYM_NO_FLAGS) { + KeySyms::KEY_NoSymbol => match xkb::keysym_from_name(&name, xkb::KEYSYM_CASE_INSENSITIVE) { + KeySyms::KEY_NoSymbol => Err(::invalid_value( + Unexpected::Str(&name), + &"One of the keysym names of xkbcommon.h without the 'KEY_' prefix", + )), + x => { + slog_scope::warn!( + "Key-Binding '{}' only matched case insensitive for {:?}", + name, + xkb::keysym_get_name(x) + ); + Ok(x) + } + }, + x => Ok(x), + } +} + +/// Describtion of a key combination that might be +/// handled by the compositor. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Hash)] +#[serde(deny_unknown_fields)] +pub struct KeyPattern { + /// What modifiers are expected to be pressed alongside the key + #[serde(deserialize_with = "deserialize_KeyModifiers")] + pub modifiers: KeyModifiers, + /// The actual key, that was pressed + #[serde(deserialize_with = "deserialize_Keysym")] + pub key: u32, +} + +impl KeyPattern { + pub fn new(modifiers: impl Into, key: u32) -> KeyPattern { + KeyPattern { + modifiers: modifiers.into(), + key, + } + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub enum Action { + Terminate, + Debug, + Close, + Workspace(u8), + MoveToWorkspace(u8), + Focus(FocusAction), + Spawn(String), +} + +#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)] +pub enum FocusAction { + Left, + Right, + Up, + Down, +} diff --git a/src/input/mod.rs b/src/input/mod.rs index 21b65990..114222fb 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,9 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only -use crate::state::Common; -use smithay::{backend::input::KeyState, wayland::seat::keysyms}; +use crate::{config::Action, state::Common}; use smithay::{ - backend::input::{Device, DeviceCapability, InputBackend, InputEvent}, + backend::input::{Device, DeviceCapability, InputBackend, InputEvent, KeyState}, desktop::{layer_map_for_output, Space, WindowSurfaceType}, reexports::wayland_server::{protocol::wl_surface::WlSurface, Display}, utils::{Logical, Point}, @@ -213,66 +212,8 @@ impl Common { return FilterResult::Intercept(()); } - // here we can handle global shortcuts and the like - if modifiers.logo - && handle.raw_syms().contains(&keysyms::KEY_Return) - && state == KeyState::Pressed - { - if let Err(err) = std::process::Command::new("gnome-terminal") - .env("WAYLAND_DISPLAY", &self.socket) - .spawn() - { - slog_scope::warn!("Failed to spawn terminal: {}", err); - } - userdata.get::().unwrap().add(&handle); - return FilterResult::Intercept(()); - } - - if modifiers.logo - && handle - .raw_syms() - .iter() - .any(|sym| *sym >= keysyms::KEY_0 && *sym <= keysyms::KEY_9) - && state == KeyState::Pressed - { - let current_output = active_output(seat, &self); - let key_num = handle - .raw_syms() - .iter() - .find(|sym| { - **sym >= keysyms::KEY_0 && **sym <= keysyms::KEY_9 - }) - .unwrap() - - keysyms::KEY_0; - let workspace = match key_num { - 0 => 9, - x => x - 1, - }; - self.shell.activate(¤t_output, workspace as usize); - userdata.get::().unwrap().add(&handle); - return FilterResult::Intercept(()); - } - - if modifiers.logo - && modifiers.shift - && handle.raw_syms().contains(&keysyms::KEY_Escape) - && state == KeyState::Pressed - { - self.should_stop = true; - } - #[cfg(feature = "debug")] { - self.egui.modifiers = modifiers.clone(); - if self.seats.iter().position(|x| x == seat).unwrap() == 0 - && modifiers.logo - && handle.raw_syms().contains(&keysyms::KEY_Escape) - && state == KeyState::Pressed - { - self.egui.active = !self.egui.active; - userdata.get::().unwrap().add(&handle); - return FilterResult::Intercept(()); - } if self.seats.iter().position(|x| x == seat).unwrap() == 0 && self.egui.active { @@ -295,6 +236,73 @@ impl Common { } } + // here we can handle global shortcuts and the like + for (binding, action) in self.config.key_bindings.iter() { + if state == KeyState::Pressed + && binding.modifiers == *modifiers + && handle.raw_syms().contains(&binding.key) + { + match action { + Action::Terminate => { + userdata + .get::() + .unwrap() + .add(&handle); + self.should_stop = true; + return FilterResult::Intercept(()); + } + #[cfg(feature = "debug")] + Action::Debug => { + self.egui.active = !self.egui.active; + userdata + .get::() + .unwrap() + .add(&handle); + return FilterResult::Intercept(()); + } + #[cfg(not(feature = "debug"))] + Action::Debug => slog_scope::info!( + "Debug overlay not included in this version" + ), + Action::Close => { /* TODO */ } + Action::Workspace(key_num) => { + let current_output = active_output(seat, &self); + let workspace = match key_num { + 0 => 9, + x => x - 1, + }; + self.shell + .activate(¤t_output, workspace as usize); + userdata + .get::() + .unwrap() + .add(&handle); + return FilterResult::Intercept(()); + } + Action::MoveToWorkspace(num) => { /* TODO */ } + Action::Focus(focus) => match focus { + _ => { /* TODO */ } + }, + Action::Spawn(command) => { + if let Err(err) = + std::process::Command::new("/bin/sh") + .arg("-c") + .arg(command) + .env("WAYLAND_DISPLAY", &self.socket) + .spawn() + { + slog_scope::warn!("Failed to spawn: {}", err); + } + userdata + .get::() + .unwrap() + .add(&handle); + return FilterResult::Intercept(()); + } + } + } + } + FilterResult::Forward }, ); diff --git a/src/main.rs b/src/main.rs index d5af986e..7d4c6ebd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use anyhow::{Context, Result}; use std::{ffi::OsString, sync::atomic::Ordering}; pub mod backend; +pub mod config; pub mod input; mod logger; pub mod shell; diff --git a/src/state.rs b/src/state.rs index b229640f..4d737300 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,6 +2,7 @@ use crate::{ backend::{kms::KmsState, winit::WinitState, x11::X11State}, + config::Config, logger::LogState, shell::{init_shell, Shell}, }; @@ -35,6 +36,8 @@ pub struct State { } pub struct Common { + pub config: Config, + pub display: Rc>, pub socket: OsString, pub event_loop_handle: LoopHandle<'static, State>, @@ -167,6 +170,8 @@ impl State { State { common: Common { + config: Config::load(), + display: Rc::new(RefCell::new(display)), socket, event_loop_handle: handle,