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}