⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/core/ente-localed-compat/src/main.rs
History
Who touched what
raw
chore: monorepo inicial con arje + minga + yahweh absorbidos
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}