Fork of pop-os/cosmic-comp @ upstream

This commit is contained in:
d
2026-08-09 22:37:21 +07:00
commit 6ce5e75b0b
273 changed files with 79114 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "cosmic-comp-config"
version = "1.0.0"
edition = "2024"
[dependencies]
cosmic-config = { git = "https://github.com/pop-os/libcosmic/" }
cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr/", optional = true }
input = "0.10.0"
libdisplay-info = { version = "0.3.0", optional = true }
serde = { version = "1", features = ["derive"] }
ron = { version = "0.12", optional = true }
tracing = { version = "0.1.44", features = [
"max_level_debug",
"release_max_level_info",
], optional = true }
[features]
default = []
output = ["ron", "tracing"]
randr = ["cosmic-randr-shell", "output"]
+216
View File
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: GPL-3.0-only
#![allow(non_snake_case)]
pub use input::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap};
use serde::{Deserialize, Serialize};
// Note: For the following values, None is used to represent the system default
// Configuration for input devices
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct InputConfig {
pub state: DeviceState,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub acceleration: Option<AccelConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub calibration: Option<[f32; 6]>,
#[serde(with = "ClickMethodDef")]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub click_method: Option<ClickMethod>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub disable_while_typing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub left_handed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub middle_button_emulation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub rotation_angle: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub scroll_config: Option<ScrollConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub tap_config: Option<TapConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub map_to_output: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct AccelConfig {
#[serde(with = "AccelProfileDef")]
pub profile: Option<AccelProfile>,
pub speed: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct ScrollConfig {
#[serde(with = "ScrollMethodDef")]
pub method: Option<ScrollMethod>,
pub natural_scroll: Option<bool>,
pub scroll_button: Option<u32>,
pub scroll_factor: Option<f64>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum DeviceState {
#[default]
Enabled,
Disabled,
DisabledOnExternalMouse,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TouchpadOverride {
#[default]
None,
ForceDisable,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TapConfig {
pub enabled: bool,
#[serde(with = "TapButtonMapDef")]
pub button_map: Option<TapButtonMap>,
pub drag: bool,
pub drag_lock: bool,
}
mod ClickMethodDef {
use input::ClickMethod as ClickMethodOrig;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Serialize, Deserialize)]
pub enum ClickMethod {
ButtonAreas,
Clickfinger,
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ClickMethodOrig>, D::Error>
where
D: Deserializer<'de>,
{
let o = Option::deserialize(deserializer)?;
Ok(o.map(|x| match x {
ClickMethod::ButtonAreas => ClickMethodOrig::ButtonAreas,
ClickMethod::Clickfinger => ClickMethodOrig::Clickfinger,
}))
}
pub fn serialize<S>(arg: &Option<ClickMethodOrig>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let arg = match arg {
Some(ClickMethodOrig::ButtonAreas) => Some(ClickMethod::ButtonAreas),
Some(ClickMethodOrig::Clickfinger) => Some(ClickMethod::Clickfinger),
Some(_) | None => None,
};
Option::serialize(&arg, ser)
}
}
mod AccelProfileDef {
use input::AccelProfile as AccelProfileOrig;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Serialize, Deserialize)]
enum AccelProfile {
Flat,
Adaptive,
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<AccelProfileOrig>, D::Error>
where
D: Deserializer<'de>,
{
let o = Option::deserialize(deserializer)?;
Ok(o.map(|x| match x {
AccelProfile::Flat => AccelProfileOrig::Flat,
AccelProfile::Adaptive => AccelProfileOrig::Adaptive,
}))
}
pub fn serialize<S>(arg: &Option<AccelProfileOrig>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let arg = match arg {
Some(AccelProfileOrig::Flat) => Some(AccelProfile::Flat),
Some(AccelProfileOrig::Adaptive) => Some(AccelProfile::Adaptive),
Some(_) | None => None,
};
Option::serialize(&arg, ser)
}
}
mod ScrollMethodDef {
use input::ScrollMethod as ScrollMethodOrig;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Serialize, Deserialize)]
pub enum ScrollMethod {
NoScroll,
TwoFinger,
Edge,
OnButtonDown,
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ScrollMethodOrig>, D::Error>
where
D: Deserializer<'de>,
{
let o = Option::deserialize(deserializer)?;
Ok(o.map(|x| match x {
ScrollMethod::NoScroll => ScrollMethodOrig::NoScroll,
ScrollMethod::TwoFinger => ScrollMethodOrig::TwoFinger,
ScrollMethod::Edge => ScrollMethodOrig::Edge,
ScrollMethod::OnButtonDown => ScrollMethodOrig::OnButtonDown,
}))
}
pub fn serialize<S>(arg: &Option<ScrollMethodOrig>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let arg = match arg {
Some(ScrollMethodOrig::NoScroll) => Some(ScrollMethod::NoScroll),
Some(ScrollMethodOrig::TwoFinger) => Some(ScrollMethod::TwoFinger),
Some(ScrollMethodOrig::Edge) => Some(ScrollMethod::Edge),
Some(ScrollMethodOrig::OnButtonDown) => Some(ScrollMethod::OnButtonDown),
Some(_) | None => None,
};
Option::serialize(&arg, ser)
}
}
mod TapButtonMapDef {
use input::TapButtonMap as TapButtonMapOrig;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Serialize, Deserialize)]
pub enum TapButtonMap {
LeftRightMiddle,
LeftMiddleRight,
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TapButtonMapOrig>, D::Error>
where
D: Deserializer<'de>,
{
let o = Option::deserialize(deserializer)?;
Ok(o.map(|x| match x {
TapButtonMap::LeftRightMiddle => TapButtonMapOrig::LeftRightMiddle,
TapButtonMap::LeftMiddleRight => TapButtonMapOrig::LeftMiddleRight,
}))
}
pub fn serialize<S>(arg: &Option<TapButtonMapOrig>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let arg = match arg {
Some(TapButtonMapOrig::LeftRightMiddle) => Some(TapButtonMap::LeftRightMiddle),
Some(TapButtonMapOrig::LeftMiddleRight) => Some(TapButtonMap::LeftMiddleRight),
Some(_) | None => None,
};
Option::serialize(&arg, ser)
}
}
+249
View File
@@ -0,0 +1,249 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::input::TouchpadOverride;
pub mod input;
#[cfg(feature = "output")]
pub mod output;
pub mod workspace;
#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EdidProduct {
pub manufacturer: [char; 3],
pub product: u16,
pub serial: Option<u32>,
pub manufacture_week: i32,
pub manufacture_year: i32,
pub model_year: Option<i32>,
}
#[cfg(feature = "libdisplay-info")]
impl From<libdisplay_info::edid::VendorProduct> 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(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct KeyboardConfig {
/// Boot state for numlock
pub numlock_state: NumlockState,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum NumlockState {
BootOn,
#[default]
BootOff,
LastBoot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AppearanceConfig {
pub clip_floating_windows: bool,
pub clip_tiled_windows: bool,
pub shadow_tiled_windows: bool,
}
impl Default for AppearanceConfig {
fn default() -> Self {
AppearanceConfig {
clip_floating_windows: true,
clip_tiled_windows: true,
shadow_tiled_windows: false,
}
}
}
#[derive(Clone, Debug, PartialEq, CosmicConfigEntry)]
#[version = 1]
pub struct CosmicCompConfig {
pub workspaces: workspace::WorkspaceConfig,
pub pinned_workspaces: Vec<workspace::PinnedWorkspace>,
pub input_default: input::InputConfig,
pub input_touchpad: input::InputConfig,
pub input_touchpad_override: TouchpadOverride,
pub input_devices: HashMap<String, input::InputConfig>,
pub xkb_config: XkbConfig,
pub keyboard_config: KeyboardConfig,
/// Autotiling enabled
pub autotile: bool,
/// Determines the behavior of the autotile variable
/// If set to Global, autotile applies to all windows in all workspaces
/// If set to PerWorkspace, autotile only applies to new windows, and new workspaces
pub autotile_behavior: TileBehavior,
/// Active hint enabled
pub active_hint: bool,
/// Enables changing keyboard focus to windows when the cursor passes into them
pub focus_follows_cursor: bool,
/// Enables warping the cursor to the focused window when focus changes due to keyboard input
pub cursor_follows_focus: bool,
/// The delay in milliseconds before focus follows mouse (if enabled)
pub focus_follows_cursor_delay: u64,
/// Let X11 applications scale themselves
pub descale_xwayland: XwaylandDescaling,
/// Let X11 applications snoop on certain key-presses to allow for global shortcuts
pub xwayland_eavesdropping: XwaylandEavesdropping,
/// The threshold before windows snap themselves to output edges
pub edge_snap_threshold: u32,
pub accessibility_zoom: ZoomConfig,
pub appearance_settings: AppearanceConfig,
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
pub cursor_hide_timeout: Option<u32>,
pub activation_policy: ActivationPolicy,
}
impl Default for CosmicCompConfig {
fn default() -> Self {
Self {
workspaces: Default::default(),
pinned_workspaces: Vec::new(),
input_default: Default::default(),
// By default, enable tap-to-click and disable-while-typing.
input_touchpad: input::InputConfig {
state: input::DeviceState::Enabled,
click_method: Some(input::ClickMethod::Clickfinger),
disable_while_typing: Some(true),
tap_config: Some(input::TapConfig {
enabled: true,
button_map: Some(input::TapButtonMap::LeftRightMiddle),
drag: true,
drag_lock: false,
}),
..Default::default()
},
input_touchpad_override: Default::default(),
input_devices: Default::default(),
xkb_config: Default::default(),
keyboard_config: Default::default(),
autotile: Default::default(),
autotile_behavior: Default::default(),
active_hint: true,
focus_follows_cursor: false,
cursor_follows_focus: false,
focus_follows_cursor_delay: 250,
descale_xwayland: XwaylandDescaling::Fractional,
xwayland_eavesdropping: XwaylandEavesdropping::default(),
edge_snap_threshold: 0,
accessibility_zoom: ZoomConfig::default(),
appearance_settings: AppearanceConfig::default(),
cursor_hide_timeout: None,
activation_policy: ActivationPolicy::default(),
}
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize)]
pub enum TileBehavior {
#[default]
Global,
PerWorkspace,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct XkbConfig {
pub rules: String,
pub model: String,
pub layout: String,
pub variant: String,
pub options: Option<String>,
#[serde(default = "default_repeat_delay")]
pub repeat_delay: u32,
#[serde(default = "default_repeat_rate")]
pub repeat_rate: u32,
}
impl Default for XkbConfig {
fn default() -> XkbConfig {
XkbConfig {
rules: String::new(),
model: String::new(),
layout: String::new(),
variant: String::new(),
options: None,
repeat_delay: default_repeat_delay(),
repeat_rate: default_repeat_rate(),
}
}
}
fn default_repeat_rate() -> u32 {
25
}
fn default_repeat_delay() -> u32 {
600
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub struct ZoomConfig {
pub start_on_login: bool,
pub show_overlay: bool,
pub increment: u32,
pub view_moves: ZoomMovement,
pub enable_mouse_zoom_shortcuts: bool,
}
impl Default for ZoomConfig {
fn default() -> Self {
ZoomConfig {
start_on_login: false,
show_overlay: true,
increment: 50,
view_moves: ZoomMovement::Continuously,
enable_mouse_zoom_shortcuts: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum ZoomMovement {
OnEdge,
Centered,
Continuously,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub struct XwaylandEavesdropping {
pub keyboard: EavesdroppingKeyboardMode,
pub pointer: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum EavesdroppingKeyboardMode {
#[default]
None,
Modifiers,
Combinations,
All,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
pub enum ActivationPolicy {
#[default]
Focus,
FocusIfActiveWorkspace,
Urgent,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum XwaylandDescaling {
#[serde(rename = "true")]
Enabled,
#[serde(rename = "false")]
Disabled,
#[default]
Fractional,
}
+137
View File
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs::OpenOptions, path::Path};
use tracing::{error, warn};
#[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, Clone, Deserialize, Serialize)]
pub struct OutputsConfig {
pub config: HashMap<Vec<OutputInfo>, Vec<OutputConfig>>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct OutputConfig {
pub mode: ((i32, i32), Option<u32>),
#[serde(default = "default_sync")]
pub vrr: AdaptiveSync,
pub scale: f64,
pub transform: TransformDef,
pub position: (u32, u32),
#[serde(default = "default_state")]
pub enabled: OutputState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_bpc: Option<u32>,
#[serde(default)]
pub xwayland_primary: bool,
}
impl Default for OutputConfig {
fn default() -> OutputConfig {
OutputConfig {
mode: ((0, 0), None),
vrr: AdaptiveSync::Enabled,
scale: 1.0,
transform: TransformDef::Normal,
position: (0, 0),
enabled: OutputState::Enabled,
max_bpc: None,
xwayland_primary: false,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OutputInfo {
pub connector: String,
pub make: String,
pub model: String,
}
pub fn load_outputs(path: Option<impl AsRef<Path>>) -> OutputsConfig {
if let Some(path) = path.as_ref() {
let path: &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(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransformDef {
Normal,
_90,
_180,
_270,
Flipped,
Flipped90,
Flipped180,
Flipped270,
}
+6
View File
@@ -0,0 +1,6 @@
/// Internal output configurations used by cosmic-comp
pub mod comp;
#[cfg(feature = "randr")]
/// cosmic-randr style output configurations
pub mod randr;
+95
View File
@@ -0,0 +1,95 @@
use std::path::Path;
use cosmic_randr_shell::{AdaptiveSyncState, List};
use crate::output::comp::OutputState;
pub struct CompList {
infos: Vec<super::comp::OutputInfo>,
outputs: Vec<super::comp::OutputConfig>,
}
impl From<CompList> for cosmic_randr_shell::List {
fn from(CompList { infos, outputs }: CompList) -> cosmic_randr_shell::List {
let mut list = cosmic_randr_shell::List::default();
for (info, output) in infos.into_iter().zip(outputs.into_iter()) {
let current = list.modes.insert(cosmic_randr_shell::Mode {
size: (output.mode.0.0 as u32, output.mode.0.1 as u32),
refresh_rate: output.mode.1.unwrap_or_default(),
// XXX not in config as far as i can tell
preferred: false,
});
let modes = vec![current];
// for mode in output. {}
list.outputs.insert(cosmic_randr_shell::Output {
name: info.connector,
enabled: !matches!(output.enabled, OutputState::Disabled),
mirroring: match output.enabled {
OutputState::Mirroring(m) => Some(m),
_ => None,
},
make: Some(info.make).filter(|make| make != "Unknown"),
model: if info.model.as_str() == "Unknown" {
String::new()
} else {
info.model
},
position: (output.position.0 as i32, output.position.1 as i32),
scale: output.scale,
transform: Some(match output.transform {
crate::output::comp::TransformDef::Normal => {
cosmic_randr_shell::Transform::Normal
}
crate::output::comp::TransformDef::_90 => {
cosmic_randr_shell::Transform::Rotate90
}
crate::output::comp::TransformDef::_180 => {
cosmic_randr_shell::Transform::Rotate180
}
crate::output::comp::TransformDef::_270 => {
cosmic_randr_shell::Transform::Rotate270
}
crate::output::comp::TransformDef::Flipped => {
cosmic_randr_shell::Transform::Flipped
}
crate::output::comp::TransformDef::Flipped90 => {
cosmic_randr_shell::Transform::Flipped90
}
crate::output::comp::TransformDef::Flipped180 => {
cosmic_randr_shell::Transform::Flipped180
}
crate::output::comp::TransformDef::Flipped270 => {
cosmic_randr_shell::Transform::Flipped270
}
}),
modes,
current: Some(current),
adaptive_sync: Some(match output.vrr {
crate::output::comp::AdaptiveSync::Enabled => AdaptiveSyncState::Auto,
crate::output::comp::AdaptiveSync::Disabled => AdaptiveSyncState::Disabled,
crate::output::comp::AdaptiveSync::Force => AdaptiveSyncState::Always,
}),
xwayland_primary: Some(output.xwayland_primary),
// XXX no physical output size in the config
physical: (0, 0),
adaptive_sync_availability: None,
serial_number: String::new(),
});
}
list
}
}
pub fn load_outputs(path: Option<impl AsRef<Path>>) -> Vec<List> {
let output_config = crate::output::comp::load_outputs(path);
output_config
.config
.into_iter()
.map(|(infos, outputs)| {
let comp_config = CompList { infos, outputs };
List::from(comp_config)
})
.collect()
}
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
use crate::EdidProduct;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub workspace_mode: WorkspaceMode,
#[serde(default)]
pub workspace_layout: WorkspaceLayout,
#[serde(default)]
pub action_on_typing: Action,
#[serde(default = "default_wraparound")]
pub workspace_wraparound: bool,
}
fn default_wraparound() -> bool {
true
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self {
workspace_mode: WorkspaceMode::default(),
workspace_layout: WorkspaceLayout::default(),
action_on_typing: Action::default(),
workspace_wraparound: default_wraparound(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkspaceMode {
#[default]
OutputBound,
Global,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkspaceLayout {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Action {
#[default]
None,
OpenLauncher,
OpenApplications,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputMatch {
pub name: String,
pub edid: Option<EdidProduct>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinnedWorkspace {
pub output: OutputMatch,
pub tiling_enabled: bool,
pub id: Option<String>,
pub name: Option<String>,
}