app-list: Add xdg-activation

This commit is contained in:
Victoria Brekenfeld 2023-11-06 16:46:44 +01:00 committed by Ashley Wulber
parent e69396dac0
commit 99da3eda4a
4 changed files with 218 additions and 115 deletions

View file

@ -2,9 +2,11 @@ use crate::config;
use crate::config::AppListConfig; use crate::config::AppListConfig;
use crate::config::APP_ID; use crate::config::APP_ID;
use crate::fl; use crate::fl;
use crate::toplevel_subscription::toplevel_subscription; use crate::wayland_subscription::wayland_subscription;
use crate::toplevel_subscription::ToplevelRequest; use crate::wayland_subscription::ToplevelRequest;
use crate::toplevel_subscription::ToplevelUpdate; use crate::wayland_subscription::ToplevelUpdate;
use crate::wayland_subscription::WaylandRequest;
use crate::wayland_subscription::WaylandUpdate;
use cctk::sctk::reexports::calloop::channel::Sender; use cctk::sctk::reexports::calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo; use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
@ -207,7 +209,7 @@ struct CosmicAppList {
favorite_list: Vec<DockItem>, favorite_list: Vec<DockItem>,
dnd_source: Option<(window::Id, DockItem, DndAction)>, dnd_source: Option<(window::Id, DockItem, DndAction)>,
config: AppListConfig, config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>, wayland_sender: Option<Sender<WaylandRequest>>,
seat: Option<WlSeat>, seat: Option<WlSeat>,
rectangle_tracker: Option<RectangleTracker<u32>>, rectangle_tracker: Option<RectangleTracker<u32>>,
rectangles: HashMap<u32, iced::Rectangle>, rectangles: HashMap<u32, iced::Rectangle>,
@ -218,7 +220,7 @@ struct CosmicAppList {
// TODO DnD after sctk merges DnD // TODO DnD after sctk merges DnD
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Toplevel(ToplevelUpdate), Wayland(WaylandUpdate),
Favorite(String), Favorite(String),
UnFavorite(String), UnFavorite(String),
Popup(String), Popup(String),
@ -490,8 +492,8 @@ impl cosmic::Application for CosmicAppList {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
return destroy_popup(p.0); return destroy_popup(p.0);
} }
if let Some(tx) = self.toplevel_sender.as_ref() { if let Some(tx) = self.wayland_sender.as_ref() {
let _ = tx.send(ToplevelRequest::Activate(handle)); let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
} }
} }
Message::Quit(id) => { Message::Quit(id) => {
@ -502,8 +504,10 @@ impl cosmic::Application for CosmicAppList {
.find(|t| t.desktop_info.id == id) .find(|t| t.desktop_info.id == id)
{ {
for (handle, _) in &toplevel_group.toplevels { for (handle, _) in &toplevel_group.toplevels {
if let Some(tx) = self.toplevel_sender.as_ref() { if let Some(tx) = self.wayland_sender.as_ref() {
let _ = tx.send(ToplevelRequest::Quit(handle.clone())); let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Quit(
handle.clone(),
)));
} }
} }
} }
@ -695,37 +699,12 @@ impl cosmic::Application for CosmicAppList {
} }
return finish_dnd(); return finish_dnd();
} }
Message::Toplevel(event) => { Message::Wayland(event) => {
match event { match event {
ToplevelUpdate::AddToplevel(handle, info) => { WaylandUpdate::Init(tx) => {
if info.app_id.is_empty() { self.wayland_sender.replace(tx);
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,
});
}
} }
ToplevelUpdate::Init(tx) => { WaylandUpdate::Finished => {
self.toplevel_sender.replace(tx);
}
ToplevelUpdate::Finished => {
for t in &mut self.favorite_list { for t in &mut self.favorite_list {
t.toplevels.clear(); t.toplevels.clear();
} }
@ -748,33 +727,78 @@ impl cosmic::Application for CosmicAppList {
) )
.map(cosmic::app::message::app); .map(cosmic::app::message::app);
} }
ToplevelUpdate::RemoveToplevel(handle) => { WaylandUpdate::Toplevel(event) => match event {
for t in self ToplevelUpdate::AddToplevel(handle, info) => {
.active_list if info.app_id.is_empty() {
.iter_mut() return Command::none();
.chain(self.favorite_list.iter_mut()) }
{ if let Some(t) = self
t.toplevels.retain(|(t_handle, _)| t_handle != &handle); .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::RemoveToplevel(handle) => {
} for t in self
ToplevelUpdate::UpdateToplevel(handle, info) => { .active_list
// TODO probably want to make sure it is removed .iter_mut()
if info.app_id.is_empty() { .chain(self.favorite_list.iter_mut())
return Command::none(); {
t.toplevels.retain(|(t_handle, _)| t_handle != &handle);
}
self.active_list.retain(|t| !t.toplevels.is_empty());
} }
'toplevel_loop: for toplevel_list in self ToplevelUpdate::UpdateToplevel(handle, info) => {
.active_list // TODO probably want to make sure it is removed
.iter_mut() if info.app_id.is_empty() {
.chain(self.favorite_list.iter_mut()) return Command::none();
{ }
for (t_handle, t_info) in &mut toplevel_list.toplevels { 'toplevel_loop: for toplevel_list in self
if &handle == t_handle { .active_list
*t_info = info; .iter_mut()
break 'toplevel_loop; .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(_) => { Message::RemovedSeat(_) => {
self.seat.take(); self.seat.take();
} }
Message::Exec(exec_str) => { Message::Exec(exec) => {
let mut exec = shlex::Shlex::new(&exec_str); if let Some(tx) = self.wayland_sender.as_ref() {
let mut cmd = match exec.next() { let _ = tx.send(WaylandRequest::TokenRequest {
Some(cmd) if !cmd.contains('=') => tokio::process::Command::new(cmd), app_id: Self::APP_ID.to_string(),
_ => return Command::none(), exec,
}; });
for arg in exec {
// TODO handle "%" args here if necessary?
if !arg.starts_with('%') {
cmd.arg(arg);
}
} }
let _ = cmd.spawn();
} }
Message::Rectangle(u) => match u { Message::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => { RectangleUpdate::Rectangle(r) => {
@ -1080,7 +1098,7 @@ impl cosmic::Application for CosmicAppList {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ Subscription::batch(vec![
toplevel_subscription(self.subscription_ctr).map(Message::Toplevel), wayland_subscription(self.subscription_ctr).map(Message::Wayland),
events_with(|e, _| match e { events_with(|e, _| match e {
cosmic::iced_runtime::core::Event::PlatformSpecific( cosmic::iced_runtime::core::Event::PlatformSpecific(
event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)), event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)),

View file

@ -2,8 +2,8 @@
mod app; mod app;
mod config; mod config;
mod localize; mod localize;
mod toplevel_handler; mod wayland_handler;
mod toplevel_subscription; mod wayland_subscription;
use log::info; use log::info;

View file

@ -1,28 +1,36 @@
use crate::toplevel_subscription::{ToplevelRequest, ToplevelUpdate}; use crate::wayland_subscription::{ToplevelRequest, ToplevelUpdate, WaylandRequest, WaylandUpdate};
use cctk::{ use cctk::{
sctk::{ sctk::{
self, self,
reexports::{ activation::{RequestData, RequestDataExt},
calloop, calloop_wayland_source::WaylandSource, client::protocol::wl_seat::WlSeat, reexports::{calloop, calloop_wayland_source::WaylandSource},
},
seat::{SeatHandler, SeatState}, seat::{SeatHandler, SeatState},
}, },
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState}, toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
toplevel_management::{ToplevelManagerHandler, ToplevelManagerState}, toplevel_management::{ToplevelManagerHandler, ToplevelManagerState},
wayland_client::{self, WEnum}, wayland_client::{
self,
protocol::{wl_seat::WlSeat, wl_surface::WlSurface},
WEnum,
},
}; };
use cosmic_protocols::{ use cosmic_protocols::{
toplevel_info::v1::client::zcosmic_toplevel_handle_v1, toplevel_info::v1::client::zcosmic_toplevel_handle_v1,
toplevel_management::v1::client::zcosmic_toplevel_manager_v1, toplevel_management::v1::client::zcosmic_toplevel_manager_v1,
}; };
use futures::channel::mpsc::UnboundedSender; 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}; use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
struct AppData { struct AppData {
exit: bool, exit: bool,
tx: UnboundedSender<ToplevelUpdate>, tx: UnboundedSender<WaylandUpdate>,
queue_handle: QueueHandle<Self>,
registry_state: RegistryState, registry_state: RegistryState,
activation_state: Option<ActivationState>,
toplevel_info_state: ToplevelInfoState, toplevel_info_state: ToplevelInfoState,
toplevel_manager_state: ToplevelManagerState, toplevel_manager_state: ToplevelManagerState,
seat_state: SeatState, seat_state: SeatState,
@ -36,6 +44,35 @@ impl ProvidesRegistryState for AppData {
sctk::registry_handlers!(); 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 { impl SeatHandler for AppData {
fn seat_state(&mut self) -> &mut sctk::seat::SeatState { fn seat_state(&mut self) -> &mut sctk::seat::SeatState {
&mut self.seat_state &mut self.seat_state
@ -93,7 +130,10 @@ impl ToplevelInfoHandler for AppData {
if let Some(info) = self.toplevel_info_state.info(toplevel) { if let Some(info) = self.toplevel_info_state.info(toplevel) {
let _ = self let _ = self
.tx .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, toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) { ) {
if let Some(info) = self.toplevel_info_state.info(toplevel) { if let Some(info) = self.toplevel_info_state.info(toplevel) {
let _ = self.tx.unbounded_send(ToplevelUpdate::UpdateToplevel( let _ =
toplevel.clone(), self.tx
info.clone(), .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::UpdateToplevel(
)); toplevel.clone(),
info.clone(),
)));
} }
} }
@ -119,16 +161,19 @@ impl ToplevelInfoHandler for AppData {
) { ) {
let _ = self let _ = self
.tx .tx
.unbounded_send(ToplevelUpdate::RemoveToplevel(toplevel.clone())); .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::RemoveToplevel(
toplevel.clone(),
)));
} }
} }
pub(crate) fn toplevel_handler( pub(crate) fn wayland_handler(
tx: UnboundedSender<ToplevelUpdate>, tx: UnboundedSender<WaylandUpdate>,
rx: calloop::channel::Channel<ToplevelRequest>, rx: calloop::channel::Channel<WaylandRequest>,
) { ) {
let conn = Connection::connect_to_env().unwrap(); let conn = Connection::connect_to_env().unwrap();
let (globals, event_queue) = registry_queue_init(&conn).unwrap(); let (globals, event_queue) = registry_queue_init(&conn).unwrap();
let mut event_loop = calloop::EventLoop::<AppData>::try_new().unwrap(); let mut event_loop = calloop::EventLoop::<AppData>::try_new().unwrap();
let qh = event_queue.handle(); let qh = event_queue.handle();
let wayland_source = WaylandSource::new(conn, event_queue); let wayland_source = WaylandSource::new(conn, event_queue);
@ -140,18 +185,43 @@ pub(crate) fn toplevel_handler(
if handle if handle
.insert_source(rx, |event, _, state| match event { .insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(req) => match req { calloop::channel::Event::Msg(req) => match req {
ToplevelRequest::Activate(handle) => { WaylandRequest::Toplevel(req) => match req {
if let Some(seat) = state.seat_state.seats().next() { ToplevelRequest::Activate(handle) => {
let manager = &state.toplevel_manager_state.manager; if let Some(seat) = state.seat_state.seats().next() {
manager.activate(&handle, &seat); 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 => { calloop::channel::Event::Closed => {
@ -166,6 +236,8 @@ pub(crate) fn toplevel_handler(
let mut app_data = AppData { let mut app_data = AppData {
exit: false, exit: false,
tx, tx,
queue_handle: qh.clone(),
activation_state: ActivationState::bind::<AppData>(&globals, &qh).ok(),
seat_state: SeatState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh),
toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh), toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh),
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh), toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
@ -180,6 +252,7 @@ pub(crate) fn toplevel_handler(
} }
} }
sctk::delegate_activation!(AppData, ExecRequestData);
sctk::delegate_seat!(AppData); sctk::delegate_seat!(AppData);
sctk::delegate_registry!(AppData); sctk::delegate_registry!(AppData);
cctk::delegate_toplevel_info!(AppData); cctk::delegate_toplevel_info!(AppData);

View file

@ -2,7 +2,7 @@
//! //!
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. //! 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`. //! 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 cctk::toplevel_info::ToplevelInfo;
use cosmic::iced; use cosmic::iced;
use cosmic::iced::subscription; use cosmic::iced::subscription;
@ -13,11 +13,11 @@ use futures::{
}; };
use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
use crate::toplevel_handler::toplevel_handler; use crate::wayland_handler::wayland_handler;
pub fn toplevel_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn wayland_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced::Subscription<ToplevelUpdate> { ) -> iced::Subscription<WaylandUpdate> {
subscription::channel(id, 50, move |mut output| async move { subscription::channel(id, 50, move |mut output| async move {
let mut state = State::Ready; let mut state = State::Ready;
@ -30,8 +30,8 @@ pub fn toplevel_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
pub enum State { pub enum State {
Ready, Ready,
Waiting( Waiting(
UnboundedReceiver<ToplevelUpdate>, UnboundedReceiver<WaylandUpdate>,
calloop::channel::Sender<ToplevelRequest>, calloop::channel::Sender<WaylandRequest>,
JoinHandle<()>, JoinHandle<()>,
), ),
Finished, Finished,
@ -39,22 +39,22 @@ pub enum State {
async fn start_listening( async fn start_listening(
state: State, state: State,
output: &mut futures::channel::mpsc::Sender<ToplevelUpdate>, output: &mut futures::channel::mpsc::Sender<WaylandUpdate>,
) -> State { ) -> State {
match state { match state {
State::Ready => { State::Ready => {
let (calloop_tx, calloop_rx) = calloop::channel::channel(); let (calloop_tx, calloop_rx) = calloop::channel::channel();
let (toplevel_tx, toplevel_rx) = unbounded(); let (toplevel_tx, toplevel_rx) = unbounded();
let handle = std::thread::spawn(move || { let handle = std::thread::spawn(move || {
toplevel_handler(toplevel_tx, calloop_rx); wayland_handler(toplevel_tx, calloop_rx);
}); });
let tx = calloop_tx.clone(); 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(toplevel_rx, calloop_tx, handle)
} }
State::Waiting(mut rx, tx, handle) => { State::Waiting(mut rx, tx, handle) => {
if handle.is_finished() { if handle.is_finished() {
_ = output.send(ToplevelUpdate::Finished).await; _ = output.send(WaylandUpdate::Finished).await;
return State::Finished; return State::Finished;
} }
match rx.next().await { match rx.next().await {
@ -63,7 +63,7 @@ async fn start_listening(
State::Waiting(rx, tx, handle) State::Waiting(rx, tx, handle)
} }
None => { None => {
_ = output.send(ToplevelUpdate::Finished).await; _ = output.send(WaylandUpdate::Finished).await;
return State::Finished; return State::Finished;
} }
} }
@ -73,12 +73,24 @@ async fn start_listening(
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ToplevelUpdate { pub enum WaylandUpdate {
Init(calloop::channel::Sender<WaylandRequest>),
Finished, Finished,
Toplevel(ToplevelUpdate),
ActivationToken { token: Option<String>, exec: String },
}
#[derive(Clone, Debug)]
pub enum ToplevelUpdate {
AddToplevel(ZcosmicToplevelHandleV1, ToplevelInfo), AddToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
UpdateToplevel(ZcosmicToplevelHandleV1, ToplevelInfo), UpdateToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
RemoveToplevel(ZcosmicToplevelHandleV1), RemoveToplevel(ZcosmicToplevelHandleV1),
Init(calloop::channel::Sender<ToplevelRequest>), }
#[derive(Clone, Debug)]
pub enum WaylandRequest {
Toplevel(ToplevelRequest),
TokenRequest { app_id: String, exec: String },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]