Skip to main content

crabshell_services/
lib.rs

1//! The services a Crabshell bar reads: the clock, Sway's workspaces, audio
2//! and the tray.
3//!
4//! The shell calls [`provide`] once, with its root context and event loop.
5//! After that, any context hands out services through [`ServicesExt`],
6//! such as `cx.clock(Precision::Minute)`. Each service starts the first
7//! time a config asks for it and is shared by every caller, so two outputs
8//! never mean two connections. Its signals belong to the root context's
9//! owner: bars on every output share them, and they go when the shell does.
10//!
11//! Asked for on a context the shell didn't set up, as in a ui test, a
12//! service logs once and hands out a value that never changes.
13
14#![forbid(unsafe_code)]
15#![warn(missing_docs)]
16
17mod audio;
18mod clock;
19mod sway;
20#[cfg(test)]
21mod testing;
22mod tray;
23
24use std::cell::{Cell, OnceCell, RefCell};
25use std::future::{Future, poll_fn};
26use std::os::fd::OwnedFd;
27use std::path::PathBuf;
28use std::pin::pin;
29use std::rc::Rc;
30use std::task::{Poll, Waker};
31use std::time::{Duration, Instant};
32
33use calloop::channel::Channel;
34use calloop::futures::{Executor, Scheduler};
35use calloop::generic::Generic;
36use calloop::timer::{TimeoutAction, Timer};
37use calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken};
38use crabshell_reactive::{Owner, ReadSignal};
39use crabshell_ui::Cx;
40
41pub use audio::{Audio, Sink};
42pub use clock::Precision;
43pub use sway::{Sway, Workspace};
44pub use tray::{MenuEntry, MenuToggle, Tray, TrayHost, TrayItem, TrayMenu, TrayStatus};
45
46/// Makes the services reachable from `cx` and every context made from it.
47/// The shell calls this once, with its root context and its event loop.
48///
49/// Nothing starts here: each service starts when a config first asks for
50/// it. The loop is kept weakly, so it and the compositor connection still
51/// go when the shell drops.
52pub fn provide<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>) {
53  provide_with(cx, handle, Config::system());
54}
55
56/// [`provide`] with the system's parts swapped for a test's.
57fn provide_with<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>, config: Config) {
58  let shared = Rc::new(Shared::new(cx.owner(), handle, config));
59  cx.provide(clock::Clocks::new(Rc::clone(&shared)));
60  cx.provide(sway::Service::new(Rc::clone(&shared)));
61  cx.provide(audio::AudioService::new(Rc::clone(&shared)));
62  cx.provide(tray::Service::new(Rc::clone(&shared)));
63}
64
65/// The services a context hands out.
66pub trait ServicesExt {
67  /// The time, truncated to `precision`: it changes on each whole second
68  /// or minute and holds the one just passed, in the system time zone.
69  /// Every caller asking for the same precision gets the same signal.
70  ///
71  /// A minute clock wakes the shell once a minute and at no other time.
72  /// After a suspend, and when timedated changes the time zone, it is
73  /// right again at once. A zone changed any other way shows from the
74  /// next tick.
75  fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned>;
76
77  /// Sway's workspaces, over the IPC socket at `$SWAYSOCK`. Every caller
78  /// shares one connection, made on the first call.
79  ///
80  /// The list is empty while Sway isn't there. When `SWAYSOCK` is unset
81  /// or the first connect fails, it stays empty and the service never
82  /// wakes the shell. When a working connection closes, the list is
83  /// empty until a reconnect, tried every 2 s, works. While connected,
84  /// it wakes the shell only when Sway sends something.
85  fn sway(&self) -> Sway;
86
87  /// The audio server's default sink, with its volume and mute. Every
88  /// caller shares one connection, which starts on the loop's next turn
89  /// and never waits for the server. It wakes the shell only when the
90  /// server reports a change.
91  ///
92  /// With no server, the sink is `None` and the service tries again
93  /// every 2 s, for up to a minute after startup and after each drop of
94  /// a working connection. Then it stops, logs that once and never wakes
95  /// the shell again; a restart tries again.
96  fn audio(&self) -> Audio;
97
98  /// The system tray: every app's StatusNotifierItem on the session bus.
99  /// Every caller shares one connection, which starts on the loop's next
100  /// turn and never waits for the bus.
101  ///
102  /// The shell runs the StatusNotifierWatcher while no other program
103  /// does, and otherwise reads the list from the one that runs, taking
104  /// over when it leaves. With no session bus the list stays empty and
105  /// the service never wakes the shell. It wakes the shell only when
106  /// something arrives on the bus for it.
107  fn tray(&self) -> TrayHost;
108}
109
110impl ServicesExt for Cx {
111  fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned> {
112    clock::clock(self, precision)
113  }
114
115  fn sway(&self) -> Sway {
116    sway::sway(self)
117  }
118
119  fn audio(&self) -> Audio {
120    audio::audio(self)
121  }
122
123  fn tray(&self) -> TrayHost {
124    tray::tray(self)
125  }
126}
127
128/// What every service of one shell starts from.
129struct Shared {
130  /// The root context's owner. Services create their signals under it,
131  /// so bars on every output share them and they go with the shell.
132  root: Owner,
133  /// The event loop, held weakly. Services' timers and futures live in
134  /// the loop and hold the services, so a strong handle here would be an
135  /// `Rc` cycle that keeps the loop and the compositor connection alive
136  /// after the shell drops. calloop's weak handle has no type we can
137  /// name, so a closure keeps it.
138  event_loop: Box<dyn Fn() -> Option<Box<dyn EventLoop>>>,
139  /// The one futures executor every service's D-Bus calls run on, added
140  /// to the loop on first use. `None` if that failed.
141  executor: OnceCell<Option<Scheduler<()>>>,
142  config: Config,
143}
144
145impl Shared {
146  fn new<D: 'static>(root: Owner, handle: &LoopHandle<'static, D>, config: Config) -> Shared {
147    let weak = handle.downgrade();
148    Shared {
149      root,
150      event_loop: Box::new(move || {
151        weak
152          .upgrade()
153          .map(|handle| Box::new(handle) as Box<dyn EventLoop>)
154      }),
155      executor: OnceCell::new(),
156      config,
157    }
158  }
159
160  /// The event loop, while it is still there.
161  fn event_loop(&self) -> Option<Box<dyn EventLoop>> {
162    (self.event_loop)()
163  }
164
165  /// Runs `future` on the main thread, between the loop's other sources,
166  /// so no D-Bus call delays a frame.
167  fn spawn(&self, future: impl Future<Output = ()> + 'static) {
168    let scheduler = self.executor.get_or_init(|| {
169      let (executor, scheduler) = calloop::futures::executor()
170        .inspect_err(|error| tracing::error!(%error, "no executor for the services"))
171        .ok()?;
172      self
173        .event_loop()?
174        .insert_executor(executor)
175        .then_some(scheduler)
176    });
177    if let Some(scheduler) = scheduler
178      && let Err(error) = scheduler.schedule(future)
179    {
180      tracing::error!(%error, "a service's future couldn't start");
181    }
182  }
183}
184
185impl Shared {
186  /// Awaits `call`, or gives up after `limit` with a timed-out error. The
187  /// timer is the loop's and goes as soon as the call ends. zbus's own
188  /// timeout left its timer behind, which woke zbus's thread once, `limit`
189  /// after every call, while nothing changed.
190  async fn timeout<T>(
191    &self,
192    limit: Duration,
193    call: impl Future<Output = zbus::Result<T>>,
194  ) -> zbus::Result<T> {
195    let fired = Rc::new(Cell::new(false));
196    let waker: Rc<RefCell<Option<Waker>>> = Rc::default();
197    let tick = {
198      let (fired, waker) = (Rc::clone(&fired), Rc::clone(&waker));
199      Box::new(move || {
200        fired.set(true);
201        if let Some(waker) = waker.take() {
202          waker.wake();
203        }
204        TimeoutAction::Drop
205      })
206    };
207    let token = self
208      .event_loop()
209      .and_then(|event_loop| event_loop.insert_timer(Instant::now() + limit, tick));
210    let _timer = RemoveOnDrop(self, token);
211    let mut call = pin!(call);
212    poll_fn(|context| {
213      if let Poll::Ready(result) = call.as_mut().poll(context) {
214        return Poll::Ready(result);
215      }
216      if fired.get() {
217        let error = std::io::Error::new(std::io::ErrorKind::TimedOut, "no answer in time");
218        return Poll::Ready(Err(zbus::Error::InputOutput(error.into())));
219      }
220      waker.replace(Some(context.waker().clone()));
221      Poll::Pending
222    })
223    .await
224  }
225}
226
227/// Removes a loop source when dropped, as a call that ends removes its
228/// timer.
229struct RemoveOnDrop<'a>(&'a Shared, Option<RegistrationToken>);
230
231impl Drop for RemoveOnDrop<'_> {
232  fn drop(&mut self) {
233    if let (Some(token), Some(event_loop)) = (self.1, self.0.event_loop()) {
234      event_loop.remove(token);
235    }
236  }
237}
238
239/// The parts of the event loop services use. It hides the loop's data
240/// type, since one services map serves any shell.
241trait EventLoop {
242  /// Calls `tick` at `deadline`, then again at whatever it returns.
243  /// `None`, logged, if the loop refused it.
244  fn insert_timer(
245    &self,
246    deadline: Instant,
247    tick: Box<dyn FnMut() -> TimeoutAction>,
248  ) -> Option<RegistrationToken>;
249
250  /// Calls `ready` whenever `fd` is ready for `interest`. The loop owns
251  /// `fd` and closes it when the source is removed. `None`, logged, if
252  /// the loop refused it.
253  fn insert_fd(
254    &self,
255    fd: OwnedFd,
256    interest: Interest,
257    ready: Box<dyn FnMut()>,
258  ) -> Option<RegistrationToken>;
259
260  /// Removes a source. One that is already gone is ignored.
261  fn remove(&self, token: RegistrationToken);
262
263  /// Adds the services' executor. `false`, logged, if the loop refused it.
264  fn insert_executor(&self, executor: Executor<()>) -> bool;
265
266  /// Calls `on_message` with each message libpulse's thread sends on
267  /// `channel`. `false`, logged, if the loop refused it.
268  fn insert_audio_channel(
269    &self,
270    channel: Channel<audio::Message>,
271    on_message: Box<dyn FnMut(audio::Message)>,
272  ) -> bool;
273}
274
275impl<D: 'static> EventLoop for LoopHandle<'static, D> {
276  fn insert_timer(
277    &self,
278    deadline: Instant,
279    mut tick: Box<dyn FnMut() -> TimeoutAction>,
280  ) -> Option<RegistrationToken> {
281    self
282      .insert_source(Timer::from_deadline(deadline), move |_, _, _| tick())
283      .inspect_err(|error| tracing::error!(%error, "a service's timer couldn't be added"))
284      .ok()
285  }
286
287  fn insert_fd(
288    &self,
289    fd: OwnedFd,
290    interest: Interest,
291    mut ready: Box<dyn FnMut()>,
292  ) -> Option<RegistrationToken> {
293    let source = Generic::new(fd, interest, Mode::Level);
294    self
295      .insert_source(source, move |_, _, _| {
296        ready();
297        Ok(PostAction::Continue)
298      })
299      .inspect_err(|error| tracing::error!(%error, "a service's socket couldn't be added"))
300      .ok()
301  }
302
303  fn remove(&self, token: RegistrationToken) {
304    LoopHandle::remove(self, token);
305  }
306
307  fn insert_executor(&self, executor: Executor<()>) -> bool {
308    self
309      .insert_source(executor, |(), _, _| {})
310      .inspect_err(|error| tracing::error!(%error, "the services' executor couldn't be added"))
311      .is_ok()
312  }
313
314  fn insert_audio_channel(
315    &self,
316    channel: Channel<audio::Message>,
317    mut on_message: Box<dyn FnMut(audio::Message)>,
318  ) -> bool {
319    self
320      .insert_source(channel, move |event, _, _| {
321        if let calloop::channel::Event::Msg(message) = event {
322          on_message(message);
323        }
324      })
325      .inspect_err(|error| tracing::error!(%error, "the audio channel couldn't be added"))
326      .is_ok()
327  }
328}
329
330/// Where the services find the system. Tests point it at their own files
331/// and a private bus.
332struct Config {
333  /// The link that names the system time zone, `/etc/localtime`. `None`
334  /// when `TZ` is set, since it names the zone then, as it does for libc.
335  localtime: Option<PathBuf>,
336  /// The system bus, where logind and timedated are. `None` for none.
337  system_bus: Option<zbus::Address>,
338  /// The session bus, where tray items are. `None` for none.
339  session_bus: Option<zbus::Address>,
340  /// Sway's IPC socket, from `SWAYSOCK`. `None` when that is unset.
341  sway: Option<sway::Socket>,
342  /// The audio server. `None` for none, so a test never reaches the
343  /// user's.
344  audio: Option<audio::Server>,
345}
346
347impl Config {
348  fn system() -> Config {
349    Config {
350      localtime: std::env::var_os("TZ")
351        .is_none()
352        .then(|| PathBuf::from("/etc/localtime")),
353      system_bus: zbus::Address::system()
354        .inspect_err(|error| tracing::warn!(%error, "no system bus address"))
355        .ok(),
356      session_bus: zbus::Address::session()
357        .inspect_err(|error| tracing::warn!(%error, "no session bus address"))
358        .ok(),
359      sway: std::env::var_os("SWAYSOCK")
360        .filter(|path| !path.is_empty())
361        .map(|path| sway::Socket {
362          path: path.into(),
363          retry: sway::RETRY,
364        }),
365      audio: Some(audio::Server {
366        address: None,
367        retry: audio::RETRY,
368        retry_for: audio::RETRY_FOR,
369      }),
370    }
371  }
372}