git.lucas.co / cce-status-interface
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 }