diff --git a/.gitignore b/.gitignore index 5c0031bd..e178ac1a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ debian/* !debian/links !debian/rules !debian/source + +cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service +cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service diff --git a/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in b/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in new file mode 100644 index 00000000..4f2067ee --- /dev/null +++ b/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in @@ -0,0 +1,7 @@ +[Unit] +Description=COSMIC Status Notifier Watcher backend + +[Service] +Type=dbus +BusName=com.system76.CosmicStatusNotifierWatcher +ExecStart=@bindir@/cosmic-applet-status-area --status-notifier-watcher diff --git a/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in b/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in new file mode 100644 index 00000000..0c3345d5 --- /dev/null +++ b/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=com.system76.CosmicStatusNotifierWatcher +Exec=@bindir@/cosmic-applet-status-area --status-notifier-watcher +SystemdService=com.system76.CosmicStatusNotifierWatcher.service diff --git a/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in b/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in new file mode 100644 index 00000000..4f2067ee --- /dev/null +++ b/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in @@ -0,0 +1,7 @@ +[Unit] +Description=COSMIC Status Notifier Watcher backend + +[Service] +Type=dbus +BusName=com.system76.CosmicStatusNotifierWatcher +ExecStart=@bindir@/cosmic-applet-status-area --status-notifier-watcher diff --git a/cosmic-applet-status-area/src/components/app.rs b/cosmic-applet-status-area/src/components/app.rs index e8d9f0b1..03e56e9d 100644 --- a/cosmic-applet-status-area/src/components/app.rs +++ b/cosmic-applet-status-area/src/components/app.rs @@ -16,7 +16,10 @@ use cosmic::{ }; use std::collections::BTreeMap; -use crate::{components::status_menu, subscriptions::status_notifier_watcher}; +use crate::{ + components::status_menu, + subscriptions::{status_notifier_item::StatusNotifierItem, status_notifier_watcher}, +}; #[derive(Clone, Debug)] pub enum Msg { @@ -158,13 +161,7 @@ impl cosmic::Application for App { }); } else { if let Some(menu) = self.menus.get(&id) { - let item_proxy = menu.item.item_proxy().clone(); - return Task::future(async move { - match item_proxy.activate(0, 0).await { - Ok(_) => cosmic::action::app(Msg::None), - Err(_) => cosmic::action::app(Msg::TogglePopup(id)), - } - }); + return activate(id, &menu.item, None); } } Task::none() @@ -290,29 +287,7 @@ impl cosmic::Application for App { if let Some(id_str) = id.strip_prefix("activate:") { if let Ok(real_id) = id_str.parse::() { if let Some(menu) = self.menus.get(&real_id) { - let item_proxy = menu.item.item_proxy().clone(); - let token = token.clone(); - let id = real_id; - return Task::future(async move { - if let Some(t) = token { - match item_proxy.provide_xdg_activation_token(t).await { - Ok(_) => { - println!("Token provided successfully to {}", id) - } - Err(e) => eprintln!( - "Failed to provide token to {}: {}", - id, e - ), - } - } - match item_proxy.activate(0, 0).await { - Ok(_) => cosmic::action::app(Msg::None), - Err(err) => { - eprintln!("Activate failed: {}", err); - cosmic::action::app(Msg::TogglePopup(id)) - } - } - }); + return activate(real_id, &menu.item, token.clone()); } } return Task::none(); @@ -563,6 +538,34 @@ impl cosmic::Application for App { } } +fn activate( + id: usize, + item: &StatusNotifierItem, + activation_token: Option, +) -> Task> { + if item.is_menu() { + return Task::done(cosmic::action::app(Msg::TogglePopup(id))); + } + let item_proxy = item.item_proxy().clone(); + Task::future(async move { + if let Some(t) = activation_token { + match item_proxy.provide_xdg_activation_token(t).await { + Ok(_) => { + tracing::debug!("Token provided successfully to {}", id) + } + Err(e) => tracing::error!("Failed to provide token to {}: {}", id, e), + } + } + match item_proxy.activate(0, 0).await { + Ok(_) => cosmic::action::app(Msg::None), + Err(err) => { + tracing::error!("Activate failed: {}", err); + cosmic::action::app(Msg::TogglePopup(id)) + } + } + }) +} + fn menu_icon_button<'a>( applet: &'a cosmic::applet::Context, menu: &'a status_menu::State, diff --git a/cosmic-applet-status-area/src/components/status_menu.rs b/cosmic-applet-status-area/src/components/status_menu.rs index f76ea784..8ca6bd85 100644 --- a/cosmic-applet-status-area/src/components/status_menu.rs +++ b/cosmic-applet-status-area/src/components/status_menu.rs @@ -121,14 +121,6 @@ impl State { let item_proxy = self.item.item_proxy().clone(); let Some(menu_proxy) = self.item.menu_proxy().cloned() else { - tokio::spawn(async move { - let _ = item_proxy.provide_xdg_activation_token(token).await; - if let Err(err) = item_proxy.activate(0, 0).await { - tracing::error!( - "Error activating status notifier item without menu proxy: {err:?}" - ); - } - }); return iced::Task::none(); }; tokio::spawn(async move { @@ -242,7 +234,11 @@ fn layout_view(layout: &Layout, expanded: Option) -> cosmic::Element<'_, Ms .symbolic(true); children.push(icon.into()); } - let button = row_button(children).on_press(Msg::Click(i.id(), is_submenu)); + + let mut button = row_button(children); + if i.enabled() { + button = button.on_press(Msg::Click(i.id(), is_submenu)); + } if is_submenu && is_expanded { Some( diff --git a/cosmic-applet-status-area/src/lib.rs b/cosmic-applet-status-area/src/lib.rs index 371b8aff..17166cbc 100644 --- a/cosmic-applet-status-area/src/lib.rs +++ b/cosmic-applet-status-area/src/lib.rs @@ -1,9 +1,23 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use std::{env, process}; + mod components; mod subscriptions; +pub mod status_notifier_watcher; +mod unique_names; + pub fn run() -> cosmic::iced::Result { - components::app::main() + if let Some(arg) = env::args().nth(1) { + if arg == "--status-notifier-watcher" { + status_notifier_watcher::run() + } else { + tracing::error!("Invalid argument `{arg}` for status-area applet`"); + process::exit(1); + } + } else { + components::app::main() + } } diff --git a/cosmic-applet-status-area/src/status_notifier_watcher.rs b/cosmic-applet-status-area/src/status_notifier_watcher.rs new file mode 100644 index 00000000..1bb75d79 --- /dev/null +++ b/cosmic-applet-status-area/src/status_notifier_watcher.rs @@ -0,0 +1,155 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +//! A seperate DBus socket-activated daemon to serve as the status notifier watcher +//! +//! By socket-activating this daemon, panel configuration changes do not end up terminating +//! the daemon and having different applet instances race to start it. +//! +//! This provides a seperate interface from the standard one, with a single register method, so it +//! can be socket-activated and not conflict with anything else running as a status notifier +//! watcher. +//! +//! The daemon runs as long as as there is at least one client still connected. Which it checks +//! for every `REFRESH_INTERVAL`. + +use crate::subscriptions::status_notifier_watcher::server::create_service; +use crate::unique_names::UniqueNames; + +use futures::StreamExt; +use std::{collections::HashSet, time::Duration}; +use zbus::fdo; +use zbus::message::Header; + +const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher"; +const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher"; +const REFRESH_INTERVAL: Duration = Duration::from_secs(60); + +/// Run daemon +pub fn run() -> cosmic::iced::Result { + if let Err(err) = run_inner() { + eprintln!("Zbus error running status notifier watcher: {}", err); + std::process::exit(1); + } + Ok(()) +} + +/// Register client with daemon +pub async fn cosmic_register(conn: &zbus::Connection) -> zbus::Result<()> { + let cosmic_watcher = CosmicAppletStatusNotifierWatcherProxy::new(conn).await?; + cosmic_watcher.register_applet().await?; + let mut stream = cosmic_watcher.0.receive_owner_changed().await?; + tokio::spawn(async move { + while let Some(value) = stream.next().await { + if let Some(_unique_name) = value { + /// Register with new owner + let _ = cosmic_watcher.register_applet().await; + } + } + }); + Ok(()) +} + +#[zbus::proxy( + interface = "com.system76.CosmicStatusNotifierWatcher", + default_service = "com.system76.CosmicStatusNotifierWatcher", + default_path = "/CosmicStatusNotifierWatcher" +)] +trait CosmicAppletStatusNotifierWatcher { + async fn register_applet(&self) -> zbus::Result<()>; +} + +struct CosmicAppletStatusNotifierWatcher { + applets: HashSet>, + unique_names: UniqueNames, +} + +#[zbus::interface(name = "com.system76.CosmicStatusNotifierWatcher")] +impl CosmicAppletStatusNotifierWatcher { + fn register_applet(&mut self, #[zbus(header)] hdr: Header<'_>) { + if let Some(sender) = hdr.sender() { + if self.unique_names.has_unique_name(sender) { + self.applets.insert(sender.to_owned()); + } + } + } +} + +impl CosmicAppletStatusNotifierWatcher { + fn has_client(&self) -> bool { + !self.applets.is_empty() + } + + /// Purge registered clients that no longer exist on bus + fn refresh(&mut self) { + self.applets + .retain(|n| self.unique_names.has_unique_name(n)); + } +} + +#[tokio::main] +pub async fn run_inner() -> zbus::Result<()> { + let (running, abort_handle) = futures::future::abortable(std::future::pending::<()>()); + + let conn = zbus::Connection::session().await?; + create_service(&conn).await?; + let dbus = zbus::fdo::DBusProxy::new(&conn).await?; + conn.object_server() + .at( + OBJECT_PATH, + CosmicAppletStatusNotifierWatcher { + applets: HashSet::new(), + unique_names: UniqueNames::new(&conn).await?, + }, + ) + .await?; + let interface = conn + .object_server() + .interface::<_, CosmicAppletStatusNotifierWatcher>(OBJECT_PATH) + .await?; + tokio::spawn(refresh_task(interface.clone(), abort_handle.clone())); + let name_lost_stream = dbus.receive_name_lost().await?; + tokio::spawn(name_lost_task(name_lost_stream, abort_handle)); + conn.request_name(DBUS_NAME).await?; + + let _ = running.await; + Ok(()) +} + +// Task to terminate daemon with the owned name is lost. +// (If a different instance of this daemon is manually started.) +async fn name_lost_task( + mut name_lost_stream: fdo::NameLostStream, + abort_handle: futures::future::AbortHandle, +) { + while let Some(name_lost) = name_lost_stream.next().await { + let Ok(args) = name_lost.args() else { + return; + }; + if args.name == DBUS_NAME { + eprintln!("'{}' name on bus lost. Exiting.", DBUS_NAME); + abort_handle.abort(); + return; + } + } +} + +async fn refresh_task( + interface: zbus::object_server::InterfaceRef, + abort_handle: futures::future::AbortHandle, +) { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // Initial tick, waiting for first client to connect + interval.tick().await; + loop { + interval.tick().await; + let mut watcher = interface.get_mut().await; + if !watcher.has_client() { + // No clients since last refresh; exit + abort_handle.abort(); + return; + } + watcher.refresh(); + } +} diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs index ee12cbe0..5a67f07b 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs @@ -10,6 +10,7 @@ use zbus::zvariant::{self, OwnedValue}; #[derive(Clone, Debug)] pub struct StatusNotifierItem { name: String, + is_menu: bool, item_proxy: StatusNotifierItemProxy<'static>, menu_proxy: Option>, } @@ -44,32 +45,25 @@ impl StatusNotifierItem { .build() .await?; - // XX: some items will not implement this but have a menu anyway - let is_menu = item_proxy.item_is_menu().await; + let is_menu = item_proxy.item_is_menu().await.unwrap_or(false); - let menu_path = item_proxy.menu().await; - - // Why would an item say it has no menu but provide a menu path? Slack does this. - let is_menu = menu_path.is_ok() || is_menu.unwrap_or(false); - - if !is_menu { - return Ok(Self { - name, - item_proxy, - menu_proxy: None, - }); - } - let menu_path = menu_path?; - let menu_proxy = DBusMenuProxy::builder(connection) - .destination(dest.to_string())? - .path(menu_path)? - .build() - .await?; + let menu_proxy = if let Ok(menu_path) = item_proxy.menu().await { + Some( + DBusMenuProxy::builder(connection) + .destination(dest.to_string())? + .path(menu_path)? + .build() + .await?, + ) + } else { + None + }; Ok(Self { name, + is_menu, item_proxy, - menu_proxy: Some(menu_proxy), + menu_proxy, }) } @@ -126,6 +120,11 @@ impl StatusNotifierItem { ) } + /// Item is only a menu, with no `Activate` action + pub fn is_menu(&self) -> bool { + self.is_menu + } + pub fn menu_proxy(&self) -> Option<&DBusMenuProxy<'static>> { self.menu_proxy.as_ref() } diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs index af0f74ae..9f9c7d14 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs @@ -9,7 +9,7 @@ use futures::{StreamExt, stream}; use crate::subscriptions::status_notifier_item::StatusNotifierItem; mod client; -mod server; +pub(crate) mod server; #[derive(Clone, Debug)] pub enum Event { @@ -51,7 +51,9 @@ async fn connect() -> zbus::Result<(zbus::Connection, client::EventStream)> { let connection = zbus::Connection::session().await?; // Start `StatusNotifierWatcher` service, if there isn't one running already - server::create_service(&connection).await?; + if let Err(err) = crate::status_notifier_watcher::cosmic_register(&connection).await { + eprintln!("Failed to start status notifier watcher: {}", err); + } // Connect client and listen for registered/unregistered let stream = client::watch(&connection).await?; diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/server.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/server.rs index 5cf2455f..816a0be7 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/server.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/server.rs @@ -37,11 +37,15 @@ impl StatusNotifierWatcher { } else { service.to_string() }; - Self::status_notifier_item_registered(&ctxt, &service) - .await - .unwrap(); - self.items.push((sender.to_owned(), service)); + // Ignore duplicate + if !self.items.iter().any(|(a, b)| (a, b) == (sender, &service)) { + Self::status_notifier_item_registered(&ctxt, &service) + .await + .unwrap(); + + self.items.push((sender.to_owned(), service)); + } } fn register_status_notifier_host(&self, _service: &str) { diff --git a/cosmic-applet-status-area/src/unique_names.rs b/cosmic-applet-status-area/src/unique_names.rs new file mode 100644 index 00000000..cab6483e --- /dev/null +++ b/cosmic-applet-status-area/src/unique_names.rs @@ -0,0 +1,107 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +// Based on https://github.com/pop-os/cosmic-comp/blob/master/src/dbus/name_owners.rs, +// but only tracking unique names, and using tokio executor. + +use futures::StreamExt; +use std::{ + collections::HashSet, + future::{Future, poll_fn}, + sync::{Arc, Mutex, Weak}, + task::{Context, Poll, Waker}, +}; +use zbus::{ + fdo, + names::{BusName, UniqueName}, +}; + +#[derive(Debug)] +struct Inner { + unique_names: HashSet>, + stream: fdo::NameOwnerChangedStream, + // Waker from `update_task` is stored, so that task will still be woken after + // polling elsewhere. + waker: Waker, +} + +impl Drop for Inner { + fn drop(&mut self) { + // Wake `update_task` so it can terminate + self.waker.wake_by_ref(); + } +} + +impl Inner { + /// Process all events so far on `stream`, and update `unique_names`. + fn update_if_needed(&mut self) { + let mut context = Context::from_waker(&self.waker); + while let Poll::Ready(val) = self.stream.poll_next_unpin(&mut context) { + let val = val.unwrap(); + let args = val.args().unwrap(); + match args.name { + BusName::Unique(name) => { + if args.new_owner.is_some() { + self.unique_names.insert(name.to_owned()); + } else { + self.unique_names.remove(&name.to_owned()); + } + } + BusName::WellKnown(_) => {} + } + } + } +} + +/// This task polls the steam regularly, to make sure events on the stream aren't just +/// buffered indefinitely. +fn update_task(inner: Weak>) -> impl Future { + poll_fn(move |context| { + if let Some(inner) = inner.upgrade() { + let mut inner = inner.lock().unwrap(); + inner.waker = context.waker().clone(); + inner.update_if_needed(); + // Nothing to do now until waker is invoked + Poll::Pending + } else { + // All strong references have been dropped, so task has nothing left to do. + Poll::Ready(()) + } + }) +} + +#[derive(Clone, Debug)] +pub struct UniqueNames(Arc>); + +impl UniqueNames { + pub async fn new(connection: &zbus::Connection) -> zbus::Result { + let dbus = fdo::DBusProxy::new(connection).await?; + let stream = dbus.receive_name_owner_changed().await?; + + let names = dbus.list_names().await?; + let unique_names = names + .iter() + .filter_map(|n| match n.inner() { + BusName::Unique(name) => Some(name.to_owned()), + BusName::WellKnown(_) => None, + }) + .collect(); + + let inner = Arc::new(Mutex::new(Inner { + unique_names, + stream, + waker: Waker::noop().clone(), + })); + + tokio::spawn(update_task(Arc::downgrade(&inner))); + + Ok(UniqueNames(inner)) + } + + #[allow(dead_code)] + pub fn has_unique_name(&self, name: &UniqueName<'_>) -> bool { + let mut inner = self.0.lock().unwrap(); + inner.update_if_needed(); + inner.unique_names.contains(name) + } +} diff --git a/justfile b/justfile index a1ea651c..6c5033d3 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ sharedir := rootdir + prefix + '/share' iconsdir := sharedir + '/icons/hicolor' prefixdir := prefix + '/bin' bindir := rootdir + prefixdir +libdir := rootdir + prefix + '/lib' default-schema-target := sharedir / 'cosmic' cosmic-applets-bin := prefixdir / 'cosmic-applets' @@ -57,8 +58,14 @@ _install_button id name: (_install_icons name) (_install_desktop name + '/data/' _install_metainfo: install -Dm0644 {{metainfo-src}} {{metainfo-dst}} +_install_status_notifier_watcher: + sed "s|@bindir@|{{prefixdir}}|" cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service + install -Dm0644 cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service {{sharedir}}/dbus-1/services/com.system76.CosmicStatusNotifierWatcher.service + sed "s|@bindir@|{{prefixdir}}|" cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service + install -Dm0644 cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service {{libdir}}/systemd/user/com.system76.CosmicStatusNotifierWatcher.service + # Installs files into the system -install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo) +install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo) (_install_status_notifier_watcher) # Vendor Cargo dependencies locally vendor: