gioser
⎇ d88a9c57914ac7870b43a7a4f616510161278237 crates/ente-localed-compat/src/main.rs Rust · 219 siq'ikuna · 6.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

Persistencia setters, compat-resolved, journald→CAS, compat-polkit d88a9c579 · Sergio ·

1//! ente-localed-compat: shim de `org.freedesktop.locale1`.
2//!
3//! GNOME settings panel "Region & Language" llama aquí. Properties leen
4//! /etc/locale.conf y /etc/vconsole.conf; setters log + forward.
5
6use ente_bus::{BusClient, BusRequest, BusResponse};
7use ente_card::Capability;
8use std::sync::Mutex;
9use tokio::signal::unix::{signal, SignalKind};
10use tracing::{info, warn};
11use tracing_subscriber::EnvFilter;
12use zbus::{fdo, interface};
13
14const BUS_NAME: &str = "org.freedesktop.locale1";
15const OBJ_PATH: &str = "/org/freedesktop/locale1";
16
17#[tokio::main(flavor = "current_thread")]
18async fn main() -> anyhow::Result<()> {
19    init_tracing();
20    info!("ente-localed-compat: arrancando");
21    announce_to_fractal().await;
22
23    let manager = LocaleManager::default();
24    let conn_result = zbus::connection::Builder::system()
25        .and_then(|b| b.name(BUS_NAME))
26        .and_then(|b| b.serve_at(OBJ_PATH, manager));
27    match conn_result {
28        Ok(builder) => match builder.build().await {
29            Ok(_conn) => {
30                info!(name = BUS_NAME, "name acquired, sirviendo");
31                wait_for_term().await
32            }
33            Err(e) => {
34                warn!(?e, "build conn falló — modo idle");
35                wait_for_term().await
36            }
37        },
38        Err(e) => {
39            warn!(?e, "builder D-Bus falló — modo idle");
40            wait_for_term().await
41        }
42    }
43}
44
45#[derive(Default)]
46struct LocaleManager {
47    transient_locale: Mutex<Option<Vec<String>>>,
48}
49
50#[interface(name = "org.freedesktop.locale1")]
51impl LocaleManager {
52    /// Locale actual como array de "KEY=value" (LANG=en_US.UTF-8, LC_TIME=...).
53    /// Default: leer /etc/locale.conf.
54    #[zbus(property)]
55    async fn locale(&self) -> Vec<String> {
56        if let Some(v) = self.transient_locale.lock().unwrap().clone() {
57            return v;
58        }
59        match std::fs::read_to_string("/etc/locale.conf") {
60            Ok(c) => c.lines()
61                .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
62                .map(|s| s.trim().to_string())
63                .collect(),
64            Err(_) => vec!["LANG=C.UTF-8".into()],
65        }
66    }
67
68    #[zbus(property)]
69    async fn x11layout(&self) -> String {
70        read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbLayout").unwrap_or_default()
71    }
72
73    #[zbus(property)]
74    async fn x11model(&self) -> String {
75        read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbModel").unwrap_or_default()
76    }
77
78    #[zbus(property)]
79    async fn x11variant(&self) -> String {
80        read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbVariant").unwrap_or_default()
81    }
82
83    #[zbus(property)]
84    async fn x11options(&self) -> String {
85        read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbOptions").unwrap_or_default()
86    }
87
88    #[zbus(property)]
89    async fn vconsole_keymap(&self) -> String {
90        read_vconsole("KEYMAP").unwrap_or_default()
91    }
92
93    #[zbus(property)]
94    async fn vconsole_keymap_toggle(&self) -> String {
95        read_vconsole("KEYMAP_TOGGLE").unwrap_or_default()
96    }
97
98    async fn set_locale(&self, locale: Vec<String>, _interactive: bool) -> fdo::Result<()> {
99        // Validar formato KEY=value en cada entry.
100        for entry in &locale {
101            if !entry.contains('=') {
102                return Err(fdo::Error::InvalidArgs(
103                    format!("locale entry inválido (sin '='): {entry}")
104                ));
105            }
106        }
107        let content: String = locale.iter()
108            .map(|s| format!("{s}\n"))
109            .collect();
110        atomic_write("/etc/locale.conf", content.as_bytes())
111            .map_err(|e| fdo::Error::Failed(format!("write /etc/locale.conf: {e}")))?;
112        *self.transient_locale.lock().unwrap() = Some(locale.clone());
113        info!(?locale, "SetLocale → /etc/locale.conf");
114        Ok(())
115    }
116
117    async fn set_vconsole_keymap(
118        &self,
119        keymap: String,
120        keymap_toggle: String,
121        _convert: bool,
122        _interactive: bool,
123    ) -> fdo::Result<()> {
124        info!(%keymap, %keymap_toggle, "SetVConsoleKeymap (stub)");
125        Ok(())
126    }
127
128    async fn set_x11_keyboard(
129        &self,
130        layout: String,
131        model: String,
132        variant: String,
133        options: String,
134        _convert: bool,
135        _interactive: bool,
136    ) -> fdo::Result<()> {
137        info!(%layout, %model, %variant, %options, "SetX11Keyboard (stub)");
138        Ok(())
139    }
140}
141
142fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
143    use std::io::Write;
144    use std::os::unix::fs::OpenOptionsExt;
145    let p = std::path::Path::new(path);
146    if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
147    let tmp = p.with_extension("tmp");
148    {
149        let mut f = std::fs::OpenOptions::new()
150            .create(true).write(true).truncate(true)
151            .mode(0o644)
152            .open(&tmp)?;
153        f.write_all(content)?;
154        f.sync_all()?;
155    }
156    std::fs::rename(&tmp, p)?;
157    Ok(())
158}
159
160fn read_kv(path: &str, key: &str) -> Option<String> {
161    let content = std::fs::read_to_string(path).ok()?;
162    for line in content.lines() {
163        let trimmed = line.trim();
164        if trimmed.starts_with(&format!("Option \"{key}\"")) || trimmed.starts_with(key) {
165            // Best-effort parse: tomar lo que está entre comillas.
166            if let Some(start) = trimmed.find('"') {
167                let rest = &trimmed[start + 1..];
168                if let Some(end) = rest.find('"') {
169                    return Some(rest[..end].to_string());
170                }
171            }
172        }
173    }
174    None
175}
176
177fn read_vconsole(key: &str) -> Option<String> {
178    let content = std::fs::read_to_string("/etc/vconsole.conf").ok()?;
179    for line in content.lines() {
180        if let Some((k, v)) = line.split_once('=') {
181            if k.trim() == key {
182                return Some(v.trim().trim_matches('"').to_string());
183            }
184        }
185    }
186    None
187}
188
189async fn announce_to_fractal() {
190    if let Ok(mut client) = BusClient::from_env().await {
191        let req = BusRequest::Announce {
192            capabilities: vec![Capability::Endpoint {
193                interface: ente_card::InterfaceId([0xa2; 16]),
194                version: 1,
195            }],
196        };
197        match client.call(req).await {
198            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
199            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
200            Err(e) => warn!(?e, "Announce falló"),
201        }
202    }
203}
204
205async fn wait_for_term() -> anyhow::Result<()> {
206    let mut term = signal(SignalKind::terminate())?;
207    let mut int_ = signal(SignalKind::interrupt())?;
208    tokio::select! {
209        _ = term.recv() => info!("SIGTERM"),
210        _ = int_.recv() => info!("SIGINT"),
211    }
212    Ok(())
213}
214
215fn init_tracing() {
216    let filter = EnvFilter::try_from_default_env()
217        .unwrap_or_else(|_| EnvFilter::new("ente_localed_compat=info"));
218    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
219}