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"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
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::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
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-3.0/settings.ini"), layout)?;
|
||||||
update_gtk_ini(&home_path(".config/gtk-4.0/settings.ini"), layout)?;
|
update_gtk_ini(&home_path(".config/gtk-4.0/settings.ini"), layout)?;
|
||||||
update_gsettings(layout)?;
|
update_gsettings(layout)?;
|
||||||
|
if let Err(err) = firefox::apply(layout) {
|
||||||
|
log::warn!("firefox propagation failed: {:#}", err);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue