srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/srdwm/src/main.rs93
-rw-r--r--docs/DEFAULTS.md25
-rw-r--r--docs/TODO.md33
3 files changed, 150 insertions, 1 deletions
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index 3f4fe34..7670b99 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -126,6 +126,95 @@ const CONFIG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs
/// not an error worth interrupting startup for. The compositor's own
/// titlebars are already correct either way; this only brings the
/// self-decorating clients into line.
+/// The stylesheet srdwm owns and rewrites, named so it is obvious in a
+/// directory listing which file is generated and which is the user's.
+const GTK_STYLE_FILE: &str = "srdwm-buttons.css";
+
+/// The one line srdwm adds to the user's own `gtk.css`. First in the file,
+/// because CSS `@import` is only valid before other rules - which also
+/// gives the user's own rules the last word, since later rules win.
+const GTK_STYLE_IMPORT: &str = "@import url(\"srdwm-buttons.css\");";
+
+/// The generated stylesheet body for the configured button style.
+///
+/// Only `button_style` matters here. Which *side* the buttons sit on is not
+/// a CSS question - that is the desktop's button-layout preference, which
+/// `publish_gtk_button_layout` handles.
+fn gtk_button_css(traffic_lights: bool) -> String {
+ let header = "/* Generated by srdwm. Do not edit: rewritten on every start and every\n config reload, from theme.decorations.title_bar.button_style.\n\n Imported from gtk.css, which is yours - srdwm only ever adds the one\n @import line to it and never touches anything else in that file. Rules\n you write there come after this import and therefore win. */\n\n";
+ // WhiteSur, and themes like it, paint the control as the button's own
+ // `background-image` from a compiled gresource rather than as a child
+ // `image` widget. Clearing that background is what leaves a button with
+ // nothing drawn in it at all, so each style sets a background of its
+ // own rather than relying on un-hiding something underneath.
+ let base = "headerbar button.titlebutton,\n.solid-csd headerbar button.titlebutton {\n background-image: none;\n background-color: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n}\n\n";
+ if traffic_lights {
+ format!(
+ "{header}{base}headerbar button.titlebutton {{\n min-width: 13px;\n min-height: 13px;\n border-radius: 9999px;\n}}\n\nheaderbar button.titlebutton > image {{\n opacity: 0;\n}}\n\nheaderbar button.titlebutton.close {{\n background-image: radial-gradient(circle at 32% 28%, #ffb3ad 0%, #ff5f57 42%, #dd4b43 100%);\n}}\n\nheaderbar button.titlebutton.minimize {{\n background-image: radial-gradient(circle at 32% 28%, #ffe2a8 0%, #ffbd2e 42%, #dba024 100%);\n}}\n\nheaderbar button.titlebutton.maximize {{\n background-image: radial-gradient(circle at 32% 28%, #9df0a6 0%, #28c840 42%, #1f9f34 100%);\n}}\n\nheaderbar button.titlebutton:hover {{\n filter: brightness(1.12);\n}}\n\nheaderbar button.titlebutton:active {{\n filter: brightness(0.9);\n}}\n"
+ )
+ } else {
+ let icon = |class: &str, name: &str| {
+ format!(
+ "headerbar button.titlebutton.{class} {{\n background-image: -gtk-icontheme(\"{name}\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n}}\n\n"
+ )
+ };
+ format!(
+ "{header}{base}headerbar button.titlebutton {{\n min-width: 24px;\n min-height: 24px;\n border-radius: 4px;\n}}\n\nheaderbar button.titlebutton > image {{\n opacity: 1;\n}}\n\n{}{}{}headerbar button.titlebutton:hover {{\n background-color: alpha(currentColor, 0.14);\n}}\n\nheaderbar button.titlebutton:active {{\n background-color: alpha(currentColor, 0.22);\n}}\n",
+ icon("close", "window-close-symbolic"),
+ icon("minimize", "window-minimize-symbolic"),
+ icon("maximize", "window-maximize-symbolic"),
+ )
+ }
+}
+
+/// Writes srdwm's own GTK stylesheet and makes sure the user's `gtk.css`
+/// imports it, for both GTK 3 and GTK 4.
+///
+/// A GTK app draws its own window buttons from its GTK theme, and no
+/// compositor protocol reaches those (GTK never negotiates decoration at
+/// all). A user stylesheet does, because it loads after the theme - so
+/// this is the only channel by which `button_style` can govern a
+/// self-decorating app, and without it "set the style once" stops at
+/// srdwm's own titlebars.
+///
+/// srdwm writes only its own file. The user's `gtk.css` gets one `@import`
+/// line added if it is missing, and is otherwise never touched: it is
+/// theirs, it routinely holds hand-written work, and silently rewriting it
+/// would destroy customisation this code cannot understand. Their rules
+/// come after the import and therefore still win.
+///
+/// Best-effort: a missing directory or an unwritable file is logged at
+/// debug and skipped. srdwm's own titlebars are already correct regardless.
+fn publish_gtk_stylesheet(wm: &Rc<RefCell<WindowManager>>) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let traffic_lights = wm.borrow().theme.traffic_light_buttons;
+ let css = gtk_button_css(traffic_lights);
+ for version in ["gtk-3.0", "gtk-4.0"] {
+ let dir = std::path::Path::new(&home).join(".config").join(version);
+ if !dir.is_dir() {
+ continue;
+ }
+ if let Err(e) = std::fs::write(dir.join(GTK_STYLE_FILE), &css) {
+ log::debug!("couldn't write {version}/{GTK_STYLE_FILE} ({e}); GTK keeps its own button style");
+ continue;
+ }
+ let user_css = dir.join("gtk.css");
+ let existing = std::fs::read_to_string(&user_css).unwrap_or_default();
+ if existing.contains(GTK_STYLE_IMPORT) {
+ continue;
+ }
+ // Prepended, not appended: `@import` is only valid before any other
+ // rule, and putting it first is also what leaves the user's own
+ // rules able to override these.
+ let merged = format!("{GTK_STYLE_IMPORT}\n\n{existing}");
+ if let Err(e) = std::fs::write(&user_css, merged) {
+ log::debug!("couldn't add the srdwm import to {version}/gtk.css ({e})");
+ } else {
+ log::info!("added {GTK_STYLE_IMPORT} to {version}/gtk.css");
+ }
+ }
+}
+
fn publish_gtk_button_layout(wm: &Rc<RefCell<WindowManager>>) {
let layout = if wm.borrow().theme.buttons_left { "close,minimize,maximize:" } else { ":minimize,maximize,close" };
match std::process::Command::new("gsettings")
@@ -758,6 +847,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// `button_side` into the theme - publishing before it would broadcast
// the built-in default rather than the user's choice.
publish_gtk_button_layout(&wm);
+ publish_gtk_stylesheet(&wm);
apply_default_layout(&engine, &wm);
let running = engine.running_flag();
// `general.config_reload_on_write` - on by default. A programmable
@@ -868,6 +958,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
apply_general_settings(&engine, &wm);
publish_keybindings(&engine, &wm);
publish_gtk_button_layout(&wm);
+ publish_gtk_stylesheet(&wm);
// After `apply_general_settings`, which rebuilds the
// theme from the config file - see
// `WindowManager::live_settings` for why a hand-made
@@ -892,6 +983,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
apply_general_settings(&engine, &wm);
publish_keybindings(&engine, &wm);
publish_gtk_button_layout(&wm);
+ publish_gtk_stylesheet(&wm);
srdwm_platform::replay_live_settings(&wm);
// After the reload, so a handler edited in the config since
// startup is the one that runs.
@@ -911,6 +1003,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
apply_general_settings(&engine, &wm);
publish_keybindings(&engine, &wm);
publish_gtk_button_layout(&wm);
+ publish_gtk_stylesheet(&wm);
srdwm_platform::replay_live_settings(&wm);
} else if !engine.dispatch_keybinding(&combo) {
log::debug!("no binding for '{combo}'");
diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md
index 217075b..014c6f5 100644
--- a/docs/DEFAULTS.md
+++ b/docs/DEFAULTS.md
@@ -1052,7 +1052,30 @@ setting: buttons moved from x=25/49/73 (left) to x=817/841/865 (right),
landing in the same place and the same order as srdwm's own titlebar buttons
directly above them.
-### Button style: a user CSS override
+### Button style: srdwm generates the CSS
+
+srdwm writes `~/.config/gtk-3.0/srdwm-buttons.css` (and the GTK 4 copy) from
+`theme.decorations.title_bar.button_style`, and rewrites it on every start
+and every config reload. It adds one line to your own `gtk.css`:
+
+```css
+@import url("srdwm-buttons.css");
+```
+
+That line is added once if missing and nothing else in the file is ever
+touched. The import sits first because CSS only allows `@import` before
+other rules - which also means your own rules come after it and win, so
+anything you write in `gtk.css` still overrides the generated style.
+
+Verified end to end on a real GTK app, both ways: with `button_style =
+"traffic_lights"` Nemo's own header draws coloured dots, and with
+`"traditional"` it draws a dash, a square and an X - in both cases matching
+srdwm's own titlebar directly above it.
+
+srdwm writes only its own file, and skips a GTK version whose config
+directory does not exist rather than creating one.
+
+### Doing it by hand instead
srdwm's `button_style` draws srdwm's own titlebar. A GTK app draws its own
buttons from its GTK theme, and no compositor setting reaches those - but
diff --git a/docs/TODO.md b/docs/TODO.md
index 74eed81..18e551a 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,5 +1,38 @@
# TODO / planned features - master checklist
+## srdwm now generates the GTK button stylesheet too (2026-08-28)
+
+The previous entry recorded a deliberate decision *not* to write GTK CSS,
+because the user's `gtk.css` holds hand-written work a compositor cannot
+safely rewrite. Asked to build it anyway, which is reasonable - the
+objection was to clobbering their file, not to the feature.
+
+The split solves both: srdwm owns `srdwm-buttons.css`, generated from
+`button_style` and rewritten on every start and reload, and adds exactly one
+`@import` line to `gtk.css` if it is missing. Nothing else in that file is
+ever touched. The import is first because CSS only permits `@import` ahead
+of other rules, which conveniently also leaves the user's own rules last and
+therefore winning.
+
+The generated CSS sets a background per button rather than un-hiding a child
+`image`, for the reason the previous attempt found the hard way: WhiteSur
+paints the control as the button's own `background-image` from a compiled
+gresource, so clearing the background leaves a blank button.
+
+Verified end to end on a real GTK app, both directions, through the
+generated file rather than a hand-written one: `traffic_lights` gives Nemo
+coloured dots, `traditional` gives it a dash, a square and an X, each time
+matching srdwm's own titlebar directly above. Also verified that the import
+is added once and not duplicated on a second run, that switching the style
+rewrites only the generated file, and that a home directory without GTK
+config directories has nothing written to it at all.
+
+Left in place: `gtk-traffic-lights.css`, the user's original hand-written
+override, kept as reference. `gtk.css` is now the import plus a comment
+saying what owns what.
+
+277 core tests, 527 total, clippy clean.
+
## Firefox and Nemo's traffic lights: the compositor was never drawing them (2026-08-28)
Reported as still using traffic lights after the button *side* was fixed.