Skip to main content

crabshell_services/
lib.rs

1//! The services a Crabshell bar reads: the clock, Sway's and Hyprland's
2//! workspaces, audio, the tray, power, notifications, the installed
3//! applications, PAM, greetd and a polkit agent.
4//!
5//! The shell calls [`provide`] once, with its root context and event loop.
6//! After that, any context hands out services through [`ServicesExt`],
7//! such as `cx.clock(Precision::Minute)`. Each service starts the first
8//! time a config asks for it and is shared by every caller, so two outputs
9//! never mean two connections. Its signals belong to the root context's
10//! owner: bars on every output share them, and they go when the shell does.
11//!
12//! Asked for on a context the shell didn't set up, as in a ui test, a
13//! service logs once and hands out a value that never changes.
14
15#![forbid(unsafe_code)]
16#![warn(missing_docs)]
17
18mod audio;
19mod bluetooth;
20mod clock;
21mod dbus;
22mod desktop_entries;
23mod file;
24mod greetd;
25mod hyprland;
26mod lines;
27mod mpris;
28mod network;
29mod notifications;
30mod pam;
31mod polkit;
32mod power;
33mod process;
34mod socket;
35mod socket_server;
36mod spawn;
37mod sway;
38#[cfg(test)]
39mod testing;
40mod tray;
41mod watch;
42
43use std::cell::{Cell, OnceCell, RefCell};
44use std::future::{Future, poll_fn};
45use std::os::fd::OwnedFd;
46use std::path::PathBuf;
47use std::pin::pin;
48use std::rc::Rc;
49use std::sync::Arc;
50use std::task::{Poll, Waker};
51use std::time::{Duration, Instant};
52
53use calloop::channel::Channel;
54use calloop::futures::{Executor, Scheduler};
55use calloop::generic::Generic;
56use calloop::timer::{TimeoutAction, Timer};
57use calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken};
58use crabshell_reactive::{Owner, ReadSignal, Signal};
59use crabshell_ui::Cx;
60
61pub use audio::{Audio, AudioChannel, AudioNode, AudioStream, PeakTarget, Sink};
62pub use bluetooth::{
63  Bluetooth, BluetoothAdapter, BluetoothAdapterState, BluetoothDevice, BluetoothDeviceState,
64};
65pub use clock::Precision;
66pub use desktop_entries::{DesktopAction, DesktopEntries, DesktopEntry};
67pub use file::{FileError, FileErrorKind, FileView};
68pub use greetd::{GreetMessage, GreetState, Greetd};
69pub use hyprland::{Hyprland, HyprlandEvent, HyprlandMonitor, HyprlandToplevel, HyprlandWorkspace};
70pub use mpris::{LoopStatus, Mpris, PlaybackStatus, Player};
71pub use network::{
72  ConnectionFailReason, Connectivity, Network, NetworkDevice, NetworkKind, NetworkState,
73  SavedConnection, WifiMode, WifiNetwork, WifiSecurity,
74};
75pub use notifications::{CloseReason, Notification, Notifications, Timeout, Urgency};
76pub use pam::{Pam, PamError, PamMessage, PamResult, PamState};
77pub use polkit::{AuthRequest, AuthState, PolkitAgent};
78pub use power::{
79  BatteryState, DeviceKind, PerformanceDegradation, PowerDevice, PowerProfile, PowerProfiles,
80  UPower,
81};
82pub use process::{IntoCommand, Process};
83pub use socket::Socket;
84pub use socket_server::SocketServer;
85pub use sway::{Sway, SwayEvent, SwayEventKind, SwayOutput, Workspace};
86pub use tray::{
87  MenuEntry, MenuToggle, Tray, TrayCategory, TrayHost, TrayItem, TrayMenu, TrayStatus,
88};
89
90/// Makes the services reachable from `cx` and every context made from it.
91/// The shell calls this once, with its root context and its event loop.
92///
93/// Nothing starts here: each service starts when a config first asks for
94/// it. The loop is kept weakly, so it and the compositor connection still
95/// go when the shell drops.
96pub fn provide<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>) {
97  provide_with(cx, handle, Config::system());
98}
99
100/// [`provide`] with the system's parts swapped for a test's.
101fn provide_with<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>, config: Config) {
102  let shared = Rc::new(Shared::new(cx.owner(), handle, config));
103  cx.provide(clock::Clocks::new(Rc::clone(&shared)));
104  cx.provide(sway::Service::new(Rc::clone(&shared)));
105  cx.provide(hyprland::Service::new(Rc::clone(&shared)));
106  cx.provide(audio::AudioService::new(Rc::clone(&shared)));
107  cx.provide(tray::Service::new(Rc::clone(&shared)));
108  cx.provide(power::UPowerService::new(Rc::clone(&shared)));
109  cx.provide(power::ProfilesService::new(Rc::clone(&shared)));
110  cx.provide(bluetooth::BluetoothService::new(Rc::clone(&shared)));
111  cx.provide(notifications::Service::new(Rc::clone(&shared)));
112  cx.provide(mpris::Service::new(Rc::clone(&shared)));
113  cx.provide(network::NetworkService::new(Rc::clone(&shared)));
114  cx.provide(socket::Sockets(Rc::clone(&shared)));
115  cx.provide(process::Processes::new(Rc::clone(&shared)));
116  cx.provide(desktop_entries::Service::new(Rc::clone(&shared)));
117  cx.provide(file::Files(Rc::clone(&shared)));
118  cx.provide(greetd::Service::new(Rc::clone(&shared)));
119  cx.provide(pam::Service(Rc::clone(&shared)));
120  cx.provide(polkit::Service::new(Rc::clone(&shared)));
121}
122
123/// Stops every program a config started with `cx.process`. The shell
124/// calls this before it exits, since `std::process::exit` skips the drops
125/// that would, and each program has a process group of its own.
126#[doc(hidden)]
127pub fn stop_programs(cx: &Cx) {
128  process::stop_all(cx);
129}
130
131/// The services a context hands out.
132pub trait ServicesExt {
133  /// The time, truncated to `precision`: it changes on each whole second
134  /// or minute and holds the one just passed, in the system time zone.
135  /// Every caller asking for the same precision gets the same signal.
136  ///
137  /// A minute clock wakes the shell once a minute and at no other time.
138  /// After a suspend, and when timedated changes the time zone, it is
139  /// right again at once. A zone changed any other way shows from the
140  /// next tick.
141  fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned>;
142
143  /// Sway's workspaces, outputs, commands and events, over the IPC socket
144  /// at `$SWAYSOCK`, as Quickshell's `I3`. Every caller shares one
145  /// connection, made on the first call.
146  ///
147  /// The lists are empty while Sway isn't there. When `SWAYSOCK` is unset
148  /// or the first connect fails, they stay empty and the service never
149  /// wakes the shell. When a working connection closes, they are empty
150  /// until a reconnect, tried every 2 s, works. While connected, it wakes
151  /// the shell only for a workspace event, an output event once a config
152  /// asked for the outputs, and the kinds of event a handler asked for.
153  fn sway(&self) -> Sway;
154
155  /// Hyprland's workspaces, monitors and windows, over its IPC sockets
156  /// in `$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/`. Every
157  /// caller shares one event connection, made on the first call.
158  ///
159  /// The lists are empty while Hyprland isn't there: when the signature
160  /// is unset or its sockets are missing, they stay empty, which is
161  /// logged once, and the service never wakes the shell. When Hyprland
162  /// closes its event socket, the lists empty and nothing reconnects.
163  /// While connected, it wakes the shell only when Hyprland sends
164  /// something.
165  fn hyprland(&self) -> Hyprland;
166
167  /// The audio server's default sink, with its volume and mute. Every
168  /// caller shares one connection, which starts on the loop's next turn
169  /// and never waits for the server. It wakes the shell only when the
170  /// server reports a change, and while a peak meter from
171  /// [`Audio::peaks`] records.
172  ///
173  /// With no server, the sink is `None` and the service tries again
174  /// every 2 s, for up to a minute after startup and after each drop of
175  /// a working connection. Then it stops, logs that once and never wakes
176  /// the shell again; a restart tries again.
177  fn audio(&self) -> Audio;
178
179  /// The system tray: every app's StatusNotifierItem on the session bus.
180  /// Every caller shares one connection, which starts on the loop's next
181  /// turn and never waits for the bus.
182  ///
183  /// The shell runs the StatusNotifierWatcher while no other program
184  /// does, and otherwise reads the list from the one that runs, taking
185  /// over when it leaves. With no session bus the list stays empty and
186  /// the service never wakes the shell. It wakes the shell only when
187  /// something arrives on the bus for it.
188  fn tray(&self) -> TrayHost;
189
190  /// UPower's batteries and power supplies, on the system bus. Every
191  /// caller shares one connection, which starts on the loop's next turn.
192  ///
193  /// While UPower isn't running the lists are empty, and they fill when
194  /// it starts. With no system bus they stay empty and the service never
195  /// wakes the shell. It wakes the shell only when UPower says something
196  /// changed.
197  fn upower(&self) -> UPower;
198
199  /// The power profile, from power-profiles-daemon on the system bus.
200  /// Every caller shares one connection. It behaves as
201  /// [`upower`](Self::upower) does while the daemon isn't there.
202  fn power_profiles(&self) -> PowerProfiles;
203
204  /// Bluetooth adapters and devices, from BlueZ on the system bus. Every
205  /// caller shares one connection. It behaves as [`upower`](Self::upower)
206  /// does while BlueZ isn't there, and wakes the shell only when BlueZ
207  /// says something changed.
208  fn bluetooth(&self) -> Bluetooth;
209
210  /// The notification server on the session bus: what apps sent, and
211  /// the popups still showing. Every caller shares one connection, which
212  /// starts on the loop's next turn and never waits for the bus.
213  ///
214  /// The shell serves while no other program does. While another daemon
215  /// has the name, the shell waits for it to leave, and keeps what it was
216  /// sent if one takes the name over. With no session bus the lists stay
217  /// empty and the service never wakes the shell. It wakes the shell only
218  /// when an app calls and when a popup expires.
219  fn notifications(&self) -> Notifications;
220
221  /// The media players on the session bus, over MPRIS, such as a music
222  /// player or a browser playing a video. Every caller shares one
223  /// connection, which starts on the loop's next turn and never waits
224  /// for the bus.
225  ///
226  /// With no session bus the list stays empty and the service never
227  /// wakes the shell. It wakes the shell only when a player says
228  /// something changed. MPRIS doesn't say when the position moves, so a
229  /// bar works it out with [`Player::position_at`].
230  fn mpris(&self) -> Mpris;
231
232  /// Network devices and Wi-Fi networks, from NetworkManager on the
233  /// system bus. Every caller shares one connection. It behaves as
234  /// [`upower`](Self::upower) does while NetworkManager isn't there, and
235  /// wakes the shell only when NetworkManager says something changed.
236  fn network(&self) -> Network;
237
238  /// A connection to the Unix socket at `path`, made at once, as
239  /// Quickshell's `Socket`. Each call opens its own, which closes when
240  /// this context goes. It wakes the shell only when the other end sends
241  /// something or takes what was written.
242  fn socket(&self, path: impl Into<PathBuf>) -> Socket;
243
244  /// A server on the Unix socket at `path`, as Quickshell's
245  /// `SocketServer`: it listens at once, and each client that connects
246  /// becomes a [`Socket`] for [`SocketServer::on_connection`]. Each call
247  /// makes its own, which stops when this context goes, closing its
248  /// connections and removing its socket file. It wakes the shell only
249  /// when a client connects, sends something or takes what was written.
250  fn socket_server(&self, path: impl Into<PathBuf>) -> SocketServer;
251
252  /// Runs the program `command` builds, as Quickshell's `Process`, and
253  /// keeps what it prints and how it ended. `command` returns a
254  /// [`Command`](std::process::Command) or a list such as
255  /// `["uptime", "-p"]`. It starts on the loop's next turn, and again
256  /// whenever a signal `command` read changes or [`Process::restart`] is
257  /// called, stopping the run before. It is stopped when this context
258  /// goes. It wakes the shell only when the program prints, takes input
259  /// or ends.
260  fn process<C: IntoCommand>(&self, command: impl Fn() -> C + 'static) -> Process;
261
262  /// Starts `command` and lets it run on its own, as a launcher does:
263  /// its input and outputs are empty, it has a process group of its own,
264  /// and it is reaped when it ends. A failure to start is logged.
265  fn exec_detached(&self, command: impl IntoCommand);
266
267  /// The text file at `path`, as Quickshell's `FileView`: read now, and
268  /// again when it changes on disk. Each call reads its own, which goes
269  /// with this context. It is read on the main thread, so a file on a
270  /// slow disk delays the frame.
271  fn file_view(&self, path: impl Into<PathBuf>) -> FileView;
272
273  /// greetd, for a greeter: makes a session for a user, answers its
274  /// questions and starts it, over the socket `GREETD_SOCK` names. One
275  /// connection per shell, made at the first request; it wakes the shell
276  /// only when greetd answers.
277  fn greetd(&self) -> Greetd;
278
279  /// A value kept in the JSON file at `path`, as Quickshell's
280  /// `JsonAdapter`: `default` until the file is read, the file's value
281  /// once it is, and the file rewritten whenever the config sets another.
282  /// A missing file, and its directory, is made at the first set. A file
283  /// that isn't `T`'s
284  /// JSON is logged and left alone, and the value stays.
285  fn json_file<T>(&self, path: impl Into<PathBuf>, default: T) -> Signal<T>
286  where
287    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + 'static;
288
289  /// The installed applications, from the desktop entries in the XDG
290  /// data directories: what a launcher lists and starts, and where a
291  /// window's app id finds its icon. Every caller shares one list, read
292  /// on the loop's next turn after the first call.
293  ///
294  /// The directories are watched, and the list is read again once the
295  /// files have been still for 100 ms after a change. It wakes the shell
296  /// at no other time.
297  fn desktop_entries(&self) -> DesktopEntries;
298
299  /// Authentication through the PAM service `service`, the name of a
300  /// file in `/etc/pam.d`, as a lock screen asks for the user's
301  /// password. Each call has its own, which goes with this context.
302  ///
303  /// Nothing runs until [`Pam::start`]. Each attempt runs on a thread of
304  /// its own, since libpam blocks, and wakes the shell only when PAM asks
305  /// or says something, or the attempt ends. After a success PAM's
306  /// account step runs too, which clears `pam_faillock`'s count of past
307  /// failures; its refusal is only logged. An attempt ended while PAM
308  /// waits for an answer counts as a failure, as [`Pam::start`] says.
309  fn pam(&self, service: &str) -> Pam;
310
311  /// The shell's polkit authentication agent, as Quickshell's
312  /// `PolkitAgent`: when a program asks polkit for something only some
313  /// users may do, its request shows here for the config's dialog. The
314  /// first call registers the agent with polkit on the system bus for
315  /// this session; a shell that never calls it never reaches polkit.
316  ///
317  /// A refused registration is logged and no requests arrive. It wakes
318  /// the shell only when polkit calls and while a request is answered.
319  fn polkit_agent(&self) -> PolkitAgent;
320}
321
322impl ServicesExt for Cx {
323  fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned> {
324    clock::clock(self, precision)
325  }
326
327  fn sway(&self) -> Sway {
328    sway::sway(self)
329  }
330
331  fn hyprland(&self) -> Hyprland {
332    hyprland::hyprland(self)
333  }
334
335  fn audio(&self) -> Audio {
336    audio::audio(self)
337  }
338
339  fn tray(&self) -> TrayHost {
340    tray::tray(self)
341  }
342
343  fn upower(&self) -> UPower {
344    power::upower(self)
345  }
346
347  fn power_profiles(&self) -> PowerProfiles {
348    power::power_profiles(self)
349  }
350
351  fn bluetooth(&self) -> Bluetooth {
352    bluetooth::bluetooth(self)
353  }
354
355  fn notifications(&self) -> Notifications {
356    notifications::notifications(self)
357  }
358
359  fn mpris(&self) -> Mpris {
360    mpris::mpris(self)
361  }
362
363  fn network(&self) -> Network {
364    network::network(self)
365  }
366
367  fn socket(&self, path: impl Into<PathBuf>) -> Socket {
368    socket::socket(self, path.into())
369  }
370
371  fn socket_server(&self, path: impl Into<PathBuf>) -> SocketServer {
372    socket_server::socket_server(self, path.into())
373  }
374
375  #[track_caller]
376  fn process<C: IntoCommand>(&self, command: impl Fn() -> C + 'static) -> Process {
377    process::process(self, command)
378  }
379
380  fn exec_detached(&self, command: impl IntoCommand) {
381    process::exec_detached(self, command);
382  }
383
384  fn file_view(&self, path: impl Into<PathBuf>) -> FileView {
385    file::file_view(self, path.into())
386  }
387
388  fn greetd(&self) -> Greetd {
389    greetd::greetd(self)
390  }
391
392  fn json_file<T>(&self, path: impl Into<PathBuf>, default: T) -> Signal<T>
393  where
394    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + 'static,
395  {
396    file::json_file(self, path.into(), default)
397  }
398
399  fn desktop_entries(&self) -> DesktopEntries {
400    desktop_entries::desktop_entries(self)
401  }
402
403  fn pam(&self, service: &str) -> Pam {
404    pam::pam(self, service)
405  }
406
407  fn polkit_agent(&self) -> PolkitAgent {
408    polkit::polkit_agent(self)
409  }
410}
411
412/// What every service of one shell starts from.
413struct Shared {
414  /// The root context's owner. Services create their signals under it,
415  /// so bars on every output share them and they go with the shell.
416  root: Owner,
417  /// The event loop, held weakly. Services' timers and futures live in
418  /// the loop and hold the services, so a strong handle here would be an
419  /// `Rc` cycle that keeps the loop and the compositor connection alive
420  /// after the shell drops. calloop's weak handle has no type we can
421  /// name, so a closure keeps it.
422  event_loop: Box<dyn Fn() -> Option<Box<dyn EventLoop>>>,
423  /// The one futures executor every service's D-Bus calls run on, added
424  /// to the loop on first use. `None` if that failed.
425  executor: OnceCell<Option<Scheduler<()>>>,
426  /// The one inotify instance, made for the first directory watched.
427  watcher: watch::Watcher,
428  config: Config,
429}
430
431impl Shared {
432  fn new<D: 'static>(root: Owner, handle: &LoopHandle<'static, D>, config: Config) -> Shared {
433    let weak = handle.downgrade();
434    Shared {
435      root,
436      event_loop: Box::new(move || {
437        weak
438          .upgrade()
439          .map(|handle| Box::new(handle) as Box<dyn EventLoop>)
440      }),
441      executor: OnceCell::new(),
442      watcher: watch::Watcher::default(),
443      config,
444    }
445  }
446
447  /// The event loop, while it is still there.
448  fn event_loop(&self) -> Option<Box<dyn EventLoop>> {
449    (self.event_loop)()
450  }
451
452  /// Runs `future` on the main thread, between the loop's other sources,
453  /// so no D-Bus call delays a frame.
454  fn spawn(&self, future: impl Future<Output = ()> + 'static) {
455    let scheduler = self.executor.get_or_init(|| {
456      let (executor, scheduler) = calloop::futures::executor()
457        .inspect_err(|error| tracing::error!(%error, "no executor for the services"))
458        .ok()?;
459      self
460        .event_loop()?
461        .insert_executor(executor)
462        .then_some(scheduler)
463    });
464    if let Some(scheduler) = scheduler
465      && let Err(error) = scheduler.schedule(future)
466    {
467      tracing::error!(%error, "a service's future couldn't start");
468    }
469  }
470}
471
472impl Shared {
473  /// Awaits `call`, or gives up after `limit` with a timed-out error. The
474  /// timer is the loop's and goes as soon as the call ends. zbus's own
475  /// timeout left its timer behind, which woke zbus's thread once, `limit`
476  /// after every call, while nothing changed.
477  async fn timeout<T>(
478    &self,
479    limit: Duration,
480    call: impl Future<Output = zbus::Result<T>>,
481  ) -> zbus::Result<T> {
482    let fired = Rc::new(Cell::new(false));
483    let waker: Rc<RefCell<Option<Waker>>> = Rc::default();
484    let tick = {
485      let (fired, waker) = (Rc::clone(&fired), Rc::clone(&waker));
486      Box::new(move || {
487        fired.set(true);
488        if let Some(waker) = waker.take() {
489          waker.wake();
490        }
491        TimeoutAction::Drop
492      })
493    };
494    let token = self
495      .event_loop()
496      .and_then(|event_loop| event_loop.insert_timer(Instant::now() + limit, tick));
497    let _timer = RemoveOnDrop(self, token);
498    let mut call = pin!(call);
499    poll_fn(|context| {
500      if let Poll::Ready(result) = call.as_mut().poll(context) {
501        return Poll::Ready(result);
502      }
503      if fired.get() {
504        let error = std::io::Error::new(std::io::ErrorKind::TimedOut, "no answer in time");
505        return Poll::Ready(Err(zbus::Error::InputOutput(error.into())));
506      }
507      waker.replace(Some(context.waker().clone()));
508      Poll::Pending
509    })
510    .await
511  }
512}
513
514/// Removes a loop source when dropped, as a call that ends removes its
515/// timer.
516struct RemoveOnDrop<'a>(&'a Shared, Option<RegistrationToken>);
517
518impl Drop for RemoveOnDrop<'_> {
519  fn drop(&mut self) {
520    if let (Some(token), Some(event_loop)) = (self.1, self.0.event_loop()) {
521      event_loop.remove(token);
522    }
523  }
524}
525
526/// The parts of the event loop services use. It hides the loop's data
527/// type, since one services map serves any shell.
528trait EventLoop {
529  /// Calls `tick` at `deadline`, then again at whatever it returns.
530  /// `None`, logged, if the loop refused it.
531  fn insert_timer(
532    &self,
533    deadline: Instant,
534    tick: Box<dyn FnMut() -> TimeoutAction>,
535  ) -> Option<RegistrationToken>;
536
537  /// Calls `ready` whenever `fd` is ready for `interest`. The loop owns
538  /// `fd` and closes it when the source is removed. `None`, logged, if
539  /// the loop refused it.
540  fn insert_fd(
541    &self,
542    fd: OwnedFd,
543    interest: Interest,
544    ready: Box<dyn FnMut()>,
545  ) -> Option<RegistrationToken>;
546
547  /// Removes a source. One that is already gone is ignored.
548  fn remove(&self, token: RegistrationToken);
549
550  /// Adds the services' executor. `false`, logged, if the loop refused it.
551  fn insert_executor(&self, executor: Executor<()>) -> bool;
552
553  /// Calls `on_message` with each message libpulse's thread sends on
554  /// `channel`. `false`, logged, if the loop refused it.
555  fn insert_audio_channel(
556    &self,
557    channel: Channel<audio::Message>,
558    on_message: Box<dyn FnMut(audio::Message)>,
559  ) -> bool;
560
561  /// Calls `on_event` with each event a PAM attempt's thread sends on
562  /// `channel`, and with `Closed` once the thread is gone. `None`, logged,
563  /// if the loop refused it.
564  fn insert_pam_channel(
565    &self,
566    channel: Channel<pam::Event>,
567    on_event: Box<dyn FnMut(calloop::channel::Event<pam::Event>)>,
568  ) -> Option<RegistrationToken>;
569}
570
571impl<D: 'static> EventLoop for LoopHandle<'static, D> {
572  fn insert_timer(
573    &self,
574    deadline: Instant,
575    mut tick: Box<dyn FnMut() -> TimeoutAction>,
576  ) -> Option<RegistrationToken> {
577    self
578      .insert_source(Timer::from_deadline(deadline), move |_, _, _| tick())
579      .inspect_err(|error| tracing::error!(%error, "a service's timer couldn't be added"))
580      .ok()
581  }
582
583  fn insert_fd(
584    &self,
585    fd: OwnedFd,
586    interest: Interest,
587    mut ready: Box<dyn FnMut()>,
588  ) -> Option<RegistrationToken> {
589    let source = Generic::new(fd, interest, Mode::Level);
590    self
591      .insert_source(source, move |_, _, _| {
592        ready();
593        Ok(PostAction::Continue)
594      })
595      .inspect_err(|error| tracing::error!(%error, "a service's socket couldn't be added"))
596      .ok()
597  }
598
599  fn remove(&self, token: RegistrationToken) {
600    LoopHandle::remove(self, token);
601  }
602
603  fn insert_executor(&self, executor: Executor<()>) -> bool {
604    self
605      .insert_source(executor, |(), _, _| {})
606      .inspect_err(|error| tracing::error!(%error, "the services' executor couldn't be added"))
607      .is_ok()
608  }
609
610  fn insert_audio_channel(
611    &self,
612    channel: Channel<audio::Message>,
613    mut on_message: Box<dyn FnMut(audio::Message)>,
614  ) -> bool {
615    self
616      .insert_source(channel, move |event, _, _| {
617        if let calloop::channel::Event::Msg(message) = event {
618          on_message(message);
619        }
620      })
621      .inspect_err(|error| tracing::error!(%error, "the audio channel couldn't be added"))
622      .is_ok()
623  }
624
625  fn insert_pam_channel(
626    &self,
627    channel: Channel<pam::Event>,
628    mut on_event: Box<dyn FnMut(calloop::channel::Event<pam::Event>)>,
629  ) -> Option<RegistrationToken> {
630    self
631      .insert_source(channel, move |event, _, _| on_event(event))
632      .inspect_err(|error| tracing::error!(%error, "a PAM attempt's channel couldn't be added"))
633      .ok()
634  }
635}
636
637/// Where the services find the system. Tests point it at their own files
638/// and a private bus.
639struct Config {
640  /// The link that names the system time zone, `/etc/localtime`. `None`
641  /// when `TZ` is set, since it names the zone then, as it does for libc.
642  localtime: Option<PathBuf>,
643  /// The system bus, where logind, timedated, UPower,
644  /// power-profiles-daemon, BlueZ, NetworkManager and polkit are. `None`
645  /// for none.
646  system_bus: Option<zbus::Address>,
647  /// The session bus, where tray items, notifying apps and media
648  /// players are. `None` for none.
649  session_bus: Option<zbus::Address>,
650  /// Sway's IPC socket, from `SWAYSOCK`. `None` when that is unset.
651  sway: Option<sway::Socket>,
652  /// The directory of Hyprland's sockets,
653  /// `$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE`. `None` when
654  /// either is unset.
655  hyprland: Option<PathBuf>,
656  /// The audio server. `None` for none, so a test never reaches the
657  /// user's.
658  audio: Option<audio::Server>,
659  /// How long a popup shows when its app leaves that to the server: 5 s,
660  /// and less in tests.
661  popup_timeout: Duration,
662  /// greetd's socket, from `GREETD_SOCK`. `None` when that is unset.
663  greetd: Option<PathBuf>,
664  /// Where the desktop entries are. Tests give directories of their own.
665  desktop_entries: desktop_entries::Sources,
666  /// What `cx.pam` authenticates with: libpam, and a script in tests, so
667  /// no test counts toward locking a real account.
668  pam: Arc<dyn pam::Backend>,
669  /// polkit's setuid helper, which checks answers with PAM. Tests give a
670  /// script of their own.
671  polkit_helper: PathBuf,
672  /// polkit's socket for its helper, tried before `polkit_helper`.
673  polkit_socket: PathBuf,
674}
675
676impl Config {
677  fn system() -> Config {
678    Config {
679      localtime: std::env::var_os("TZ")
680        .is_none()
681        .then(|| PathBuf::from("/etc/localtime")),
682      system_bus: zbus::Address::system()
683        .inspect_err(|error| tracing::warn!(%error, "no system bus address"))
684        .ok(),
685      session_bus: zbus::Address::session()
686        .inspect_err(|error| tracing::warn!(%error, "no session bus address"))
687        .ok(),
688      sway: std::env::var_os("SWAYSOCK")
689        .filter(|path| !path.is_empty())
690        .map(|path| sway::Socket {
691          path: path.into(),
692          retry: sway::RETRY,
693        }),
694      hyprland: std::env::var_os("XDG_RUNTIME_DIR")
695        .filter(|dir| !dir.is_empty())
696        .zip(std::env::var_os("HYPRLAND_INSTANCE_SIGNATURE").filter(|sig| !sig.is_empty()))
697        .map(|(dir, signature)| PathBuf::from(dir).join("hypr").join(signature)),
698      audio: Some(audio::Server {
699        address: None,
700        retry: audio::RETRY,
701        retry_for: audio::RETRY_FOR,
702      }),
703      popup_timeout: notifications::POPUP_TIMEOUT,
704      greetd: std::env::var_os("GREETD_SOCK")
705        .filter(|path| !path.is_empty())
706        .map(PathBuf::from),
707      desktop_entries: desktop_entries::Sources::system(),
708      pam: Arc::new(pam::LibPam),
709      polkit_helper: PathBuf::from(polkit::HELPER),
710      polkit_socket: PathBuf::from(polkit::HELPER_SOCKET),
711    }
712  }
713}