status bar
git clone https://git.lucas.co/cce-status-interface.git
src/listeners.rs (5.9K)
1 //! Compositor push-update listeners: the status-feed subscriptions and the
2 //! switcher trigger socket.
3
4 use crate::CustomEvent;
5
6 /// Subscribe to one compositor status topic and forward its pushes as
7 /// [`CustomEvent`]s, reconnecting every second until the socket is there.
8 ///
9 /// `sub` is the whole subscription line, because one topic takes an argument:
10 /// `backdrop <app_id>` asks what THAT segment is composited over (every other
11 /// topic is the same for all subscribers, so it is a bare word).
12 pub(crate) async fn spawn_status_listener(sub: String, sender: calloop::channel::Sender<CustomEvent>) {
13 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
14 use tokio::net::UnixStream;
15 // Retry delay, doubling while connections keep ending without ever
16 // delivering a line and reset the moment one does. A compositor that
17 // does not know this topic drops the subscription on sight, so the flat
18 // 1s retry turned an unrecognized topic into a permanent once-a-second
19 // reconnect from every module process — which is exactly what a bar
20 // running ahead of its compositor does with `backdrop` (the two halves
21 // deploy separately, and cce-fx only restarts at login).
22 let mut retry_s = 1u64;
23 loop {
24 let socket_path = match std::env::var("WAYLAND_DISPLAY") {
25 Ok(display) => {
26 let primary = format!("/tmp/cce-status-interface-{}.sock", display);
27 if std::path::Path::new(&primary).exists() {
28 primary
29 } else {
30 format!("/tmp/cce-status-{}.sock", display)
31 }
32 }
33 Err(_) => {
34 let primary = "/tmp/cce-status-interface.sock".to_string();
35 if std::path::Path::new(&primary).exists() {
36 primary
37 } else {
38 "/tmp/cce-status.sock".to_string()
39 }
40 }
41 };
42 if let Ok(mut stream) = UnixStream::connect(&socket_path).await {
43 log::info!("[status-listener] connected to {} for sub '{}'", socket_path, sub);
44 if stream.write_all(format!("{}\n", sub).as_bytes()).await.is_ok() {
45 let mut reader = BufReader::new(stream);
46 let mut line = String::new();
47 while reader.read_line(&mut line).await.unwrap_or(0) > 0 {
48 // Anything at all means the topic is understood.
49 retry_s = 1;
50 let val = line.trim().to_string();
51 log::debug!("[status-listener] received '{}' update: '{}'", sub, val);
52 if !val.is_empty() {
53 let topic = sub.split_whitespace().next().unwrap_or("");
54 let ev = match topic {
55 "layout" => CustomEvent::LayoutUpdated(val.clone()),
56 "title" => CustomEvent::TitleUpdated(val.clone()),
57 // Click-away-close: the payload is the app_id of
58 // the segment the press landed on ("-" for none).
59 "dismiss" => CustomEvent::MenuDismiss(val.clone()),
60 // "<luma> <spread>", both 0-100, or "unknown"
61 // when the compositor has no sample for this
62 // segment — treated as the worst case rather
63 // than as no news.
64 "backdrop" => CustomEvent::BackdropUpdated(parse_backdrop(&val)),
65 _ => unreachable!(),
66 };
67 let _ = sender.send(ev);
68 }
69 line.clear();
70 }
71 }
72 }
73 tokio::time::sleep(std::time::Duration::from_secs(retry_s)).await;
74 retry_s = (retry_s * 2).min(30);
75 }
76 }
77
78 /// Parse a `backdrop` line into (luma, spread), both 0-100.
79 ///
80 /// Anything unreadable — the literal "unknown", a truncated line, a future
81 /// compositor's extra fields — reports the worst case: mid luminance and full
82 /// spread, which drives the scrim. Guessing "uniform and bright" from a
83 /// line we failed to understand would silently turn the treatment OFF, and
84 /// unreadable text is a worse failure than an unnecessary scrim.
85 pub(crate) fn parse_backdrop(line: &str) -> (u8, u8) {
86 const UNKNOWN: (u8, u8) = (50, 100);
87 let mut parts = line.split_whitespace();
88 let (Some(luma), Some(spread)) = (parts.next(), parts.next()) else {
89 return UNKNOWN;
90 };
91 // Parsed wide and range-checked rather than clamped, so that "101" and
92 // "300" fail the same way. Clamping would quietly turn an out-of-protocol
93 // luma into "bright and uniform" — the one answer that switches the
94 // treatment off.
95 match (luma.parse::<u16>(), spread.parse::<u16>()) {
96 (Ok(l), Ok(s)) if l <= 100 && s <= 100 => (l as u8, s as u8),
97 _ => UNKNOWN,
98 }
99 }
100
101 pub(crate) async fn spawn_switcher_listener(sender: calloop::channel::Sender<CustomEvent>) {
102 use tokio::io::AsyncBufReadExt;
103 use tokio::net::UnixListener;
104 let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
105 let socket_path = format!("/tmp/cce-status-interface-switcher-{}.sock", display);
106 let _ = std::fs::remove_file(&socket_path);
107
108 if let Ok(listener) = UnixListener::bind(&socket_path) {
109 log::info!("[switcher-listener] Listening on {}", socket_path);
110 loop {
111 if let Ok((stream, _)) = listener.accept().await {
112 let mut reader = tokio::io::BufReader::new(stream);
113 let mut line = String::new();
114 if reader.read_line(&mut line).await.is_ok() {
115 let _ = sender.send(CustomEvent::SwitcherTriggered);
116 }
117 }
118 }
119 } else {
120 log::warn!("[switcher-listener] Failed to bind to {}", socket_path);
121 }
122 }