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