#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod audio;
mod clock;
mod sway;
#[cfg(test)]
mod testing;
mod tray;
use std::cell::{Cell, OnceCell, RefCell};
use std::future::{Future, poll_fn};
use std::os::fd::OwnedFd;
use std::path::PathBuf;
use std::pin::pin;
use std::rc::Rc;
use std::task::{Poll, Waker};
use std::time::{Duration, Instant};
use calloop::channel::Channel;
use calloop::futures::{Executor, Scheduler};
use calloop::generic::Generic;
use calloop::timer::{TimeoutAction, Timer};
use calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken};
use crabshell_reactive::{Owner, ReadSignal};
use crabshell_ui::Cx;
pub use audio::{Audio, Sink};
pub use clock::Precision;
pub use sway::{Sway, Workspace};
pub use tray::{MenuEntry, MenuToggle, Tray, TrayHost, TrayItem, TrayMenu, TrayStatus};
pub fn provide<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>) {
provide_with(cx, handle, Config::system());
}
fn provide_with<D: 'static>(cx: &Cx, handle: &LoopHandle<'static, D>, config: Config) {
let shared = Rc::new(Shared::new(cx.owner(), handle, config));
cx.provide(clock::Clocks::new(Rc::clone(&shared)));
cx.provide(sway::Service::new(Rc::clone(&shared)));
cx.provide(audio::AudioService::new(Rc::clone(&shared)));
cx.provide(tray::Service::new(Rc::clone(&shared)));
}
pub trait ServicesExt {
fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned>;
fn sway(&self) -> Sway;
fn audio(&self) -> Audio;
fn tray(&self) -> TrayHost;
}
impl ServicesExt for Cx {
fn clock(&self, precision: Precision) -> ReadSignal<jiff::Zoned> {
clock::clock(self, precision)
}
fn sway(&self) -> Sway {
sway::sway(self)
}
fn audio(&self) -> Audio {
audio::audio(self)
}
fn tray(&self) -> TrayHost {
tray::tray(self)
}
}
struct Shared {
root: Owner,
event_loop: Box<dyn Fn() -> Option<Box<dyn EventLoop>>>,
executor: OnceCell<Option<Scheduler<()>>>,
config: Config,
}
impl Shared {
fn new<D: 'static>(root: Owner, handle: &LoopHandle<'static, D>, config: Config) -> Shared {
let weak = handle.downgrade();
Shared {
root,
event_loop: Box::new(move || {
weak
.upgrade()
.map(|handle| Box::new(handle) as Box<dyn EventLoop>)
}),
executor: OnceCell::new(),
config,
}
}
fn event_loop(&self) -> Option<Box<dyn EventLoop>> {
(self.event_loop)()
}
fn spawn(&self, future: impl Future<Output = ()> + 'static) {
let scheduler = self.executor.get_or_init(|| {
let (executor, scheduler) = calloop::futures::executor()
.inspect_err(|error| tracing::error!(%error, "no executor for the services"))
.ok()?;
self
.event_loop()?
.insert_executor(executor)
.then_some(scheduler)
});
if let Some(scheduler) = scheduler
&& let Err(error) = scheduler.schedule(future)
{
tracing::error!(%error, "a service's future couldn't start");
}
}
}
impl Shared {
async fn timeout<T>(
&self,
limit: Duration,
call: impl Future<Output = zbus::Result<T>>,
) -> zbus::Result<T> {
let fired = Rc::new(Cell::new(false));
let waker: Rc<RefCell<Option<Waker>>> = Rc::default();
let tick = {
let (fired, waker) = (Rc::clone(&fired), Rc::clone(&waker));
Box::new(move || {
fired.set(true);
if let Some(waker) = waker.take() {
waker.wake();
}
TimeoutAction::Drop
})
};
let token = self
.event_loop()
.and_then(|event_loop| event_loop.insert_timer(Instant::now() + limit, tick));
let _timer = RemoveOnDrop(self, token);
let mut call = pin!(call);
poll_fn(|context| {
if let Poll::Ready(result) = call.as_mut().poll(context) {
return Poll::Ready(result);
}
if fired.get() {
let error = std::io::Error::new(std::io::ErrorKind::TimedOut, "no answer in time");
return Poll::Ready(Err(zbus::Error::InputOutput(error.into())));
}
waker.replace(Some(context.waker().clone()));
Poll::Pending
})
.await
}
}
struct RemoveOnDrop<'a>(&'a Shared, Option<RegistrationToken>);
impl Drop for RemoveOnDrop<'_> {
fn drop(&mut self) {
if let (Some(token), Some(event_loop)) = (self.1, self.0.event_loop()) {
event_loop.remove(token);
}
}
}
trait EventLoop {
fn insert_timer(
&self,
deadline: Instant,
tick: Box<dyn FnMut() -> TimeoutAction>,
) -> Option<RegistrationToken>;
fn insert_fd(
&self,
fd: OwnedFd,
interest: Interest,
ready: Box<dyn FnMut()>,
) -> Option<RegistrationToken>;
fn remove(&self, token: RegistrationToken);
fn insert_executor(&self, executor: Executor<()>) -> bool;
fn insert_audio_channel(
&self,
channel: Channel<audio::Message>,
on_message: Box<dyn FnMut(audio::Message)>,
) -> bool;
}
impl<D: 'static> EventLoop for LoopHandle<'static, D> {
fn insert_timer(
&self,
deadline: Instant,
mut tick: Box<dyn FnMut() -> TimeoutAction>,
) -> Option<RegistrationToken> {
self
.insert_source(Timer::from_deadline(deadline), move |_, _, _| tick())
.inspect_err(|error| tracing::error!(%error, "a service's timer couldn't be added"))
.ok()
}
fn insert_fd(
&self,
fd: OwnedFd,
interest: Interest,
mut ready: Box<dyn FnMut()>,
) -> Option<RegistrationToken> {
let source = Generic::new(fd, interest, Mode::Level);
self
.insert_source(source, move |_, _, _| {
ready();
Ok(PostAction::Continue)
})
.inspect_err(|error| tracing::error!(%error, "a service's socket couldn't be added"))
.ok()
}
fn remove(&self, token: RegistrationToken) {
LoopHandle::remove(self, token);
}
fn insert_executor(&self, executor: Executor<()>) -> bool {
self
.insert_source(executor, |(), _, _| {})
.inspect_err(|error| tracing::error!(%error, "the services' executor couldn't be added"))
.is_ok()
}
fn insert_audio_channel(
&self,
channel: Channel<audio::Message>,
mut on_message: Box<dyn FnMut(audio::Message)>,
) -> bool {
self
.insert_source(channel, move |event, _, _| {
if let calloop::channel::Event::Msg(message) = event {
on_message(message);
}
})
.inspect_err(|error| tracing::error!(%error, "the audio channel couldn't be added"))
.is_ok()
}
}
struct Config {
localtime: Option<PathBuf>,
system_bus: Option<zbus::Address>,
session_bus: Option<zbus::Address>,
sway: Option<sway::Socket>,
audio: Option<audio::Server>,
}
impl Config {
fn system() -> Config {
Config {
localtime: std::env::var_os("TZ")
.is_none()
.then(|| PathBuf::from("/etc/localtime")),
system_bus: zbus::Address::system()
.inspect_err(|error| tracing::warn!(%error, "no system bus address"))
.ok(),
session_bus: zbus::Address::session()
.inspect_err(|error| tracing::warn!(%error, "no session bus address"))
.ok(),
sway: std::env::var_os("SWAYSOCK")
.filter(|path| !path.is_empty())
.map(|path| sway::Socket {
path: path.into(),
retry: sway::RETRY,
}),
audio: Some(audio::Server {
address: None,
retry: audio::RETRY,
retry_for: audio::RETRY_FOR,
}),
}
}
}