From f33ee1c88946cc0c7dcb7012e6059e8bc6b05fb8 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Tue, 5 May 2026 14:07:02 +0200 Subject: [PATCH] feat(firefox): propage le toggle vers les profils Firefox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module firefox.rs : parse ~/.mozilla/firefox/profiles.ini, écrit un bloc délimité (BEGIN/END markers) dans user.js de chaque profil avec widget.gtk.decoration-layout aligné sur le layout COSMIC. Sans toucher aux préfs existantes du profil. Limite : Firefox doit être redémarré pour relire user.js. Vérifié sur 2 profils (default et "Profil 1") — boutons positionnés correctement après restart Firefox. By Leyoda 2026 — MIT --- Cargo.toml | 3 + src/firefox.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 ++ 3 files changed, 231 insertions(+) create mode 100644 src/firefox.rs diff --git a/Cargo.toml b/Cargo.toml index 15596d1..e8eb9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ anyhow = "1" log = "0.4" env_logger = "0.11" +[dev-dependencies] +tempfile = "3" + [profile.release] lto = "thin" codegen-units = 1 diff --git a/src/firefox.rs b/src/firefox.rs new file mode 100644 index 0000000..bf98db9 --- /dev/null +++ b/src/firefox.rs @@ -0,0 +1,223 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; + +const BEGIN_MARKER: &str = "// === BEGIN cosmic-gtk-config ==="; +const END_MARKER: &str = "// === END cosmic-gtk-config ==="; + +pub fn apply(layout: &str) -> Result<()> { + let Some(home) = std::env::var_os("HOME") else { + return Ok(()); + }; + let firefox_dir = PathBuf::from(home).join(".mozilla/firefox"); + let profiles = read_profiles_ini(&firefox_dir); + if profiles.is_empty() { + log::debug!("firefox: no profile detected, skipping"); + return Ok(()); + } + for profile in profiles { + if let Err(err) = update_user_js(&profile, layout) { + log::warn!("firefox profile {}: {:#}", profile.display(), err); + } + } + Ok(()) +} + +fn read_profiles_ini(firefox_dir: &Path) -> Vec { + let Ok(content) = std::fs::read_to_string(firefox_dir.join("profiles.ini")) else { + return Vec::new(); + }; + + let mut profiles = Vec::new(); + let mut in_profile = false; + let mut path_value: Option = None; + let mut is_relative = true; + + let commit = |relative: bool, path: Option, profiles: &mut Vec| { + if let Some(p) = path { + let pb = if relative { + firefox_dir.join(&p) + } else { + PathBuf::from(p) + }; + profiles.push(pb); + } + }; + + for line in content.lines() { + let line = line.trim(); + if let Some(section) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + if in_profile { + commit(is_relative, path_value.take(), &mut profiles); + } + in_profile = section.starts_with("Profile"); + path_value = None; + is_relative = true; + } else if in_profile { + if let Some(value) = line.strip_prefix("Path=") { + path_value = Some(value.to_string()); + } else if let Some(value) = line.strip_prefix("IsRelative=") { + is_relative = value != "0"; + } + } + } + if in_profile { + commit(is_relative, path_value, &mut profiles); + } + profiles +} + +fn update_user_js(profile: &Path, layout: &str) -> Result<()> { + if !profile.is_dir() { + log::debug!("skipping non-existent profile {}", profile.display()); + return Ok(()); + } + let user_js = profile.join("user.js"); + let content = if user_js.exists() { + std::fs::read_to_string(&user_js) + .with_context(|| format!("read {}", user_js.display()))? + } else { + String::new() + }; + + let new_content = rewrite_user_js(&content, layout); + if new_content == content { + log::debug!("{}: already up-to-date", user_js.display()); + return Ok(()); + } + + let tmp = user_js.with_extension("js.tmp"); + std::fs::write(&tmp, &new_content) + .with_context(|| format!("write {}", tmp.display()))?; + std::fs::rename(&tmp, &user_js) + .with_context(|| format!("rename to {}", user_js.display()))?; + + log::info!("updated {} (Firefox restart required)", user_js.display()); + Ok(()) +} + +fn rewrite_user_js(content: &str, layout: &str) -> String { + let block = format!( + "{BEGIN_MARKER}\n\ + user_pref(\"widget.gtk.non-native-titlebar-buttons.enabled\", false);\n\ + user_pref(\"widget.gtk.decoration-layout\", \"{layout}\");\n\ + {END_MARKER}\n", + ); + + if let (Some(begin), Some(end)) = (content.find(BEGIN_MARKER), content.find(END_MARKER)) { + let end_line_end = content[end..] + .find('\n') + .map(|n| end + n + 1) + .unwrap_or(content.len()); + let mut out = String::with_capacity(content.len()); + out.push_str(&content[..begin]); + out.push_str(&block); + out.push_str(&content[end_line_end..]); + return out; + } + + let mut out = String::with_capacity(content.len() + block.len() + 1); + out.push_str(content); + if !content.is_empty() && !content.ends_with('\n') { + out.push('\n'); + } + if !content.is_empty() && !content.ends_with("\n\n") { + out.push('\n'); + } + out.push_str(&block); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rewrite_appends_to_empty() { + let out = rewrite_user_js("", "close,minimize,maximize:"); + assert!(out.starts_with(BEGIN_MARKER)); + assert!(out.contains("decoration-layout\", \"close,minimize,maximize:\"")); + assert!(out.contains(END_MARKER)); + } + + #[test] + fn rewrite_replaces_existing_block() { + let original = format!( + "user_pref(\"general.warnOnAboutConfig\", false);\n\n{BEGIN_MARKER}\n\ + user_pref(\"widget.gtk.non-native-titlebar-buttons.enabled\", false);\n\ + user_pref(\"widget.gtk.decoration-layout\", \"close,minimize,maximize:\");\n\ + {END_MARKER}\n\nuser_pref(\"browser.tabs.warnOnClose\", false);\n" + ); + let out = rewrite_user_js(&original, "menu:minimize,maximize,close"); + + assert!(out.contains("decoration-layout\", \"menu:minimize,maximize,close\"")); + assert!(!out.contains("decoration-layout\", \"close,minimize,maximize:\"")); + assert!(out.contains("user_pref(\"general.warnOnAboutConfig\", false)")); + assert!(out.contains("user_pref(\"browser.tabs.warnOnClose\", false)")); + + let begin_count = out.matches(BEGIN_MARKER).count(); + let end_count = out.matches(END_MARKER).count(); + assert_eq!(begin_count, 1); + assert_eq!(end_count, 1); + } + + #[test] + fn rewrite_appends_after_existing_content_with_blank_line() { + let original = "user_pref(\"foo\", true);\n"; + let out = rewrite_user_js(original, "close,minimize,maximize:"); + assert!(out.contains("user_pref(\"foo\", true);")); + assert!(out.contains(BEGIN_MARKER)); + assert!(out.contains("\n\n// === BEGIN")); + } + + #[test] + fn rewrite_idempotent() { + let out1 = rewrite_user_js("", "close,minimize,maximize:"); + let out2 = rewrite_user_js(&out1, "close,minimize,maximize:"); + assert_eq!(out1, out2); + } + + #[test] + fn parse_profiles_ini_simple() { + let dir = tempfile::tempdir().unwrap(); + let ini = "\ +[Install4F96D1932A9F858E]\n\ +Default=YyVZB8DZ.Profil 1\n\ +Locked=1\n\ +\n\ +[Profile1]\n\ +Name=default\n\ +IsRelative=1\n\ +Path=p65kjdux.default\n\ +Default=1\n\ +\n\ +[Profile0]\n\ +Name=default-release\n\ +IsRelative=1\n\ +Path=YyVZB8DZ.Profil 1\n\ +\n\ +[General]\n\ +StartWithLastProfile=1\n\ +"; + std::fs::write(dir.path().join("profiles.ini"), ini).unwrap(); + let profiles = read_profiles_ini(dir.path()); + assert_eq!(profiles.len(), 2); + assert!(profiles.iter().any(|p| p.ends_with("p65kjdux.default"))); + assert!(profiles.iter().any(|p| p.ends_with("YyVZB8DZ.Profil 1"))); + } + + #[test] + fn parse_profiles_ini_absolute_path() { + let dir = tempfile::tempdir().unwrap(); + let ini = "\ +[Profile0]\n\ +Name=custom\n\ +IsRelative=0\n\ +Path=/tmp/nowhere/profile\n\ +"; + std::fs::write(dir.path().join("profiles.ini"), ini).unwrap(); + let profiles = read_profiles_ini(dir.path()); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0], PathBuf::from("/tmp/nowhere/profile")); + } +} diff --git a/src/main.rs b/src/main.rs index cebade9..f315187 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod firefox; + use std::path::{Path, PathBuf}; use std::process::Command; @@ -113,6 +115,9 @@ fn apply(pos: WindowControlsPosition) -> Result<()> { 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)?; + if let Err(err) = firefox::apply(layout) { + log::warn!("firefox propagation failed: {:#}", err); + } Ok(()) }