From 99da3eda4a98f4f91cf2dea509b29bc2ef6881d6 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Mon, 6 Nov 2023 16:46:44 +0100 Subject: [PATCH] app-list: Add xdg-activation --- cosmic-app-list/src/app.rs | 164 ++++++++++-------- cosmic-app-list/src/main.rs | 4 +- ...toplevel_handler.rs => wayland_handler.rs} | 127 +++++++++++--- ...ubscription.rs => wayland_subscription.rs} | 38 ++-- 4 files changed, 218 insertions(+), 115 deletions(-) rename cosmic-app-list/src/{toplevel_handler.rs => wayland_handler.rs} (54%) rename cosmic-app-list/src/{toplevel_subscription.rs => wayland_subscription.rs} (70%) diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index cab8be2f..5e789ff2 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -2,9 +2,11 @@ use crate::config; use crate::config::AppListConfig; use crate::config::APP_ID; use crate::fl; -use crate::toplevel_subscription::toplevel_subscription; -use crate::toplevel_subscription::ToplevelRequest; -use crate::toplevel_subscription::ToplevelUpdate; +use crate::wayland_subscription::wayland_subscription; +use crate::wayland_subscription::ToplevelRequest; +use crate::wayland_subscription::ToplevelUpdate; +use crate::wayland_subscription::WaylandRequest; +use crate::wayland_subscription::WaylandUpdate; use cctk::sctk::reexports::calloop::channel::Sender; use cctk::toplevel_info::ToplevelInfo; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; @@ -207,7 +209,7 @@ struct CosmicAppList { favorite_list: Vec, dnd_source: Option<(window::Id, DockItem, DndAction)>, config: AppListConfig, - toplevel_sender: Option>, + wayland_sender: Option>, seat: Option, rectangle_tracker: Option>, rectangles: HashMap, @@ -218,7 +220,7 @@ struct CosmicAppList { // TODO DnD after sctk merges DnD #[derive(Debug, Clone)] enum Message { - Toplevel(ToplevelUpdate), + Wayland(WaylandUpdate), Favorite(String), UnFavorite(String), Popup(String), @@ -490,8 +492,8 @@ impl cosmic::Application for CosmicAppList { if let Some(p) = self.popup.take() { return destroy_popup(p.0); } - if let Some(tx) = self.toplevel_sender.as_ref() { - let _ = tx.send(ToplevelRequest::Activate(handle)); + if let Some(tx) = self.wayland_sender.as_ref() { + let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle))); } } Message::Quit(id) => { @@ -502,8 +504,10 @@ impl cosmic::Application for CosmicAppList { .find(|t| t.desktop_info.id == id) { for (handle, _) in &toplevel_group.toplevels { - if let Some(tx) = self.toplevel_sender.as_ref() { - let _ = tx.send(ToplevelRequest::Quit(handle.clone())); + if let Some(tx) = self.wayland_sender.as_ref() { + let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Quit( + handle.clone(), + ))); } } } @@ -695,37 +699,12 @@ impl cosmic::Application for CosmicAppList { } return finish_dnd(); } - Message::Toplevel(event) => { + Message::Wayland(event) => { match event { - ToplevelUpdate::AddToplevel(handle, info) => { - if info.app_id.is_empty() { - return Command::none(); - } - if let Some(t) = self - .active_list - .iter_mut() - .chain(self.favorite_list.iter_mut()) - .find(|DockItem { desktop_info, .. }| { - desktop_info.id == info.app_id - || desktop_info.wm_class.as_ref() == Some(&info.app_id) - }) - { - t.toplevels.push((handle, info)); - } else { - let desktop_info = - desktop_info_for_app_ids(vec![info.app_id.clone()]).remove(0); - self.item_ctr += 1; - self.active_list.push(DockItem { - id: self.item_ctr, - toplevels: vec![(handle, info)], - desktop_info, - }); - } + WaylandUpdate::Init(tx) => { + self.wayland_sender.replace(tx); } - ToplevelUpdate::Init(tx) => { - self.toplevel_sender.replace(tx); - } - ToplevelUpdate::Finished => { + WaylandUpdate::Finished => { for t in &mut self.favorite_list { t.toplevels.clear(); } @@ -748,33 +727,78 @@ impl cosmic::Application for CosmicAppList { ) .map(cosmic::app::message::app); } - ToplevelUpdate::RemoveToplevel(handle) => { - for t in self - .active_list - .iter_mut() - .chain(self.favorite_list.iter_mut()) - { - t.toplevels.retain(|(t_handle, _)| t_handle != &handle); + WaylandUpdate::Toplevel(event) => match event { + ToplevelUpdate::AddToplevel(handle, info) => { + if info.app_id.is_empty() { + return Command::none(); + } + if let Some(t) = self + .active_list + .iter_mut() + .chain(self.favorite_list.iter_mut()) + .find(|DockItem { desktop_info, .. }| { + desktop_info.id == info.app_id + || desktop_info.wm_class.as_ref() == Some(&info.app_id) + }) + { + t.toplevels.push((handle, info)); + } else { + let desktop_info = + desktop_info_for_app_ids(vec![info.app_id.clone()]).remove(0); + self.item_ctr += 1; + self.active_list.push(DockItem { + id: self.item_ctr, + toplevels: vec![(handle, info)], + desktop_info, + }); + } } - self.active_list.retain(|t| !t.toplevels.is_empty()); - } - ToplevelUpdate::UpdateToplevel(handle, info) => { - // TODO probably want to make sure it is removed - if info.app_id.is_empty() { - return Command::none(); + ToplevelUpdate::RemoveToplevel(handle) => { + for t in self + .active_list + .iter_mut() + .chain(self.favorite_list.iter_mut()) + { + t.toplevels.retain(|(t_handle, _)| t_handle != &handle); + } + self.active_list.retain(|t| !t.toplevels.is_empty()); } - 'toplevel_loop: for toplevel_list in self - .active_list - .iter_mut() - .chain(self.favorite_list.iter_mut()) - { - for (t_handle, t_info) in &mut toplevel_list.toplevels { - if &handle == t_handle { - *t_info = info; - break 'toplevel_loop; + ToplevelUpdate::UpdateToplevel(handle, info) => { + // TODO probably want to make sure it is removed + if info.app_id.is_empty() { + return Command::none(); + } + 'toplevel_loop: for toplevel_list in self + .active_list + .iter_mut() + .chain(self.favorite_list.iter_mut()) + { + for (t_handle, t_info) in &mut toplevel_list.toplevels { + if &handle == t_handle { + *t_info = info; + break 'toplevel_loop; + } } } } + }, + WaylandUpdate::ActivationToken { token, exec } => { + let mut exec = shlex::Shlex::new(&exec); + let mut cmd = match exec.next() { + Some(cmd) if !cmd.contains('=') => tokio::process::Command::new(cmd), + _ => return Command::none(), + }; + for arg in exec { + // TODO handle "%" args here if necessary? + if !arg.starts_with('%') { + cmd.arg(arg); + } + } + if let Some(token) = token { + cmd.env("XDG_ACTIVATION_TOKEN", &token); + cmd.env("DESKTOP_STARTUP_ID", &token); + } + let _ = cmd.spawn(); } } } @@ -784,19 +808,13 @@ impl cosmic::Application for CosmicAppList { Message::RemovedSeat(_) => { self.seat.take(); } - Message::Exec(exec_str) => { - let mut exec = shlex::Shlex::new(&exec_str); - let mut cmd = match exec.next() { - Some(cmd) if !cmd.contains('=') => tokio::process::Command::new(cmd), - _ => return Command::none(), - }; - for arg in exec { - // TODO handle "%" args here if necessary? - if !arg.starts_with('%') { - cmd.arg(arg); - } + Message::Exec(exec) => { + if let Some(tx) = self.wayland_sender.as_ref() { + let _ = tx.send(WaylandRequest::TokenRequest { + app_id: Self::APP_ID.to_string(), + exec, + }); } - let _ = cmd.spawn(); } Message::Rectangle(u) => match u { RectangleUpdate::Rectangle(r) => { @@ -1080,7 +1098,7 @@ impl cosmic::Application for CosmicAppList { fn subscription(&self) -> Subscription { Subscription::batch(vec![ - toplevel_subscription(self.subscription_ctr).map(Message::Toplevel), + wayland_subscription(self.subscription_ctr).map(Message::Wayland), events_with(|e, _| match e { cosmic::iced_runtime::core::Event::PlatformSpecific( event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)), diff --git a/cosmic-app-list/src/main.rs b/cosmic-app-list/src/main.rs index 4d0ff468..a68df1f8 100644 --- a/cosmic-app-list/src/main.rs +++ b/cosmic-app-list/src/main.rs @@ -2,8 +2,8 @@ mod app; mod config; mod localize; -mod toplevel_handler; -mod toplevel_subscription; +mod wayland_handler; +mod wayland_subscription; use log::info; diff --git a/cosmic-app-list/src/toplevel_handler.rs b/cosmic-app-list/src/wayland_handler.rs similarity index 54% rename from cosmic-app-list/src/toplevel_handler.rs rename to cosmic-app-list/src/wayland_handler.rs index 76030e9d..36f9e319 100644 --- a/cosmic-app-list/src/toplevel_handler.rs +++ b/cosmic-app-list/src/wayland_handler.rs @@ -1,28 +1,36 @@ -use crate::toplevel_subscription::{ToplevelRequest, ToplevelUpdate}; +use crate::wayland_subscription::{ToplevelRequest, ToplevelUpdate, WaylandRequest, WaylandUpdate}; use cctk::{ sctk::{ self, - reexports::{ - calloop, calloop_wayland_source::WaylandSource, client::protocol::wl_seat::WlSeat, - }, + activation::{RequestData, RequestDataExt}, + reexports::{calloop, calloop_wayland_source::WaylandSource}, seat::{SeatHandler, SeatState}, }, toplevel_info::{ToplevelInfoHandler, ToplevelInfoState}, toplevel_management::{ToplevelManagerHandler, ToplevelManagerState}, - wayland_client::{self, WEnum}, + wayland_client::{ + self, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + WEnum, + }, }; use cosmic_protocols::{ toplevel_info::v1::client::zcosmic_toplevel_handle_v1, toplevel_management::v1::client::zcosmic_toplevel_manager_v1, }; use futures::channel::mpsc::UnboundedSender; -use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::{ + activation::{ActivationHandler, ActivationState}, + registry::{ProvidesRegistryState, RegistryState}, +}; use wayland_client::{globals::registry_queue_init, Connection, QueueHandle}; struct AppData { exit: bool, - tx: UnboundedSender, + tx: UnboundedSender, + queue_handle: QueueHandle, registry_state: RegistryState, + activation_state: Option, toplevel_info_state: ToplevelInfoState, toplevel_manager_state: ToplevelManagerState, seat_state: SeatState, @@ -36,6 +44,35 @@ impl ProvidesRegistryState for AppData { sctk::registry_handlers!(); } +struct ExecRequestData { + data: RequestData, + exec: String, +} + +impl RequestDataExt for ExecRequestData { + fn app_id(&self) -> Option<&str> { + self.data.app_id() + } + + fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { + self.data.seat_and_serial() + } + + fn surface(&self) -> Option<&WlSurface> { + self.data.surface() + } +} + +impl ActivationHandler for AppData { + type RequestData = ExecRequestData; + fn new_token(&mut self, token: String, data: &ExecRequestData) { + let _ = self.tx.unbounded_send(WaylandUpdate::ActivationToken { + token: Some(token), + exec: data.exec.clone(), + }); + } +} + impl SeatHandler for AppData { fn seat_state(&mut self) -> &mut sctk::seat::SeatState { &mut self.seat_state @@ -93,7 +130,10 @@ impl ToplevelInfoHandler for AppData { if let Some(info) = self.toplevel_info_state.info(toplevel) { let _ = self .tx - .unbounded_send(ToplevelUpdate::AddToplevel(toplevel.clone(), info.clone())); + .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::AddToplevel( + toplevel.clone(), + info.clone(), + ))); } } @@ -104,10 +144,12 @@ impl ToplevelInfoHandler for AppData { toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, ) { if let Some(info) = self.toplevel_info_state.info(toplevel) { - let _ = self.tx.unbounded_send(ToplevelUpdate::UpdateToplevel( - toplevel.clone(), - info.clone(), - )); + let _ = + self.tx + .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::UpdateToplevel( + toplevel.clone(), + info.clone(), + ))); } } @@ -119,16 +161,19 @@ impl ToplevelInfoHandler for AppData { ) { let _ = self .tx - .unbounded_send(ToplevelUpdate::RemoveToplevel(toplevel.clone())); + .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::RemoveToplevel( + toplevel.clone(), + ))); } } -pub(crate) fn toplevel_handler( - tx: UnboundedSender, - rx: calloop::channel::Channel, +pub(crate) fn wayland_handler( + tx: UnboundedSender, + rx: calloop::channel::Channel, ) { let conn = Connection::connect_to_env().unwrap(); let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let mut event_loop = calloop::EventLoop::::try_new().unwrap(); let qh = event_queue.handle(); let wayland_source = WaylandSource::new(conn, event_queue); @@ -140,18 +185,43 @@ pub(crate) fn toplevel_handler( if handle .insert_source(rx, |event, _, state| match event { calloop::channel::Event::Msg(req) => match req { - ToplevelRequest::Activate(handle) => { - if let Some(seat) = state.seat_state.seats().next() { - let manager = &state.toplevel_manager_state.manager; - manager.activate(&handle, &seat); + WaylandRequest::Toplevel(req) => match req { + ToplevelRequest::Activate(handle) => { + if let Some(seat) = state.seat_state.seats().next() { + let manager = &state.toplevel_manager_state.manager; + manager.activate(&handle, &seat); + } + } + ToplevelRequest::Quit(handle) => { + let manager = &state.toplevel_manager_state.manager; + manager.close(&handle); + } + ToplevelRequest::Exit => { + state.exit = true; + } + }, + WaylandRequest::TokenRequest { app_id, exec } => { + if let Some(activation_state) = state.activation_state.as_ref() { + activation_state.request_token_with_data( + &state.queue_handle, + ExecRequestData { + data: RequestData { + app_id: Some(app_id), + seat_and_serial: state + .seat_state + .seats() + .next() + .map(|seat| (seat, 0)), + surface: None, + }, + exec, + }, + ); + } else { + let _ = state + .tx + .unbounded_send(WaylandUpdate::ActivationToken { token: None, exec }); } - } - ToplevelRequest::Quit(handle) => { - let manager = &state.toplevel_manager_state.manager; - manager.close(&handle); - } - ToplevelRequest::Exit => { - state.exit = true; } }, calloop::channel::Event::Closed => { @@ -166,6 +236,8 @@ pub(crate) fn toplevel_handler( let mut app_data = AppData { exit: false, tx, + queue_handle: qh.clone(), + activation_state: ActivationState::bind::(&globals, &qh).ok(), seat_state: SeatState::new(&globals, &qh), toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh), toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh), @@ -180,6 +252,7 @@ pub(crate) fn toplevel_handler( } } +sctk::delegate_activation!(AppData, ExecRequestData); sctk::delegate_seat!(AppData); sctk::delegate_registry!(AppData); cctk::delegate_toplevel_info!(AppData); diff --git a/cosmic-app-list/src/toplevel_subscription.rs b/cosmic-app-list/src/wayland_subscription.rs similarity index 70% rename from cosmic-app-list/src/toplevel_subscription.rs rename to cosmic-app-list/src/wayland_subscription.rs index b285fae6..aeda11d5 100644 --- a/cosmic-app-list/src/toplevel_subscription.rs +++ b/cosmic-app-list/src/wayland_subscription.rs @@ -2,7 +2,7 @@ //! //! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. //! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`. -use cctk::sctk::reexports::{calloop, client::protocol::wl_seat::WlSeat}; +use cctk::sctk::reexports::calloop; use cctk::toplevel_info::ToplevelInfo; use cosmic::iced; use cosmic::iced::subscription; @@ -13,11 +13,11 @@ use futures::{ }; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; -use crate::toplevel_handler::toplevel_handler; +use crate::wayland_handler::wayland_handler; -pub fn toplevel_subscription( +pub fn wayland_subscription( id: I, -) -> iced::Subscription { +) -> iced::Subscription { subscription::channel(id, 50, move |mut output| async move { let mut state = State::Ready; @@ -30,8 +30,8 @@ pub fn toplevel_subscription( pub enum State { Ready, Waiting( - UnboundedReceiver, - calloop::channel::Sender, + UnboundedReceiver, + calloop::channel::Sender, JoinHandle<()>, ), Finished, @@ -39,22 +39,22 @@ pub enum State { async fn start_listening( state: State, - output: &mut futures::channel::mpsc::Sender, + output: &mut futures::channel::mpsc::Sender, ) -> State { match state { State::Ready => { let (calloop_tx, calloop_rx) = calloop::channel::channel(); let (toplevel_tx, toplevel_rx) = unbounded(); let handle = std::thread::spawn(move || { - toplevel_handler(toplevel_tx, calloop_rx); + wayland_handler(toplevel_tx, calloop_rx); }); let tx = calloop_tx.clone(); - _ = output.send(ToplevelUpdate::Init(tx)).await; + _ = output.send(WaylandUpdate::Init(tx)).await; State::Waiting(toplevel_rx, calloop_tx, handle) } State::Waiting(mut rx, tx, handle) => { if handle.is_finished() { - _ = output.send(ToplevelUpdate::Finished).await; + _ = output.send(WaylandUpdate::Finished).await; return State::Finished; } match rx.next().await { @@ -63,7 +63,7 @@ async fn start_listening( State::Waiting(rx, tx, handle) } None => { - _ = output.send(ToplevelUpdate::Finished).await; + _ = output.send(WaylandUpdate::Finished).await; return State::Finished; } } @@ -73,12 +73,24 @@ async fn start_listening( } #[derive(Clone, Debug)] -pub enum ToplevelUpdate { +pub enum WaylandUpdate { + Init(calloop::channel::Sender), Finished, + Toplevel(ToplevelUpdate), + ActivationToken { token: Option, exec: String }, +} + +#[derive(Clone, Debug)] +pub enum ToplevelUpdate { AddToplevel(ZcosmicToplevelHandleV1, ToplevelInfo), UpdateToplevel(ZcosmicToplevelHandleV1, ToplevelInfo), RemoveToplevel(ZcosmicToplevelHandleV1), - Init(calloop::channel::Sender), +} + +#[derive(Clone, Debug)] +pub enum WaylandRequest { + Toplevel(ToplevelRequest), + TokenRequest { app_id: String, exec: String }, } #[derive(Debug, Clone)]