Skip to content

Commit

Permalink
xdg-desktop-portal: add basic support for access dialogs
Browse files Browse the repository at this point in the history
Accessing certain devices like cameras might require permissions.
This is handled in xdg-desktop-portal by issuing an access dialog.
The backend is expected to expose the org.freedesktop.impl.portal.Access dbus
interface.
  • Loading branch information
cmeissl committed Nov 1, 2024
1 parent d3e7224 commit 2f4f193
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 0 deletions.
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
''
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
install -Dm644 resources/niri.portal -t $out/share/xdg-desktop-portal/portals
''
+ lib.optionalString withSystemd ''
install -Dm755 resources/niri-session $out/bin/niri-session
Expand Down
2 changes: 2 additions & 0 deletions niri.spec.rpkg
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ sed -i 's/.*please-remove-me$//' .cargo/config.toml
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal/portals ./resources/niri.portal
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target

Expand All @@ -140,6 +141,7 @@ install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
%{_datadir}/wayland-sessions/niri.desktop
%dir %{_datadir}/xdg-desktop-portal
%{_datadir}/xdg-desktop-portal/niri-portals.conf
%{_datadir}/xdg-desktop-portal/portals/niri.portal
%{_userunitdir}/niri.service
%{_userunitdir}/niri-shutdown.target

Expand Down
1 change: 1 addition & 0 deletions resources/niri-portals.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=niri;
org.freedesktop.impl.portal.Secret=gnome-keyring;
4 changes: 4 additions & 0 deletions resources/niri.portal
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.niri
Interfaces=org.freedesktop.impl.portal.Access;
UseIn=niri
17 changes: 17 additions & 0 deletions src/dbus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod gnome_shell_introspect;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
pub mod xdg_deskop_portal_impl_access;

#[cfg(feature = "xdp-gnome-screencast")]
pub mod mutter_screen_cast;
Expand All @@ -32,6 +33,7 @@ pub struct DBusServers {
pub conn_introspect: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
pub conn_portal: Option<Connection>,
}

impl DBusServers {
Expand Down Expand Up @@ -98,6 +100,21 @@ impl DBusServers {
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}

let (to_niri, from_access_dialog) = calloop::channel::channel();
niri.event_loop
.insert_source(from_access_dialog, {
move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => {
state.niri.access_dialog_ui.enque_request(msg);
state.niri.queue_redraw_all();
}
calloop::channel::Event::Closed => (),
}
})
.unwrap();
let portal = xdg_deskop_portal_impl_access::AccessPortalImpl::new(to_niri);
dbus.conn_portal = try_start(portal);
}

niri.dbus = Some(dbus);
Expand Down
150 changes: 150 additions & 0 deletions src/dbus/xdg_deskop_portal_impl_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::ops::Not;

use zbus::dbus_interface;

use super::Start;

pub struct AccessPortalImpl {
to_niri: calloop::channel::Sender<AccessDialogRequest>,
}

#[dbus_interface(name = "org.freedesktop.impl.portal.Access")]
impl AccessPortalImpl {
/// AccessDialog method
#[allow(clippy::too_many_arguments)]
async fn access_dialog(
&self,
_handle: zbus::zvariant::ObjectPath<'_>,
app_id: &str,
parent_window: &str,
title: &str,
subtitle: &str,
body: &str,
options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>,
) -> zbus::fdo::Result<(
u32,
std::collections::HashMap<String, zbus::zvariant::OwnedValue>,
)> {
let (response_channel_sender, response_channel_receiver) = async_channel::bounded(1);

let options = AccessDialogOptions::from_options(options);
let request = AccessDialogRequest {
app_id: app_id.to_string(),
parent_window: parent_window
.is_empty()
.not()
.then(|| parent_window.to_string()),
title: title.to_string(),
subtitle: subtitle.to_string(),
body: body.is_empty().not().then(|| body.to_string()),
options,

response_channel_sender,
};

if let Err(err) = self.to_niri.send(request) {
tracing::warn!(?err, "failed to send access dialog request");
return Ok((2, Default::default()));
};

let result = std::collections::HashMap::<String, zbus::zvariant::OwnedValue>::new();
let response = match response_channel_receiver.recv().await {
Ok(AccessDialogResponse::Grant) => {
// FIXME: Add selected choices to the result
0
}
Ok(AccessDialogResponse::Deny) => 1,
Err(err) => {
tracing::warn!(?err, "failed to receive response for access dialog request");
return Ok((2, Default::default()));
}
};

return Ok((response, result));
}
}

impl AccessPortalImpl {
pub fn new(to_niri: calloop::channel::Sender<AccessDialogRequest>) -> Self {
Self { to_niri }
}
}

impl Start for AccessPortalImpl {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::ConnectionBuilder::session()?
.name("org.freedesktop.impl.portal.desktop.niri")?
.serve_at("/org/freedesktop/portal/desktop", self)?
.build()?;
Ok(conn)
}
}

pub struct AccessDialogRequest {
pub app_id: String,
pub parent_window: Option<String>,
pub title: String,
pub subtitle: String,
pub body: Option<String>,
pub options: AccessDialogOptions,

response_channel_sender: async_channel::Sender<AccessDialogResponse>,
}

pub struct AccessDialogOptions {
/// Whether to make the dialog modal. Defaults to true.
pub modal: bool,
/// Label for the Deny button.
pub deny_label: Option<String>,
/// Label for the Grant button.
pub grant_label: Option<String>,
/// Icon name for an icon to show in the dialog. This should be a symbolic icon name.
pub icon: Option<String>,
}

impl AccessDialogOptions {
fn from_options(options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>) -> Self {
let modal: bool = options
.get("modal")
.and_then(|option| option.downcast_ref())
.copied()
.unwrap_or(true);
let deny_label: Option<String> = options
.get("deny_label")
.and_then(|option| option.clone().downcast());
let grant_label: Option<String> = options
.get("grant_label")
.and_then(|option| option.clone().downcast());
let icon: Option<String> = options
.get("icon")
.and_then(|option| option.clone().downcast());

// FIXME: Add support for choices

Self {
modal,
deny_label,
grant_label,
icon,
}
}
}

impl AccessDialogRequest {
pub fn grant(self) -> anyhow::Result<()> {
self.response_channel_sender
.send_blocking(AccessDialogResponse::Grant)?;
Ok(())
}

pub fn deny(self) -> anyhow::Result<()> {
self.response_channel_sender
.send_blocking(AccessDialogResponse::Deny)?;
Ok(())
}
}

pub enum AccessDialogResponse {
Grant,
Deny,
}
15 changes: 15 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::niri::State;
#[cfg(feature = "dbus")]
use crate::ui::access_dialog_ui::AccessDialogUi;
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
use crate::utils::{center, get_monotonic_time, ResizeEdge};
Expand Down Expand Up @@ -372,6 +374,8 @@ impl State {
pressed,
*mods,
&this.niri.screenshot_ui,
#[cfg(feature = "dbus")]
&this.niri.access_dialog_ui,
this.niri.config.borrow().input.disable_power_key_handling,
)
},
Expand Down Expand Up @@ -2447,6 +2451,7 @@ fn should_intercept_key(
pressed: bool,
mods: ModifiersState,
screenshot_ui: &ScreenshotUi,
#[cfg(feature = "dbus")] access_dialog_ui: &AccessDialogUi,
disable_power_key_handling: bool,
) -> FilterResult<Option<Bind>> {
// Actions are only triggered on presses, release of the key
Expand Down Expand Up @@ -2493,6 +2498,13 @@ fn should_intercept_key(
}
}

#[cfg(feature = "dbus")]
if access_dialog_ui.is_visible() {
access_dialog_ui.handle_key(raw, mods);
suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
}

match (final_bind, pressed) {
(Some(bind), true) => {
suppressed_keys.insert(key_code);
Expand Down Expand Up @@ -2991,6 +3003,7 @@ mod tests {
let mut suppressed_keys = HashSet::new();

let screenshot_ui = ScreenshotUi::new(Default::default());
let access_dialog_ui = AccessDialogUi::new();
let disable_power_key_handling = false;

// The key_code we pick is arbitrary, the only thing
Expand All @@ -3008,6 +3021,7 @@ mod tests {
pressed,
mods,
&screenshot_ui,
&access_dialog_ui,
disable_power_key_handling,
)
};
Expand All @@ -3024,6 +3038,7 @@ mod tests {
pressed,
mods,
&screenshot_ui,
&access_dialog_ui,
disable_power_key_handling,
)
};
Expand Down
25 changes: 25 additions & 0 deletions src/niri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ use crate::render_helpers::{
render_to_dmabuf, render_to_encompassing_texture, render_to_shm, render_to_texture,
render_to_vec, shaders, RenderTarget,
};
#[cfg(feature = "dbus")]
use crate::ui::access_dialog_ui::AccessDialogUi;
use crate::ui::config_error_notification::ConfigErrorNotification;
use crate::ui::exit_confirm_dialog::ExitConfirmDialog;
use crate::ui::hotkey_overlay::HotkeyOverlay;
Expand Down Expand Up @@ -286,6 +288,8 @@ pub struct Niri {
pub config_error_notification: ConfigErrorNotification,
pub hotkey_overlay: HotkeyOverlay,
pub exit_confirm_dialog: Option<ExitConfirmDialog>,
#[cfg(feature = "dbus")]
pub access_dialog_ui: AccessDialogUi,

pub debug_draw_opaque_regions: bool,
pub debug_draw_damage: bool,
Expand Down Expand Up @@ -1729,6 +1733,9 @@ impl Niri {
}
};

#[cfg(feature = "dbus")]
let access_dialog_ui = AccessDialogUi::new();

event_loop
.insert_source(
Timer::from_duration(Duration::from_secs(1)),
Expand Down Expand Up @@ -1892,6 +1899,8 @@ impl Niri {
config_error_notification,
hotkey_overlay,
exit_confirm_dialog,
#[cfg(feature = "dbus")]
access_dialog_ui,

debug_draw_opaque_regions: false,
debug_draw_damage: false,
Expand Down Expand Up @@ -2294,6 +2303,11 @@ impl Niri {
return None;
}

#[cfg(feature = "dbus")]
if self.access_dialog_ui.is_visible() {
return None;
}

let (output, pos_within_output) = self.output_under(pos)?;

// Check if some layer-shell surface is on top.
Expand Down Expand Up @@ -2364,6 +2378,11 @@ impl Niri {
return rv;
}

#[cfg(feature = "dbus")]
if self.access_dialog_ui.is_visible() {
return rv;
}

let layers = layer_map_for_output(output);
let layer_surface_under = |layer| {
layers
Expand Down Expand Up @@ -3072,6 +3091,12 @@ impl Niri {
elements.push(element.into());
}

// Draw the access dialogs.
#[cfg(feature = "dbus")]
if let Some(element) = self.access_dialog_ui.render(renderer, output) {
elements.push(element.into());
}

// Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap();
let monitor_elements: Vec<_> = mon.render_elements(renderer, target).collect();
Expand Down
Loading

0 comments on commit 2f4f193

Please sign in to comment.