feat: runtime configurable keybindings

This commit is contained in:
Michael Aaron Murphy 2024-04-03 16:02:27 +02:00 committed by Michael Murphy
parent 62afa4cf61
commit 553c49b42b
25 changed files with 674 additions and 829 deletions

View file

@ -1,221 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::shell::{focus::FocusDirection, grabs::ResizeEdge, Direction, ResizeDirection};
use cosmic_comp_config::workspace::WorkspaceLayout;
use serde::Deserialize;
use smithay::{
backend::input::KeyState,
input::keyboard::{xkb::keysym_get_name, ModifiersState},
};
use std::collections::HashMap;
use cosmic_settings_config::shortcuts::State as KeyState;
use cosmic_settings_config::shortcuts::{self, Modifiers, Shortcuts};
use smithay::input::keyboard::ModifiersState;
use xkbcommon::xkb;
use super::types::*;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub enum KeyModifier {
Ctrl,
Alt,
Shift,
Super,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct KeyModifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub logo: bool,
}
impl PartialEq<ModifiersState> for KeyModifiers {
fn eq(&self, other: &ModifiersState) -> bool {
self.ctrl == other.ctrl
&& self.alt == other.alt
&& self.shift == other.shift
&& self.logo == other.logo
}
}
impl Into<KeyModifiers> for ModifiersState {
fn into(self) -> KeyModifiers {
KeyModifiers {
ctrl: self.ctrl,
alt: self.alt,
shift: self.shift,
logo: self.logo,
}
}
}
impl std::ops::AddAssign<KeyModifier> 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::Super => self.logo = 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<KeyModifiers> for KeyModifier {
fn into(self) -> KeyModifiers {
let mut modifiers = KeyModifiers {
ctrl: false,
alt: false,
shift: false,
logo: false,
};
modifiers += self;
modifiers
}
}
/// 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", default)]
pub key: Option<Keysym>,
}
impl KeyPattern {
pub fn new(modifiers: impl Into<KeyModifiers>, key: Option<Keysym>) -> KeyPattern {
KeyPattern {
modifiers: modifiers.into(),
key,
}
}
pub fn inferred_direction(&self) -> Option<Direction> {
match self.key? {
Keysym::Left | Keysym::h | Keysym::H => Some(Direction::Left),
Keysym::Down | Keysym::j | Keysym::J => Some(Direction::Down),
Keysym::Up | Keysym::k | Keysym::K => Some(Direction::Up),
Keysym::Right | Keysym::l | Keysym::L => Some(Direction::Right),
_ => None,
}
}
}
impl ToString for KeyPattern {
fn to_string(&self) -> String {
let mut result = String::new();
if self.modifiers.logo {
result += "Super+";
}
if self.modifiers.ctrl {
result += "Ctrl+";
}
if self.modifiers.alt {
result += "Alt+";
}
if self.modifiers.shift {
result += "Shift+";
}
if let Some(key) = self.key {
result += &keysym_get_name(key);
} else {
result.remove(result.len() - 1);
}
result
}
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Action {
Terminate,
Debug,
Close,
#[serde(skip)]
/// Behaviors managed internally by cosmic-comp.
Private(PrivateAction),
/// Behaviors managed via cosmic-settings.
Shortcut(shortcuts::Action),
}
#[derive(Clone, Debug, Eq, PartialEq)]
// Behaviors which are internally defined and emitted.
pub enum PrivateAction {
Escape,
Workspace(u8),
NextWorkspace,
PreviousWorkspace,
LastWorkspace,
MoveToWorkspace(u8),
MoveToNextWorkspace,
MoveToPreviousWorkspace,
MoveToLastWorkspace,
SendToWorkspace(u8),
SendToNextWorkspace,
SendToPreviousWorkspace,
SendToLastWorkspace,
NextOutput,
PreviousOutput,
MoveToNextOutput,
MoveToPreviousOutput,
SendToNextOutput,
SendToPreviousOutput,
SwitchOutput(Direction),
MoveToOutput(Direction),
SendToOutput(Direction),
MigrateWorkspaceToNextOutput,
MigrateWorkspaceToPreviousOutput,
MigrateWorkspaceToOutput(Direction),
Focus(FocusDirection),
Move(Direction),
ToggleOrientation,
Orientation(crate::shell::layout::Orientation),
ToggleStacking,
ToggleTiling,
ToggleWindowFloating,
ToggleSticky,
SwapWindow,
Resizing(ResizeDirection),
#[serde(skip)]
_ResizingInternal(ResizeDirection, ResizeEdge, KeyState),
Minimize,
Maximize,
Spawn(String),
Resizing(
shortcuts::action::ResizeDirection,
shortcuts::action::ResizeEdge,
shortcuts::State,
),
}
fn insert_binding(
key_bindings: &mut HashMap<KeyPattern, Action>,
modifiers: KeyModifiers,
keys: impl Iterator<Item = Keysym>,
action: Action,
) {
if !key_bindings.values().any(|a| a == &action) {
for key in keys {
let pattern = KeyPattern {
modifiers: modifiers.clone(),
key: Some(key),
};
if !key_bindings.contains_key(&pattern) {
key_bindings.insert(pattern, action.clone());
}
}
}
}
pub fn add_default_bindings(
key_bindings: &mut HashMap<KeyPattern, Action>,
workspace_layout: WorkspaceLayout,
) {
pub fn add_default_bindings(shortcuts: &mut Shortcuts, workspace_layout: WorkspaceLayout) {
let (
workspace_previous,
workspace_next,
@ -223,102 +31,110 @@ pub fn add_default_bindings(
(output_next, output_next_dir),
) = match workspace_layout {
WorkspaceLayout::Horizontal => (
[Keysym::Left, Keysym::h],
[Keysym::Right, Keysym::l],
([Keysym::Up, Keysym::k], Direction::Up),
([Keysym::Down, Keysym::j], Direction::Down),
[xkb::Keysym::Left, xkb::Keysym::h],
[xkb::Keysym::Right, xkb::Keysym::l],
(
[xkb::Keysym::Up, xkb::Keysym::k],
shortcuts::action::Direction::Up,
),
(
[xkb::Keysym::Down, xkb::Keysym::j],
shortcuts::action::Direction::Down,
),
),
WorkspaceLayout::Vertical => (
[Keysym::Up, Keysym::k],
[Keysym::Down, Keysym::j],
([Keysym::Left, Keysym::h], Direction::Left),
([Keysym::Right, Keysym::l], Direction::Right),
[xkb::Keysym::Up, xkb::Keysym::k],
[xkb::Keysym::Down, xkb::Keysym::j],
(
[xkb::Keysym::Left, xkb::Keysym::h],
shortcuts::action::Direction::Left,
),
(
[xkb::Keysym::Right, xkb::Keysym::l],
shortcuts::action::Direction::Right,
),
),
};
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
..Default::default()
},
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl(),
workspace_previous.iter().copied(),
Action::PreviousWorkspace,
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
..Default::default()
},
workspace_next.iter().copied(),
Action::NextWorkspace,
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
shift: true,
..Default::default()
},
workspace_previous.iter().copied(),
Action::MoveToPreviousWorkspace,
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
shift: true,
..Default::default()
},
workspace_next.iter().copied(),
Action::MoveToNextWorkspace,
shortcuts::Action::PreviousWorkspace,
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
..Default::default()
},
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl(),
workspace_next.iter().copied(),
shortcuts::Action::NextWorkspace,
);
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl().shift(),
workspace_previous.iter().copied(),
shortcuts::Action::MoveToPreviousWorkspace,
);
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl().shift(),
workspace_next.iter().copied(),
shortcuts::Action::MoveToNextWorkspace,
);
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl(),
output_previous.iter().copied(),
Action::SwitchOutput(output_previous_dir),
shortcuts::Action::SwitchOutput(output_previous_dir),
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
..Default::default()
},
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl(),
output_next.iter().copied(),
Action::SwitchOutput(output_next_dir),
shortcuts::Action::SwitchOutput(output_next_dir),
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
shift: true,
..Default::default()
},
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl().shift(),
output_previous.iter().copied(),
Action::MoveToOutput(output_previous_dir),
shortcuts::Action::MoveToOutput(output_previous_dir),
);
insert_binding(
key_bindings,
KeyModifiers {
logo: true,
ctrl: true,
shift: true,
..Default::default()
},
shortcuts.insert_default_binding(
Modifiers::new().logo().ctrl().shift(),
output_next.iter().copied(),
Action::MoveToOutput(output_next_dir),
shortcuts::Action::MoveToOutput(output_next_dir),
);
}
/// Convert `cosmic_settings_config::shortcuts::State` to `smithay::backend::input::KeyState`.
pub fn cosmic_keystate_to_smithay(value: KeyState) -> smithay::backend::input::KeyState {
match value {
KeyState::Pressed => smithay::backend::input::KeyState::Pressed,
KeyState::Released => smithay::backend::input::KeyState::Released,
}
}
/// Convert `smithay::backend::input::KeyState` to `cosmic_settings_config::shortcuts::State`.
pub fn cosmic_keystate_from_smithay(value: smithay::backend::input::KeyState) -> KeyState {
match value {
smithay::backend::input::KeyState::Pressed => KeyState::Pressed,
smithay::backend::input::KeyState::Released => KeyState::Released,
}
}
/// Compare `cosmic_settings_config::shortcuts::Modifiers` to `smithay::input::keyboard::ModifiersState`.
pub fn cosmic_modifiers_eq_smithay(this: &Modifiers, other: &ModifiersState) -> bool {
this.ctrl == other.ctrl
&& this.alt == other.alt
&& this.shift == other.shift
&& this.logo == other.logo
}
/// Convert `smithay::input::keyboard::ModifiersState` to `cosmic_settings_config::shortcuts::Modifiers`
pub fn cosmic_modifiers_from_smithay(value: ModifiersState) -> Modifiers {
Modifiers {
ctrl: value.ctrl,
alt: value.alt,
shift: value.shift,
logo: value.logo,
}
}

View file

@ -8,6 +8,7 @@ use crate::{
},
};
use cosmic_config::{ConfigGet, CosmicConfigEntry};
use cosmic_settings_config::{shortcuts, Shortcuts};
use serde::{Deserialize, Serialize};
use smithay::wayland::xdg_activation::XdgActivationState;
pub use smithay::{
@ -25,66 +26,33 @@ pub use smithay::{
};
use std::{
cell::RefCell,
collections::HashMap,
collections::{BTreeMap, HashMap},
fs::OpenOptions,
path::PathBuf,
sync::{atomic::AtomicBool, Arc, RwLock},
};
use tracing::{debug, error, info, warn};
use tracing::{error, warn};
mod input_config;
mod key_bindings;
pub use key_bindings::{Action, KeyModifier, KeyModifiers, KeyPattern};
pub mod key_bindings;
pub use key_bindings::{Action, PrivateAction};
mod types;
pub use self::types::*;
use cosmic_comp_config::{
input::InputConfig,
workspace::{WorkspaceConfig, WorkspaceLayout},
CosmicCompConfig, TileBehavior, XkbConfig,
input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, TileBehavior, XkbConfig,
};
#[derive(Debug)]
pub struct Config {
pub static_conf: StaticConfig,
pub dynamic_conf: DynamicConfig,
pub cosmic_helper: cosmic_config::Config,
pub cosmic_conf: CosmicCompConfig,
}
#[derive(Debug, Deserialize)]
pub struct StaticConfig {
pub key_bindings: HashMap<key_bindings::KeyPattern, key_bindings::Action>,
pub data_control_enabled: bool,
}
impl StaticConfig {
pub fn get_shortcut_for_action(&self, action: &Action) -> Option<String> {
let possible_variants = self
.key_bindings
.iter()
.filter(|(_, a)| *a == action)
.map(|(b, _)| b)
.collect::<Vec<_>>();
possible_variants
.iter()
.find(|b| b.key.is_none()) // prefer short bindings
.or_else(|| {
possible_variants
.iter() // prefer bindings containing arrow keys
.find(|b| {
matches!(
b.key,
Some(Keysym::Down)
| Some(Keysym::Up)
| Some(Keysym::Left)
| Some(Keysym::Right)
)
})
})
.or_else(|| possible_variants.first()) // take the first one
.map(|binding| binding.to_string())
}
/// cosmic-config context for `com.system76.CosmicSettings.Shortcuts`
pub settings_context: cosmic_config::Config,
/// Key bindings from `com.system76.CosmicSettings.Shortcuts`
pub shortcuts: Shortcuts,
/// System actions from `com.system76.CosmicSettings.Shortcuts`
pub system_actions: BTreeMap<shortcuts::action::System, String>,
}
#[derive(Debug)]
@ -185,6 +153,7 @@ impl Config {
.expect("Failed to add cosmic-config to the event loop");
let xdg = xdg::BaseDirectories::new().ok();
let workspace = get_config::<WorkspaceConfig>(&config, "workspaces");
let cosmic_comp_config =
CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| {
for err in errs {
@ -192,72 +161,48 @@ impl Config {
}
c
});
Config {
static_conf: Self::load_static(xdg.as_ref(), workspace.workspace_layout),
dynamic_conf: Self::load_dynamic(xdg.as_ref()),
cosmic_conf: cosmic_comp_config,
cosmic_helper: config,
}
}
fn load_static(
xdg: Option<&xdg::BaseDirectories>,
workspace_layout: WorkspaceLayout,
) -> StaticConfig {
let mut locations = if let Some(base) = xdg {
vec![
base.get_config_file("cosmic-comp.ron"),
base.get_config_file("cosmic-comp/config.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"));
// Source key bindings from com.system76.CosmicSettings.Shortcuts
let settings_context = shortcuts::context().unwrap();
let system_actions = shortcuts::system_actions(&config);
let mut shortcuts = shortcuts::shortcuts(&settings_context);
for path in locations {
debug!("Trying config location: {}", path.display());
if path.exists() {
info!("Using config at {}", path.display());
let Ok(file) = OpenOptions::new().read(true).open(path) else {
error!("Failed to open config file.");
continue;
};
match ron::de::from_reader::<_, StaticConfig>(file) {
Ok(mut config) => {
key_bindings::add_default_bindings(
&mut config.key_bindings,
workspace_layout,
);
return config;
// Add any missing default shortcuts recommended by the compositor.
key_bindings::add_default_bindings(&mut shortcuts, workspace.workspace_layout);
// Listen for updates to the keybindings config.
let source = cosmic_config::calloop::ConfigWatchSource::new(&settings_context).expect(
"failed to create config watch source for com.system76.CosmicSettings.Shortcuts",
);
_ = loop_handle.insert_source(source, |(config, keys), (), state| {
for key in keys {
match key.as_str() {
// Reload the keyboard shortcuts config.
"custom" | "defaults" => {
let mut shortcuts = shortcuts::shortcuts(&config);
let layout =
get_config::<WorkspaceConfig>(&config, "workspaces").workspace_layout;
key_bindings::add_default_bindings(&mut shortcuts, layout);
state.common.config.shortcuts = shortcuts;
}
Err(err) => {
error!("Malformed config file (skipping): {}", err);
continue;
"system_actions" => {
state.common.config.system_actions = shortcuts::system_actions(&config);
}
_ => (),
}
}
}
info!("No config found, consider installing a config file. Using default mapping.");
let mut config = ron::from_str(include_str!("../../config.ron")).unwrap_or_else(|err| {
debug!("Failed to load internal default config: {}", err);
StaticConfig {
// Small useful keybindings by default
key_bindings: HashMap::new(),
data_control_enabled: false,
}
});
key_bindings::add_default_bindings(&mut config.key_bindings, workspace_layout);
config
Config {
dynamic_conf: Self::load_dynamic(xdg.as_ref()),
cosmic_conf: cosmic_comp_config,
cosmic_helper: config,
settings_context,
shortcuts,
system_actions,
}
}
fn load_dynamic(xdg: Option<&xdg::BaseDirectories>) -> DynamicConfig {
@ -314,6 +259,10 @@ impl Config {
}
}
pub fn shortcut_for_action(&self, action: &shortcuts::Action) -> Option<String> {
self.shortcuts.shortcut_for_action(action)
}
pub fn read_outputs(
&mut self,
output_state: &mut OutputConfigurationState<State>,

View file

@ -1,15 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
#![allow(non_snake_case)]
use super::{KeyModifier, KeyModifiers};
use serde::{Deserialize, Serialize};
use smithay::reexports::x11rb::NO_SYMBOL;
pub use smithay::{
input::keyboard::{Keysym, XkbConfig as WlXkbConfig},
utils::Transform,
};
use tracing::warn;
use xkbcommon::xkb;
pub use smithay::{input::keyboard::XkbConfig as WlXkbConfig, utils::Transform};
#[derive(Serialize, Deserialize)]
#[serde(remote = "Transform")]
@ -23,62 +16,3 @@ pub enum TransformDef {
Flipped180,
Flipped270,
}
#[derive(Deserialize)]
#[serde(transparent)]
pub struct KeyModifiersDef(Vec<KeyModifier>);
impl From<KeyModifiersDef> for KeyModifiers {
fn from(src: KeyModifiersDef) -> Self {
src.0.into_iter().fold(
KeyModifiers {
ctrl: false,
alt: false,
shift: false,
logo: false,
},
|mut modis, modi: KeyModifier| {
modis += modi;
modis
},
)
}
}
#[allow(non_snake_case)]
pub fn deserialize_KeyModifiers<'de, D>(deserializer: D) -> Result<KeyModifiers, D::Error>
where
D: serde::Deserializer<'de>,
{
KeyModifiersDef::deserialize(deserializer).map(Into::into)
}
#[allow(non_snake_case)]
pub fn deserialize_Keysym<'de, D>(deserializer: D) -> Result<Option<Keysym>, D::Error>
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) {
x if x.raw() == NO_SYMBOL => {
match xkb::keysym_from_name(&name, xkb::KEYSYM_CASE_INSENSITIVE) {
x if x.raw() == NO_SYMBOL => Err(<D::Error as Error>::invalid_value(
Unexpected::Str(&name),
&"One of the keysym names of xkbcommon.h without the 'KEY_' prefix",
)),
x => {
warn!(
"Key-Binding '{}' only matched case insensitive for {:?}",
name,
xkb::keysym_get_name(x)
);
Ok(Some(x))
}
}
}
x => Ok(Some(x)),
}
}