diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 3e3a042..af5b059 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -17,13 +17,13 @@ jobs:
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@master
with:
- toolchain: nightly-2025-07-31
+ toolchain: nightly-2026-04-27
- name: System dependencies
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev
- name: Build documentation
run: |
RUSTDOCFLAGS="--cfg docsrs" \
- cargo +nightly-2025-07-31 doc --no-deps \
+ cargo +nightly-2026-04-27 doc --no-deps \
-p cosmic-client-toolkit \
-p cosmic-protocols \
-p libcosmic \
diff --git a/.zed/settings.json b/.zed/settings.json
new file mode 100644
index 0000000..2cc7b98
--- /dev/null
+++ b/.zed/settings.json
@@ -0,0 +1,15 @@
+{
+ "format_on_save": "on",
+ "lsp": {
+ "rust-analyzer": {
+ "initialization_options": {
+ "check": {
+ "command": "clippy",
+ },
+ "rustfmt": {
+ "extraArgs": ["+nightly"],
+ },
+ },
+ },
+ },
+}
diff --git a/Cargo.toml b/Cargo.toml
index d73da2d..738e71f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,20 +1,23 @@
[package]
-name = "libcosmic"
-version = "1.0.0"
+# Yoda fork: hard-renamed. Every consumer (leyoda/cosmic-files fork + each
+# leyoda/cosmic-* app) depends directly on `libcosmic-yoda` by path, bypassing
+# pop-os/libcosmic entirely. No [patch] shenanigans needed — transitive deps
+# that used to ask for `libcosmic` are replaced by deps on our forks that ask
+# for `libcosmic-yoda`.
+name = "libcosmic-yoda"
+version = "0.1.0-yoda.2"
edition = "2024"
-rust-version = "1.90"
+rust-version = "1.93"
[lib]
name = "cosmic"
[features]
default = [
- "winit",
"tokio",
"a11y",
"dbus-config",
- "x11",
- "iced-wayland",
+ "wayland",
"multi-window",
]
advanced-shaping = ["iced/advanced-shaping"]
@@ -35,7 +38,6 @@ animated-image = [
autosize = []
applet = [
"autosize",
- "winit",
"wayland",
"tokio",
"cosmic-panel-config",
@@ -81,32 +83,34 @@ tokio = [
"cosmic-config/tokio",
]
# Tokio async runtime
-# Wayland window support
-iced-wayland = [
+# Wayland window support (yoda fork is Wayland-only; always active in default).
+# We still need iced/winit because pop-os/iced hosts the runtime dispatcher
+# (`iced_winit as shell`) there — the name is a misnomer, it's the same crate
+# that provides both the winit path AND the sctk/cctk wayland path.
+wayland = [
"ashpd?/wayland",
"autosize",
+ "iced/winit",
"iced/wayland",
"iced_winit/wayland",
- "surface-message",
-]
-wayland = [
- "iced-wayland",
"iced_runtime/cctk",
"iced_winit/cctk",
"iced_wgpu/cctk",
"iced/cctk",
+ "dep:iced_winit",
"dep:cctk",
+ "surface-message",
]
surface-message = []
# multi-window support
multi-window = []
# Render with wgpu
wgpu = ["iced/wgpu", "iced_wgpu"]
-# X11 window support via winit
-winit = ["iced/winit", "iced_winit"]
-winit_debug = ["winit", "debug"]
-winit_tokio = ["winit", "tokio"]
-winit_wgpu = ["winit", "wgpu"]
+# Compat stubs — kept empty so upstream deps (cosmic-files, cosmic-text, …)
+# that still ask for `winit` / `x11` features resolve cleanly against the
+# yoda fork. Activating them has no effect: no code is gated on these.
+winit = []
+x11 = []
# Enables XDG portal integrations
xdg-portal = ["ashpd"]
qr_code = ["iced/qr_code"]
@@ -119,18 +123,17 @@ async-std = [
"zbus?/async-io",
"iced/async-std",
]
-x11 = ["iced/x11", "iced_winit/x11"]
[dependencies]
apply = "0.3.0"
ashpd = { version = "0.12.3", default-features = false, optional = true }
async-fs = { version = "2.2", optional = true }
-async-std = { version = "1.13", optional = true }
+async-std = { workspace = true, optional = true }
auto_enums = "0.8.8"
-cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
+cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true }
jiff = "0.2"
cosmic-config = { path = "cosmic-config" }
-cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
+cosmic-settings-config = { path = "../cosmic-settings-daemon/config", optional = true }
# Internationalization
i18n-embed = { version = "0.16.0", features = [
"fluent-system",
@@ -150,34 +153,35 @@ image-extras = { version = "0.1.0", default-features = false, features = [
"xpm",
"xbm",
], optional = true }
-libc = { version = "0.2.183", optional = true }
+libc = { version = "0.2.186", optional = true }
log = "0.4"
mime = { version = "0.3.17", optional = true }
-palette = "0.7.6"
+palette.workspace = true
rfd = { version = "0.16.0", default-features = false, features = [
"xdg-portal",
], optional = true }
rustix = { version = "1.1", features = ["pipe", "process"], optional = true }
-serde = { version = "1.0.228", features = ["derive"] }
+serde = { workspace = true, features = ["derive"] }
slotmap = "1.1.1"
smol = { version = "2.0.2", optional = true }
-thiserror = "2.0.18"
+thiserror.workspace = true
taffy = { version = "0.9.2", features = ["grid"] }
-tokio = { version = "1.50.0", optional = true }
-tracing = "0.1.44"
-unicode-segmentation = "1.12"
+tokio = { workspace = true, optional = true }
+tracing.workspace = true
+unicode-segmentation = "1.13"
url = "2.5.8"
-zbus = { version = "5.14.0", default-features = false, optional = true }
+zbus = { workspace = true, optional = true }
float-cmp = "0.10.0"
+ron = { workspace = true, optional = true }
# Enable DBus feature on Linux targets
[target.'cfg(target_os = "linux")'.dependencies]
cosmic-config = { path = "cosmic-config", features = ["dbus"] }
-cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" }
+cosmic-settings-daemon = { path = "../dbus-settings-bindings/cosmic-settings-daemon" }
zbus = { version = "5.14.0", default-features = false }
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
-freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
+freedesktop-icons = { package = "cosmic-freedesktop-icons", path = "../cosmic-freedesktop-icons" }
freedesktop-desktop-entry = { version = "0.8.1", optional = true }
shlex = { version = "1.3.0", optional = true }
@@ -223,35 +227,77 @@ optional = true
[dependencies.iced_tiny_skia]
path = "./iced/tiny_skia"
+# Yoda: drop the x11 default → softbuffer no longer pulls tiny-xlib/x11-dl/etc.
+default-features = false
+features = ["wayland"]
[dependencies.iced_winit]
path = "./iced/winit"
optional = true
+# Yoda: drop the x11 default → winit won't pull winit-x11/tiny-xlib/x11-dl.
+# Keep wayland + wayland-dlopen (default behaviour minus x11).
+default-features = false
+features = ["wayland", "wayland-dlopen"]
[dependencies.iced_wgpu]
path = "./iced/wgpu"
optional = true
[dependencies.cosmic-panel-config]
-git = "https://github.com/pop-os/cosmic-panel"
-# path = "../cosmic-panel/cosmic-panel-config"
+path = "../cosmic-panel/cosmic-panel-config"
optional = true
-[dependencies.ron]
-version = "0.12"
-optional = true
+[patch.'https://github.com/pop-os/freedesktop-icons']
+cosmic-freedesktop-icons = { path = "../cosmic-freedesktop-icons" }
+
+[patch.'https://github.com/pop-os/softbuffer']
+softbuffer = { path = "../softbuffer" }
+
+[patch.'https://github.com/pop-os/smithay-clipboard']
+smithay-clipboard = { path = "../smithay-clipboard" }
+
+[patch.'https://github.com/pop-os/winit.git']
+dpi = { path = "../winit/dpi" }
+winit = { path = "../winit/winit" }
+winit-android = { path = "../winit/winit-android" }
+winit-appkit = { path = "../winit/winit-appkit" }
+winit-common = { path = "../winit/winit-common" }
+winit-core = { path = "../winit/winit-core" }
+winit-orbital = { path = "../winit/winit-orbital" }
+winit-uikit = { path = "../winit/winit-uikit" }
+winit-wayland = { path = "../winit/winit-wayland" }
+winit-web = { path = "../winit/winit-web" }
+winit-win32 = { path = "../winit/winit-win32" }
+winit-x11 = { path = "../winit/winit-x11" }
[workspace]
members = [
"cosmic-config",
"cosmic-config-derive",
"cosmic-theme",
- "examples/*",
]
-exclude = ["iced"]
+# examples/* excluded — many depend on the removed winit/x11 features.
+# They will be revisited and adapted in a later phase.
+exclude = ["iced", "examples"]
[workspace.dependencies]
-dirs = "6.0.0"
+async-std = "1.13"
+dirs = "6.0"
+palette = "0.7"
+ron = "0.12"
+serde = "1.0"
+thiserror = "2.0"
+tracing = "0.1"
+tokio = "1.52"
+zbus = {version = "5.15", default-features = false}
+
+# Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores
+# [profile.*] blocks in non-root manifests, so this lives at the
+# workspace root.
+[profile.dev.package.insta]
+opt-level = 3
+[profile.dev.package.similar]
+opt-level = 3
[dev-dependencies]
tempfile = "3.27.0"
diff --git a/README.md b/README.md
index 23da97b..698316d 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,24 @@ While libcosmic is written entirely in Rust, some of its dependencies may requir
sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf
```
+## Made-for-COSMIC Flatpak IDs
+
+To identify a project as a COSMIC Application, add `com.system76.CosmicApplication` to the provides section of the project's metainfo.
+
+```xml
+
+ com.system76.CosmicApplication
+
+```
+
+For COSMIC Applets, use `com.system76.CosmicApplet`.
+
+```xml
+
+ com.system76.CosmicApplet
+
+```
+
## Examples
Some examples are included in the [examples](./examples) directory to to kickstart your
diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml
index 0a7653e..a81d13f 100644
--- a/cosmic-config/Cargo.toml
+++ b/cosmic-config/Cargo.toml
@@ -10,21 +10,21 @@ macro = ["cosmic-config-derive"]
subscription = ["iced_futures"]
[dependencies]
-cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
+cosmic-settings-daemon = { path = "../../dbus-settings-bindings/cosmic-settings-daemon", optional = true }
zbus = { version = "5.14.0", default-features = false, optional = true }
atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" }
calloop = { version = "0.14.4", optional = true }
notify = "8.2.0"
-ron = "0.12.0"
-serde = "1.0.228"
+ron.workspace = true
+serde.workspace = true
cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true }
iced = { path = "../iced/", default-features = false, optional = true }
iced_futures = { path = "../iced/futures/", default-features = false, optional = true }
futures-util = { version = "0.3", optional = true }
dirs.workspace = true
-tokio = { version = "1.50", optional = true, features = ["time"] }
-async-std = { version = "1.13", optional = true }
-tracing = "0.1"
+tokio = { workspace = true, optional = true, features = ["time"] }
+async-std = { workspace = true, optional = true }
+tracing.workspace = true
[target.'cfg(unix)'.dependencies]
xdg = "3.0"
diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs
index da7bcb6..9b6e869 100644
--- a/cosmic-config/src/dbus.rs
+++ b/cosmic-config/src/dbus.rs
@@ -1,13 +1,12 @@
-use std::{any::TypeId, ops::Deref};
+use std::any::TypeId;
+use std::ops::Deref;
use crate::{CosmicConfigEntry, Update};
use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy};
use futures_util::SinkExt;
-use iced_futures::{
- Subscription,
- futures::{self, StreamExt, future::pending},
- stream,
-};
+use iced_futures::futures::future::pending;
+use iced_futures::futures::{self, StreamExt};
+use iced_futures::{Subscription, stream};
pub async fn settings_daemon_proxy() -> zbus::Result> {
let conn = zbus::Connection::session().await?;
diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs
index c8eda06..2af498f 100644
--- a/cosmic-config/src/lib.rs
+++ b/cosmic-config/src/lib.rs
@@ -1,16 +1,13 @@
//! Integrations for cosmic-config — the cosmic configuration system.
-use notify::{
- RecommendedWatcher, Watcher,
- event::{EventKind, ModifyKind, RenameMode},
-};
-use serde::{Serialize, de::DeserializeOwned};
-use std::{
- env, fmt, fs,
- io::Write,
- path::{Path, PathBuf},
- sync::Mutex,
-};
+use notify::event::{EventKind, ModifyKind, RenameMode};
+use notify::{RecommendedWatcher, Watcher};
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
+use std::{env, fmt, fs};
/// Get the config directory, with Flatpak sandbox support.
/// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory,
@@ -54,6 +51,22 @@ fn get_state_dir() -> Option {
dirs::state_dir()
}
+/// Get the data directory, with Flatpak sandbox support.
+fn get_data_dir() -> Option {
+ // Check if we're running in Flatpak
+ if env::var_os("FLATPAK_ID").is_some() {
+ // Try HOST_XDG_DATA_HOME first
+ if let Some(host_data) = env::var_os("HOST_XDG_DATA_HOME") {
+ return Some(PathBuf::from(host_data));
+ }
+ // Fallback: try to construct from HOME
+ if let Some(home) = env::var_os("HOME") {
+ return Some(PathBuf::from(home).join(".local").join("share"));
+ }
+ }
+ dirs::data_dir()
+}
+
#[cfg(feature = "subscription")]
mod subscription;
#[cfg(feature = "subscription")]
@@ -266,6 +279,24 @@ impl Config {
})
}
+ /// Get data for the given application name and config version.
+ pub fn new_data(name: &str, version: u64) -> Result {
+ // Look for [name]/v[version]
+ let path = sanitize_name(name)?.join(format!("v{}", version));
+
+ // Get libcosmic user data directory
+ let mut user_path = get_data_dir().ok_or(Error::NoConfigDirectory)?;
+ user_path.push("cosmic");
+ user_path.push(path);
+ // Create new data directory if not found.
+ fs::create_dir_all(&user_path)?;
+
+ Ok(Self {
+ system_path: None,
+ user_path: Some(user_path),
+ })
+ }
+
// Start a transaction (to set multiple configs at the same time)
#[inline]
pub fn transaction(&self) -> ConfigTransaction<'_> {
diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs
index d16b9b6..f038787 100644
--- a/cosmic-config/src/subscription.rs
+++ b/cosmic-config/src/subscription.rs
@@ -1,7 +1,9 @@
+use iced_futures::futures::channel::mpsc;
use iced_futures::futures::{SinkExt, Stream};
-use iced_futures::{futures::channel::mpsc, stream};
+use iced_futures::stream;
use notify::RecommendedWatcher;
-use std::{borrow::Cow, hash::Hash};
+use std::borrow::Cow;
+use std::hash::Hash;
use crate::{Config, CosmicConfigEntry};
@@ -77,7 +79,8 @@ async fn start_listening,
output: &mut mpsc::Sender>,
) -> ConfigState {
- use iced_futures::futures::{StreamExt, future::pending};
+ use iced_futures::futures::StreamExt;
+ use iced_futures::futures::future::pending;
match state {
ConfigState::Init(config_id, version, is_state) => {
diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml
index 7e408d8..b5bffa1 100644
--- a/cosmic-theme/Cargo.toml
+++ b/cosmic-theme/Cargo.toml
@@ -15,13 +15,13 @@ export = ["serde_json"]
no-default = []
[dependencies]
-palette = { version = "0.7.6", features = ["serializing"] }
+palette = { workspace = true, features = ["serializing"] }
almost = "0.2"
-serde = { version = "1.0.228", features = ["derive"] }
+serde = { workspace = true, features = ["derive"] }
serde_json = { version = "1.0.149", optional = true, features = [
"preserve_order",
] }
-ron = "0.12.0"
+ron.workspace = true
csscolorparser = { version = "0.8.3", features = ["serde"] }
cosmic-config = { path = "../cosmic-config/", default-features = false, features = [
"subscription",
@@ -29,11 +29,8 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features
] }
configparser = "3.1.0"
dirs.workspace = true
-thiserror = "2.0.18"
+thiserror.workspace = true
[dev-dependencies]
insta = "1.47.2"
-[profile.dev.package]
-insta.opt-level = 3
-similar.opt-level = 3
diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs
index ecd18c0..f2fa95e 100644
--- a/cosmic-theme/src/model/corner.rs
+++ b/cosmic-theme/src/model/corner.rs
@@ -29,3 +29,51 @@ impl Default for CornerRadii {
}
}
}
+
+/// Roundness options for the Cosmic theme
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub enum Roundness {
+ /// Round style
+ #[default]
+ Round,
+ /// Slightly round style
+ SlightlyRound,
+ /// Square style
+ Square,
+}
+
+impl From for CornerRadii {
+ fn from(value: Roundness) -> Self {
+ match value {
+ Roundness::Round => CornerRadii::default(),
+ Roundness::SlightlyRound => CornerRadii {
+ radius_0: [0.0; 4],
+ radius_xs: [2.0; 4],
+ radius_s: [8.0; 4],
+ radius_m: [8.0; 4],
+ radius_l: [8.0; 4],
+ radius_xl: [8.0; 4],
+ },
+ Roundness::Square => CornerRadii {
+ radius_0: [0.0; 4],
+ radius_xs: [2.0; 4],
+ radius_s: [2.0; 4],
+ radius_m: [2.0; 4],
+ radius_l: [2.0; 4],
+ radius_xl: [2.0; 4],
+ },
+ }
+ }
+}
+
+impl From for Roundness {
+ fn from(value: CornerRadii) -> Self {
+ if (value.radius_m[0] - 16.0).abs() < 0.01 {
+ Self::Round
+ } else if (value.radius_m[0] - 8.0).abs() < 0.01 {
+ Self::SlightlyRound
+ } else {
+ Self::Square
+ }
+ }
+}
diff --git a/cosmic-theme/src/model/density.rs b/cosmic-theme/src/model/density.rs
deleted file mode 100644
index 7655361..0000000
--- a/cosmic-theme/src/model/density.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-use crate::Spacing;
-use serde::{Deserialize, Serialize};
-
-/// Density options for the Cosmic theme
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
-pub enum Density {
- /// Lower padding/spacing of elements
- Compact,
- /// Higher padding/spacing of elements
- Spacious,
- /// Standard padding/spacing of elements
- #[default]
- Standard,
-}
-
-impl From for Spacing {
- fn from(value: Density) -> Self {
- match value {
- Density::Compact => Spacing {
- space_none: 0,
- space_xxxs: 4,
- space_xxs: 4,
- space_xs: 8,
- space_s: 8,
- space_m: 16,
- space_l: 24,
- space_xl: 32,
- space_xxl: 48,
- space_xxxl: 64,
- },
- Density::Spacious => Spacing {
- space_none: 4,
- space_xxxs: 8,
- space_xxs: 12,
- space_xs: 16,
- space_s: 24,
- space_m: 32,
- space_l: 48,
- space_xl: 64,
- space_xxl: 128,
- space_xxxl: 160,
- },
- Density::Standard => Spacing {
- space_none: 0,
- space_xxxs: 4,
- space_xxs: 8,
- space_xs: 12,
- space_s: 16,
- space_m: 24,
- space_l: 32,
- space_xl: 48,
- space_xxl: 64,
- space_xxxl: 128,
- },
- }
- }
-}
-
-impl From for Density {
- fn from(value: Spacing) -> Self {
- if value.space_m.saturating_sub(16) == 0 {
- Self::Compact
- } else if value.space_m.saturating_sub(24) == 0 {
- Self::Standard
- } else {
- Self::Spacious
- }
- }
-}
diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs
index f48d1a8..19370de 100644
--- a/cosmic-theme/src/model/mod.rs
+++ b/cosmic-theme/src/model/mod.rs
@@ -1,6 +1,5 @@
pub use corner::*;
pub use cosmic_palette::*;
-pub use density::*;
pub use derivation::*;
pub use mode::*;
pub use spacing::*;
@@ -8,7 +7,6 @@ pub use theme::*;
mod corner;
mod cosmic_palette;
-mod density;
mod derivation;
mod mode;
mod spacing;
diff --git a/cosmic-theme/src/model/spacing.rs b/cosmic-theme/src/model/spacing.rs
index 93b1bf4..f02cf51 100644
--- a/cosmic-theme/src/model/spacing.rs
+++ b/cosmic-theme/src/model/spacing.rs
@@ -41,3 +41,59 @@ impl Default for Spacing {
}
}
}
+
+/// Density options for the Cosmic theme
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub enum Density {
+ /// Lower padding/spacing of elements
+ Compact,
+ /// Standard padding/spacing of elements
+ #[default]
+ Standard,
+ /// Higher padding/spacing of elements
+ Spacious,
+}
+
+impl From for Spacing {
+ fn from(value: Density) -> Self {
+ match value {
+ Density::Compact => Spacing {
+ space_none: 0,
+ space_xxxs: 4,
+ space_xxs: 4,
+ space_xs: 8,
+ space_s: 8,
+ space_m: 16,
+ space_l: 24,
+ space_xl: 32,
+ space_xxl: 48,
+ space_xxxl: 64,
+ },
+ Density::Standard => Spacing::default(),
+ Density::Spacious => Spacing {
+ space_none: 4,
+ space_xxxs: 8,
+ space_xxs: 12,
+ space_xs: 16,
+ space_s: 24,
+ space_m: 32,
+ space_l: 48,
+ space_xl: 64,
+ space_xxl: 128,
+ space_xxxl: 160,
+ },
+ }
+ }
+}
+
+impl From for Density {
+ fn from(value: Spacing) -> Self {
+ if value.space_m.saturating_sub(16) == 0 {
+ Self::Compact
+ } else if value.space_m.saturating_sub(24) == 0 {
+ Self::Standard
+ } else {
+ Self::Spacious
+ }
+ }
+}
diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs
index 5db0f32..0c06005 100644
--- a/cosmic-theme/src/model/theme.rs
+++ b/cosmic-theme/src/model/theme.rs
@@ -1,13 +1,13 @@
+use crate::composite::over;
+use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps};
use crate::{
Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE,
LIGHT_PALETTE, NAME, Spacing, ThemeMode,
- composite::over,
- steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps},
};
use cosmic_config::{Config, CosmicConfigEntry};
-use palette::{
- IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb,
-};
+use palette::color_difference::Wcag21RelativeContrast;
+use palette::rgb::Rgb;
+use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha};
use serde::{Deserialize, Serialize};
use std::num::NonZeroUsize;
@@ -75,6 +75,8 @@ pub struct Theme {
pub icon_button: Component,
/// link button element colors
pub link_button: Component,
+ /// list button element colors
+ pub list_button: Component,
/// text button element colors
pub text_button: Component,
/// button component styling
@@ -953,6 +955,12 @@ impl ThemeBuilder {
}
#[allow(clippy::too_many_lines)]
+ // The component_hovered/pressed_overlay vars are seeded once near the
+ // top of this fn and then reassigned inside each container block
+ // (primary, secondary, …) before being read again. The initial seed
+ // is therefore overwritten before any read, which is what the
+ // unused_assignments lint flags below.
+ #[allow(unused_assignments)]
/// build the theme
pub fn build(self) -> Theme {
let Self {
@@ -1285,6 +1293,15 @@ impl ThemeBuilder {
component.on_disabled = over(component.on.with_alpha(0.5), component.base);
component
},
+ list_button: Component::component(
+ Srgba::new(0.0, 0.0, 0.0, 0.0),
+ accent,
+ on_bg_component,
+ Srgba::new(0.0, 0.0, 0.0, 0.0),
+ button_pressed_overlay,
+ is_high_contrast,
+ control_steps_array[8],
+ ),
success: Component::colored_component(
success,
control_steps_array[0],
diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs
index 40eba5b..bbb4f24 100644
--- a/cosmic-theme/src/output/gtk4_output.rs
+++ b/cosmic-theme/src/output/gtk4_output.rs
@@ -1,10 +1,12 @@
use crate::{Component, Theme, composite::over, steps::steps};
+use configparser::ini::Ini;
use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba};
use std::{
fs::{self, File},
io::{self, Write},
num::NonZeroUsize,
path::Path,
+ process::Command,
};
use super::{OutputError, to_rgba};
@@ -217,6 +219,50 @@ impl Theme {
Ok(())
}
+ /// Apply the preferred GTK client-side decoration button layout.
+ ///
+ /// This writes the GTK 3/4 `settings.ini` value used by GTK header bars and
+ /// also best-effort updates GNOME's `button-layout` GSettings key for apps
+ /// that still consult it.
+ ///
+ /// # Errors
+ ///
+ /// Returns an `OutputError` if the GTK settings files cannot be written.
+ #[cold]
+ pub fn apply_gtk_decoration_layout(buttons_at_start: bool) -> Result<(), OutputError> {
+ let Some(config_dir) = dirs::config_dir() else {
+ return Err(OutputError::MissingConfigDir);
+ };
+
+ let layout = if buttons_at_start {
+ "close,minimize,maximize:"
+ } else {
+ ":minimize,maximize,close"
+ };
+
+ for gtk_version in ["gtk-3.0", "gtk-4.0"] {
+ let gtk_dir = config_dir.join(gtk_version);
+ fs::create_dir_all(>k_dir).map_err(OutputError::Io)?;
+ Self::write_gtk_settings_key(
+ >k_dir.join("settings.ini"),
+ "gtk-decoration-layout",
+ layout,
+ )?;
+ }
+
+ // best-effort: gsettings is absent on non-GNOME systems
+ let _ = Command::new("gsettings")
+ .args([
+ "set",
+ "org.gnome.desktop.wm.preferences",
+ "button-layout",
+ layout,
+ ])
+ .status();
+
+ Ok(())
+ }
+
/// Reset the applied gtk css
///
/// # Errors
@@ -256,6 +302,20 @@ impl Theme {
Ok(())
}
+ #[cold]
+ fn write_gtk_settings_key(path: &Path, key: &str, value: &str) -> Result<(), OutputError> {
+ let mut ini = Ini::new_cs();
+
+ if path.exists() {
+ let file_content = fs::read_to_string(path).map_err(OutputError::Io)?;
+ ini.read(file_content).map_err(OutputError::Ini)?;
+ }
+
+ ini.setstr("Settings", key, Some(value));
+ ini.pretty_write(path, &super::qt_settings_ini_style())
+ .map_err(OutputError::Io)
+ }
+
fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result