feat(compat): arje-compat-common — núcleo puro y testeado para los shims
El hallazgo del repaso del monorepo: la capa compat (14 shims D-Bus de systemd) era lo más incompleto relativo a su peso — load-bearing para correr GNOME/KDE sobre arje, y con CERO tests. Cada shim copiaba su propio `atomic_write`, su parseo `KEY=value` y sus validadores. Primer golpe: - `arje-compat-common`: crate nuevo con la lógica pura compartida (atomic_write, parse_kv, merge_kv, conf_entries, is_valid_hostname), cubierta con 8 tests. Antes esa lógica vivía duplicada y sin un test. - `arje-hostnamed-compat` y `arje-localed-compat` migrados al núcleo — quedan más finos y su lógica pasa a estar cubierta. - localed: los dos setters que eran stub (sólo loggeaban) ahora escriben de verdad — `SetVConsoleKeymap` → /etc/vconsole.conf, `SetX11Keyboard` → 00-keyboard.conf. + 2 tests propios. - Bug corregido de paso: el parser xorg de localed devolvía el NOMBRE de la opción en vez del valor (tomaba la 1ª comilla); ahora toma la 2ª cadena, la correcta. Compat: de 0 a 10 tests. Quedan 12 shims con la misma migración mecánica pendiente; el plato fuerte real es `Inhibit` en logind. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -448,6 +448,10 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arje-compat-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arje-echo"
|
name = "arje-echo"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -467,6 +471,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"arje-bus",
|
"arje-bus",
|
||||||
"arje-card",
|
"arje-card",
|
||||||
|
"arje-compat-common",
|
||||||
"libc",
|
"libc",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -522,6 +527,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"arje-bus",
|
"arje-bus",
|
||||||
"arje-card",
|
"arje-card",
|
||||||
|
"arje-compat-common",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -3059,6 +3065,7 @@ dependencies = [
|
|||||||
name = "cosmobiologia-engine"
|
name = "cosmobiologia-engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cosmobiologia-corpus",
|
||||||
"cosmobiologia-model",
|
"cosmobiologia-model",
|
||||||
"cosmobiologia-render",
|
"cosmobiologia-render",
|
||||||
"eternal-astrology",
|
"eternal-astrology",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ members = [
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# compat/ — Shims D-Bus para correr software systemd-aware
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
"crates/compat/arje-compat-common",
|
||||||
"crates/compat/arje-policy-provider",
|
"crates/compat/arje-policy-provider",
|
||||||
"crates/compat/arje-logind-compat",
|
"crates/compat/arje-logind-compat",
|
||||||
"crates/compat/arje-hostnamed-compat",
|
"crates/compat/arje-hostnamed-compat",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "arje-compat-common"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description = "Lógica pura compartida por los shims D-Bus de compat: parseo KEY=value, escritura atómica, validadores. Testeada en un solo lugar."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
//! `arje-compat-common` — lógica pura compartida por los shims D-Bus de
|
||||||
|
//! compat (`arje-*-compat`).
|
||||||
|
//!
|
||||||
|
//! Los shims son binarios que traducen interfaces de systemd al bus
|
||||||
|
//! interno de `arje`. Antes cada uno copiaba su propio parseo de
|
||||||
|
//! archivos `KEY=value`, su escritura atómica y sus validadores —
|
||||||
|
//! código duplicado catorce veces y **sin un solo test**. Esta crate
|
||||||
|
//! junta esa lógica pura en un lugar y la cubre con tests; los shims
|
||||||
|
//! quedan como pura cáscara D-Bus.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Escribe `content` en `path` de forma atómica: vuelca a un archivo
|
||||||
|
/// temporal vecino, hace `fsync`, y renombra sobre el destino —de modo
|
||||||
|
/// que un lector nunca ve un archivo a medio escribir—. Permisos 0644.
|
||||||
|
/// Crea el directorio padre si falta.
|
||||||
|
pub fn atomic_write(path: impl AsRef<Path>, content: &[u8]) -> io::Result<()> {
|
||||||
|
use io::Write;
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
let p = path.as_ref();
|
||||||
|
if let Some(parent) = p.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let tmp = p.with_extension("tmp");
|
||||||
|
{
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(0o644)
|
||||||
|
.open(&tmp)?;
|
||||||
|
f.write_all(content)?;
|
||||||
|
f.sync_all()?;
|
||||||
|
}
|
||||||
|
std::fs::rename(&tmp, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca `key` en un contenido de líneas `KEY=value` y devuelve su
|
||||||
|
/// valor, trimeado y sin las comillas envolventes. Ignora líneas en
|
||||||
|
/// blanco y comentarios (`#`). `None` si la clave no aparece.
|
||||||
|
pub fn parse_kv(content: &str, key: &str) -> Option<String> {
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((k, v)) = line.split_once('=') {
|
||||||
|
if k.trim() == key {
|
||||||
|
return Some(v.trim().trim_matches('"').to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actualiza —o inserta, si no existe— la clave `key` con `value` en un
|
||||||
|
/// contenido de líneas `KEY=value`. Conserva el resto de las líneas y
|
||||||
|
/// su orden. El resultado termina en `\n`.
|
||||||
|
pub fn merge_kv(existing: &str, key: &str, value: &str) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut found = false;
|
||||||
|
for line in existing.lines() {
|
||||||
|
if let Some((k, _)) = line.split_once('=') {
|
||||||
|
if k.trim() == key {
|
||||||
|
out.push_str(&format!("{key}={value}\n"));
|
||||||
|
found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
out.push_str(&format!("{key}={value}\n"));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Las entradas significativas de un archivo de config tipo
|
||||||
|
/// `locale.conf`: las líneas no vacías y no comentadas, trimeadas.
|
||||||
|
pub fn conf_entries(content: &str) -> Vec<String> {
|
||||||
|
content
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si `s` es un hostname válido: ASCII alfanumérico más `-`,
|
||||||
|
/// `.`, `_`; longitud `1..=253`; sin espacios ni caracteres de control.
|
||||||
|
pub fn is_valid_hostname(s: &str) -> bool {
|
||||||
|
if s.is_empty() || s.len() > 253 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_kv_encuentra_trimea_y_quita_comillas() {
|
||||||
|
let c = "# comentario\nLANG=en_US.UTF-8\n LC_TIME = \"es_AR.UTF-8\" \n";
|
||||||
|
assert_eq!(parse_kv(c, "LANG").as_deref(), Some("en_US.UTF-8"));
|
||||||
|
assert_eq!(parse_kv(c, "LC_TIME").as_deref(), Some("es_AR.UTF-8"));
|
||||||
|
assert_eq!(parse_kv(c, "AUSENTE"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_kv_ignora_comentarios_y_blancos() {
|
||||||
|
// Una clave comentada no cuenta.
|
||||||
|
assert_eq!(parse_kv("#LANG=x\n\nLANG=real\n", "LANG").as_deref(), Some("real"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_kv_actualiza_la_clave_existente_y_conserva_el_resto() {
|
||||||
|
let antes = "LANG=C\nKEYMAP=us\n";
|
||||||
|
let despues = merge_kv(antes, "KEYMAP", "es");
|
||||||
|
assert!(despues.contains("LANG=C\n"), "conserva otras líneas");
|
||||||
|
assert!(despues.contains("KEYMAP=es\n"));
|
||||||
|
assert!(!despues.contains("KEYMAP=us"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_kv_inserta_la_clave_nueva_al_final() {
|
||||||
|
let despues = merge_kv("LANG=C\n", "KEYMAP", "us");
|
||||||
|
assert!(despues.contains("LANG=C\n"));
|
||||||
|
assert!(despues.ends_with("KEYMAP=us\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_kv_sobre_contenido_vacio() {
|
||||||
|
assert_eq!(merge_kv("", "KEYMAP", "us"), "KEYMAP=us\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conf_entries_descarta_blancos_y_comentarios() {
|
||||||
|
let c = "\n# header\nLANG=en\n \nLC_TIME=es\n";
|
||||||
|
assert_eq!(conf_entries(c), vec!["LANG=en", "LC_TIME=es"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_valid_hostname_acepta_y_rechaza() {
|
||||||
|
assert!(is_valid_hostname("arje-host"));
|
||||||
|
assert!(is_valid_hostname("host.local"));
|
||||||
|
assert!(!is_valid_hostname(""), "vacío");
|
||||||
|
assert!(!is_valid_hostname("con espacio"));
|
||||||
|
assert!(!is_valid_hostname("tab\tinside"));
|
||||||
|
assert!(!is_valid_hostname(&"x".repeat(254)), "demasiado largo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atomic_write_escribe_y_sobrescribe() {
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join(format!("arje-compat-common-test-{}.conf", std::process::id()));
|
||||||
|
atomic_write(&path, b"primero\n").expect("escribe");
|
||||||
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), "primero\n");
|
||||||
|
atomic_write(&path, b"segundo\n").expect("sobrescribe");
|
||||||
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), "segundo\n");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
arje-card = { path = "../../protocol/arje-card" }
|
arje-card = { path = "../../protocol/arje-card" }
|
||||||
arje-bus = { path = "../../runtime/arje-bus" }
|
arje-bus = { path = "../../runtime/arje-bus" }
|
||||||
|
arje-compat-common = { path = "../arje-compat-common" }
|
||||||
nix = { workspace = true }
|
nix = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
use arje_bus::{BusClient, BusRequest, BusResponse};
|
use arje_bus::{BusClient, BusRequest, BusResponse};
|
||||||
use arje_card::Capability;
|
use arje_card::Capability;
|
||||||
|
use arje_compat_common::{atomic_write, is_valid_hostname, merge_kv, parse_kv};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
@@ -230,23 +231,11 @@ fn gethostname_libc() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_os_release_field(field: &str) -> Option<String> {
|
fn read_os_release_field(field: &str) -> Option<String> {
|
||||||
parse_kv_file("/etc/os-release", field)
|
parse_kv(&std::fs::read_to_string("/etc/os-release").ok()?, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_machine_info_field(field: &str) -> Option<String> {
|
fn read_machine_info_field(field: &str) -> Option<String> {
|
||||||
parse_kv_file("/etc/machine-info", field)
|
parse_kv(&std::fs::read_to_string("/etc/machine-info").ok()?, field)
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_kv_file(path: &str, field: &str) -> Option<String> {
|
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
|
||||||
for line in content.lines() {
|
|
||||||
if let Some((k, v)) = line.split_once('=') {
|
|
||||||
if k.trim() == field {
|
|
||||||
return Some(v.trim().trim_matches('"').to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dmi(path: &str) -> String {
|
fn read_dmi(path: &str) -> String {
|
||||||
@@ -255,56 +244,12 @@ fn read_dmi(path: &str) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RFC 1123 + extra: ASCII alfanumérico, dash, dot. Longitud 1..253.
|
/// Lee `/etc/machine-info`, actualiza/inserta una clave y reescribe el
|
||||||
/// Rechaza vacíos, espacios, control chars.
|
/// archivo de forma atómica.
|
||||||
fn is_valid_hostname(s: &str) -> bool {
|
|
||||||
if s.is_empty() || s.len() > 253 { return false; }
|
|
||||||
s.chars().all(|c|
|
|
||||||
c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Escritura atómica via tmp + rename. fsync del directorio para
|
|
||||||
/// garantizar durabilidad post-crash. Permisos 0644.
|
|
||||||
fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
|
|
||||||
use std::io::Write;
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
let p = std::path::Path::new(path);
|
|
||||||
if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
|
|
||||||
let tmp = p.with_extension("tmp");
|
|
||||||
{
|
|
||||||
let mut f = std::fs::OpenOptions::new()
|
|
||||||
.create(true).write(true).truncate(true)
|
|
||||||
.mode(0o644)
|
|
||||||
.open(&tmp)?;
|
|
||||||
f.write_all(content)?;
|
|
||||||
f.sync_all()?;
|
|
||||||
}
|
|
||||||
std::fs::rename(&tmp, p)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lee /etc/machine-info, actualiza/inserta una clave, escribe atómico.
|
|
||||||
fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> {
|
fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> {
|
||||||
let path = "/etc/machine-info";
|
let path = "/etc/machine-info";
|
||||||
let existing = std::fs::read_to_string(path).unwrap_or_default();
|
let existing = std::fs::read_to_string(path).unwrap_or_default();
|
||||||
let mut found = false;
|
atomic_write(path, merge_kv(&existing, key, value).as_bytes())
|
||||||
let mut out = String::new();
|
|
||||||
for line in existing.lines() {
|
|
||||||
if let Some((k, _)) = line.split_once('=') {
|
|
||||||
if k.trim() == key {
|
|
||||||
out.push_str(&format!("{key}={value}\n"));
|
|
||||||
found = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push_str(line);
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
out.push_str(&format!("{key}={value}\n"));
|
|
||||||
}
|
|
||||||
atomic_write(path, out.as_bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn announce_to_fractal() {
|
async fn announce_to_fractal() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
arje-card = { path = "../../protocol/arje-card" }
|
arje-card = { path = "../../protocol/arje-card" }
|
||||||
arje-bus = { path = "../../runtime/arje-bus" }
|
arje-bus = { path = "../../runtime/arje-bus" }
|
||||||
|
arje-compat-common = { path = "../arje-compat-common" }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
use arje_bus::{BusClient, BusRequest, BusResponse};
|
use arje_bus::{BusClient, BusRequest, BusResponse};
|
||||||
use arje_card::Capability;
|
use arje_card::Capability;
|
||||||
|
use arje_compat_common::{atomic_write, conf_entries, merge_kv, parse_kv};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
@@ -57,10 +58,7 @@ impl LocaleManager {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
match std::fs::read_to_string("/etc/locale.conf") {
|
match std::fs::read_to_string("/etc/locale.conf") {
|
||||||
Ok(c) => c.lines()
|
Ok(c) => conf_entries(&c),
|
||||||
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.collect(),
|
|
||||||
Err(_) => vec!["LANG=C.UTF-8".into()],
|
Err(_) => vec!["LANG=C.UTF-8".into()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +119,14 @@ impl LocaleManager {
|
|||||||
_convert: bool,
|
_convert: bool,
|
||||||
_interactive: bool,
|
_interactive: bool,
|
||||||
) -> fdo::Result<()> {
|
) -> fdo::Result<()> {
|
||||||
info!(%keymap, %keymap_toggle, "SetVConsoleKeymap (stub)");
|
let existing = std::fs::read_to_string("/etc/vconsole.conf").unwrap_or_default();
|
||||||
|
let mut out = merge_kv(&existing, "KEYMAP", &keymap);
|
||||||
|
if !keymap_toggle.is_empty() {
|
||||||
|
out = merge_kv(&out, "KEYMAP_TOGGLE", &keymap_toggle);
|
||||||
|
}
|
||||||
|
atomic_write("/etc/vconsole.conf", out.as_bytes())
|
||||||
|
.map_err(|e| fdo::Error::Failed(format!("write /etc/vconsole.conf: {e}")))?;
|
||||||
|
info!(%keymap, %keymap_toggle, "SetVConsoleKeymap → /etc/vconsole.conf");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,56 +139,53 @@ impl LocaleManager {
|
|||||||
_convert: bool,
|
_convert: bool,
|
||||||
_interactive: bool,
|
_interactive: bool,
|
||||||
) -> fdo::Result<()> {
|
) -> fdo::Result<()> {
|
||||||
info!(%layout, %model, %variant, %options, "SetX11Keyboard (stub)");
|
let conf = format_x11_keyboard_conf(&layout, &model, &variant, &options);
|
||||||
|
atomic_write("/etc/X11/xorg.conf.d/00-keyboard.conf", conf.as_bytes())
|
||||||
|
.map_err(|e| fdo::Error::Failed(format!("write 00-keyboard.conf: {e}")))?;
|
||||||
|
info!(%layout, %model, %variant, %options, "SetX11Keyboard → 00-keyboard.conf");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
|
/// Lee el valor de un `Option "Clave" "valor"` de un snippet
|
||||||
use std::io::Write;
|
/// `xorg.conf.d` — el valor es la SEGUNDA cadena entre comillas.
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
fn parse_xorg_option(content: &str, key: &str) -> Option<String> {
|
||||||
let p = std::path::Path::new(path);
|
for line in content.lines() {
|
||||||
if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
|
let t = line.trim();
|
||||||
let tmp = p.with_extension("tmp");
|
if t.starts_with(&format!("Option \"{key}\"")) {
|
||||||
{
|
return t.split('"').nth(3).map(str::to_string);
|
||||||
let mut f = std::fs::OpenOptions::new()
|
}
|
||||||
.create(true).write(true).truncate(true)
|
|
||||||
.mode(0o644)
|
|
||||||
.open(&tmp)?;
|
|
||||||
f.write_all(content)?;
|
|
||||||
f.sync_all()?;
|
|
||||||
}
|
}
|
||||||
std::fs::rename(&tmp, p)?;
|
None
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
/// Genera el snippet `xorg.conf.d` que fija el teclado X11 — el mismo
|
||||||
|
/// `InputClass` que escribe systemd-localed. Omite los campos vacíos.
|
||||||
|
fn format_x11_keyboard_conf(layout: &str, model: &str, variant: &str, options: &str) -> String {
|
||||||
|
let mut s = String::from("# Generado por arje-localed-compat\n");
|
||||||
|
s.push_str("Section \"InputClass\"\n");
|
||||||
|
s.push_str(" Identifier \"system-keyboard\"\n");
|
||||||
|
s.push_str(" MatchIsKeyboard \"on\"\n");
|
||||||
|
for (opt, val) in [
|
||||||
|
("XkbLayout", layout),
|
||||||
|
("XkbModel", model),
|
||||||
|
("XkbVariant", variant),
|
||||||
|
("XkbOptions", options),
|
||||||
|
] {
|
||||||
|
if !val.is_empty() {
|
||||||
|
s.push_str(&format!(" Option \"{opt}\" \"{val}\"\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str("EndSection\n");
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_kv(path: &str, key: &str) -> Option<String> {
|
fn read_kv(path: &str, key: &str) -> Option<String> {
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
parse_xorg_option(&std::fs::read_to_string(path).ok()?, key)
|
||||||
for line in content.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed.starts_with(&format!("Option \"{key}\"")) || trimmed.starts_with(key) {
|
|
||||||
// Best-effort parse: tomar lo que está entre comillas.
|
|
||||||
if let Some(start) = trimmed.find('"') {
|
|
||||||
let rest = &trimmed[start + 1..];
|
|
||||||
if let Some(end) = rest.find('"') {
|
|
||||||
return Some(rest[..end].to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_vconsole(key: &str) -> Option<String> {
|
fn read_vconsole(key: &str) -> Option<String> {
|
||||||
let content = std::fs::read_to_string("/etc/vconsole.conf").ok()?;
|
parse_kv(&std::fs::read_to_string("/etc/vconsole.conf").ok()?, key)
|
||||||
for line in content.lines() {
|
|
||||||
if let Some((k, v)) = line.split_once('=') {
|
|
||||||
if k.trim() == key {
|
|
||||||
return Some(v.trim().trim_matches('"').to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn announce_to_fractal() {
|
async fn announce_to_fractal() {
|
||||||
@@ -217,3 +219,26 @@ fn init_tracing() {
|
|||||||
.unwrap_or_else(|_| EnvFilter::new("arje_localed_compat=info"));
|
.unwrap_or_else(|_| EnvFilter::new("arje_localed_compat=info"));
|
||||||
tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
|
tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_xorg_option_toma_la_segunda_cadena() {
|
||||||
|
let c = "Section \"InputClass\"\n Option \"XkbLayout\" \"us\"\n Option \"XkbVariant\" \"intl\"\nEndSection\n";
|
||||||
|
assert_eq!(parse_xorg_option(c, "XkbLayout").as_deref(), Some("us"));
|
||||||
|
assert_eq!(parse_xorg_option(c, "XkbVariant").as_deref(), Some("intl"));
|
||||||
|
assert_eq!(parse_xorg_option(c, "XkbModel"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_x11_keyboard_conf_omite_los_campos_vacios() {
|
||||||
|
let conf = format_x11_keyboard_conf("us", "pc105", "", "");
|
||||||
|
assert!(conf.contains("Option \"XkbLayout\" \"us\""));
|
||||||
|
assert!(conf.contains("Option \"XkbModel\" \"pc105\""));
|
||||||
|
assert!(!conf.contains("XkbVariant"), "el variant vacío se omite");
|
||||||
|
assert!(conf.contains("Section \"InputClass\""));
|
||||||
|
assert!(conf.contains("EndSection"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user