// SPDX-License-Identifier: GPL-3.0-only use crate::{ shell::{Shell, WorkspaceAmount}, state::{BackendData, Data, State}, wayland::protocols::output_configuration::OutputConfigurationState, }; use cosmic_config::ConfigGet; use serde::{Deserialize, Serialize}; use smithay::input::Seat; pub use smithay::{ backend::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}, }; use std::{cell::RefCell, collections::HashMap, fs::OpenOptions, path::PathBuf}; use tracing::{debug, error, info, warn}; mod input_config; mod key_bindings; pub use key_bindings::{Action, KeyModifier, KeyModifiers, KeyPattern}; mod types; pub use self::types::*; use cosmic_comp_config::{input::InputConfig, XkbConfig}; #[derive(Debug)] pub struct Config { pub static_conf: StaticConfig, pub dynamic_conf: DynamicConfig, pub config: cosmic_config::Config, pub xkb: XkbConfig, pub input_default: InputConfig, pub input_touchpad: InputConfig, pub input_devices: HashMap, } #[derive(Debug, Deserialize)] pub struct StaticConfig { pub key_bindings: HashMap, pub workspace_mode: WorkspaceMode, pub workspace_amount: WorkspaceAmount, #[serde(default = "default_workspace_layout")] pub workspace_layout: WorkspaceLayout, pub tiling_enabled: bool, #[serde(default = "default_active_hint")] pub active_hint: u8, #[serde(default = "default_gaps")] pub gaps: (u8, u8), } #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum WorkspaceMode { OutputBound, Global, } #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum WorkspaceLayout { Vertical, Horizontal, } #[derive(Debug)] pub struct DynamicConfig { outputs: (Option, OutputsConfig), } #[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, } } } fn default_enabled() -> bool { true } fn default_active_hint() -> u8 { 4 } fn default_gaps() -> (u8, u8) { (0, 4) } fn default_workspace_layout() -> WorkspaceLayout { WorkspaceLayout::Vertical } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct OutputConfig { pub mode: ((i32, i32), Option), pub vrr: bool, pub scale: f64, #[serde(with = "TransformDef")] pub transform: Transform, pub position: (i32, i32), #[serde(default = "default_enabled")] pub enabled: bool, #[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: false, scale: 1.0, transform: Transform::Normal, position: (0, 0), enabled: true, 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 output_mode(&self) -> Mode { Mode { size: self.mode_size(), refresh: self.mode_refresh() as i32, } } } impl Config { pub fn load(loop_handle: &LoopHandle<'_, Data>) -> 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), (), shared_data| { config_changed(config, keys, &mut shared_data.state); }) .expect("Failed to add cosmic-config to the event loop"); let xdg = xdg::BaseDirectories::new().ok(); Config { static_conf: Self::load_static(xdg.as_ref()), dynamic_conf: Self::load_dynamic(xdg.as_ref()), xkb: get_config(&config, "xkb-config"), input_default: get_config(&config, "input-default"), input_touchpad: get_config(&config, "input-touchpad"), input_devices: get_config(&config, "input-devices"), config, } } fn load_static(xdg: Option<&xdg::BaseDirectories>) -> 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")); for path in locations { debug!("Trying config location: {}", path.display()); if path.exists() { info!("Using config at {}", path.display()); let mut config: StaticConfig = ron::de::from_reader(OpenOptions::new().read(true).open(path).unwrap()) .expect("Malformed config file"); key_bindings::add_default_bindings( &mut config.key_bindings, config.workspace_layout, ); return config; } } StaticConfig { key_bindings: HashMap::new(), workspace_mode: WorkspaceMode::Global, workspace_amount: WorkspaceAmount::Dynamic, workspace_layout: WorkspaceLayout::Vertical, tiling_enabled: false, active_hint: default_active_hint(), gaps: default_gaps(), } } 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); DynamicConfig { outputs: (output_path, outputs), } } fn load_outputs(path: &Option) -> OutputsConfig { if let Some(path) = path.as_ref() { if path.exists() { match ron::de::from_reader(OpenOptions::new().read(true).open(path).unwrap()) { Ok(config) => 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(), } } pub fn read_outputs( &mut self, output_state: &mut OutputConfigurationState, backend: &mut BackendData, shell: &mut Shell, seats: impl Iterator>, loop_handle: &LoopHandle<'_, Data>, ) { let seats = seats.collect::>(); 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 mut reset = false; let known_good_configs = outputs .iter() .map(|output| { output .user_data() .get::>() .unwrap() .borrow() .clone() }) .collect::>(); 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; *output .user_data() .get::>() .unwrap() .borrow_mut() = output_config; if let Err(err) = backend.apply_config_for_output( &output, false, shell, seats.iter().cloned(), loop_handle, ) { warn!( ?err, "Failed to set new config for output {}.", output.name(), ); reset = true; break; } else { if enabled { output_state.enable_head(&output); } else { output_state.disable_head(&output); } } } if reset { for (output, output_config) in outputs .clone() .into_iter() .zip(known_good_configs.into_iter()) { let enabled = output_config.enabled; *output .user_data() .get::>() .unwrap() .borrow_mut() = output_config; if let Err(err) = backend.apply_config_for_output( &output, false, shell, seats.iter().cloned(), loop_handle, ) { error!(?err, "Failed to reset config for output {}.", output.name()); } else { if enabled { output_state.enable_head(&output); } else { output_state.disable_head(&output); } } } } output_state.update(); self.write_outputs(output_state.outputs()); } else { for output in outputs { if let Err(err) = backend.apply_config_for_output( &output, false, shell, seats.iter().cloned(), loop_handle, ) { warn!( ?err, "Failed to set new config for output {}.", output.name(), ); } else { if output .user_data() .get::>() .unwrap() .borrow() .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.xkb.clone() } pub fn read_device(&self, device: &mut InputDevice) { let default_config = if device.config_tap_finger_count() > 0 { &self.input_touchpad } else { &self.input_default }; let device_config = self.input_devices.get(device.name()); input_config::update_device(device, 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 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) = ron::ser::to_writer_pretty(writer, &self.1, Default::default()) { warn!(?err, "Failed to persist {}", path.display()); } } } } impl DynamicConfig { pub fn outputs(&self) -> &OutputsConfig { &self.outputs.1 } pub fn outputs_mut<'a>(&'a mut self) -> PersistenceGuard<'a, OutputsConfig> { PersistenceGuard(self.outputs.0.clone(), &mut self.outputs.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); } } } 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"); for seat in state.common.seats().cloned().collect::>().iter() { if let Some(keyboard) = seat.get_keyboard() { 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? } } } state.common.config.xkb = value; } "input-default" => { let value = get_config::(&config, "input-default"); state.common.config.input_default = value; update_input(state); } "input-touchpad" => { let value = get_config::(&config, "input-touchpad"); state.common.config.input_touchpad = value; update_input(state); } "input-devices" => { let value = get_config::>(&config, "input-devices"); state.common.config.input_devices = value; update_input(state); } _ => {} } } } 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(), } }