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}
648
649impl Config {
650 fn system() -> Config {
651 Config {
652 localtime: std::env::var_os("TZ")
653 .is_none()
654 .then(|| PathBuf::from("/etc/localtime")),
655 system_bus: zbus::Address::system()
656 .inspect_err(|error| tracing::warn!(%error, "no system bus address"))
657 .ok(),
658 session_bus: zbus::Address::session()
659 .inspect_err(|error| tracing::warn!(%error, "no session bus address"))
660 .ok(),
661 sway: std::env::var_os("SWAYSOCK")
662 .filter(|path| !path.is_empty())
663 .map(|path| sway::Socket {
664 path: path.into(),
665 retry: sway::RETRY,
666 }),
667 hyprland: std::env::var_os("XDG_RUNTIME_DIR")
668 .filter(|dir| !dir.is_empty())
669 .zip(std::env::var_os("HYPRLAND_INSTANCE_SIGNATURE").filter(|sig| !sig.is_empty()))
670 .map(|(dir, signature)| PathBuf::from(dir).join("hypr").join(signature)),
671 audio: Some(audio::Server {
672 address: None,
673 retry: audio::RETRY,
674 retry_for: audio::RETRY_FOR,
675 }),
676 popup_timeout: notifications::POPUP_TIMEOUT,
677 greetd: std::env::var_os("GREETD_SOCK")
678 .filter(|path| !path.is_empty())
679 .map(PathBuf::from),
680 desktop_entries: desktop_entries::Sources::system(),
681 pam: Arc::new(pam::LibPam),
682 polkit_helper: PathBuf::from(polkit::HELPER),
683 }
684 }
685}