zng_view/config/
dconf.rs

1use std::{io::BufRead as _, time::Duration};
2
3use zng_unit::{Rgba, TimeUnits as _};
4use zng_view_api::{
5    Event,
6    config::{
7        AnimationsConfig, ChromeConfig, ColorScheme, ColorsConfig, FontAntiAliasing, KeyRepeatConfig, LocaleConfig, MultiClickConfig,
8        TouchConfig,
9    },
10};
11
12use crate::AppEvent;
13
14pub fn font_aa() -> FontAntiAliasing {
15    super::other::font_aa()
16}
17
18pub fn multi_click_config() -> MultiClickConfig {
19    let mut cfg = MultiClickConfig::default();
20    if let Some(d) = dconf_uint("/org/gnome/desktop/peripherals/mouse/double-click") {
21        cfg.time = d.ms();
22    }
23    cfg
24}
25
26pub fn animations_config() -> AnimationsConfig {
27    let mut cfg = AnimationsConfig::default();
28    if let Some(e) = dconf_bool("/org/gnome/desktop/interface/enable-animations") {
29        cfg.enabled = e;
30    }
31    if let Some(d) = dconf_uint("/org/gnome/desktop/interface/cursor-blink-time") {
32        cfg.caret_blink_interval = (d / 2).ms();
33    }
34    if let Some(e) = dconf_bool("/org/gnome/desktop/interface/cursor-blink") {
35        if !e {
36            cfg.caret_blink_interval = Duration::MAX;
37        }
38    }
39    cfg
40}
41
42pub fn key_repeat_config() -> KeyRepeatConfig {
43    let mut cfg = KeyRepeatConfig::default();
44    if let Some(d) = dconf_uint("/org/gnome/desktop/peripherals/keyboard/delay") {
45        cfg.start_delay = d.ms();
46    }
47    if let Some(d) = dconf_uint("/org/gnome/desktop/peripherals/keyboard/repeat-interval") {
48        cfg.interval = d.ms();
49    }
50    cfg
51}
52
53pub fn touch_config() -> TouchConfig {
54    super::other::touch_config()
55}
56
57pub fn colors_config() -> ColorsConfig {
58    let scheme = match dconf("/org/gnome/desktop/interface/color-scheme") {
59        Some(cs) => {
60            if cs.contains("dark") {
61                ColorScheme::Dark
62            } else {
63                ColorScheme::Light
64            }
65        }
66        None => ColorScheme::Light,
67    };
68
69    // the color value is not in any config, need to parse theme name
70    let theme = dconf("/org/gnome/desktop/interface/gtk-theme");
71    let theme = match theme.as_ref() {
72        Some(n) => n.strip_prefix("Yaru-").unwrap_or("").split('-').next().unwrap_or(""),
73        None => "?",
74    };
75    // see https://github.com/ubuntu/yaru/blob/6e28865e0ce55c0f95d17a25871618b1660e97b5/common/accent-colors.scss.in
76    let accent = match theme {
77        "" => Rgba::new(233, 84, 32, 255),               // #E95420
78        "bark" => Rgba::new(120, 120, 89, 255),          // #787859
79        "sage" => Rgba::new(101, 123, 105, 255),         // #657b69
80        "olive" => Rgba::new(75, 133, 1, 255),           // #4B8501
81        "viridian" => Rgba::new(3, 135, 91, 255),        // #03875b
82        "prussiangreen" => Rgba::new(48, 130, 128, 255), // #308280
83        "blue" => Rgba::new(0, 115, 229, 255),           // #0073E5
84        "purple" => Rgba::new(119, 100, 216, 255),       // #7764d8
85        "magenta" => Rgba::new(179, 76, 179, 255),       // #b34cb3
86        "red" => Rgba::new(218, 52, 80, 255),            // #DA3450
87        _ => ColorsConfig::default().accent,
88    };
89
90    ColorsConfig { scheme, accent }
91}
92
93pub fn locale_config() -> LocaleConfig {
94    // sys_locale
95    super::other::locale_config()
96}
97
98pub fn chrome_config() -> ChromeConfig {
99    let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
100    // VSCode/Electron, it changes XDG_CURRENT_DESKTOP to "Unity" and sets ORIGINAL_XDG_CURRENT_DESKTOP,
101    // so running from VSCode gets the wrong value.
102    let is_gnome = std::env::var("ORIGINAL_XDG_CURRENT_DESKTOP")
103        .or_else(|_| std::env::var("XDG_CURRENT_DESKTOP"))
104        .is_ok_and(|val| val.contains("GNOME"));
105    ChromeConfig {
106        prefer_custom: is_gnome,
107        provided: !(is_wayland && is_gnome),
108    }
109}
110
111fn on_change(key: &str, s: &crate::AppEventSender) {
112    // println!("{key}"); // to discover keys, uncomment and change the config in system config app.
113
114    match key {
115        "/org/gnome/desktop/interface/color-scheme" | "/org/gnome/desktop/interface/gtk-theme" => {
116            let _ = s.send(AppEvent::Notify(Event::ColorsConfigChanged(colors_config())));
117        }
118        "/org/gnome/desktop/peripherals/keyboard/delay" | "/org/gnome/desktop/peripherals/keyboard/repeat-interval" => {
119            let _ = s.send(AppEvent::Notify(Event::KeyRepeatConfigChanged(key_repeat_config())));
120        }
121        "/org/gnome/desktop/peripherals/mouse/double-click" => {
122            let _ = s.send(AppEvent::Notify(Event::MultiClickConfigChanged(multi_click_config())));
123        }
124        "/org/gnome/desktop/interface/enable-animations"
125        | "/org/gnome/desktop/interface/cursor-blink-time"
126        | "/org/gnome/desktop/interface/cursor-blink" => {
127            let _ = s.send(AppEvent::Notify(Event::AnimationsConfigChanged(animations_config())));
128        }
129        _ => {}
130    }
131}
132
133fn dconf_bool(key: &str) -> Option<bool> {
134    let s = dconf(key)?;
135    match s.parse::<bool>() {
136        Ok(b) => Some(b),
137        Err(e) => {
138            tracing::error!("unexpected value for {key} '{s}', parse error: {e}");
139            None
140        }
141    }
142}
143
144fn dconf_uint(key: &str) -> Option<u64> {
145    let s = dconf(key)?;
146    let s = if let Some((t, i)) = s.rsplit_once(' ') {
147        if !t.starts_with("uint") {
148            tracing::error!("unexpected value for {key} '{s}'");
149            return None;
150        }
151        i
152    } else {
153        s.as_str()
154    };
155    match s.parse::<u64>() {
156        Ok(i) => Some(i),
157        Err(e) => {
158            tracing::error!("unexpected value for {key} '{s}', parse error: {e}");
159            None
160        }
161    }
162}
163
164fn dconf(key: &str) -> Option<String> {
165    let out = std::process::Command::new("dconf").arg("read").arg(key).output();
166    match out {
167        Ok(s) => {
168            if s.status.success() {
169                Some(String::from_utf8_lossy(&s.stdout).trim().to_owned())
170            } else {
171                let e = String::from_utf8_lossy(&s.stderr);
172                tracing::error!("dconf read {key} error, {}", e.lines().next().unwrap_or_default());
173                None
174            }
175        }
176        Err(e) => {
177            tracing::error!("cannot run dconf, {e}");
178            None
179        }
180    }
181}
182
183pub fn spawn_listener(event_loop: crate::AppEventSender) -> Option<Box<dyn FnOnce()>> {
184    let mut w = std::process::Command::new("dconf");
185    w.arg("watch")
186        .arg("/")
187        .stdin(std::process::Stdio::null())
188        .stdout(std::process::Stdio::piped())
189        .stderr(std::process::Stdio::null());
190
191    let mut w = match w.spawn() {
192        Ok(w) => w,
193        Err(e) => {
194            tracing::error!("cannot monitor config, dconf did not spawn, {e}");
195            return None;
196        }
197    };
198    let stdout = w.stdout.take().unwrap();
199    std::thread::spawn(move || {
200        for line in std::io::BufReader::new(stdout).lines() {
201            match line {
202                Ok(l) => {
203                    if l.starts_with('/') {
204                        on_change(&l, &event_loop);
205                    }
206                }
207                Err(_) => {
208                    break;
209                }
210            }
211        }
212    });
213
214    Some(Box::new(move || {
215        let _ = w.kill();
216        let _ = w.wait();
217    }))
218}