diff --git a/ashpd-demo/Cargo.lock b/ashpd-demo/Cargo.lock
index b5b00e01c..546f02085 100644
--- a/ashpd-demo/Cargo.lock
+++ b/ashpd-demo/Cargo.lock
@@ -44,9 +44,7 @@ checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "ashpd"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
+version = "0.9.0"
dependencies = [
"async-fs",
"async-net",
diff --git a/ashpd-demo/Cargo.toml b/ashpd-demo/Cargo.toml
index 4086dee25..91af954bd 100644
--- a/ashpd-demo/Cargo.toml
+++ b/ashpd-demo/Cargo.toml
@@ -7,7 +7,7 @@ version = "0.4.1"
[dependencies]
adw = {version = "0.6", package = "libadwaita", features = ["v1_4"]}
anyhow = "1.0"
-ashpd = {version = "^0.8", features = ["gtk4", "tracing", "pipewire"]}
+ashpd = {path="..", features = ["gtk4", "tracing", "pipewire"]}
chrono = {version = "0.4", default-features = false, features = ["clock"]}
futures-util = "0.3"
gettext-rs = {version = "0.7", features = ["gettext-system"]}
diff --git a/ashpd-demo/data/resources.gresource.xml b/ashpd-demo/data/resources.gresource.xml
index 1202c6af2..a8a49333a 100644
--- a/ashpd-demo/data/resources.gresource.xml
+++ b/ashpd-demo/data/resources.gresource.xml
@@ -16,6 +16,7 @@
resources/ui/email.ui
resources/ui/file_chooser.ui
resources/ui/inhibit.ui
+ resources/ui/global_shortcuts.ui
resources/ui/location.ui
resources/ui/network_monitor.ui
resources/ui/notification.ui
diff --git a/ashpd-demo/data/resources/ui/global_shortcuts.ui b/ashpd-demo/data/resources/ui/global_shortcuts.ui
new file mode 100644
index 000000000..ad48437e3
--- /dev/null
+++ b/ashpd-demo/data/resources/ui/global_shortcuts.ui
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
diff --git a/ashpd-demo/data/resources/ui/window.ui b/ashpd-demo/data/resources/ui/window.ui
index a95869152..781040c9f 100644
--- a/ashpd-demo/data/resources/ui/window.ui
+++ b/ashpd-demo/data/resources/ui/window.ui
@@ -101,6 +101,12 @@
file_chooser
+
+
+
+
+
+ global_shortcuts
+
+
+
+
+
location
diff --git a/ashpd-demo/po/POTFILES.in b/ashpd-demo/po/POTFILES.in
index 64d5fd9b6..451fcc1f4 100644
--- a/ashpd-demo/po/POTFILES.in
+++ b/ashpd-demo/po/POTFILES.in
@@ -7,6 +7,7 @@ data/resources/ui/camera.ui
data/resources/ui/email.ui
data/resources/ui/file_chooser.ui
data/resources/ui/inhibit.ui
+data/resources/ui/global_shortcuts.ui
data/resources/ui/location.ui
data/resources/ui/notification.ui
data/resources/ui/open_uri.ui
diff --git a/ashpd-demo/src/portals/desktop/global_shortcuts.rs b/ashpd-demo/src/portals/desktop/global_shortcuts.rs
new file mode 100644
index 000000000..ee164dbda
--- /dev/null
+++ b/ashpd-demo/src/portals/desktop/global_shortcuts.rs
@@ -0,0 +1,279 @@
+use std::{collections::HashSet, sync::Arc};
+
+use adw::subclass::prelude::*;
+use ashpd::{
+ desktop::{
+ global_shortcuts::{Activated, Deactivated, ShortcutsChanged, GlobalShortcuts, NewShortcut, Shortcut},
+ ResponseError,
+ Session,
+ },
+ WindowIdentifier,
+};
+use gtk::{glib, prelude::*};
+use futures_util::{
+ future::{AbortHandle, Abortable},
+ lock::Mutex,
+ stream::{select_all, Stream, StreamExt},
+};
+use crate::widgets::{PortalPage, PortalPageImpl};
+
+#[derive(Debug, Clone)]
+pub struct RegisteredShortcut {
+ id: String,
+ activation: String,
+}
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, gtk::CompositeTemplate, Default)]
+ #[template(resource = "/com/belmoussaoui/ashpd/demo/global_shortcuts.ui")]
+ pub struct GlobalShortcutsPage {
+ #[template_child]
+ pub shortcuts: TemplateChild,
+ #[template_child]
+ pub response_group: TemplateChild,
+ #[template_child]
+ pub session_state_label: TemplateChild,
+ #[template_child]
+ pub activations_group: TemplateChild,
+ #[template_child]
+ pub activations_label: TemplateChild,
+ #[template_child]
+ pub shortcuts_status_label: TemplateChild,
+ #[template_child]
+ pub rebind_count_label: TemplateChild,
+ pub session: Arc>>>,
+ pub abort_handle: Arc>>,
+ /// Id, trigger
+ pub triggers: Arc>>,
+ pub activations: Arc>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for GlobalShortcutsPage {
+ const NAME: &'static str = "GlobalShortcutsPage";
+ type Type = super::GlobalShortcutsPage;
+ type ParentType = PortalPage;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+
+ klass.install_action_async("global_shortcuts.start_session", None, |page, _, _| async move {
+ if let Err(err) = page.start_session().await {
+ tracing::error!("Failed to request {}", err);
+ }
+ });
+ klass.install_action_async("global_shortcuts.stop", None, |page, _, _| async move {
+ page.stop().await;
+ });
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+ impl ObjectImpl for GlobalShortcutsPage {
+ fn constructed(&self) {
+ self.parent_constructed();
+ self.obj().action_set_enabled("global_shortcuts.stop", false);
+ }
+ }
+ impl WidgetImpl for GlobalShortcutsPage {}
+ impl BinImpl for GlobalShortcutsPage {}
+ impl PortalPageImpl for GlobalShortcutsPage {}
+}
+
+glib::wrapper! {
+ pub struct GlobalShortcutsPage(ObjectSubclass)
+ @extends gtk::Widget, adw::Bin;
+}
+
+impl GlobalShortcutsPage {
+ async fn start_session(&self) -> ashpd::Result<()> {
+ let root = self.native().unwrap();
+ let imp = self.imp();
+ let identifier = WindowIdentifier::from_native(&root).await;
+ let shortcuts = imp.shortcuts.text();
+ let shortcuts: Option> = shortcuts.as_str().split(',')
+ .map(|desc| {
+ let mut split = desc.splitn(3, ':');
+ let name = split.next()?;
+ let desc = split.next()?;
+ let trigger = split.next();
+ Some(NewShortcut::new(name, desc).preferred_trigger(trigger))
+ }).collect();
+
+ match shortcuts {
+ Some(shortcuts) => {
+ let global_shortcuts = GlobalShortcuts::new().await?;
+ let session = global_shortcuts.create_session().await?;
+ let request = global_shortcuts.bind_shortcuts(&session, &shortcuts[..], &identifier).await?;
+ imp.response_group.set_visible(true);
+ let response = request.response();
+ imp.session_state_label.set_text(
+ &match &response {
+ Ok(_) => "OK".into(),
+ Err(ashpd::Error::Response(ResponseError::Cancelled)) => "Cancelled".into(),
+ Err(ashpd::Error::Response(ResponseError::Other)) => "Other response error".into(),
+ Err(e) => format!("{}", e),
+ }
+ );
+ imp.activations_group.set_visible(response.is_ok());
+ self.action_set_enabled("global_shortcuts.stop", response.is_ok());
+ self.action_set_enabled("global_shortcuts.start_session", !response.is_ok());
+ self.imp().shortcuts.set_editable(!response.is_ok());
+ match response {
+ Ok(resp) => {
+ let triggers: Vec<_>
+ = resp.shortcuts().iter()
+ .map(|s: &Shortcut| RegisteredShortcut {
+ id: s.id().into(),
+ activation: s.trigger_description().into(),
+ })
+ .collect();
+ *imp.triggers.lock().await = triggers;
+ self.display_activations().await;
+ self.imp().rebind_count_label.set_text("0");
+ imp.session.lock().await.replace(session);
+ loop {
+ if imp.session.lock().await.is_none() {
+ break;
+ }
+
+ let (abort_handle, abort_registration) = AbortHandle::new_pair();
+ let future = Abortable::new(
+ async {
+ enum Event {
+ Activated(Activated),
+ Deactivated(Deactivated),
+ ShortcutsChanged(ShortcutsChanged),
+ }
+
+ let Ok(activated_stream) = global_shortcuts.receive_activated().await
+ else {
+ return;
+ };
+ let Ok(deactivated_stream) = global_shortcuts.receive_deactivated().await
+ else {
+ return;
+ };
+ let Ok(changed_stream) = global_shortcuts.receive_shortcuts_changed().await
+ else {
+ return;
+ };
+
+ let bact: Box + Unpin> = Box::new(activated_stream.map(Event::Activated));
+ let bdeact: Box + Unpin> = Box::new(deactivated_stream.map(Event::Deactivated));
+ let bchg: Box + Unpin> = Box::new(changed_stream.map(Event::ShortcutsChanged));
+
+ let mut events = select_all([
+ bact, bdeact, bchg,
+ ]);
+
+ while let Some(event) = events.next().await {
+ match event {
+ Event::Activated(activation) => {
+ self.on_activated(activation).await;
+ },
+ Event::Deactivated(deactivation) => {
+ self.on_deactivated(deactivation).await;
+ },
+ Event::ShortcutsChanged(change) => {
+ self.on_changed(change).await;
+ },
+ }
+ }
+ },
+ abort_registration,
+ );
+ imp.abort_handle.lock().await.replace(abort_handle);
+ let _ = future.await;
+ }
+ },
+ Err(e) => {
+ tracing::warn!("Failure {:?}", e);
+ }
+ }
+ },
+ _ => {
+ imp.session_state_label.set_text("Shortcut list invalid");
+ imp.response_group.set_visible(true);
+ }
+ };
+
+ Ok(())
+ }
+
+ async fn stop(&self) {
+ let imp = self.imp();
+ self.action_set_enabled("global_shortcuts.stop", false);
+ self.action_set_enabled("global_shortcuts.start_session", true);
+ self.imp().shortcuts.set_editable(true);
+
+ if let Some(abort_handle) = self.imp().abort_handle.lock().await.take() {
+ abort_handle.abort();
+ }
+
+ if let Some(session) = imp.session.lock().await.take() {
+ let _ = session.close().await;
+ }
+ imp.response_group.set_visible(false);
+ imp.activations_group.set_visible(false);
+ imp.rebind_count_label.set_text("");
+ imp.activations.lock().await.clear();
+ imp.triggers.lock().await.clear();
+ }
+
+ async fn display_activations(&self) {
+ let activations = self.imp().activations.lock().await.clone();
+ let triggers = self.imp().triggers.lock().await.clone();
+ let text: Vec = triggers.into_iter()
+ .map(|RegisteredShortcut { id, activation }| {
+ let escape = |s: &str| s.replace("<", "<").replace(">", ">");
+ let id = escape(&id);
+ let activation = escape(&activation);
+ if activations.contains(&id) {
+ format!("{}: {}", id, activation)
+ } else {
+ format!("{}: {}", id, activation)
+ }
+ })
+ .collect();
+ self.imp().activations_label.set_markup(&text.join("\n"))
+ }
+
+ async fn on_activated(&self, activation: Activated) {
+ {
+ let mut activations = self.imp().activations.lock().await;
+ activations.insert(activation.shortcut_id().into());
+ }
+ self.display_activations().await
+ }
+
+ async fn on_deactivated(&self, deactivation: Deactivated) {
+ {
+ let mut activations = self.imp().activations.lock().await;
+ if !activations.remove(deactivation.shortcut_id()) {
+ tracing::warn!("Received deactivation without previous activation: {:?}", deactivation);
+ }
+ }
+ self.display_activations().await
+ }
+
+ async fn on_changed(&self, change: ShortcutsChanged) {
+ *self.imp().triggers.lock().await
+ = change.shortcuts().iter()
+ .map(|s| RegisteredShortcut{
+ id: s.id().into(),
+ activation: s.trigger_description().into(),
+ })
+ .collect();
+ let label = &self.imp().rebind_count_label;
+ label.set_text(&format!(
+ "{}",
+ label.text().parse::().unwrap_or(0) + 1
+ ));
+ self.display_activations().await
+ }
+}
diff --git a/ashpd-demo/src/portals/desktop/mod.rs b/ashpd-demo/src/portals/desktop/mod.rs
index 79a12f47d..8c174d4cf 100644
--- a/ashpd-demo/src/portals/desktop/mod.rs
+++ b/ashpd-demo/src/portals/desktop/mod.rs
@@ -5,6 +5,7 @@ mod device;
mod dynamic_launcher;
mod email;
mod file_chooser;
+mod global_shortcuts;
mod inhibit;
mod location;
mod network_monitor;
@@ -25,6 +26,7 @@ pub use device::DevicePage;
pub use dynamic_launcher::DynamicLauncherPage;
pub use email::EmailPage;
pub use file_chooser::FileChooserPage;
+pub use global_shortcuts::GlobalShortcutsPage;
pub use inhibit::InhibitPage;
pub use location::LocationPage;
pub use network_monitor::NetworkMonitorPage;
diff --git a/ashpd-demo/src/window.rs b/ashpd-demo/src/window.rs
index 370307fff..2c61e8ef5 100644
--- a/ashpd-demo/src/window.rs
+++ b/ashpd-demo/src/window.rs
@@ -12,7 +12,7 @@ use crate::{
portals::{
desktop::{
AccountPage, BackgroundPage, CameraPage, DevicePage, DynamicLauncherPage, EmailPage,
- FileChooserPage, InhibitPage, LocationPage, NetworkMonitorPage, NotificationPage,
+ FileChooserPage, InhibitPage, GlobalShortcutsPage, LocationPage, NetworkMonitorPage, NotificationPage,
OpenUriPage, PrintPage, ProxyResolverPage, RemoteDesktopPage, ScreenCastPage,
ScreenshotPage, SecretPage, WallpaperPage,
},
@@ -63,6 +63,8 @@ mod imp {
#[template_child]
pub inhibit: TemplateChild,
#[template_child]
+ pub global_shortcuts: TemplateChild,
+ #[template_child]
pub secret: TemplateChild,
#[template_child]
pub remote_desktop: TemplateChild,
@@ -98,6 +100,7 @@ mod imp {
file_chooser: TemplateChild::default(),
open_uri: TemplateChild::default(),
inhibit: TemplateChild::default(),
+ global_shortcuts: TemplateChild::default(),
secret: TemplateChild::default(),
remote_desktop: TemplateChild::default(),
print: TemplateChild::default(),