Skip to content

Commit

Permalink
feat: Background portal
Browse files Browse the repository at this point in the history
I based my implementation on the official specs as well as code from
GNOME, KDE, Xapp (Xfce), and Pantheon (elementary). KDE's imminently
readable codebase served as this implementation's primary inspiration.

Autostart is deprecated but seemingly still used, so this implementation
will still support it for compatibility.

The Background portal depends on a working Access portal as
`xdg-desktop-portal` calls it to show the initial warning.

References:
* https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.Background.html
* https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/blob/main/src/background.c
* https://invent.kde.org/plasma/xdg-desktop-portal-kde/-/blob/master/src/background.cpp
* https://github.com/linuxmint/xdg-desktop-portal-xapp/blob/f1c24244f90571209c56b7f45802b70e80da4922/src/background.c
* https://github.com/elementary/portals/blob/d868cfa854c731e0f37615e225d5db07cc3f4604/src/Background/Portal.vala
* flatpak/xdg-desktop-portal#1188
  • Loading branch information
joshuamegnauth54 committed Sep 14, 2024
1 parent 05c37cd commit 1885362
Show file tree
Hide file tree
Showing 11 changed files with 651 additions and 52 deletions.
21 changes: 21 additions & 0 deletions cosmic-portal-config/src/background.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-only

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Background {
/// Default preference for NotifyBackground's dialog
pub default_perm: PermissionDialog,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
pub enum PermissionDialog {
/// Grant apps permission to run in the background
Allow,
/// Deny apps permission to run in the background
Deny,
/// Always ask if new apps should be granted background permissions
#[default]
Ask,
}
4 changes: 4 additions & 0 deletions cosmic-portal-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only

pub mod background;
pub mod screenshot;

use cosmic_config::{cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry};
use serde::{Deserialize, Serialize};

use background::Background;
use screenshot::Screenshot;

pub const APP_ID: &str = "com.system76.CosmicPortal";
Expand All @@ -17,6 +19,8 @@ pub const CONFIG_VERSION: u64 = 1;
pub struct Config {
/// Interactive screenshot settings
pub screenshot: Screenshot,
/// Background portal settings
pub background: Background,
}

impl Config {
Expand Down
2 changes: 1 addition & 1 deletion data/cosmic.portal
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.cosmic
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
UseIn=cosmic
124 changes: 124 additions & 0 deletions examples/background.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-3.0-only

use ashpd::desktop::background::Background;
use cosmic::{
app::{self, message, Core},
executor,
iced::{Length, Size},
widget, Command,
};

#[derive(Clone, Debug)]
pub enum Message {
BackgroundResponse(bool),
RequestBackground,
}

pub struct App {
core: Core,
executable: String,
background_allowed: bool,
}

impl App {
async fn request_background(executable: String) -> ashpd::Result<Background> {
log::info!("Requesting permission to run in the background for: {executable}");
// Based off of the ashpd docs
// https://docs.rs/ashpd/latest/ashpd/desktop/background/index.html
Background::request()
.reason("Testing the background portal")
.auto_start(false)
.dbus_activatable(false)
.command(&[executable])
.send()
.await?
.response()
}
}

impl cosmic::Application for App {
type Executor = executor::single::Executor;
type Flags = ();
type Message = Message;
const APP_ID: &'static str = "org.cosmic.BackgroundPortalExample";

fn core(&self) -> &Core {
&self.core
}

fn core_mut(&mut self) -> &mut Core {
&mut self.core
}

fn init(core: Core, _: Self::Flags) -> (Self, app::Command<Self::Message>) {
(
Self {
core,
executable: std::env::args().next().unwrap(),
background_allowed: false,
},
Command::none(),
)
}

fn view(&self) -> cosmic::Element<Self::Message> {
widget::row::with_children(vec![
widget::text::title3(if self.background_allowed {
"Running in background"
} else {
"Not running in background"
})
.width(Length::Fill)
.into(),
widget::button("Run in background")
.on_press(Message::RequestBackground)
.padding(8.0)
.into(),
])
.width(Length::Fill)
.height(Length::Fixed(64.0))
.padding(16.0)
.into()
}

fn update(&mut self, message: Self::Message) -> app::Command<Self::Message> {
match message {
Message::BackgroundResponse(background_allowed) => {
log::info!("Permission to run in the background: {background_allowed}");
self.background_allowed = background_allowed;
Command::none()
}
Message::RequestBackground => {
let executable = self.executable.clone();
Command::perform(Self::request_background(executable), |result| {
let background_allowed = match result {
Ok(response) => {
assert!(
!response.auto_start(),
"Auto start shouldn't have been enabled"
);
response.run_in_background()
}
Err(e) => {
log::error!("Background portal request failed: {e:?}");
false
}
};

message::app(Message::BackgroundResponse(background_allowed))
})
}
}
}
}

// TODO: Write a small flatpak manifest in order to test this better
#[tokio::main]
async fn main() -> cosmic::iced::Result {
env_logger::Builder::from_default_env().init();
let settings = app::Settings::default()
.resizable(None)
.size(Size::new(512.0, 128.0))
.exit_on_close(false);
app::run::<App>(settings, ())
}
6 changes: 6 additions & 0 deletions i18n/en/xdg_desktop_portal_cosmic.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ share-screen = Share your screen
unknown-application = Unknown Application
output = Output
window = Window
# Background portal
allow-once = Allow once
deny = Deny
bg-dialog-title = Background
bg-dialog-body = {$appname} requests to run in the background. This will allow it to run without any open windows.
88 changes: 55 additions & 33 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{access, config, file_chooser, fl, screencast_dialog, screenshot, subscription};
use crate::{
access, background, config, file_chooser, fl, screencast_dialog, screenshot, subscription,
};
use cosmic::iced_core::event::wayland::OutputEvent;
use cosmic::widget::{self, dropdown};
use cosmic::Command;
Expand All @@ -14,12 +16,7 @@ pub(crate) fn run() -> cosmic::iced::Result {
let settings = cosmic::app::Settings::default()
.no_main_window(true)
.exit_on_close(false);
let (config, config_handler) = config::Config::load();
let flags = Flags {
config,
config_handler,
};
cosmic::app::run::<CosmicPortal>(settings, flags)
cosmic::app::run::<CosmicPortal>(settings, ())
}

// run iced app with no main surface
Expand All @@ -28,7 +25,7 @@ pub struct CosmicPortal {
pub tx: Option<tokio::sync::mpsc::Sender<subscription::Event>>,

pub config_handler: Option<cosmic_config::Config>,
pub config: config::Config,
pub tx_conf: Option<tokio::sync::watch::Sender<config::Config>>,

pub access_args: Option<access::AccessDialogArgs>,
pub access_choices: Vec<(Option<usize>, Vec<String>)>,
Expand All @@ -43,6 +40,8 @@ pub struct CosmicPortal {
pub prev_rectangle: Option<screenshot::Rect>,
pub wayland_helper: crate::wayland::WaylandHelper,

pub background_prompts: HashMap<window::Id, background::Args>,

pub outputs: Vec<OutputState>,
pub active_output: Option<WlOutput>,
}
Expand All @@ -64,23 +63,18 @@ pub enum Msg {
FileChooser(window::Id, file_chooser::Msg),
Screenshot(screenshot::Msg),
Screencast(screencast_dialog::Msg),
Background(background::Msg),
Portal(subscription::Event),
Output(OutputEvent, WlOutput),
ConfigSetScreenshot(config::screenshot::Screenshot),
/// Update config from external changes
ConfigSubUpdate(config::Config),
}

#[derive(Clone, Debug)]
pub struct Flags {
pub config_handler: Option<cosmic_config::Config>,
pub config: config::Config,
}

impl cosmic::Application for CosmicPortal {
type Executor = cosmic::executor::Default;

type Flags = Flags;
type Flags = ();

type Message = Msg;

Expand All @@ -96,10 +90,7 @@ impl cosmic::Application for CosmicPortal {

fn init(
core: app::Core,
Flags {
config_handler,
config,
}: Self::Flags,
_: Self::Flags,
) -> (Self, cosmic::iced::Command<app::Message<Self::Message>>) {
let mut model = cosmic::widget::dropdown::multi::model();
model.insert(dropdown::multi::list(
Expand All @@ -119,14 +110,14 @@ impl cosmic::Application for CosmicPortal {
),
],
));
model.selected = Some(config.screenshot.save_location);
model.selected = None;
let wayland_conn = crate::wayland::connect_to_wayland();
let wayland_helper = crate::wayland::WaylandHelper::new(wayland_conn);
(
Self {
core,
config_handler,
config,
config_handler: None,
tx_conf: None,
access_args: Default::default(),
access_choices: Default::default(),
file_choosers: Default::default(),
Expand All @@ -135,6 +126,7 @@ impl cosmic::Application for CosmicPortal {
screencast_tab_model: Default::default(),
location_options: Vec::new(),
prev_rectangle: Default::default(),
background_prompts: Default::default(),
outputs: Default::default(),
active_output: Default::default(),
wayland_helper,
Expand All @@ -155,6 +147,8 @@ impl cosmic::Application for CosmicPortal {
screencast_dialog::view(self).map(Msg::Screencast)
} else if self.outputs.iter().any(|o| o.id == id) {
screenshot::view(self, id).map(Msg::Screenshot)
} else if self.background_prompts.contains_key(&id) {
background::view(self, id).map(Msg::Background)
} else {
file_chooser::view(self, id)
}
Expand All @@ -181,19 +175,31 @@ impl cosmic::Application for CosmicPortal {
subscription::Event::CancelScreencast(handle) => {
screencast_dialog::cancel(self, handle).map(cosmic::app::Message::App)
}
subscription::Event::Background(args) => {
background::update_args(self, args).map(cosmic::app::Message::App)
}
subscription::Event::Config(config) => self.update(Msg::ConfigSubUpdate(config)),
subscription::Event::Accent(_)
| subscription::Event::IsDark(_)
| subscription::Event::HighContrast(_) => cosmic::iced::Command::none(),
subscription::Event::Init(tx) => {
| subscription::Event::HighContrast(_)
| subscription::Event::BackgroundToplevels => cosmic::iced::Command::none(),
subscription::Event::Init {
tx,
tx_conf,
handler,
} => {
let config = tx_conf.borrow().clone();
self.tx = Some(tx);
Command::none()
self.tx_conf = Some(tx_conf);
self.config_handler = handler;
self.update(Msg::ConfigSubUpdate(config))
}
},
Msg::Screenshot(m) => screenshot::update_msg(self, m).map(cosmic::app::Message::App),
Msg::Screencast(m) => {
screencast_dialog::update_msg(self, m).map(cosmic::app::Message::App)
}
Msg::Background(m) => background::update_msg(self, m).map(cosmic::app::Message::App),
Msg::Output(o_event, wl_output) => {
match o_event {
OutputEvent::Created(Some(info))
Expand Down Expand Up @@ -267,19 +273,35 @@ impl cosmic::Application for CosmicPortal {
cosmic::iced::Command::none()
}
Msg::ConfigSetScreenshot(screenshot) => {
match &mut self.config_handler {
Some(handler) => {
if let Err(e) = self.config.set_screenshot(handler, screenshot) {
log::error!("Failed to save screenshot config: {e}")
}
match (self.tx_conf.as_mut(), &mut self.config_handler) {
(Some(tx), Some(handler)) => {
tx.send_if_modified(|config| {
if screenshot != config.screenshot {
if let Err(e) = config.set_screenshot(handler, screenshot) {
log::error!("Failed to save screenshot config: {e}");
}
true
} else {
false
}
});
}
None => log::error!("Failed to save config: No config handler"),
_ => log::error!("Failed to save config: No config handler"),
}

cosmic::iced::Command::none()
}
Msg::ConfigSubUpdate(config) => {
self.config = config;
if let Some(tx) = self.tx_conf.as_ref() {
tx.send_if_modified(|current| {
if config != *current {
*current = config;
true
} else {
false
}
});
}

cosmic::iced::Command::none()
}
}
Expand Down
Loading

0 comments on commit 1885362

Please sign in to comment.