Settings and Inhibit portal backends
git clone https://git.lucas.co/cce-desktop-portal.git
src/main.rs (5.2K)
1 //! cce-desktop-portal — the cce desktop's own `Settings`, `Inhibit`,
2 //! `AppChooser`, `Notification` and `Print` portal backends, and
3 //! `org.freedesktop.ScreenSaver`.
4 //!
5 //! Settings and Inhibit used to fall to xdg-desktop-portal-gtk (`default=gtk` in
6 //! `cce-portals.conf`): Settings published GNOME's GSettings rather than cce's
7 //! config, and Inhibit forwarded to session services no cce session runs, so
8 //! it silently did nothing. AppChooser ("Open with…") was a GTK window; it is
9 //! now cce-cloud's chooser mode. Notification forwarded to cce-notifier
10 //! through gtk, which kept the text and lost the actions; it now maps them
11 //! both ways. Print was a GTK dialog; it is a cce-cloud panel sending to CUPS.
12 //! See `settings.rs`, `inhibit.rs`, `app_chooser.rs`, `notification.rs` and
13 //! `print.rs`.
14 //!
15 //! One bus-activated process owns both names; whichever is asked for first
16 //! starts it. It lives for the session — Settings has to be there to signal
17 //! changes — and costs nothing at rest: config changes arrive by inotify,
18 //! and the lease-renewal loop sleeps while nothing is held.
19
20 mod app_chooser;
21 mod inhibit;
22 mod notification;
23 mod print;
24 mod settings;
25
26 use std::sync::Arc;
27
28 use futures_util::StreamExt;
29 use tokio::sync::RwLock;
30 use zbus::fdo::{RequestNameFlags, RequestNameReply};
31 use zbus::names::BusName;
32
33 const PORTAL_NAME: &str = "org.freedesktop.impl.portal.desktop.cce-desktop";
34 const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
35 const SCREENSAVER_NAME: &str = "org.freedesktop.ScreenSaver";
36
37 #[tokio::main]
38 async fn main() {
39 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
40 if let Err(e) = run().await {
41 log::error!("{e}");
42 std::process::exit(1);
43 }
44 }
45
46 async fn run() -> Result<(), Box<dyn std::error::Error>> {
47 let appearance = Arc::new(RwLock::new(settings::load(&settings::config_path())));
48 log::info!("appearance: {:?}", *appearance.read().await);
49 let inhibitor = inhibit::Inhibitor::new();
50 let cards = notification::SharedCards::default();
51
52 // Leases a predecessor held die with it here rather than after their
53 // ttl. An older compositor answers an error; external inhibitors then
54 // wait for a newer one (see `Inhibitor::send_lease`).
55 match inhibit::ctl("idle inhibit-clear").await {
56 Ok(r) if r.starts_with("ok") => {}
57 Ok(r) => log::warn!("compositor has no external inhibitors yet: {}", r.trim()),
58 Err(e) => log::warn!("compositor control socket: {e}"),
59 }
60
61 let screensaver = |i: &inhibit::Inhibitor| inhibit::ScreenSaver { inhibitor: i.clone() };
62 let conn = zbus::connection::Builder::session()?
63 .serve_at(PORTAL_PATH, settings::SettingsPortal { state: appearance.clone() })?
64 .serve_at(PORTAL_PATH, inhibit::InhibitPortal { inhibitor: inhibitor.clone() })?
65 .serve_at(PORTAL_PATH, app_chooser::AppChooser::default())?
66 .serve_at(PORTAL_PATH, notification::NotificationPortal { cards: cards.clone() })?
67 .serve_at(PORTAL_PATH, print::PrintPortal::default())?
68 // Both paths are in use in the wild (KDE answered at /ScreenSaver).
69 .serve_at("/org/freedesktop/ScreenSaver", screensaver(&inhibitor))?
70 .serve_at("/ScreenSaver", screensaver(&inhibitor))?
71 .build()
72 .await?;
73
74 // Objects first, names second: a call can only arrive once a name is
75 // owned, and by then every interface is there to answer it.
76 let flags = RequestNameFlags::DoNotQueue.into();
77 match conn.request_name_with_flags(PORTAL_NAME, flags).await? {
78 RequestNameReply::PrimaryOwner | RequestNameReply::AlreadyOwner => {}
79 // A racing activation won; it serves both names.
80 _ => {
81 log::info!("{PORTAL_NAME} is already owned; exiting");
82 return Ok(());
83 }
84 }
85 match conn.request_name_with_flags(SCREENSAVER_NAME, flags).await {
86 Ok(RequestNameReply::PrimaryOwner | RequestNameReply::AlreadyOwner) => {}
87 other => log::warn!("{SCREENSAVER_NAME} not taken ({other:?}); serving the portal only"),
88 }
89 log::info!("serving {PORTAL_NAME} and {SCREENSAVER_NAME}");
90
91 tokio::spawn(inhibitor.clone().renew_loop());
92 {
93 let conn = conn.clone();
94 tokio::spawn(async move {
95 if let Err(e) = notification::relay(conn, PORTAL_PATH, cards).await {
96 log::error!("notification relay stopped: {e}");
97 }
98 });
99 }
100 {
101 let (conn, appearance) = (conn.clone(), appearance.clone());
102 tokio::spawn(async move {
103 if let Err(e) = settings::watch(conn, PORTAL_PATH, appearance).await {
104 log::error!("appearance watch stopped: {e}");
105 }
106 });
107 }
108
109 // A ScreenSaver caller that leaves the bus without UnInhibit (a crash,
110 // a kill) releases what it held.
111 let dbus = zbus::fdo::DBusProxy::new(&conn).await?;
112 let mut owners = dbus.receive_name_owner_changed().await?;
113 while let Some(signal) = owners.next().await {
114 let Ok(args) = signal.args() else { continue };
115 if args.new_owner().is_none() {
116 if let BusName::Unique(name) = args.name() {
117 inhibitor.owner_gone(name).await;
118 }
119 }
120 }
121 Ok(())
122 }