git.lucas.co / cce-desktop-portal
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 }