// SPDX-License-Identifier: GPL-3.0-only use crate::{ shell::Shell, state::{BackendData, State}, utils::prelude::OutputExt, wayland::protocols::{ output_configuration::OutputConfigurationState, workspace::WorkspaceUpdateGuard, }, }; use cosmic_config::{ConfigGet, CosmicConfigEntry}; use cosmic_settings_config::window_rules::ApplicationException; use cosmic_settings_config::{shortcuts, window_rules, Shortcuts}; use serde::{Deserialize, Serialize}; use smithay::utils::{Clock, Monotonic}; use smithay::wayland::xdg_activation::XdgActivationState; pub use smithay::{ backend::input::{self as smithay_input, KeyState}, input::keyboard::{keysyms as KeySyms, Keysym, ModifiersState}, output::{Mode, Output}, reexports::{ calloop::LoopHandle, input::{ AccelProfile, ClickMethod, Device as InputDevice, ScrollMethod, SendEventsMode, TapButtonMap, }, }, utils::{Logical, Physical, Point, Size, Transform, SERIAL_COUNTER}, }; use std::{ cell::RefCell, collections::{BTreeMap, HashMap}, fs::OpenOptions, io::Write, path::PathBuf, sync::{atomic::AtomicBool, Arc, RwLock}, }; use tracing::{error, warn}; mod input_config; pub mod key_bindings; pub use key_bindings::{Action, PrivateAction}; mod types; pub use self::types::*; use cosmic::config::CosmicTk; use cosmic_comp_config::{ input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, }; #[derive(Debug)] pub struct Config { pub dynamic_conf: DynamicConfig, pub cosmic_helper: cosmic_config::Config, /// cosmic-config comp configuration for `com.system76.CosmicComp` pub cosmic_conf: CosmicCompConfig, /// cosmic-config context for `com.system76.CosmicSettings.Shortcuts` pub settings_context: cosmic_config::Config, /// Key bindings from `com.system76.CosmicSettings.Shortcuts` pub shortcuts: Shortcuts, // Tiling exceptions from `com.system76.CosmicSettings.WindowRules` pub tiling_exceptions: Vec, /// System actions from `com.system76.CosmicSettings.Shortcuts` pub system_actions: BTreeMap, } #[derive(Debug)] pub struct DynamicConfig { outputs: (Option, OutputsConfig), numlock: (Option, NumlockStateConfig), accessibility_filter: (Option, ScreenFilter), } #[derive(Debug, Deserialize, Serialize)] pub struct OutputsConfig { pub config: HashMap, Vec>, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OutputInfo { pub connector: String, pub make: String, pub model: String, } impl From for OutputInfo { fn from(o: Output) -> OutputInfo { let physical = o.physical_properties(); OutputInfo { connector: o.name(), make: physical.make, model: physical.model, } } } #[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct EdidProduct { pub manufacturer: [char; 3], pub product: u16, pub serial: Option, pub manufacture_week: i32, pub manufacture_year: i32, pub model_year: Option, } impl From for EdidProduct { fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { Self { manufacturer: vp.manufacturer, product: vp.product, serial: vp.serial, manufacture_week: vp.manufacture_week, manufacture_year: vp.manufacture_year, model_year: vp.model_year, } } } #[derive(Default, Debug, Deserialize, Serialize)] pub struct NumlockStateConfig { pub last_state: bool, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum OutputState { #[serde(rename = "true")] Enabled, #[serde(rename = "false")] Disabled, Mirroring(String), } fn default_state() -> OutputState { OutputState::Enabled } #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] #[serde(rename_all = "lowercase")] pub enum AdaptiveSync { #[serde(rename = "true")] Enabled, #[serde(rename = "false")] Disabled, Force, } fn default_sync() -> AdaptiveSync { AdaptiveSync::Enabled } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct OutputConfig { pub mode: ((i32, i32), Option), #[serde(default = "default_sync")] pub vrr: AdaptiveSync, pub scale: f64, #[serde(with = "TransformDef")] pub transform: Transform, pub position: (u32, u32), #[serde(default = "default_state")] pub enabled: OutputState, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_bpc: Option, } impl Default for OutputConfig { fn default() -> OutputConfig { OutputConfig { mode: ((0, 0), None), vrr: AdaptiveSync::Enabled, scale: 1.0, transform: Transform::Normal, position: (0, 0), enabled: OutputState::Enabled, max_bpc: None, } } } impl OutputConfig { pub fn mode_size(&self) -> Size { self.mode.0.into() } pub fn mode_refresh(&self) -> u32 { self.mode.1.unwrap_or(60_000) } pub fn transformed_size(&self) -> Size { self.transform.transform_size(self.mode_size()) } pub fn output_mode(&self) -> Mode { Mode { size: self.mode_size(), refresh: self.mode_refresh() as i32, } } } #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] pub struct ScreenFilter { pub inverted: bool, #[serde(skip_serializing_if = "Option::is_none")] pub color_filter: Option, } impl ScreenFilter { pub fn is_noop(&self) -> bool { self.inverted == false && self.color_filter.is_none() } } #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] // these values need to match with offscreen.frag pub enum ColorFilter { Greyscale = 1, Protanopia = 2, Deuteranopia = 3, Tritanopia = 4, } impl Config { pub fn load(loop_handle: &LoopHandle<'_, State>) -> Config { let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap(); let source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap(); loop_handle .insert_source(source, |(config, keys), (), state| { config_changed(config, keys, state); }) .expect("Failed to add cosmic-config to the event loop"); let xdg = xdg::BaseDirectories::new().ok(); let cosmic_comp_config = CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| { for err in errs { error!(?err, ""); } c }); // Listen for updates to the toolkit config if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) { fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) { let mut workspace_guard = state.common.workspace_state.update(); state.common.shell.write().unwrap().update_toolkit( config, &state.common.xdg_activation_state, &mut workspace_guard, ); } if let Ok(config) = CosmicTk::get_entry(&tk_config) { let _ = loop_handle.insert_idle(move |state| { handle_new_toolkit_config(config, state); }); } match cosmic_config::calloop::ConfigWatchSource::new(&tk_config) { Ok(source) => { if let Err(err) = loop_handle.insert_source(source, |(config, _keys), (), state| { if let Ok(config) = CosmicTk::get_entry(&config) { handle_new_toolkit_config(config, state); } }) { warn!(?err, "Failed to watch com.system76.CosmicTk config"); } } Err(err) => warn!( ?err, "failed to create config watch source for com.system76.CosmicTk" ), } } // Source key bindings from com.system76.CosmicSettings.Shortcuts let settings_context = shortcuts::context().expect("Failed to load shortcuts config"); let system_actions = shortcuts::system_actions(&settings_context); let shortcuts = shortcuts::shortcuts(&settings_context); // Listen for updates to the keybindings config. match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) { Ok(source) => { if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| { for key in keys { match key.as_str() { // Reload the keyboard shortcuts config. "custom" | "defaults" => { state.common.config.shortcuts = shortcuts::shortcuts(&config); } "system_actions" => { state.common.config.system_actions = shortcuts::system_actions(&config); } _ => (), } } }) { warn!( ?err, "Failed to watch com.system76.CosmicSettings.Shortcuts config" ); } } Err(err) => warn!( ?err, "failed to create config watch source for com.system76.CosmicSettings.Shortcuts" ), }; let window_rules_context = window_rules::context().expect("Failed to load window rules config"); let tiling_exceptions = window_rules::tiling_exceptions(&window_rules_context); match cosmic_config::calloop::ConfigWatchSource::new(&window_rules_context) { Ok(source) => { if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| { for key in keys { match key.as_str() { "tiling_exception_defaults" | "tiling_exception_custom" => { let new_exceptions = window_rules::tiling_exceptions(&config); state.common.config.tiling_exceptions = new_exceptions; state .common .shell .write() .unwrap() .update_tiling_exceptions( state.common.config.tiling_exceptions.iter(), ); } _ => (), } } }) { warn!( ?err, "Failed to watch com.system76.CosmicSettings.WindowRules config" ); } } Err(err) => warn!( ?err, "failed to create config watch source for com.system76.CosmicSettings.WindowRules" ), }; let _ = loop_handle.insert_idle(|state| { let filter_conf = state.common.config.dynamic_conf.screen_filter(); state .common .a11y_state .set_screen_inverted(filter_conf.inverted); state .common .a11y_state .set_screen_filter(filter_conf.color_filter); }); Config { dynamic_conf: Self::load_dynamic(xdg.as_ref()), cosmic_conf: cosmic_comp_config, cosmic_helper: config, settings_context, shortcuts, system_actions, tiling_exceptions, } } fn load_dynamic(xdg: Option<&xdg::BaseDirectories>) -> DynamicConfig { let output_path = xdg.and_then(|base| base.place_state_file("cosmic-comp/outputs.ron").ok()); let outputs = Self::load_outputs(&output_path); let numlock_path = xdg.and_then(|base| base.place_state_file("cosmic-comp/numlock.ron").ok()); let numlock = Self::load_numlock(&numlock_path); let filter_path = xdg.and_then(|base| { base.place_state_file("cosmic-comp/a11y_screen_filter.ron") .ok() }); let filter = Self::load_filter_state(&filter_path); DynamicConfig { outputs: (output_path, outputs), numlock: (numlock_path, numlock), accessibility_filter: (filter_path, filter), } } fn load_outputs(path: &Option) -> OutputsConfig { if let Some(path) = path.as_ref() { if path.exists() { match ron::de::from_reader::<_, OutputsConfig>( OpenOptions::new().read(true).open(path).unwrap(), ) { Ok(mut config) => { for (info, config) in config.config.iter_mut() { let config_clone = config.clone(); for conf in config.iter_mut() { if let OutputState::Mirroring(conn) = &conf.enabled { if let Some((j, _)) = info .iter() .enumerate() .find(|(_, info)| &info.connector == conn) { if config_clone[j].enabled != OutputState::Enabled { warn!("Invalid Mirroring tag, overriding with `Enabled` instead"); conf.enabled = OutputState::Enabled; } } else { warn!("Invalid Mirroring tag, overriding with `Enabled` instead"); conf.enabled = OutputState::Enabled; } } } } return config; } Err(err) => { warn!(?err, "Failed to read output_config, resetting.."); if let Err(err) = std::fs::remove_file(path) { error!(?err, "Failed to remove output_config."); } } }; } } OutputsConfig { config: HashMap::new(), } } fn load_numlock(path: &Option) -> NumlockStateConfig { path.as_deref() .filter(|path| path.exists()) .and_then(|path| { ron::de::from_reader::<_, NumlockStateConfig>( OpenOptions::new().read(true).open(path).unwrap(), ) .map_err(|err| { warn!(?err, "Failed to read numlock.ron, resetting.."); if let Err(err) = std::fs::remove_file(path) { error!(?err, "Failed to remove numlock.ron."); } }) .ok() }) .unwrap_or_default() } fn load_filter_state(path: &Option) -> ScreenFilter { if let Some(path) = path.as_ref() { if path.exists() { match ron::de::from_reader::<_, ScreenFilter>( OpenOptions::new().read(true).open(path).unwrap(), ) { Ok(config) => return config, Err(err) => { warn!(?err, "Failed to read screen_filter state, resetting.."); if let Err(err) = std::fs::remove_file(path) { error!(?err, "Failed to remove screen_filter state."); } } }; } } ScreenFilter { inverted: false, color_filter: None, } } pub fn shortcut_for_action(&self, action: &shortcuts::Action) -> Option { self.shortcuts.shortcut_for_action(action) } pub fn read_outputs( &mut self, output_state: &mut OutputConfigurationState, backend: &mut BackendData, shell: &Arc>, loop_handle: &LoopHandle<'static, State>, workspace_state: &mut WorkspaceUpdateGuard<'_, State>, xdg_activation_state: &XdgActivationState, startup_done: Arc, clock: &Clock, ) { let outputs = output_state.outputs().collect::>(); let mut infos = outputs .iter() .cloned() .map(Into::::into) .collect::>(); infos.sort(); if let Some(configs) = self.dynamic_conf.outputs().config.get(&infos).cloned() { let known_good_configs = outputs .iter() .map(|output| { output .user_data() .get::>() .unwrap() .borrow() .clone() }) .collect::>(); let mut found_outputs = Vec::new(); for (name, output_config) in infos.iter().map(|o| &o.connector).zip(configs.into_iter()) { let output = outputs.iter().find(|o| &o.name() == name).unwrap().clone(); let enabled = output_config.enabled.clone(); *output .user_data() .get::>() .unwrap() .borrow_mut() = output_config; found_outputs.push((output.clone(), enabled)); } if let Err(err) = backend.apply_config_for_outputs( false, loop_handle, self.dynamic_conf.screen_filter(), shell.clone(), workspace_state, xdg_activation_state, startup_done.clone(), clock, ) { warn!(?err, "Failed to set new config."); found_outputs.clear(); for (output, output_config) in outputs .clone() .into_iter() .zip(known_good_configs.into_iter()) { let enabled = output_config.enabled.clone(); *output .user_data() .get::>() .unwrap() .borrow_mut() = output_config; found_outputs.push((output.clone(), enabled)); } if let Err(err) = backend.apply_config_for_outputs( false, loop_handle, self.dynamic_conf.screen_filter(), shell.clone(), workspace_state, xdg_activation_state, startup_done, clock, ) { error!(?err, "Failed to reset config."); } else { for (output, enabled) in found_outputs { if enabled == OutputState::Enabled { output_state.enable_head(&output); } else { output_state.disable_head(&output); } } } } else { for (output, enabled) in found_outputs { if enabled == OutputState::Enabled { output_state.enable_head(&output); } else { output_state.disable_head(&output); } } } output_state.update(); self.write_outputs(output_state.outputs()); } else { // we don't have a config, so lets generate somewhat sane positions let mut w = 0; for output in outputs.iter().filter(|o| o.mirroring().is_none()) { { let mut config = output.config_mut(); config.position = (w, 0); } w += output.geometry().size.w as u32; } if let Err(err) = backend.apply_config_for_outputs( false, loop_handle, self.dynamic_conf.screen_filter(), shell.clone(), workspace_state, xdg_activation_state, startup_done, clock, ) { warn!(?err, "Failed to set new config.",); } else { for output in outputs { if output .user_data() .get::>() .unwrap() .borrow() .enabled == OutputState::Enabled { output_state.enable_head(&output); } else { output_state.disable_head(&output); } } } output_state.update(); self.write_outputs(output_state.outputs()); } } pub fn write_outputs( &mut self, outputs: impl Iterator>, ) { let mut infos = outputs .map(|o| { let o = o.borrow(); ( Into::::into(o.clone()), o.user_data() .get::>() .unwrap() .borrow() .clone(), ) }) .collect::>(); infos.sort_by(|&(ref a, _), &(ref b, _)| a.cmp(b)); let (infos, configs) = infos.into_iter().unzip(); self.dynamic_conf .outputs_mut() .config .insert(infos, configs); } pub fn xkb_config(&self) -> XkbConfig { self.cosmic_conf.xkb_config.clone() } pub fn read_device(&self, device: &mut InputDevice) { let (device_config, default_config) = self.get_device_config(device); input_config::update_device(device, device_config, default_config); } pub fn scroll_factor(&self, device: &InputDevice) -> f64 { let (device_config, default_config) = self.get_device_config(device); input_config::get_config(device_config, default_config, |x| { x.scroll_config.as_ref()?.scroll_factor }) .map_or(1.0, |x| x.0) } pub fn map_to_output(&self, device: &InputDevice) -> Option<&str> { let (device_config, default_config) = self.get_device_config(device); Some( input_config::get_config(device_config, default_config, |x| { x.map_to_output.as_deref() })? .0, ) } fn get_device_config(&self, device: &InputDevice) -> (Option<&InputConfig>, &InputConfig) { let default_config = if device.config_tap_finger_count() > 0 { &self.cosmic_conf.input_touchpad } else { &self.cosmic_conf.input_default }; let device_config = self.cosmic_conf.input_devices.get(device.name()); (device_config, default_config) } } pub struct PersistenceGuard<'a, T: Serialize>(Option, &'a mut T); impl<'a, T: Serialize> std::ops::Deref for PersistenceGuard<'a, T> { type Target = T; fn deref(&self) -> &T { &self.1 } } impl<'a, T: Serialize> std::ops::DerefMut for PersistenceGuard<'a, T> { fn deref_mut(&mut self) -> &mut T { &mut self.1 } } impl<'a, T: Serialize> Drop for PersistenceGuard<'a, T> { fn drop(&mut self) { if let Some(path) = self.0.as_ref() { let content = match ron::ser::to_string_pretty(&self.1, Default::default()) { Ok(content) => content, Err(err) => { warn!("Failed to serialize: {:?}", err); return; } }; let mut writer = match OpenOptions::new() .create(true) .truncate(true) .write(true) .open(path) { Ok(writer) => writer, Err(err) => { warn!(?err, "Failed to persist {}.", path.display()); return; } }; if let Err(err) = writer.write_all(content.as_bytes()) { warn!(?err, "Failed to persist {}", path.display()); } else { let _ = writer.flush(); } } } } impl DynamicConfig { pub fn outputs(&self) -> &OutputsConfig { &self.outputs.1 } pub fn outputs_mut(&mut self) -> PersistenceGuard<'_, OutputsConfig> { PersistenceGuard(self.outputs.0.clone(), &mut self.outputs.1) } pub fn numlock(&self) -> &NumlockStateConfig { &self.numlock.1 } pub fn numlock_mut(&mut self) -> PersistenceGuard<'_, NumlockStateConfig> { PersistenceGuard(self.numlock.0.clone(), &mut self.numlock.1) } pub fn screen_filter(&self) -> &ScreenFilter { &self.accessibility_filter.1 } pub fn screen_filter_mut(&mut self) -> PersistenceGuard<'_, ScreenFilter> { PersistenceGuard( self.accessibility_filter.0.clone(), &mut self.accessibility_filter.1, ) } } fn get_config( config: &cosmic_config::Config, key: &str, ) -> T { config.get(key).unwrap_or_else(|err| { error!(?err, "Failed to read config '{}'", key); T::default() }) } fn update_input(state: &mut State) { if let BackendData::Kms(ref mut kms_state) = &mut state.backend { for device in kms_state.input_devices.values_mut() { state.common.config.read_device(device); } } } pub fn change_modifier_state( keyboard: &smithay::input::keyboard::KeyboardHandle, scan_code: u32, state: &mut State, ) { /// Offset used to convert Linux scancode to X11 keycode. const X11_KEYCODE_OFFSET: u32 = 8; let mut input = |key_state, scan_code| { let time = state.common.clock.now().as_millis(); let _ = keyboard.input( state, smithay_input::Keycode::new(scan_code + X11_KEYCODE_OFFSET), key_state, SERIAL_COUNTER.next_serial(), time, |_, _, _| smithay::input::keyboard::FilterResult::<()>::Forward, ); }; input(smithay_input::KeyState::Pressed, scan_code); input(smithay_input::KeyState::Released, scan_code); } fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut State) { for key in &keys { match key.as_str() { "xkb_config" => { let value = get_config::(&config, "xkb_config"); let seats = state .common .shell .read() .unwrap() .seats .iter() .cloned() .collect::>(); for seat in seats.into_iter() { if let Some(keyboard) = seat.get_keyboard() { let old_modifier_state = keyboard.modifier_state(); keyboard.change_repeat_info( (value.repeat_rate as i32).abs(), // Negative values are illegal (value.repeat_delay as i32).abs(), ); if let Err(err) = keyboard.set_xkb_config(state, xkb_config_to_wl(&value)) { error!(?err, "Failed to load provided xkb config"); // TODO Revert to default? } // Press and release the numlock key to update modifiers. if old_modifier_state.num_lock != keyboard.modifier_state().num_lock { const NUMLOCK_SCANCODE: u32 = 69; change_modifier_state(&keyboard, NUMLOCK_SCANCODE, state); } if old_modifier_state.caps_lock != keyboard.modifier_state().caps_lock { const CAPSLOCK_SCANCODE: u32 = 58; change_modifier_state(&keyboard, CAPSLOCK_SCANCODE, state); } } } state.common.atspi_ei.update_keymap(value.clone()); state.common.config.cosmic_conf.xkb_config = value; } "keyboard_config" => { let value = get_config::(&config, "keyboard_config"); state.common.config.cosmic_conf.keyboard_config = value; let shell = state.common.shell.read().unwrap(); let seat = shell.seats.last_active(); state.common.config.dynamic_conf.numlock_mut().last_state = seat.get_keyboard().unwrap().modifier_state().num_lock; } "input_default" => { let value = get_config::(&config, "input_default"); state.common.config.cosmic_conf.input_default = value; update_input(state); } "input_touchpad" => { let value = get_config::(&config, "input_touchpad"); state.common.config.cosmic_conf.input_touchpad = value; update_input(state); } "input_devices" => { let value = get_config::>(&config, "input_devices"); state.common.config.cosmic_conf.input_devices = value; update_input(state); } "workspaces" => { state.common.config.cosmic_conf.workspaces = get_config::(&config, "workspaces"); state.common.update_config(); } "autotile" => { let new = get_config::(&config, "autotile"); if new != state.common.config.cosmic_conf.autotile { state.common.config.cosmic_conf.autotile = new; let mut shell = state.common.shell.write().unwrap(); let shell_ref = &mut *shell; shell_ref.workspaces.update_autotile( new, &mut state.common.workspace_state.update(), shell_ref.seats.iter(), ); } } "autotile_behavior" => { let new = get_config::(&config, "autotile_behavior"); if new != state.common.config.cosmic_conf.autotile_behavior { state.common.config.cosmic_conf.autotile_behavior = new; let mut shell = state.common.shell.write().unwrap(); let shell_ref = &mut *shell; shell_ref.workspaces.update_autotile_behavior( new, &mut state.common.workspace_state.update(), shell_ref.seats.iter(), ); } } "active_hint" => { let new = get_config::(&config, "active_hint"); if new != state.common.config.cosmic_conf.active_hint { state.common.config.cosmic_conf.active_hint = new; state.common.update_config(); } } "descale_xwayland" => { let new = get_config::(&config, "descale_xwayland"); if new != state.common.config.cosmic_conf.descale_xwayland { state.common.config.cosmic_conf.descale_xwayland = new; state.common.update_xwayland_scale(); } } "xwayland_eavesdropping" => { let new = get_config::(&config, "xwayland_eavesdropping"); if new != state.common.config.cosmic_conf.xwayland_eavesdropping { state.common.config.cosmic_conf.xwayland_eavesdropping = new; state .common .xwayland_reset_eavesdropping(SERIAL_COUNTER.next_serial()); } } "focus_follows_cursor" => { let new = get_config::(&config, "focus_follows_cursor"); if new != state.common.config.cosmic_conf.focus_follows_cursor { state.common.config.cosmic_conf.focus_follows_cursor = new; } } "cursor_follows_focus" => { let new = get_config::(&config, "cursor_follows_focus"); if new != state.common.config.cosmic_conf.cursor_follows_focus { state.common.config.cosmic_conf.cursor_follows_focus = new; } } "focus_follows_cursor_delay" => { let new = get_config::(&config, "focus_follows_cursor_delay"); if new != state.common.config.cosmic_conf.focus_follows_cursor_delay { state.common.config.cosmic_conf.focus_follows_cursor_delay = new; } } "edge_snap_threshold" => { let new = get_config::(&config, "edge_snap_threshold"); if new != state.common.config.cosmic_conf.edge_snap_threshold { state.common.config.cosmic_conf.edge_snap_threshold = new; } } "accessibility_zoom" => { let new = get_config::(&config, "accessibility_zoom"); if new != state.common.config.cosmic_conf.accessibility_zoom { state.common.config.cosmic_conf.accessibility_zoom = new; state.common.update_config(); } } _ => {} } } } pub fn xkb_config_to_wl(config: &XkbConfig) -> WlXkbConfig<'_> { WlXkbConfig { rules: &config.rules, model: &config.model, layout: &config.layout, variant: &config.variant, options: config.options.clone(), } }