feat: initial cosmic-gtk-config bridge
Daemon user-mode qui relie le setting CosmicTk.window_controls_position
aux fenêtres non-libcosmic :
- met à jour gtk-decoration-layout dans ~/.config/gtk-{3,4}.0/settings.ini
- aligne org.gnome.desktop.wm.preferences button-layout via gsettings
- abonné via Config::watch (cosmic-config) — propagation à chaud
Pendant : kde-gtk-config / Plasma. Limites connues : Firefox + apps
Electron qui dessinent leur propre titlebar (phase 2).
By Leyoda 2026 — MIT
This commit is contained in:
commit
a61c95040e
5 changed files with 249 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "cosmic-gtk-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
description = "Bridges the COSMIC window-controls-position setting to GTK3, GTK4 and gsettings, à la kde-gtk-config."
|
||||||
|
authors = ["Lionel Darnis <ldarnis@gmail.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cosmic-config = { path = "../libcosmic/cosmic-config", default-features = false, features = ["macro"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
anyhow = "1"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Lionel Darnis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
13
services/cosmic-gtk-config.service
Normal file
13
services/cosmic-gtk-config.service
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[Unit]
|
||||||
|
Description=COSMIC -> GTK config bridge (window controls position, theme...)
|
||||||
|
PartOf=cosmic-session.target
|
||||||
|
After=cosmic-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/cosmic-gtk-config
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=cosmic-session.target
|
||||||
192
src/main.rs
Normal file
192
src/main.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use cosmic_config::{Config, ConfigGet};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const COSMIC_TK_ID: &str = "com.system76.CosmicTk";
|
||||||
|
const COSMIC_TK_VERSION: u64 = 1;
|
||||||
|
const KEY: &str = "window_controls_position";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
enum WindowControlsPosition {
|
||||||
|
Start,
|
||||||
|
#[default]
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowControlsPosition {
|
||||||
|
fn gtk_layout(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Start => "close,minimize,maximize:",
|
||||||
|
Self::End => "menu:minimize,maximize,close",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home_path(rel: &str) -> PathBuf {
|
||||||
|
let home = std::env::var_os("HOME").expect("HOME unset");
|
||||||
|
PathBuf::from(home).join(rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_decoration_layout(content: &str, layout: &str) -> String {
|
||||||
|
let mut found = false;
|
||||||
|
let mut out = String::with_capacity(content.len() + 64);
|
||||||
|
let trailing_newline = content.ends_with('\n');
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.starts_with("gtk-decoration-layout=") {
|
||||||
|
out.push_str("gtk-decoration-layout=");
|
||||||
|
out.push_str(layout);
|
||||||
|
out.push('\n');
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if !out.contains("[Settings]") {
|
||||||
|
out.push_str("[Settings]\n");
|
||||||
|
}
|
||||||
|
out.push_str("gtk-decoration-layout=");
|
||||||
|
out.push_str(layout);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if !trailing_newline && !out.is_empty() {
|
||||||
|
out.pop();
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gtk_ini(path: &Path, layout: &str) -> Result<()> {
|
||||||
|
let content = if path.exists() {
|
||||||
|
std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("read {}", path.display()))?
|
||||||
|
} else {
|
||||||
|
String::from("[Settings]\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_content = rewrite_decoration_layout(&content, layout);
|
||||||
|
if new_content == content {
|
||||||
|
log::debug!("{}: already up-to-date", path.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = path.with_extension("ini.tmp");
|
||||||
|
std::fs::write(&tmp, &new_content)
|
||||||
|
.with_context(|| format!("write {}", tmp.display()))?;
|
||||||
|
std::fs::rename(&tmp, path)
|
||||||
|
.with_context(|| format!("rename to {}", path.display()))?;
|
||||||
|
|
||||||
|
log::info!("updated {}", path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gsettings(layout: &str) -> Result<()> {
|
||||||
|
let status = Command::new("gsettings")
|
||||||
|
.args([
|
||||||
|
"set",
|
||||||
|
"org.gnome.desktop.wm.preferences",
|
||||||
|
"button-layout",
|
||||||
|
layout,
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.context("spawn gsettings")?;
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("gsettings exited with {}", status);
|
||||||
|
}
|
||||||
|
log::info!("gsettings button-layout = {}", layout);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(pos: WindowControlsPosition) -> Result<()> {
|
||||||
|
let layout = pos.gtk_layout();
|
||||||
|
log::info!("applying position={:?} layout={}", pos, layout);
|
||||||
|
update_gtk_ini(&home_path(".config/gtk-3.0/settings.ini"), layout)?;
|
||||||
|
update_gtk_ini(&home_path(".config/gtk-4.0/settings.ini"), layout)?;
|
||||||
|
update_gsettings(layout)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pos(config: &Config) -> WindowControlsPosition {
|
||||||
|
config
|
||||||
|
.get::<WindowControlsPosition>(KEY)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
env_logger::Builder::from_env(
|
||||||
|
env_logger::Env::default().default_filter_or("info"),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let config = Config::new(COSMIC_TK_ID, COSMIC_TK_VERSION)
|
||||||
|
.context("open com.system76.CosmicTk config")?;
|
||||||
|
|
||||||
|
let initial = read_pos(&config);
|
||||||
|
log::info!("initial position: {:?}", initial);
|
||||||
|
apply(initial).context("initial apply failed")?;
|
||||||
|
|
||||||
|
let _watcher = config
|
||||||
|
.watch(|cfg, keys| {
|
||||||
|
if !keys.iter().any(|k| k == KEY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pos = cfg
|
||||||
|
.get::<WindowControlsPosition>(KEY)
|
||||||
|
.unwrap_or_default();
|
||||||
|
log::info!("change detected: {:?}", pos);
|
||||||
|
if let Err(err) = apply(pos) {
|
||||||
|
log::error!("apply failed: {:#}", err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("setup watcher")?;
|
||||||
|
|
||||||
|
log::info!("daemon running, watching {}", COSMIC_TK_ID);
|
||||||
|
std::thread::park();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_replaces_existing_line() {
|
||||||
|
let input = "[Settings]\ngtk-theme-name=Adwaita\ngtk-decoration-layout=close,minimize,maximize:\ngtk-font-name=Sans 10\n";
|
||||||
|
let out = rewrite_decoration_layout(input, "menu:minimize,maximize,close");
|
||||||
|
assert!(out.contains("gtk-decoration-layout=menu:minimize,maximize,close\n"));
|
||||||
|
assert!(!out.contains("close,minimize,maximize:"));
|
||||||
|
assert!(out.contains("gtk-theme-name=Adwaita"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_appends_when_absent() {
|
||||||
|
let input = "[Settings]\ngtk-theme-name=Adwaita\n";
|
||||||
|
let out = rewrite_decoration_layout(input, "close,minimize,maximize:");
|
||||||
|
assert!(out.contains("gtk-decoration-layout=close,minimize,maximize:\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_creates_section_when_empty() {
|
||||||
|
let input = "";
|
||||||
|
let out = rewrite_decoration_layout(input, "close,minimize,maximize:");
|
||||||
|
assert!(out.contains("[Settings]"));
|
||||||
|
assert!(out.contains("gtk-decoration-layout=close,minimize,maximize:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_preserves_no_trailing_newline() {
|
||||||
|
let input = "[Settings]\ngtk-decoration-layout=:minimize,maximize,close";
|
||||||
|
let out = rewrite_decoration_layout(input, "close,minimize,maximize:");
|
||||||
|
assert!(!out.ends_with('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue