feat(firefox): propage le toggle vers les profils Firefox
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
This commit is contained in:
parent
a61c95040e
commit
f33ee1c889
3 changed files with 231 additions and 0 deletions
|
|
@ -13,6 +13,9 @@ anyhow = "1"
|
|||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
|
|
|||
223
src/firefox.rs
Normal file
223
src/firefox.rs
Normal file
|
|
@ -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<PathBuf> {
|
||||
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<String> = None;
|
||||
let mut is_relative = true;
|
||||
|
||||
let commit = |relative: bool, path: Option<String>, profiles: &mut Vec<PathBuf>| {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue