2022-04-25 23:00:30 +02:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
|
|
|
|
use crate::{
|
2026-06-05 10:42:19 -06:00
|
|
|
input::InputBackendId,
|
2023-09-07 13:28:08 -07:00
|
|
|
shell::Shell,
|
2023-09-29 21:33:16 +02:00
|
|
|
state::{BackendData, State},
|
2025-01-06 19:47:12 +01:00
|
|
|
utils::prelude::OutputExt,
|
2024-04-10 15:49:08 +02:00
|
|
|
wayland::protocols::{
|
|
|
|
|
output_configuration::OutputConfigurationState, workspace::WorkspaceUpdateGuard,
|
|
|
|
|
},
|
2022-04-25 23:00:30 +02:00
|
|
|
};
|
2025-09-10 18:25:33 +02:00
|
|
|
use anyhow::Context;
|
2024-02-08 14:25:18 -05:00
|
|
|
use cosmic_config::{ConfigGet, CosmicConfigEntry};
|
2024-09-04 18:35:09 +03:00
|
|
|
use cosmic_settings_config::window_rules::ApplicationException;
|
2025-10-16 18:53:57 +02:00
|
|
|
use cosmic_settings_config::{Shortcuts, shortcuts, window_rules};
|
2022-04-25 23:00:30 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-02-12 05:35:22 -08:00
|
|
|
use smithay::utils::{Clock, Monotonic};
|
|
|
|
|
use smithay::wayland::xdg_activation::XdgActivationState;
|
2022-04-25 23:00:30 +02:00
|
|
|
pub use smithay::{
|
2025-02-12 12:13:54 +02:00
|
|
|
backend::input::{self as smithay_input, KeyState},
|
2025-10-16 18:53:57 +02:00
|
|
|
input::keyboard::{Keysym, ModifiersState, keysyms as KeySyms},
|
2022-09-09 20:00:00 -07:00
|
|
|
output::{Mode, Output},
|
2022-05-12 14:04:21 +02:00
|
|
|
reexports::{
|
|
|
|
|
calloop::LoopHandle,
|
|
|
|
|
input::{
|
|
|
|
|
AccelProfile, ClickMethod, Device as InputDevice, ScrollMethod, SendEventsMode,
|
|
|
|
|
TapButtonMap,
|
|
|
|
|
},
|
2022-05-03 13:37:51 +02:00
|
|
|
},
|
2025-10-16 18:53:57 +02:00
|
|
|
utils::{Logical, Physical, Point, SERIAL_COUNTER, Size, Transform},
|
2022-04-25 23:00:30 +02:00
|
|
|
};
|
2024-06-07 19:10:13 +02:00
|
|
|
use std::{
|
2025-08-19 16:51:37 -04:00
|
|
|
cell::{Ref, RefCell},
|
2024-04-03 16:02:27 +02:00
|
|
|
collections::{BTreeMap, HashMap},
|
2024-06-07 19:10:13 +02:00
|
|
|
fs::OpenOptions,
|
2025-01-22 19:28:56 +01:00
|
|
|
io::Write,
|
2024-06-07 19:10:13 +02:00
|
|
|
path::PathBuf,
|
2025-10-16 18:53:57 +02:00
|
|
|
sync::{Arc, atomic::AtomicBool},
|
2024-06-07 19:10:13 +02:00
|
|
|
};
|
2024-04-03 16:02:27 +02:00
|
|
|
use tracing::{error, warn};
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2023-07-27 14:22:46 -07:00
|
|
|
mod input_config;
|
2024-04-03 16:02:27 +02:00
|
|
|
pub mod key_bindings;
|
2022-04-25 23:00:30 +02:00
|
|
|
mod types;
|
2025-08-19 16:51:37 -04:00
|
|
|
|
2024-07-26 16:40:28 +02:00
|
|
|
use cosmic::config::CosmicTk;
|
2025-08-26 19:32:35 -04:00
|
|
|
pub use cosmic_comp_config::EdidProduct;
|
2023-09-07 13:28:08 -07:00
|
|
|
use cosmic_comp_config::{
|
2026-05-22 17:06:49 +02:00
|
|
|
ActivationPolicy, AppearanceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, XkbConfig,
|
|
|
|
|
XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
|
2025-08-20 16:49:08 +02:00
|
|
|
input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride},
|
2025-08-20 17:59:06 -04:00
|
|
|
output::comp::{
|
2025-10-16 18:53:57 +02:00
|
|
|
OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs,
|
2025-08-20 17:59:06 -04:00
|
|
|
},
|
2025-08-20 16:49:08 +02:00
|
|
|
workspace::WorkspaceConfig,
|
2023-09-07 13:28:08 -07:00
|
|
|
};
|
2025-08-19 16:51:37 -04:00
|
|
|
pub use key_bindings::{Action, PrivateAction};
|
|
|
|
|
use types::WlXkbConfig;
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2023-07-12 18:54:53 +02:00
|
|
|
#[derive(Debug)]
|
2022-04-25 23:00:30 +02:00
|
|
|
pub struct Config {
|
|
|
|
|
pub dynamic_conf: DynamicConfig,
|
2024-02-08 14:25:18 -05:00
|
|
|
pub cosmic_helper: cosmic_config::Config,
|
2024-08-14 21:56:20 +03:00
|
|
|
/// cosmic-config comp configuration for `com.system76.CosmicComp`
|
2024-02-08 14:25:18 -05:00
|
|
|
pub cosmic_conf: CosmicCompConfig,
|
2024-04-03 16:02:27 +02:00
|
|
|
/// cosmic-config context for `com.system76.CosmicSettings.Shortcuts`
|
|
|
|
|
pub settings_context: cosmic_config::Config,
|
|
|
|
|
/// Key bindings from `com.system76.CosmicSettings.Shortcuts`
|
|
|
|
|
pub shortcuts: Shortcuts,
|
2024-09-04 18:35:09 +03:00
|
|
|
// Tiling exceptions from `com.system76.CosmicSettings.WindowRules`
|
|
|
|
|
pub tiling_exceptions: Vec<ApplicationException>,
|
2024-04-03 16:02:27 +02:00
|
|
|
/// System actions from `com.system76.CosmicSettings.Shortcuts`
|
|
|
|
|
pub system_actions: BTreeMap<shortcuts::action::System, String>,
|
2023-12-07 19:27:00 +00:00
|
|
|
}
|
|
|
|
|
|
2023-07-12 18:54:53 +02:00
|
|
|
#[derive(Debug)]
|
2022-04-25 23:00:30 +02:00
|
|
|
pub struct DynamicConfig {
|
|
|
|
|
outputs: (Option<PathBuf>, OutputsConfig),
|
2025-02-12 05:35:22 -08:00
|
|
|
numlock: (Option<PathBuf>, NumlockStateConfig),
|
2025-03-19 13:49:28 +01:00
|
|
|
accessibility_filter: (Option<PathBuf>, ScreenFilter),
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 05:35:22 -08:00
|
|
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
|
|
|
|
pub struct NumlockStateConfig {
|
|
|
|
|
pub last_state: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 16:51:37 -04:00
|
|
|
pub struct CompOutputConfig<'a>(pub Ref<'a, OutputConfig>);
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2025-10-16 13:50:32 +02:00
|
|
|
impl CompOutputConfig<'_> {
|
2022-04-25 23:00:30 +02:00
|
|
|
pub fn mode_size(&self) -> Size<i32, Physical> {
|
2025-08-19 16:51:37 -04:00
|
|
|
self.0.mode.0.into()
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn mode_refresh(&self) -> u32 {
|
2025-08-19 16:51:37 -04:00
|
|
|
self.0.mode.1.unwrap_or(60_000)
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-07 12:12:40 -04:00
|
|
|
pub fn transformed_size(&self) -> Size<i32, Physical> {
|
2025-08-19 16:51:37 -04:00
|
|
|
self.transform().transform_size(self.mode_size())
|
2024-10-07 12:12:40 -04:00
|
|
|
}
|
|
|
|
|
|
2022-04-25 23:00:30 +02:00
|
|
|
pub fn output_mode(&self) -> Mode {
|
|
|
|
|
Mode {
|
|
|
|
|
size: self.mode_size(),
|
|
|
|
|
refresh: self.mode_refresh() as i32,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-19 16:51:37 -04:00
|
|
|
|
|
|
|
|
pub fn transform(&self) -> Transform {
|
|
|
|
|
Transform::from(CompTransformDef(self.0.transform))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct CompTransformDef(pub TransformDef);
|
|
|
|
|
|
|
|
|
|
impl From<Transform> for CompTransformDef {
|
|
|
|
|
fn from(transform: Transform) -> Self {
|
|
|
|
|
let def = match transform {
|
|
|
|
|
Transform::Normal => TransformDef::Normal,
|
|
|
|
|
Transform::_90 => TransformDef::_90,
|
|
|
|
|
Transform::_180 => TransformDef::_180,
|
|
|
|
|
Transform::_270 => TransformDef::_270,
|
|
|
|
|
Transform::Flipped => TransformDef::Flipped,
|
|
|
|
|
Transform::Flipped90 => TransformDef::Flipped90,
|
|
|
|
|
Transform::Flipped180 => TransformDef::Flipped180,
|
|
|
|
|
Transform::Flipped270 => TransformDef::Flipped270,
|
|
|
|
|
};
|
|
|
|
|
CompTransformDef(def)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<CompTransformDef> for Transform {
|
|
|
|
|
fn from(comp_transform: CompTransformDef) -> Self {
|
|
|
|
|
match comp_transform.0 {
|
|
|
|
|
TransformDef::Normal => Transform::Normal,
|
|
|
|
|
TransformDef::_90 => Transform::_90,
|
|
|
|
|
TransformDef::_180 => Transform::_180,
|
|
|
|
|
TransformDef::_270 => Transform::_270,
|
|
|
|
|
TransformDef::Flipped => Transform::Flipped,
|
|
|
|
|
TransformDef::Flipped90 => Transform::Flipped90,
|
|
|
|
|
TransformDef::Flipped180 => Transform::Flipped180,
|
|
|
|
|
TransformDef::Flipped270 => Transform::Flipped270,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:49:28 +01:00
|
|
|
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
|
|
|
|
|
pub struct ScreenFilter {
|
|
|
|
|
pub inverted: bool,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub color_filter: Option<ColorFilter>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ScreenFilter {
|
|
|
|
|
pub fn is_noop(&self) -> bool {
|
2025-10-16 13:50:32 +02:00
|
|
|
!self.inverted && self.color_filter.is_none()
|
2025-03-19 13:49:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-25 23:00:30 +02:00
|
|
|
impl Config {
|
2023-09-29 21:33:16 +02:00
|
|
|
pub fn load(loop_handle: &LoopHandle<'_, State>) -> Config {
|
2023-07-28 12:54:02 -07:00
|
|
|
let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
|
|
|
|
let source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap();
|
|
|
|
|
loop_handle
|
2023-09-29 21:33:16 +02:00
|
|
|
.insert_source(source, |(config, keys), (), state| {
|
|
|
|
|
config_changed(config, keys, state);
|
2023-07-28 12:54:02 -07:00
|
|
|
})
|
|
|
|
|
.expect("Failed to add cosmic-config to the event loop");
|
2025-07-21 14:54:21 +02:00
|
|
|
let xdg = xdg::BaseDirectories::new();
|
2024-04-03 16:02:27 +02:00
|
|
|
|
2024-02-08 14:25:18 -05:00
|
|
|
let cosmic_comp_config =
|
|
|
|
|
CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| {
|
2026-01-09 18:34:51 +01:00
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
for err in errs {
|
|
|
|
|
warn!(?err, "");
|
|
|
|
|
}
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
c
|
|
|
|
|
});
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2024-07-26 16:40:28 +02:00
|
|
|
// Listen for updates to the toolkit config
|
|
|
|
|
if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) {
|
2024-08-02 20:40:44 +02:00
|
|
|
fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) {
|
2026-01-09 18:34:51 +01:00
|
|
|
if cosmic::icon_theme::default() != config.icon_theme {
|
|
|
|
|
cosmic::icon_theme::set_default(config.icon_theme.clone());
|
|
|
|
|
state.common.update_xwayland_settings();
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-02 20:40:44 +02:00
|
|
|
let mut workspace_guard = state.common.workspace_state.update();
|
2025-05-20 17:41:27 +02:00
|
|
|
state.common.shell.write().update_toolkit(
|
2024-08-02 20:40:44 +02:00
|
|
|
config,
|
|
|
|
|
&state.common.xdg_activation_state,
|
|
|
|
|
&mut workspace_guard,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 18:34:51 +01:00
|
|
|
let config = CosmicTk::get_entry(&tk_config).unwrap_or_else(|(errs, c)| {
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
for err in errs {
|
|
|
|
|
warn!(?err, "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c
|
|
|
|
|
});
|
|
|
|
|
let _ = loop_handle.insert_idle(move |state| {
|
|
|
|
|
handle_new_toolkit_config(config, state);
|
|
|
|
|
});
|
2024-08-02 20:40:44 +02:00
|
|
|
|
2024-07-26 16:40:28 +02:00
|
|
|
match cosmic_config::calloop::ConfigWatchSource::new(&tk_config) {
|
|
|
|
|
Ok(source) => {
|
|
|
|
|
if let Err(err) =
|
2024-08-02 20:40:44 +02:00
|
|
|
loop_handle.insert_source(source, |(config, _keys), (), state| {
|
2026-01-09 18:34:51 +01:00
|
|
|
let config =
|
|
|
|
|
CosmicTk::get_entry(&config).unwrap_or_else(|(errs, c)| {
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
for err in errs {
|
|
|
|
|
warn!(?err, "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c
|
|
|
|
|
});
|
|
|
|
|
handle_new_toolkit_config(config, state);
|
2024-07-26 16:40:28 +02:00
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
warn!(?err, "Failed to watch com.system76.CosmicTk config");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => warn!(
|
|
|
|
|
?err,
|
|
|
|
|
"failed to create config watch source for com.system76.CosmicTk"
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 16:02:27 +02:00
|
|
|
// Source key bindings from com.system76.CosmicSettings.Shortcuts
|
2024-07-26 16:40:28 +02:00
|
|
|
let settings_context = shortcuts::context().expect("Failed to load shortcuts config");
|
2025-02-04 13:04:00 +01:00
|
|
|
let system_actions = shortcuts::system_actions(&settings_context);
|
2025-02-26 17:13:31 +01:00
|
|
|
let shortcuts = shortcuts::shortcuts(&settings_context);
|
2024-04-03 16:02:27 +02:00
|
|
|
|
|
|
|
|
// Listen for updates to the keybindings config.
|
2024-07-26 16:40:28 +02:00
|
|
|
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" => {
|
2025-02-26 17:13:31 +01:00
|
|
|
state.common.config.shortcuts = shortcuts::shortcuts(&config);
|
2024-07-26 16:40:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"system_actions" => {
|
|
|
|
|
state.common.config.system_actions =
|
|
|
|
|
shortcuts::system_actions(&config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
2024-04-03 16:02:27 +02:00
|
|
|
}
|
2024-07-26 16:40:28 +02:00
|
|
|
}) {
|
|
|
|
|
warn!(
|
|
|
|
|
?err,
|
|
|
|
|
"Failed to watch com.system76.CosmicSettings.Shortcuts config"
|
|
|
|
|
);
|
2024-04-03 16:02:27 +02:00
|
|
|
}
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
2024-07-26 16:40:28 +02:00
|
|
|
Err(err) => warn!(
|
|
|
|
|
?err,
|
|
|
|
|
"failed to create config watch source for com.system76.CosmicSettings.Shortcuts"
|
|
|
|
|
),
|
|
|
|
|
};
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2024-09-04 20:40:41 +03:00
|
|
|
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;
|
2025-05-20 17:41:27 +02:00
|
|
|
state.common.shell.write().update_tiling_exceptions(
|
|
|
|
|
state.common.config.tiling_exceptions.iter(),
|
|
|
|
|
);
|
2024-09-04 20:40:41 +03:00
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}) {
|
|
|
|
|
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"
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-19 13:49:28 +01:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2024-04-03 16:02:27 +02:00
|
|
|
Config {
|
2025-07-21 14:54:21 +02:00
|
|
|
dynamic_conf: Self::load_dynamic(&xdg),
|
2024-04-03 16:02:27 +02:00
|
|
|
cosmic_conf: cosmic_comp_config,
|
|
|
|
|
cosmic_helper: config,
|
|
|
|
|
settings_context,
|
|
|
|
|
shortcuts,
|
|
|
|
|
system_actions,
|
2024-09-04 18:35:09 +03:00
|
|
|
tiling_exceptions,
|
2024-04-03 16:02:27 +02:00
|
|
|
}
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-21 14:54:21 +02:00
|
|
|
fn load_dynamic(xdg: &xdg::BaseDirectories) -> DynamicConfig {
|
|
|
|
|
let output_path = xdg.place_state_file("cosmic-comp/outputs.ron").ok();
|
2025-08-19 16:51:37 -04:00
|
|
|
let outputs = load_outputs(output_path.as_ref());
|
2025-07-21 14:54:21 +02:00
|
|
|
let numlock_path = xdg.place_state_file("cosmic-comp/numlock.ron").ok();
|
2025-02-12 05:35:22 -08:00
|
|
|
let numlock = Self::load_numlock(&numlock_path);
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2025-07-21 14:54:21 +02:00
|
|
|
let filter_path = xdg
|
|
|
|
|
.place_state_file("cosmic-comp/a11y_screen_filter.ron")
|
|
|
|
|
.ok();
|
2025-03-19 13:49:28 +01:00
|
|
|
let filter = Self::load_filter_state(&filter_path);
|
|
|
|
|
|
2022-04-25 23:00:30 +02:00
|
|
|
DynamicConfig {
|
|
|
|
|
outputs: (output_path, outputs),
|
2025-02-12 05:35:22 -08:00
|
|
|
numlock: (numlock_path, numlock),
|
2025-03-19 13:49:28 +01:00
|
|
|
accessibility_filter: (filter_path, filter),
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 05:35:22 -08:00
|
|
|
fn load_numlock(path: &Option<PathBuf>) -> 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()
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:49:28 +01:00
|
|
|
fn load_filter_state(path: &Option<PathBuf>) -> ScreenFilter {
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(path) = path.as_ref()
|
|
|
|
|
&& 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.");
|
2025-03-19 13:49:28 +01:00
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
}
|
|
|
|
|
};
|
2025-03-19 13:49:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScreenFilter {
|
|
|
|
|
inverted: false,
|
|
|
|
|
color_filter: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 16:02:27 +02:00
|
|
|
pub fn shortcut_for_action(&self, action: &shortcuts::Action) -> Option<String> {
|
|
|
|
|
self.shortcuts.shortcut_for_action(action)
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-25 23:00:30 +02:00
|
|
|
pub fn read_outputs(
|
|
|
|
|
&mut self,
|
2022-11-11 21:36:42 +01:00
|
|
|
output_state: &mut OutputConfigurationState<State>,
|
2022-04-25 23:00:30 +02:00
|
|
|
backend: &mut BackendData,
|
2025-05-20 17:41:27 +02:00
|
|
|
shell: &Arc<parking_lot::RwLock<Shell>>,
|
2024-06-07 19:10:13 +02:00
|
|
|
loop_handle: &LoopHandle<'static, State>,
|
2024-04-10 15:49:08 +02:00
|
|
|
workspace_state: &mut WorkspaceUpdateGuard<'_, State>,
|
|
|
|
|
xdg_activation_state: &XdgActivationState,
|
2024-06-11 17:24:46 +02:00
|
|
|
startup_done: Arc<AtomicBool>,
|
2025-01-02 17:38:59 +01:00
|
|
|
clock: &Clock<Monotonic>,
|
2025-09-10 18:25:33 +02:00
|
|
|
) -> anyhow::Result<()> {
|
2022-11-11 21:36:42 +01:00
|
|
|
let outputs = output_state.outputs().collect::<Vec<_>>();
|
2022-04-25 23:00:30 +02:00
|
|
|
let mut infos = outputs
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
2025-08-19 16:51:37 -04:00
|
|
|
.map(Into::<crate::config::CompOutputInfo>::into)
|
|
|
|
|
.map(|i| i.0)
|
2022-04-25 23:00:30 +02:00
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
infos.sort();
|
2025-08-11 16:51:12 +02:00
|
|
|
|
|
|
|
|
if let Some(configs) = self
|
|
|
|
|
.dynamic_conf
|
|
|
|
|
.outputs()
|
|
|
|
|
.config
|
|
|
|
|
.get(&infos)
|
|
|
|
|
.filter(|configs| {
|
|
|
|
|
if configs
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|config| config.enabled == OutputState::Disabled)
|
|
|
|
|
{
|
|
|
|
|
if !configs.is_empty() {
|
|
|
|
|
error!(
|
|
|
|
|
"Broken config, all outputs disabled. Resetting... {:?}",
|
|
|
|
|
configs
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
} else {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
{
|
2022-04-25 23:00:30 +02:00
|
|
|
let known_good_configs = outputs
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|output| {
|
|
|
|
|
output
|
|
|
|
|
.user_data()
|
|
|
|
|
.get::<RefCell<OutputConfig>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.borrow()
|
|
|
|
|
.clone()
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
2024-06-07 19:10:13 +02:00
|
|
|
let mut found_outputs = Vec::new();
|
2026-04-24 14:45:01 -07:00
|
|
|
for (name, output_config) in infos.iter().map(|o| &o.connector).zip(configs) {
|
2022-05-03 13:37:51 +02:00
|
|
|
let output = outputs.iter().find(|o| &o.name() == name).unwrap().clone();
|
2024-04-24 17:25:33 +02:00
|
|
|
let enabled = output_config.enabled.clone();
|
2022-04-25 23:00:30 +02:00
|
|
|
*output
|
|
|
|
|
.user_data()
|
|
|
|
|
.get::<RefCell<OutputConfig>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.borrow_mut() = output_config;
|
2024-06-07 19:10:13 +02:00
|
|
|
found_outputs.push((output.clone(), enabled));
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-16 21:13:50 +02:00
|
|
|
let mut backend = backend.lock();
|
2024-06-07 19:10:13 +02:00
|
|
|
if let Err(err) = backend.apply_config_for_outputs(
|
|
|
|
|
false,
|
|
|
|
|
loop_handle,
|
2025-03-19 13:49:28 +01:00
|
|
|
self.dynamic_conf.screen_filter(),
|
2024-06-07 19:10:13 +02:00
|
|
|
shell.clone(),
|
|
|
|
|
workspace_state,
|
|
|
|
|
xdg_activation_state,
|
|
|
|
|
startup_done.clone(),
|
2025-01-02 17:38:59 +01:00
|
|
|
clock,
|
2024-06-07 19:10:13 +02:00
|
|
|
) {
|
|
|
|
|
warn!(?err, "Failed to set new config.");
|
|
|
|
|
found_outputs.clear();
|
2026-04-24 14:45:01 -07:00
|
|
|
for (output, output_config) in outputs.clone().into_iter().zip(known_good_configs) {
|
2024-04-24 17:25:33 +02:00
|
|
|
let enabled = output_config.enabled.clone();
|
2022-04-25 23:00:30 +02:00
|
|
|
*output
|
|
|
|
|
.user_data()
|
|
|
|
|
.get::<RefCell<OutputConfig>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.borrow_mut() = output_config;
|
2024-06-07 19:10:13 +02:00
|
|
|
found_outputs.push((output.clone(), enabled));
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 18:25:33 +02:00
|
|
|
backend
|
|
|
|
|
.apply_config_for_outputs(
|
|
|
|
|
false,
|
|
|
|
|
loop_handle,
|
|
|
|
|
self.dynamic_conf.screen_filter(),
|
|
|
|
|
shell.clone(),
|
|
|
|
|
workspace_state,
|
|
|
|
|
xdg_activation_state,
|
|
|
|
|
startup_done,
|
|
|
|
|
clock,
|
|
|
|
|
)
|
|
|
|
|
.context("Failed to reset config")?;
|
|
|
|
|
|
|
|
|
|
for (output, enabled) in found_outputs {
|
|
|
|
|
if enabled == OutputState::Enabled {
|
|
|
|
|
output_state.enable_head(&output);
|
|
|
|
|
} else {
|
|
|
|
|
output_state.disable_head(&output);
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-06-07 19:10:13 +02:00
|
|
|
} else {
|
|
|
|
|
for (output, enabled) in found_outputs {
|
|
|
|
|
if enabled == OutputState::Enabled {
|
|
|
|
|
output_state.enable_head(&output);
|
|
|
|
|
} else {
|
|
|
|
|
output_state.disable_head(&output);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
2022-11-11 21:36:42 +01:00
|
|
|
|
2022-12-05 23:36:09 +01:00
|
|
|
output_state.update();
|
|
|
|
|
self.write_outputs(output_state.outputs());
|
|
|
|
|
} else {
|
2025-08-11 16:51:12 +02:00
|
|
|
if outputs
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|o| o.config().enabled == OutputState::Disabled)
|
|
|
|
|
{
|
|
|
|
|
for output in &outputs {
|
|
|
|
|
output.config_mut().enabled = OutputState::Enabled;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 19:47:12 +01:00
|
|
|
// we don't have a config, so lets generate somewhat sane positions
|
|
|
|
|
let mut w = 0;
|
2025-04-09 19:04:45 +02:00
|
|
|
if !outputs.iter().any(|o| o.config().xwayland_primary) {
|
|
|
|
|
// if we don't have a primary output for xwayland from a previous config, pick one
|
|
|
|
|
if let Some(primary) = outputs.iter().find(|o| o.mirroring().is_none()) {
|
|
|
|
|
primary.config_mut().xwayland_primary = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-06 19:47:12 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 18:25:33 +02:00
|
|
|
let mut backend = backend.lock();
|
|
|
|
|
backend
|
|
|
|
|
.apply_config_for_outputs(
|
|
|
|
|
false,
|
|
|
|
|
loop_handle,
|
|
|
|
|
self.dynamic_conf.screen_filter(),
|
|
|
|
|
shell.clone(),
|
|
|
|
|
workspace_state,
|
|
|
|
|
xdg_activation_state,
|
|
|
|
|
startup_done.clone(),
|
|
|
|
|
clock,
|
|
|
|
|
)
|
|
|
|
|
.context("Failed to set new config")?;
|
|
|
|
|
|
|
|
|
|
for output in outputs {
|
|
|
|
|
if output
|
|
|
|
|
.user_data()
|
|
|
|
|
.get::<RefCell<OutputConfig>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.borrow()
|
|
|
|
|
.enabled
|
|
|
|
|
== OutputState::Enabled
|
|
|
|
|
{
|
|
|
|
|
output_state.enable_head(&output);
|
|
|
|
|
} else {
|
|
|
|
|
output_state.disable_head(&output);
|
2022-12-05 23:36:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2022-11-11 21:36:42 +01:00
|
|
|
output_state.update();
|
|
|
|
|
self.write_outputs(output_state.outputs());
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
2025-09-10 18:25:33 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
2022-05-03 13:37:51 +02:00
|
|
|
|
|
|
|
|
pub fn write_outputs(
|
|
|
|
|
&mut self,
|
|
|
|
|
outputs: impl Iterator<Item = impl std::borrow::Borrow<Output>>,
|
|
|
|
|
) {
|
2022-04-25 23:00:30 +02:00
|
|
|
let mut infos = outputs
|
|
|
|
|
.map(|o| {
|
|
|
|
|
let o = o.borrow();
|
|
|
|
|
(
|
2025-08-19 16:51:37 -04:00
|
|
|
Into::<CompOutputInfo>::into(o.clone()).0,
|
2022-04-25 23:00:30 +02:00
|
|
|
o.user_data()
|
|
|
|
|
.get::<RefCell<OutputConfig>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.borrow()
|
|
|
|
|
.clone(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<(OutputInfo, OutputConfig)>>();
|
2025-10-16 13:50:32 +02:00
|
|
|
infos.sort_by(|(a, _), (b, _)| a.cmp(b));
|
2022-04-25 23:00:30 +02:00
|
|
|
let (infos, configs) = infos.into_iter().unzip();
|
2022-05-03 13:37:51 +02:00
|
|
|
self.dynamic_conf
|
2022-04-25 23:00:30 +02:00
|
|
|
.outputs_mut()
|
|
|
|
|
.config
|
|
|
|
|
.insert(infos, configs);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-29 14:23:23 +02:00
|
|
|
pub fn xkb_config(&self) -> XkbConfig {
|
2024-02-08 14:25:18 -05:00
|
|
|
self.cosmic_conf.xkb_config.clone()
|
2022-07-29 14:23:23 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 13:49:41 -07:00
|
|
|
pub fn read_device(&self, device: &mut InputDevice) {
|
2023-09-01 12:33:55 -07:00
|
|
|
let (device_config, default_config) = self.get_device_config(device);
|
2025-08-20 16:49:08 +02:00
|
|
|
input_config::update_device(device, device_config.as_ref(), default_config);
|
2023-09-01 12:33:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn scroll_factor(&self, device: &InputDevice) -> f64 {
|
|
|
|
|
let (device_config, default_config) = self.get_device_config(device);
|
2025-08-20 16:49:08 +02:00
|
|
|
input_config::get_config(device_config.as_ref(), default_config, |x| {
|
2023-09-01 12:33:55 -07:00
|
|
|
x.scroll_config.as_ref()?.scroll_factor
|
|
|
|
|
})
|
|
|
|
|
.map_or(1.0, |x| x.0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 16:49:08 +02:00
|
|
|
pub fn map_to_output(&self, device: &InputDevice) -> Option<String> {
|
2023-12-27 19:17:13 -08:00
|
|
|
let (device_config, default_config) = self.get_device_config(device);
|
|
|
|
|
Some(
|
2025-08-20 16:49:08 +02:00
|
|
|
input_config::get_config(device_config.as_ref(), default_config, |x| {
|
|
|
|
|
x.map_to_output.clone()
|
2023-12-27 19:17:13 -08:00
|
|
|
})?
|
|
|
|
|
.0,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 16:49:08 +02:00
|
|
|
fn get_device_config(&self, device: &InputDevice) -> (Option<InputConfig>, &InputConfig) {
|
|
|
|
|
let is_touchpad = device.config_tap_finger_count() > 0;
|
|
|
|
|
|
|
|
|
|
let default_config = if is_touchpad {
|
2024-02-08 14:25:18 -05:00
|
|
|
&self.cosmic_conf.input_touchpad
|
2023-08-29 13:49:41 -07:00
|
|
|
} else {
|
2024-02-08 14:25:18 -05:00
|
|
|
&self.cosmic_conf.input_default
|
2023-08-29 13:49:41 -07:00
|
|
|
};
|
2025-08-20 16:49:08 +02:00
|
|
|
|
2026-04-24 14:14:59 -07:00
|
|
|
let mut device_config = self.cosmic_conf.input_devices.get(&*device.name()).cloned();
|
2025-08-20 16:49:08 +02:00
|
|
|
if is_touchpad && self.cosmic_conf.input_touchpad_override == TouchpadOverride::ForceDisable
|
|
|
|
|
{
|
|
|
|
|
device_config = Some({
|
|
|
|
|
let mut config = device_config.unwrap_or_default();
|
|
|
|
|
config.state = InputDeviceState::Disabled;
|
|
|
|
|
config
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 12:33:55 -07:00
|
|
|
(device_config, default_config)
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct PersistenceGuard<'a, T: Serialize>(Option<PathBuf>, &'a mut T);
|
|
|
|
|
|
2025-10-16 13:50:32 +02:00
|
|
|
impl<T: Serialize> std::ops::Deref for PersistenceGuard<'_, T> {
|
2022-04-25 23:00:30 +02:00
|
|
|
type Target = T;
|
|
|
|
|
fn deref(&self) -> &T {
|
2025-10-16 13:50:32 +02:00
|
|
|
self.1
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 13:50:32 +02:00
|
|
|
impl<T: Serialize> std::ops::DerefMut for PersistenceGuard<'_, T> {
|
2022-04-25 23:00:30 +02:00
|
|
|
fn deref_mut(&mut self) -> &mut T {
|
2025-10-16 13:50:32 +02:00
|
|
|
self.1
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 13:50:32 +02:00
|
|
|
impl<T: Serialize> Drop for PersistenceGuard<'_, T> {
|
2022-04-25 23:00:30 +02:00
|
|
|
fn drop(&mut self) {
|
|
|
|
|
if let Some(path) = self.0.as_ref() {
|
2025-01-22 19:28:56 +01:00
|
|
|
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()
|
2022-04-25 23:00:30 +02:00
|
|
|
.create(true)
|
|
|
|
|
.truncate(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(path)
|
|
|
|
|
{
|
|
|
|
|
Ok(writer) => writer,
|
|
|
|
|
Err(err) => {
|
2023-02-24 17:41:52 +01:00
|
|
|
warn!(?err, "Failed to persist {}.", path.display());
|
2022-04-25 23:00:30 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-01-22 19:28:56 +01:00
|
|
|
|
|
|
|
|
if let Err(err) = writer.write_all(content.as_bytes()) {
|
2023-02-24 17:41:52 +01:00
|
|
|
warn!(?err, "Failed to persist {}", path.display());
|
2025-01-22 19:28:56 +01:00
|
|
|
} else {
|
|
|
|
|
let _ = writer.flush();
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DynamicConfig {
|
|
|
|
|
pub fn outputs(&self) -> &OutputsConfig {
|
|
|
|
|
&self.outputs.1
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-30 13:58:49 +01:00
|
|
|
pub fn outputs_mut(&mut self) -> PersistenceGuard<'_, OutputsConfig> {
|
2022-04-25 23:00:30 +02:00
|
|
|
PersistenceGuard(self.outputs.0.clone(), &mut self.outputs.1)
|
|
|
|
|
}
|
2025-02-12 05:35:22 -08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2025-02-04 16:49:29 +01:00
|
|
|
|
2025-03-19 13:49:28 +01:00
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-07-28 12:54:02 -07:00
|
|
|
}
|
2022-05-03 13:37:51 +02:00
|
|
|
|
2025-08-19 16:51:37 -04:00
|
|
|
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(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 12:54:02 -07:00
|
|
|
fn get_config<T: Default + serde::de::DeserializeOwned>(
|
|
|
|
|
config: &cosmic_config::Config,
|
|
|
|
|
key: &str,
|
|
|
|
|
) -> T {
|
|
|
|
|
config.get(key).unwrap_or_else(|err| {
|
|
|
|
|
error!(?err, "Failed to read config '{}'", key);
|
|
|
|
|
T::default()
|
|
|
|
|
})
|
|
|
|
|
}
|
2022-04-25 23:00:30 +02:00
|
|
|
|
2023-08-29 13:49:41 -07:00
|
|
|
fn update_input(state: &mut State) {
|
2025-07-21 14:54:21 +02:00
|
|
|
if let BackendData::Kms(kms_state) = &mut state.backend {
|
2023-08-29 13:49:41 -07:00
|
|
|
for device in kms_state.input_devices.values_mut() {
|
|
|
|
|
state.common.config.read_device(device);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 12:13:54 +02:00
|
|
|
pub fn change_modifier_state(
|
|
|
|
|
keyboard: &smithay::input::keyboard::KeyboardHandle<State>,
|
|
|
|
|
scan_code: u32,
|
|
|
|
|
state: &mut State,
|
|
|
|
|
) {
|
2025-06-13 12:52:49 -04:00
|
|
|
/// Offset used to convert Linux scancode to X11 keycode.
|
|
|
|
|
const X11_KEYCODE_OFFSET: u32 = 8;
|
|
|
|
|
|
2025-02-12 12:13:54 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 12:54:02 -07:00
|
|
|
fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut State) {
|
|
|
|
|
for key in &keys {
|
|
|
|
|
match key.as_str() {
|
2024-01-23 14:54:55 -08:00
|
|
|
"xkb_config" => {
|
|
|
|
|
let value = get_config::<XkbConfig>(&config, "xkb_config");
|
2024-04-10 15:49:08 +02:00
|
|
|
let seats = state
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect::<Vec<_>>();
|
2024-04-05 13:53:35 +02:00
|
|
|
for seat in seats.into_iter() {
|
2023-07-28 12:54:02 -07:00
|
|
|
if let Some(keyboard) = seat.get_keyboard() {
|
2025-02-12 12:13:54 +02:00
|
|
|
let old_modifier_state = keyboard.modifier_state();
|
2024-05-19 18:19:43 +02:00
|
|
|
keyboard.change_repeat_info(
|
|
|
|
|
(value.repeat_rate as i32).abs(), // Negative values are illegal
|
|
|
|
|
(value.repeat_delay as i32).abs(),
|
|
|
|
|
);
|
2023-08-29 13:49:41 -07:00
|
|
|
if let Err(err) = keyboard.set_xkb_config(state, xkb_config_to_wl(&value)) {
|
2023-07-28 12:54:02 -07:00
|
|
|
error!(?err, "Failed to load provided xkb config");
|
|
|
|
|
// TODO Revert to default?
|
|
|
|
|
}
|
2025-02-12 12:13:54 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
2023-07-28 12:54:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-05 10:42:19 -06:00
|
|
|
let ei_connections = state
|
|
|
|
|
.common
|
|
|
|
|
.ei_keyboard_source
|
|
|
|
|
.keys()
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
for conn in &ei_connections {
|
|
|
|
|
state.release_ei_keyboard(conn);
|
|
|
|
|
state.clear_input_source_state(&InputBackendId::Ei(conn.clone()));
|
|
|
|
|
}
|
|
|
|
|
for ei_seat in state.common.ei_seats.values() {
|
|
|
|
|
if let Err(err) =
|
|
|
|
|
ei_seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&value))
|
|
|
|
|
{
|
|
|
|
|
warn!(?err, "Failed to update libei keyboard keymap");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !state.common.ei_seats.is_empty() {
|
|
|
|
|
let seat = state.common.shell.read().seats.last_active().clone();
|
|
|
|
|
state.broadcast_ei_keyboard_modifiers(&seat);
|
|
|
|
|
}
|
2024-02-08 14:25:18 -05:00
|
|
|
state.common.config.cosmic_conf.xkb_config = value;
|
2023-07-28 12:54:02 -07:00
|
|
|
}
|
2025-02-12 05:35:22 -08:00
|
|
|
"keyboard_config" => {
|
|
|
|
|
let value = get_config::<KeyboardConfig>(&config, "keyboard_config");
|
|
|
|
|
state.common.config.cosmic_conf.keyboard_config = value;
|
2025-05-20 17:41:27 +02:00
|
|
|
let shell = state.common.shell.read();
|
2025-02-12 05:35:22 -08:00
|
|
|
let seat = shell.seats.last_active();
|
|
|
|
|
state.common.config.dynamic_conf.numlock_mut().last_state =
|
|
|
|
|
seat.get_keyboard().unwrap().modifier_state().num_lock;
|
|
|
|
|
}
|
2023-12-20 16:52:44 -08:00
|
|
|
"input_default" => {
|
|
|
|
|
let value = get_config::<InputConfig>(&config, "input_default");
|
2024-02-08 14:25:18 -05:00
|
|
|
state.common.config.cosmic_conf.input_default = value;
|
2023-08-29 13:49:41 -07:00
|
|
|
update_input(state);
|
|
|
|
|
}
|
2023-12-20 16:52:44 -08:00
|
|
|
"input_touchpad" => {
|
|
|
|
|
let value = get_config::<InputConfig>(&config, "input_touchpad");
|
2024-02-08 14:25:18 -05:00
|
|
|
state.common.config.cosmic_conf.input_touchpad = value;
|
2023-08-29 13:49:41 -07:00
|
|
|
update_input(state);
|
|
|
|
|
}
|
2025-08-20 16:49:08 +02:00
|
|
|
"input_touchpad_override" => {
|
|
|
|
|
let value = get_config::<TouchpadOverride>(&config, "input_touchpad_override");
|
|
|
|
|
state.common.config.cosmic_conf.input_touchpad_override = value;
|
|
|
|
|
update_input(state)
|
|
|
|
|
}
|
2023-12-20 16:52:44 -08:00
|
|
|
"input_devices" => {
|
|
|
|
|
let value = get_config::<HashMap<String, InputConfig>>(&config, "input_devices");
|
2024-02-08 14:25:18 -05:00
|
|
|
state.common.config.cosmic_conf.input_devices = value;
|
2023-08-29 13:49:41 -07:00
|
|
|
update_input(state);
|
2023-07-28 12:54:02 -07:00
|
|
|
}
|
2023-09-07 13:28:08 -07:00
|
|
|
"workspaces" => {
|
2024-02-08 14:25:18 -05:00
|
|
|
state.common.config.cosmic_conf.workspaces =
|
2023-09-07 13:28:08 -07:00
|
|
|
get_config::<WorkspaceConfig>(&config, "workspaces");
|
2024-04-10 15:49:08 +02:00
|
|
|
state.common.update_config();
|
2023-09-07 13:28:08 -07:00
|
|
|
}
|
2024-02-08 14:25:18 -05:00
|
|
|
"autotile" => {
|
|
|
|
|
let new = get_config::<bool>(&config, "autotile");
|
|
|
|
|
if new != state.common.config.cosmic_conf.autotile {
|
|
|
|
|
state.common.config.cosmic_conf.autotile = new;
|
2024-04-10 15:49:08 +02:00
|
|
|
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = state.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
let shell_ref = &mut *shell;
|
|
|
|
|
shell_ref.workspaces.update_autotile(
|
|
|
|
|
new,
|
|
|
|
|
&mut state.common.workspace_state.update(),
|
|
|
|
|
shell_ref.seats.iter(),
|
|
|
|
|
);
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"autotile_behavior" => {
|
|
|
|
|
let new = get_config::<TileBehavior>(&config, "autotile_behavior");
|
|
|
|
|
if new != state.common.config.cosmic_conf.autotile_behavior {
|
|
|
|
|
state.common.config.cosmic_conf.autotile_behavior = new;
|
2024-04-10 15:49:08 +02:00
|
|
|
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = state.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
let shell_ref = &mut *shell;
|
|
|
|
|
shell_ref.workspaces.update_autotile_behavior(
|
|
|
|
|
new,
|
|
|
|
|
&mut state.common.workspace_state.update(),
|
|
|
|
|
shell_ref.seats.iter(),
|
|
|
|
|
);
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"active_hint" => {
|
|
|
|
|
let new = get_config::<bool>(&config, "active_hint");
|
|
|
|
|
if new != state.common.config.cosmic_conf.active_hint {
|
|
|
|
|
state.common.config.cosmic_conf.active_hint = new;
|
2024-04-10 15:49:08 +02:00
|
|
|
state.common.update_config();
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-23 18:26:08 +02:00
|
|
|
"descale_xwayland" => {
|
2025-04-04 18:49:45 +02:00
|
|
|
let new = get_config::<XwaylandDescaling>(&config, "descale_xwayland");
|
2024-08-23 18:26:08 +02:00
|
|
|
if new != state.common.config.cosmic_conf.descale_xwayland {
|
|
|
|
|
state.common.config.cosmic_conf.descale_xwayland = new;
|
2026-01-09 18:34:51 +01:00
|
|
|
state.common.update_xwayland_settings();
|
2024-08-23 18:26:08 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-28 17:45:28 +01:00
|
|
|
"xwayland_eavesdropping" => {
|
|
|
|
|
let new = get_config::<XwaylandEavesdropping>(&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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-04 11:13:59 -05:00
|
|
|
"focus_follows_cursor" => {
|
|
|
|
|
let new = get_config::<bool>(&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::<bool>(&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::<u64>(&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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-14 21:58:09 +11:00
|
|
|
"edge_snap_threshold" => {
|
|
|
|
|
let new = get_config::<u32>(&config, "edge_snap_threshold");
|
|
|
|
|
if new != state.common.config.cosmic_conf.edge_snap_threshold {
|
|
|
|
|
state.common.config.cosmic_conf.edge_snap_threshold = new;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-24 17:49:13 +01:00
|
|
|
"accessibility_zoom" => {
|
|
|
|
|
let new = get_config::<ZoomConfig>(&config, "accessibility_zoom");
|
|
|
|
|
if new != state.common.config.cosmic_conf.accessibility_zoom {
|
|
|
|
|
state.common.config.cosmic_conf.accessibility_zoom = new;
|
2025-02-13 21:08:31 +01:00
|
|
|
state.common.update_config();
|
2025-01-24 17:49:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 18:21:58 +01:00
|
|
|
"appearance_settings" => {
|
|
|
|
|
let new = get_config::<AppearanceConfig>(&config, "appearance_settings");
|
|
|
|
|
if new != state.common.config.cosmic_conf.appearance_settings {
|
|
|
|
|
state.common.config.cosmic_conf.appearance_settings = new;
|
|
|
|
|
state.common.update_config();
|
|
|
|
|
for output in state.common.shell.read().outputs() {
|
|
|
|
|
state.backend.schedule_render(output);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
"cursor_hide_timeout" => {
|
|
|
|
|
let new = get_config::<Option<u32>>(&config, "cursor_hide_timeout");
|
|
|
|
|
if new != state.common.config.cosmic_conf.cursor_hide_timeout {
|
|
|
|
|
state.common.config.cosmic_conf.cursor_hide_timeout = new;
|
|
|
|
|
let seats: Vec<_> = state.common.shell.read().seats.iter().cloned().collect();
|
|
|
|
|
let mut needs_render = false;
|
|
|
|
|
for seat in seats {
|
|
|
|
|
needs_render |=
|
|
|
|
|
crate::backend::render::cursor::notify_cursor_activity(state, &seat);
|
|
|
|
|
}
|
|
|
|
|
if needs_render {
|
|
|
|
|
let outputs: Vec<_> =
|
|
|
|
|
state.common.shell.read().outputs().cloned().collect();
|
|
|
|
|
for output in outputs {
|
|
|
|
|
state.backend.schedule_render(&output);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-22 17:06:49 +02:00
|
|
|
"activation_policy" => {
|
|
|
|
|
let new = get_config::<ActivationPolicy>(&config, "activation_policy");
|
|
|
|
|
if new != state.common.config.cosmic_conf.activation_policy {
|
|
|
|
|
state.common.config.cosmic_conf.activation_policy = new;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-28 12:54:02 -07:00
|
|
|
_ => {}
|
|
|
|
|
}
|
2022-04-25 23:00:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 13:49:41 -07:00
|
|
|
|
2025-08-19 16:51:37 -04:00
|
|
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
|
pub struct CompOutputInfo(OutputInfo);
|
|
|
|
|
|
|
|
|
|
impl From<Output> for CompOutputInfo {
|
|
|
|
|
fn from(o: Output) -> CompOutputInfo {
|
|
|
|
|
let physical = o.physical_properties();
|
|
|
|
|
CompOutputInfo(OutputInfo {
|
|
|
|
|
connector: o.name(),
|
|
|
|
|
make: physical.make,
|
|
|
|
|
model: physical.model,
|
|
|
|
|
})
|
2023-08-29 13:49:41 -07:00
|
|
|
}
|
|
|
|
|
}
|