diff --git a/Cargo.toml b/Cargo.toml index bb75f9ff27c..b4209a2d445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] debug = ["iced/debug"] # Enables pipewire support in ashpd, if ashpd is enabled pipewire = ["ashpd?/pipewire"] +# Enables process spawning helper +process = ["nix"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # smol async runtime @@ -27,7 +29,7 @@ wayland = [ "iced_runtime/wayland", "iced/wayland", "iced_sctk", - "sctk", + "cctk", ] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] @@ -40,6 +42,7 @@ winit_wgpu = ["winit", "wgpu"] xdg-portal = ["ashpd"] # XXX Use "a11y"; which is causing a panic currently applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] +applet-token = [] zbus = ["dep:zbus", "serde", "ron"] [dependencies] @@ -48,7 +51,7 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.7.3" tokio = { version = "1.24.2", optional = true } -sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", optional = true, rev = "dc8c4a0" } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "5faec87", optional = true } slotmap = "1.0.6" fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } @@ -60,6 +63,7 @@ ashpd = { version = "0.5.0", default-features = false, optional = true } url = "2.4.0" unicode-segmentation = "1.6" css-color = "0.2.5" +nix = { version = "0.26", optional = true } zbus = {version = "3.14.1", default-features = false, optional = true} serde = { version = "1.0.180", optional = true } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index b3d0548a4b2..257e43d218d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -5,6 +5,8 @@ use super::{command, Application, ApplicationExt, Core, Subscription}; use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; use crate::{keyboard_nav, Element}; +#[cfg(feature = "wayland")] +use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; #[cfg(feature = "wayland")] use iced::event::wayland::{self, WindowEvent}; @@ -15,8 +17,6 @@ use iced::window; use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] use iced_runtime::window::Action as WindowAction; -#[cfg(feature = "wayland")] -use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 839a4eae991..04c87b2a464 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,12 +1,16 @@ +#[cfg(feature = "applet-token")] +pub mod token; + use crate::{ app::Core, + cctk::sctk, iced::{ self, alignment::{Horizontal, Vertical}, widget::Container, window, Color, Length, Limits, Rectangle, }, - iced_style, iced_widget, sctk, + iced_style, iced_widget, theme::{self, Button, THEME}, widget, Application, Element, Renderer, }; diff --git a/src/applet/token/mod.rs b/src/applet/token/mod.rs new file mode 100644 index 00000000000..fc4c09c96bf --- /dev/null +++ b/src/applet/token/mod.rs @@ -0,0 +1,2 @@ +pub mod subscription; +pub mod wayland_handler; diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs new file mode 100644 index 00000000000..c48e12509a8 --- /dev/null +++ b/src/applet/token/subscription.rs @@ -0,0 +1,78 @@ +use crate::iced; +use crate::iced::subscription; +use crate::iced_futures::futures; +use cctk::sctk::reexports::calloop; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, + SinkExt, StreamExt, +}; +use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; + +use super::wayland_handler::wayland_handler; + +pub fn activation_token_subscription( + id: I, +) -> iced::Subscription { + subscription::channel(id, 50, move |mut output| async move { + let mut state = State::Ready; + + loop { + state = start_listening(state, &mut output).await; + } + }) +} + +pub enum State { + Ready, + Waiting( + UnboundedReceiver, + calloop::channel::Sender, + JoinHandle<()>, + ), + Finished, +} + +async fn start_listening( + state: State, + 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 || { + wayland_handler(toplevel_tx, calloop_rx); + }); + let tx = calloop_tx.clone(); + _ = output.send(TokenUpdate::Init(tx)).await; + State::Waiting(toplevel_rx, calloop_tx, handle) + } + State::Waiting(mut rx, tx, handle) => { + if handle.is_finished() { + _ = output.send(TokenUpdate::Finished).await; + return State::Finished; + } + if let Some(u) = rx.next().await { + _ = output.send(u).await; + State::Waiting(rx, tx, handle) + } else { + _ = output.send(TokenUpdate::Finished).await; + State::Finished + } + } + State::Finished => iced::futures::future::pending().await, + } +} + +#[derive(Clone, Debug)] +pub enum TokenUpdate { + Init(calloop::channel::Sender), + Finished, + ActivationToken { token: Option, exec: String }, +} + +#[derive(Clone, Debug)] +pub struct TokenRequest { + pub app_id: String, + pub exec: String, +} diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs new file mode 100644 index 00000000000..cb795c8ad6d --- /dev/null +++ b/src/applet/token/wayland_handler.rs @@ -0,0 +1,180 @@ +use std::os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixStream, +}; + +use super::subscription::{TokenRequest, TokenUpdate}; +use cctk::{ + sctk::{ + self, + activation::{RequestData, RequestDataExt}, + reexports::{calloop, calloop_wayland_source::WaylandSource}, + seat::{SeatHandler, SeatState}, + }, + wayland_client::{ + self, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + }, +}; +use iced_futures::futures::channel::mpsc::UnboundedSender; +use sctk::{ + activation::{ActivationHandler, ActivationState}, + registry::{ProvidesRegistryState, RegistryState}, +}; +use wayland_client::{globals::registry_queue_init, Connection, QueueHandle}; + +struct AppData { + exit: bool, + queue_handle: QueueHandle, + registry_state: RegistryState, + activation_state: Option, + tx: UnboundedSender, + seat_state: SeatState, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + 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(TokenUpdate::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 + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} + + fn new_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + _: WlSeat, + _: sctk::seat::Capability, + ) { + } + + fn remove_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + _: WlSeat, + _: sctk::seat::Capability, + ) { + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} +} + +pub(crate) fn wayland_handler( + tx: UnboundedSender, + rx: calloop::channel::Channel, +) { + let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET") + .ok() + .and_then(|fd| { + fd.parse::() + .ok() + .map(|fd| unsafe { UnixStream::from_raw_fd(fd) }) + }); + + let conn = if let Some(socket) = socket { + Connection::from_socket(socket).unwrap() + } else { + 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); + let handle = event_loop.handle(); + wayland_source + .insert(handle.clone()) + .expect("Failed to insert wayland source."); + + if handle + .insert_source(rx, |event, _, state| match event { + calloop::channel::Event::Msg(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(TokenUpdate::ActivationToken { token: None, exec }); + } + } + calloop::channel::Event::Closed => { + state.exit = true; + } + }) + .is_err() + { + return; + } + let registry_state = RegistryState::new(&globals); + let mut app_data = AppData { + exit: false, + tx, + seat_state: SeatState::new(&globals, &qh), + queue_handle: qh.clone(), + activation_state: ActivationState::bind::(&globals, &qh).ok(), + registry_state, + }; + + loop { + if app_data.exit { + break; + } + event_loop.dispatch(None, &mut app_data).unwrap(); + } +} + +sctk::delegate_activation!(AppData, ExecRequestData); +sctk::delegate_seat!(AppData); +sctk::delegate_registry!(AppData); diff --git a/src/lib.rs b/src/lib.rs index 2785e4e40a8..6a65e18d50e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,11 @@ pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; +#[cfg(feature = "process")] +pub mod process; + #[cfg(feature = "wayland")] -pub use sctk; +pub use cctk; pub mod theme; pub use theme::{style, Theme}; diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 00000000000..1f58531a2de --- /dev/null +++ b/src/process.rs @@ -0,0 +1,31 @@ +use std::process::{exit, Command, Stdio}; + +use nix::sys::wait::waitpid; +use nix::unistd::{fork, ForkResult}; + +/// Performs a double fork with setsid to spawn and detach a command. +pub fn spawn(mut command: Command) { + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + unsafe { + match fork() { + Ok(ForkResult::Parent { child }) => { + let _res = waitpid(Some(child), None); + } + + Ok(ForkResult::Child) => { + let _res = nix::unistd::setsid(); + let _res = command.spawn(); + + exit(0); + } + + Err(why) => { + println!("failed to fork and spawn command: {}", why.desc()); + } + } + } +} diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4cb948945ce..bbff1e254fc 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -41,9 +41,9 @@ use iced_runtime::command::platform_specific; use iced_runtime::Command; #[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; +use cctk::sctk::reexports::client::protocol::wl_data_device_manager::DndAction; #[cfg(feature = "wayland")] -use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; /// Creates a new [`TextInput`]. ///