1#![forbid(unsafe_code)]
15#![warn(missing_docs)]
16
17mod audio;
18mod clock;
19mod sway;
20#[cfg(test)]
21mod testing;
22mod tray;
23
24use std::cell::{Cell, OnceCell, RefCell};
25use std::future::{Future, poll_fn};
26use std::os::fd::OwnedFd;
27use std::path::PathBuf;
28use std::pin::pin;
29use std::rc::Rc;
30use std::task::{Poll, Waker};
31use std::time::{Duration, Instant};
32
33use calloop::channel::Channel;
34use calloop::futures::{Executor, Scheduler};
35use calloop::generic::Generic;
36use calloop::timer::{TimeoutAction, Timer};
37use calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken};
38use crabshell_reactive::{Owner, ReadSignal};
39use crabshell_ui::Cx;
40
41pub use audio::{Audio, Sink};
42pub use clock::Precision;
43pub use sway::{Sway, Workspace};
44pub use tray::{MenuEntry, MenuToggle, Tray, TrayHost, TrayItem, TrayMenu, TrayStatus};
45
46pub fn provide<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>) {
53 provide_with(cx, handle, Config::system());
54}
55
56fn provide_with<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>, config: Config) {
58 let shared = Rc::new(Shared::new(cx.owner(), handle, config));
59 cx.provide(clock::Clocks::new(Rc::clone(&shared)));
60 cx.provide(sway::Service::new(Rc::clone(&shared)));
61 cx.provide(audio::AudioService::new(Rc::clone(&shared)));
62 cx.provide(tray::Service::new(Rc::clone(&shared)));
63}
64
65pub trait ServicesExt {
67 fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned>;
76
77 fn sway(&self) -> Sway;
86
87 fn audio(&self) -> Audio;
97
98 fn tray(&self) -> TrayHost;
108}
109
110impl ServicesExt for Cx {
111 fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned> {
112 clock::clock(self, precision)
113 }
114
115 fn sway(&self) -> Sway {
116 sway::sway(self)
117 }
118
119 fn audio(&self) -> Audio {
120 audio::audio(self)
121 }
122
123 fn tray(&self) -> TrayHost {
124 tray::tray(self)
125 }
126}
127
128struct Shared {
130 root: Owner,
133 event_loop: Box<dyn Fn() -> Option<Box<dyn EventLoop>>>,
139 executor: OnceCell<Option<Scheduler<()>>>,
142 config: Config,
143}
144
145impl Shared {
146 fn new<D: 'static>(root: Owner, handle: &LoopHandle<'static, D>, config: Config) -> Shared {
147 let weak = handle.downgrade();
148 Shared {
149 root,
150 event_loop: Box::new(move || {
151 weak
152 .upgrade()
153 .map(|handle| Box::new(handle) as Box<dyn EventLoop>)
154 }),
155 executor: OnceCell::new(),
156 config,
157 }
158 }
159
160 fn event_loop(&self) -> Option<Box<dyn EventLoop>> {
162 (self.event_loop)()
163 }
164
165 fn spawn(&self, future: impl Future<Output = ()> + 'static) {
168 let scheduler = self.executor.get_or_init(|| {
169 let (executor, scheduler) = calloop::futures::executor()
170 .inspect_err(|error| tracing::error!(%error, "no executor for the services"))
171 .ok()?;
172 self
173 .event_loop()?
174 .insert_executor(executor)
175 .then_some(scheduler)
176 });
177 if let Some(scheduler) = scheduler
178 && let Err(error) = scheduler.schedule(future)
179 {
180 tracing::error!(%error, "a service's future couldn't start");
181 }
182 }
183}
184
185impl Shared {
186 async fn timeout<T>(
191 &self,
192 limit: Duration,
193 call: impl Future<Output = zbus::Result<T>>,
194 ) -> zbus::Result<T> {
195 let fired = Rc::new(Cell::new(false));
196 let waker: Rc<RefCell<Option<Waker>>> = Rc::default();
197 let tick = {
198 let (fired, waker) = (Rc::clone(&fired), Rc::clone(&waker));
199 Box::new(move || {
200 fired.set(true);
201 if let Some(waker) = waker.take() {
202 waker.wake();
203 }
204 TimeoutAction::Drop
205 })
206 };
207 let token = self
208 .event_loop()
209 .and_then(|event_loop| event_loop.insert_timer(Instant::now() + limit, tick));
210 let _timer = RemoveOnDrop(self, token);
211 let mut call = pin!(call);
212 poll_fn(|context| {
213 if let Poll::Ready(result) = call.as_mut().poll(context) {
214 return Poll::Ready(result);
215 }
216 if fired.get() {
217 let error = std::io::Error::new(std::io::ErrorKind::TimedOut, "no answer in time");
218 return Poll::Ready(Err(zbus::Error::InputOutput(error.into())));
219 }
220 waker.replace(Some(context.waker().clone()));
221 Poll::Pending
222 })
223 .await
224 }
225}
226
227struct RemoveOnDrop<'a>(&'a Shared, Option<RegistrationToken>);
230
231impl Drop for RemoveOnDrop<'_> {
232 fn drop(&mut self) {
233 if let (Some(token), Some(event_loop)) = (self.1, self.0.event_loop()) {
234 event_loop.remove(token);
235 }
236 }
237}
238
239trait EventLoop {
242 fn insert_timer(
245 &self,
246 deadline: Instant,
247 tick: Box<dyn FnMut() -> TimeoutAction>,
248 ) -> Option<RegistrationToken>;
249
250 fn insert_fd(
254 &self,
255 fd: OwnedFd,
256 interest: Interest,
257 ready: Box<dyn FnMut()>,
258 ) -> Option<RegistrationToken>;
259
260 fn remove(&self, token: RegistrationToken);
262
263 fn insert_executor(&self, executor: Executor<()>) -> bool;
265
266 fn insert_audio_channel(
269 &self,
270 channel: Channel<audio::Message>,
271 on_message: Box<dyn FnMut(audio::Message)>,
272 ) -> bool;
273}
274
275impl<D: 'static> EventLoop for LoopHandle<'static, D> {
276 fn insert_timer(
277 &self,
278 deadline: Instant,
279 mut tick: Box<dyn FnMut() -> TimeoutAction>,
280 ) -> Option<RegistrationToken> {
281 self
282 .insert_source(Timer::from_deadline(deadline), move |_, _, _| tick())
283 .inspect_err(|error| tracing::error!(%error, "a service's timer couldn't be added"))
284 .ok()
285 }
286
287 fn insert_fd(
288 &self,
289 fd: OwnedFd,
290 interest: Interest,
291 mut ready: Box<dyn FnMut()>,
292 ) -> Option<RegistrationToken> {
293 let source = Generic::new(fd, interest, Mode::Level);
294 self
295 .insert_source(source, move |_, _, _| {
296 ready();
297 Ok(PostAction::Continue)
298 })
299 .inspect_err(|error| tracing::error!(%error, "a service's socket couldn't be added"))
300 .ok()
301 }
302
303 fn remove(&self, token: RegistrationToken) {
304 LoopHandle::remove(self, token);
305 }
306
307 fn insert_executor(&self, executor: Executor<()>) -> bool {
308 self
309 .insert_source(executor, |(), _, _| {})
310 .inspect_err(|error| tracing::error!(%error, "the services' executor couldn't be added"))
311 .is_ok()
312 }
313
314 fn insert_audio_channel(
315 &self,
316 channel: Channel<audio::Message>,
317 mut on_message: Box<dyn FnMut(audio::Message)>,
318 ) -> bool {
319 self
320 .insert_source(channel, move |event, _, _| {
321 if let calloop::channel::Event::Msg(message) = event {
322 on_message(message);
323 }
324 })
325 .inspect_err(|error| tracing::error!(%error, "the audio channel couldn't be added"))
326 .is_ok()
327 }
328}
329
330struct Config {
333 localtime: Option<PathBuf>,
336 system_bus: Option<zbus::Address>,
338 session_bus: Option<zbus::Address>,
340 sway: Option<sway::Socket>,
342 audio: Option<audio::Server>,
345}
346
347impl Config {
348 fn system() -> Config {
349 Config {
350 localtime: std::env::var_os("TZ")
351 .is_none()
352 .then(|| PathBuf::from("/etc/localtime")),
353 system_bus: zbus::Address::system()
354 .inspect_err(|error| tracing::warn!(%error, "no system bus address"))
355 .ok(),
356 session_bus: zbus::Address::session()
357 .inspect_err(|error| tracing::warn!(%error, "no session bus address"))
358 .ok(),
359 sway: std::env::var_os("SWAYSOCK")
360 .filter(|path| !path.is_empty())
361 .map(|path| sway::Socket {
362 path: path.into(),
363 retry: sway::RETRY,
364 }),
365 audio: Some(audio::Server {
366 address: None,
367 retry: audio::RETRY,
368 retry_for: audio::RETRY_FOR,
369 }),
370 }
371 }
372}