diff --git a/Cargo.lock b/Cargo.lock
index 6f97815c..47fe8b6e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -927,6 +927,28 @@ dependencies = [
"tracing-subscriber",
]
+[[package]]
+name = "cosmic-applet-minimize"
+version = "0.1.1"
+dependencies = [
+ "anyhow",
+ "i18n-embed 0.13.9",
+ "i18n-embed-fl 0.6.7",
+ "image",
+ "libcosmic",
+ "memmap2 0.9.4",
+ "once_cell",
+ "png",
+ "rust-embed 6.8.1",
+ "rust-embed-utils 7.8.1",
+ "rustix 0.38.31",
+ "tempfile",
+ "tokio",
+ "tracing",
+ "tracing-log",
+ "tracing-subscriber",
+]
+
[[package]]
name = "cosmic-applet-network"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 006caf4c..41a6b3bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ members = [
"cosmic-applet-audio",
"cosmic-applet-battery",
"cosmic-applet-bluetooth",
+ "cosmic-applet-minimize",
"cosmic-applet-network",
"cosmic-applet-notifications",
"cosmic-applet-power",
@@ -18,6 +19,7 @@ members = [
resolver = "2"
[workspace.dependencies]
+anyhow = "1.0.79"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [
"client",
diff --git a/cosmic-applet-minimize/Cargo.toml b/cosmic-applet-minimize/Cargo.toml
new file mode 100644
index 00000000..1dd1b9ac
--- /dev/null
+++ b/cosmic-applet-minimize/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "cosmic-applet-minimize"
+version = "0.1.1"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow.workspace = true
+image = "0.24"
+libcosmic.workspace = true
+memmap2 = "0.9.0"
+rustix = { version = "0.38.0", features = ["fs"] }
+png = "0.17.5"
+tokio = { version = "1.17.0", features = ["sync", "macros"] }
+tracing = "0.1.40"
+tracing-subscriber.workspace = true
+tracing-log.workspace = true
+tempfile = "3.5.0"
+# Application i18n
+i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
+i18n-embed-fl = "0.6"
+rust-embed = "6.6"
+rust-embed-utils = "7.5.0"
+once_cell = "1"
diff --git a/cosmic-applet-minimize/data/com.system76.CosmicAppletMinimize.desktop b/cosmic-applet-minimize/data/com.system76.CosmicAppletMinimize.desktop
new file mode 100644
index 00000000..076c90b9
--- /dev/null
+++ b/cosmic-applet-minimize/data/com.system76.CosmicAppletMinimize.desktop
@@ -0,0 +1,15 @@
+[Desktop Entry]
+Name=Cosmic Applet Minimize Windows
+Comment=Applet for Cosmic Panel
+Type=Application
+Exec=cosmic-applet-minimize
+Terminal=false
+Categories=Cosmic;Iced;
+Keywords=Cosmic;Iced;
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=com.system76.CosmicAppletMinimize
+StartupNotify=true
+NoDisplay=true
+X-CosmicApplet=true
+X-MinimizeApplet=true
+X-HostWaylandDisplay=true
diff --git a/cosmic-applet-minimize/data/icons/scalable/apps/com.system76.CosmicAppletMinimize.svg b/cosmic-applet-minimize/data/icons/scalable/apps/com.system76.CosmicAppletMinimize.svg
new file mode 100644
index 00000000..7d1b1a58
--- /dev/null
+++ b/cosmic-applet-minimize/data/icons/scalable/apps/com.system76.CosmicAppletMinimize.svg
@@ -0,0 +1,29 @@
+
diff --git a/cosmic-applet-minimize/i18n.toml b/cosmic-applet-minimize/i18n.toml
new file mode 100644
index 00000000..05c50ba2
--- /dev/null
+++ b/cosmic-applet-minimize/i18n.toml
@@ -0,0 +1,4 @@
+fallback_language = "en"
+
+[fluent]
+assets_dir = "i18n"
\ No newline at end of file
diff --git a/cosmic-applet-minimize/i18n/en/cosmic_applet_minimize.ftl b/cosmic-applet-minimize/i18n/en/cosmic_applet_minimize.ftl
new file mode 100644
index 00000000..e69de29b
diff --git a/cosmic-applet-minimize/src/localize.rs b/cosmic-applet-minimize/src/localize.rs
new file mode 100644
index 00000000..44521f68
--- /dev/null
+++ b/cosmic-applet-minimize/src/localize.rs
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MPL-2.0-only
+
+use i18n_embed::{
+ fluent::{fluent_language_loader, FluentLanguageLoader},
+ DefaultLocalizer, LanguageLoader, Localizer,
+};
+use once_cell::sync::Lazy;
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "i18n/"]
+struct Localizations;
+
+pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| {
+ let loader: FluentLanguageLoader = fluent_language_loader!();
+
+ loader
+ .load_fallback_language(&Localizations)
+ .expect("Error while loading fallback language");
+
+ loader
+});
+
+#[macro_export]
+macro_rules! fl {
+ ($message_id:literal) => {{
+ i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
+ }};
+
+ ($message_id:literal, $($args:expr),*) => {{
+ i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
+ }};
+}
+
+// Get the `Localizer` to be used for localizing this library.
+pub fn localizer() -> Box {
+ Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
+}
+
+pub fn localize() {
+ let localizer = localizer();
+ let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
+
+ if let Err(error) = localizer.select(&requested_languages) {
+ eprintln!("Error while loading language for Minimize {}", error);
+ }
+}
diff --git a/cosmic-applet-minimize/src/main.rs b/cosmic-applet-minimize/src/main.rs
new file mode 100644
index 00000000..5b6abaac
--- /dev/null
+++ b/cosmic-applet-minimize/src/main.rs
@@ -0,0 +1,187 @@
+mod localize;
+pub(crate) mod wayland_handler;
+pub(crate) mod wayland_subscription;
+pub(crate) mod window_image;
+
+use crate::localize::localize;
+use cosmic::app::Command;
+use cosmic::applet::cosmic_panel_config::PanelAnchor;
+use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
+use cosmic::cctk::sctk::reexports::calloop;
+use cosmic::cctk::toplevel_info::ToplevelInfo;
+use cosmic::desktop::DesktopEntryData;
+use cosmic::iced::{widget::text, Length, Subscription};
+
+use cosmic::iced_style::application;
+use cosmic::iced_widget::{Column, Row};
+
+use cosmic::widget::tooltip;
+use cosmic::{Element, Theme};
+use wayland_subscription::{
+ ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
+};
+
+const VERSION: &str = env!("CARGO_PKG_VERSION");
+
+pub fn main() -> cosmic::iced::Result {
+ tracing_subscriber::fmt::init();
+ let _ = tracing_log::LogTracer::init();
+
+ // Prepare i18n
+ localize();
+
+ tracing::info!("Starting minimize applet with version {VERSION}");
+
+ cosmic::applet::run::(true, ())
+}
+
+#[derive(Default)]
+struct Minimize {
+ core: cosmic::app::Core,
+ apps: Vec<(
+ ZcosmicToplevelHandleV1,
+ ToplevelInfo,
+ DesktopEntryData,
+ Option,
+ )>,
+ tx: Option>,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Wayland(WaylandUpdate),
+ Activate(ZcosmicToplevelHandleV1),
+}
+
+impl cosmic::Application for Minimize {
+ type Message = Message;
+ type Executor = cosmic::SingleThreadExecutor;
+ type Flags = ();
+ const APP_ID: &'static str = "com.system76.CosmicAppletMinimize";
+
+ fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command) {
+ (
+ Self {
+ core,
+ ..Default::default()
+ },
+ Command::none(),
+ )
+ }
+
+ fn core(&self) -> &cosmic::app::Core {
+ &self.core
+ }
+
+ fn core_mut(&mut self) -> &mut cosmic::app::Core {
+ &mut self.core
+ }
+
+ fn style(&self) -> Option<::Style> {
+ Some(cosmic::applet::style())
+ }
+
+ fn update(&mut self, message: Message) -> Command {
+ match message {
+ Message::Wayland(update) => match update {
+ WaylandUpdate::Init(tx) => {
+ self.tx = Some(tx);
+ }
+ WaylandUpdate::Finished => {
+ panic!("Wayland Subscription ended...")
+ }
+ WaylandUpdate::Toplevel(t) => match t {
+ ToplevelUpdate::Add(handle, info) | ToplevelUpdate::Update(handle, info) => {
+ let data = |id| {
+ cosmic::desktop::load_applications_for_app_ids(
+ None,
+ std::iter::once(id),
+ true,
+ )
+ .remove(0)
+ };
+ if let Some(pos) = self.apps.iter_mut().position(|a| a.0 == handle) {
+ if self.apps[pos].1.app_id != info.app_id {
+ self.apps[pos].2 = data(&info.app_id)
+ }
+ self.apps[pos].1 = info;
+ } else {
+ let data = data(&info.app_id);
+ self.apps.push((handle, info, data, None));
+ }
+ }
+ ToplevelUpdate::Remove(handle) => self.apps.retain(|a| a.0 != handle),
+ },
+ WaylandUpdate::Image(handle, img) => {
+ if let Some(pos) = self.apps.iter().position(|a| a.0 == handle) {
+ self.apps[pos].3 = Some(img);
+ }
+ }
+ },
+ Message::Activate(handle) => {
+ if let Some(tx) = self.tx.as_ref() {
+ let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
+ }
+ }
+ };
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription {
+ wayland_subscription::wayland_subscription().map(Message::Wayland)
+ }
+
+ fn view(&self) -> Element {
+ let (width, _) = self.core.applet.suggested_size();
+ let theme = self.core.system_theme().cosmic();
+ let space_xxs = theme.space_xxs();
+ let icon_buttons = self.apps.iter().map(|(handle, _, data, img)| {
+ tooltip(
+ Element::from(crate::window_image::WindowImage::new(
+ img.clone(),
+ &data.icon,
+ width as f32,
+ Message::Activate(handle.clone()),
+ space_xxs,
+ )),
+ data.name.clone(),
+ // tooltip::Position::FollowCursor,
+ // FIXME tooltip fails to appear when created as indicated in design
+ // maybe it should be a subsurface
+ match self.core.applet.anchor {
+ PanelAnchor::Left => tooltip::Position::Right,
+ PanelAnchor::Right => tooltip::Position::Left,
+ PanelAnchor::Top => tooltip::Position::Bottom,
+ PanelAnchor::Bottom => tooltip::Position::Top,
+ },
+ )
+ .snap_within_viewport(false)
+ .text_shaping(text::Shaping::Advanced)
+ .into()
+ });
+
+ // TODO optional dividers on ends if detects app list neighbor
+ // not sure the best way to tell if there is an adjacent app-list
+
+ if matches!(
+ self.core.applet.anchor,
+ PanelAnchor::Top | PanelAnchor::Bottom
+ ) {
+ Row::with_children(icon_buttons)
+ .align_items(cosmic::iced_core::Alignment::Center)
+ .height(Length::Shrink)
+ .width(Length::Shrink)
+ .spacing(space_xxs)
+ .padding([0, space_xxs])
+ .into()
+ } else {
+ Column::with_children(icon_buttons)
+ .align_items(cosmic::iced_core::Alignment::Center)
+ .height(Length::Shrink)
+ .width(Length::Shrink)
+ .spacing(space_xxs)
+ .padding([space_xxs, 0])
+ .into()
+ }
+ }
+}
diff --git a/cosmic-applet-minimize/src/wayland_handler.rs b/cosmic-applet-minimize/src/wayland_handler.rs
new file mode 100644
index 00000000..d17a6a3b
--- /dev/null
+++ b/cosmic-applet-minimize/src/wayland_handler.rs
@@ -0,0 +1,555 @@
+use crate::wayland_subscription::{
+ ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
+};
+use std::{
+ os::{
+ fd::{AsFd, FromRawFd, RawFd},
+ unix::net::UnixStream,
+ },
+ sync::{Arc, Condvar, Mutex, MutexGuard},
+};
+
+use cctk::{
+ sctk::{
+ self,
+ reexports::{calloop, calloop_wayland_source::WaylandSource},
+ seat::{SeatHandler, SeatState},
+ },
+ toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
+ toplevel_management::{ToplevelManagerHandler, ToplevelManagerState},
+ wayland_client::{self, protocol::wl_seat::WlSeat, WEnum},
+};
+use cosmic::{
+ cctk::{
+ self,
+ cosmic_protocols::{
+ self,
+ screencopy::v1::client::{
+ zcosmic_screencopy_manager_v1, zcosmic_screencopy_session_v1,
+ },
+ toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
+ },
+ screencopy::{
+ BufferInfo, ScreencopyHandler, ScreencopySessionData, ScreencopySessionDataExt,
+ ScreencopyState,
+ },
+ sctk::shm::{Shm, ShmHandler},
+ wayland_client::{
+ protocol::{
+ wl_buffer,
+ wl_shm::{self, WlShm},
+ wl_shm_pool,
+ },
+ Dispatch, Proxy,
+ },
+ },
+ iced_futures::futures,
+};
+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 wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
+
+#[derive(Default)]
+struct SessionInner {
+ buffer_infos: Option>,
+ res: Option>>,
+}
+
+// TODO: dmabuf? need to handle modifier negotation
+#[derive(Default)]
+struct Session {
+ condvar: Condvar,
+ inner: Mutex,
+}
+
+#[derive(Default)]
+struct SessionData {
+ session: Arc,
+ session_data: ScreencopySessionData,
+}
+
+impl Session {
+ pub fn for_session(
+ session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
+ ) -> Option<&Self> {
+ Some(&session.data::()?.session)
+ }
+
+ fn update(&self, f: F) {
+ f(&mut self.inner.lock().unwrap());
+ self.condvar.notify_all();
+ }
+
+ fn wait_while bool>(&self, mut f: F) -> MutexGuard {
+ self.condvar
+ .wait_while(self.inner.lock().unwrap(), |data| f(data))
+ .unwrap()
+ }
+}
+
+impl ScreencopySessionDataExt for SessionData {
+ fn screencopy_session_data(&self) -> &ScreencopySessionData {
+ &self.session_data
+ }
+}
+struct AppData {
+ exit: bool,
+ tx: UnboundedSender,
+ queue_handle: QueueHandle,
+ conn: Connection,
+ screencopy_state: ScreencopyState,
+ shm_state: Shm,
+ registry_state: RegistryState,
+ toplevel_info_state: ToplevelInfoState,
+ toplevel_manager_state: ToplevelManagerState,
+ seat_state: SeatState,
+}
+
+struct CaptureData {
+ qh: QueueHandle,
+ conn: Connection,
+ wl_shm: WlShm,
+ screencopy_manager: zcosmic_screencopy_manager_v1::ZcosmicScreencopyManagerV1,
+}
+
+impl CaptureData {
+ pub fn capture_source_shm_fd(
+ &self,
+ overlay_cursor: bool,
+ source: ZcosmicToplevelHandleV1,
+ fd: Fd,
+ len: Option,
+ ) -> Option> {
+ // XXX error type?
+ // TODO: way to get cursor metadata?
+
+ #[allow(unused_variables)] // TODO
+ let overlay_cursor = if overlay_cursor { 1 } else { 0 };
+
+ let session = Arc::new(Session::default());
+ let screencopy_session = self.screencopy_manager.capture_toplevel(
+ &source,
+ zcosmic_screencopy_manager_v1::CursorMode::Hidden, // XXX take into account adventised capabilities
+ &self.qh,
+ SessionData {
+ session: session.clone(),
+ session_data: Default::default(),
+ },
+ );
+ self.conn.flush().unwrap();
+
+ let buffer_infos = session
+ .wait_while(|data| data.buffer_infos.is_none())
+ .buffer_infos
+ .take()
+ .unwrap();
+
+ // XXX
+ let Some(buffer_info) = buffer_infos.iter().find(|x| {
+ x.type_ == WEnum::Value(zcosmic_screencopy_session_v1::BufferType::WlShm)
+ && x.format == wl_shm::Format::Abgr8888.into()
+ }) else {
+ tracing::error!("No suitable buffer format found");
+ tracing::warn!("Available formats: {:#?}", buffer_infos);
+ return None;
+ };
+
+ let buf_len = buffer_info.stride * buffer_info.height;
+ if let Some(len) = len {
+ if len != buf_len {
+ return None;
+ }
+ } else if let Err(_err) = rustix::fs::ftruncate(&fd, buf_len as _) {
+ };
+ let pool = self
+ .wl_shm
+ .create_pool(fd.as_fd(), buf_len as i32, &self.qh, ());
+ let buffer = pool.create_buffer(
+ 0,
+ buffer_info.width as i32,
+ buffer_info.height as i32,
+ buffer_info.stride as i32,
+ wl_shm::Format::Abgr8888,
+ &self.qh,
+ (),
+ );
+
+ screencopy_session.attach_buffer(&buffer, None, 0); // XXX age?
+ screencopy_session.commit(zcosmic_screencopy_session_v1::Options::empty());
+ self.conn.flush().unwrap();
+
+ // TODO: wait for server to release buffer?
+ let res = session
+ .wait_while(|data| data.res.is_none())
+ .res
+ .take()
+ .unwrap();
+ pool.destroy();
+ buffer.destroy();
+
+ //std::thread::sleep(std::time::Duration::from_millis(16));
+
+ if res.is_ok() {
+ Some(ShmImage {
+ fd,
+ width: buffer_info.width,
+ height: buffer_info.height,
+ })
+ } else {
+ None
+ }
+ }
+}
+
+pub struct ShmImage {
+ fd: T,
+ pub width: u32,
+ pub height: u32,
+}
+
+impl ShmImage {
+ pub fn image(&self) -> anyhow::Result {
+ let mmap = unsafe { memmap2::Mmap::map(&self.fd.as_fd())? };
+ image::RgbaImage::from_raw(self.width, self.height, mmap.to_vec())
+ .ok_or_else(|| anyhow::anyhow!("ShmImage had incorrect size"))
+ }
+}
+
+impl ProvidesRegistryState for AppData {
+ fn registry(&mut self) -> &mut RegistryState {
+ &mut self.registry_state
+ }
+
+ sctk::registry_handlers!();
+}
+
+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) {}
+}
+
+impl ToplevelManagerHandler for AppData {
+ fn toplevel_manager_state(&mut self) -> &mut cctk::toplevel_management::ToplevelManagerState {
+ &mut self.toplevel_manager_state
+ }
+
+ fn capabilities(
+ &mut self,
+ _: &Connection,
+ _: &QueueHandle,
+ _: Vec>,
+ ) {
+ // TODO capabilities could affect the options in the applet
+ }
+}
+impl AppData {
+ fn send_image(&self, handle: ZcosmicToplevelHandleV1) {
+ let tx = self.tx.clone();
+ let capure_data = CaptureData {
+ qh: self.queue_handle.clone(),
+ conn: self.conn.clone(),
+ wl_shm: self.shm_state.wl_shm().clone(),
+ screencopy_manager: self.screencopy_state.screencopy_manager.clone(),
+ };
+ std::thread::spawn(move || {
+ use std::ffi::CStr;
+ let name =
+ unsafe { CStr::from_bytes_with_nul_unchecked(b"minimize-applet-screencopy\0") };
+ let Ok(fd) = rustix::fs::memfd_create(name, rustix::fs::MemfdFlags::CLOEXEC) else {
+ tracing::error!("Failed to get fd for capture");
+ return;
+ };
+
+ // XXX is this going to use to much memory?
+ let img = capure_data.capture_source_shm_fd(false, handle.clone(), fd, None);
+ if let Some(img) = img {
+ let Ok(img) = img.image() else {
+ tracing::error!("Failed to get RgbaImage");
+ return;
+ };
+
+ // resize to 128x128
+ let max = img.width().max(img.height());
+ let ratio = max as f32 / 128.0;
+
+ let img = if ratio > 1.0 {
+ let new_width = (img.width() as f32 / ratio).round();
+ let new_height = (img.height() as f32 / ratio).round();
+
+ image::imageops::resize(
+ &img,
+ new_width as u32,
+ new_height as u32,
+ image::imageops::FilterType::Lanczos3,
+ )
+ } else {
+ img
+ };
+
+ if let Err(err) =
+ tx.unbounded_send(WaylandUpdate::Image(handle, WaylandImage::new(img)))
+ {
+ tracing::error!("Failed to send image event to subscription {err:?}");
+ };
+ } else {
+ tracing::error!("Failed to capture image");
+ }
+ });
+ }
+}
+
+impl ToplevelInfoHandler for AppData {
+ fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState {
+ &mut self.toplevel_info_state
+ }
+
+ fn new_toplevel(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
+ ) {
+ if let Some(info) = self.toplevel_info_state.info(toplevel) {
+ if info
+ .state
+ .contains(&zcosmic_toplevel_handle_v1::State::Minimized)
+ {
+ // spawn thread for sending the image
+ self.send_image(toplevel.clone());
+ let _ = self
+ .tx
+ .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Add(
+ toplevel.clone(),
+ info.clone(),
+ )));
+ } else {
+ let _ = self
+ .tx
+ .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
+ toplevel.clone(),
+ )));
+ }
+ }
+ }
+
+ fn update_toplevel(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
+ ) {
+ if let Some(info) = self.toplevel_info_state.info(toplevel) {
+ if info
+ .state
+ .contains(&zcosmic_toplevel_handle_v1::State::Minimized)
+ {
+ self.send_image(toplevel.clone());
+ let _ = self
+ .tx
+ .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Update(
+ toplevel.clone(),
+ info.clone(),
+ )));
+ } else {
+ let _ = self
+ .tx
+ .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
+ toplevel.clone(),
+ )));
+ }
+ }
+ }
+
+ fn toplevel_closed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
+ ) {
+ let _ = self
+ .tx
+ .unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
+ toplevel.clone(),
+ )));
+ }
+}
+
+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.clone(), 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(req) => match req {
+ 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);
+ }
+ }
+ },
+ },
+ calloop::channel::Event::Closed => {
+ state.exit = true;
+ }
+ })
+ .is_err()
+ {
+ return;
+ }
+ let registry_state = RegistryState::new(&globals);
+ let screencopy_state = ScreencopyState::new(&globals, &qh);
+ let shm_state = Shm::bind(&globals, &qh).expect("Failed to get shm state");
+
+ let mut app_data = AppData {
+ exit: false,
+ tx,
+ conn,
+ queue_handle: qh.clone(),
+ shm_state,
+ screencopy_state,
+ seat_state: SeatState::new(&globals, &qh),
+ toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh),
+ toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh),
+ registry_state,
+ };
+
+ loop {
+ if app_data.exit {
+ break;
+ }
+ event_loop.dispatch(None, &mut app_data).unwrap();
+ }
+}
+
+impl ShmHandler for AppData {
+ fn shm_state(&mut self) -> &mut Shm {
+ &mut self.shm_state
+ }
+}
+
+impl ScreencopyHandler for AppData {
+ fn screencopy_state(&mut self) -> &mut ScreencopyState {
+ &mut self.screencopy_state
+ }
+
+ fn init_done(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
+ buffer_infos: &[BufferInfo],
+ ) {
+ Session::for_session(session).unwrap().update(|data| {
+ data.buffer_infos = Some(buffer_infos.to_vec());
+ });
+ }
+
+ fn ready(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
+ ) {
+ Session::for_session(session).unwrap().update(|data| {
+ data.res = Some(Ok(()));
+ });
+ session.destroy();
+ }
+
+ fn failed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle,
+ session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
+ reason: WEnum,
+ ) {
+ // TODO send message to thread
+ Session::for_session(session).unwrap().update(|data| {
+ data.res = Some(Err(reason));
+ });
+ session.destroy();
+ }
+}
+
+impl Dispatch for AppData {
+ fn event(
+ _app_data: &mut Self,
+ _buffer: &wl_shm_pool::WlShmPool,
+ _event: wl_shm_pool::Event,
+ _: &(),
+ _: &Connection,
+ _qh: &QueueHandle,
+ ) {
+ }
+}
+
+impl Dispatch for AppData {
+ fn event(
+ _app_data: &mut Self,
+ _buffer: &wl_buffer::WlBuffer,
+ _event: wl_buffer::Event,
+ _: &(),
+ _: &Connection,
+ _qh: &QueueHandle,
+ ) {
+ }
+}
+
+sctk::delegate_shm!(AppData);
+sctk::delegate_seat!(AppData);
+sctk::delegate_registry!(AppData);
+cctk::delegate_toplevel_info!(AppData);
+cctk::delegate_toplevel_manager!(AppData);
+cctk::delegate_screencopy!(AppData, session: [SessionData]);
diff --git a/cosmic-applet-minimize/src/wayland_subscription.rs b/cosmic-applet-minimize/src/wayland_subscription.rs
new file mode 100644
index 00000000..2372a6ee
--- /dev/null
+++ b/cosmic-applet-minimize/src/wayland_subscription.rs
@@ -0,0 +1,121 @@
+//! # DBus interface proxy for: `org.freedesktop.UPower.KbdBacklight`
+//!
+//! 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;
+use cctk::toplevel_info::ToplevelInfo;
+use cosmic::cctk::cosmic_protocols;
+use cosmic::iced::subscription;
+use cosmic::iced_futures::futures;
+use cosmic::{cctk, iced};
+use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
+use futures::{
+ channel::mpsc::{unbounded, UnboundedReceiver},
+ SinkExt, StreamExt,
+};
+use image::EncodableLayout;
+use once_cell::sync::Lazy;
+use std::fmt::Debug;
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+use crate::wayland_handler::wayland_handler;
+
+pub static WAYLAND_RX: Lazy>>> =
+ Lazy::new(|| Mutex::new(None));
+
+pub fn wayland_subscription() -> iced::Subscription {
+ subscription::channel(
+ std::any::TypeId::of::(),
+ 50,
+ move |mut output| async move {
+ let mut state = State::Waiting;
+
+ loop {
+ state = start_listening(state, &mut output).await;
+ }
+ },
+ )
+}
+
+pub enum State {
+ Waiting,
+ Finished,
+}
+
+async fn start_listening(
+ state: State,
+ output: &mut futures::channel::mpsc::Sender,
+) -> State {
+ match state {
+ State::Waiting => {
+ let mut guard = WAYLAND_RX.lock().await;
+ let rx = {
+ if guard.is_none() {
+ let (calloop_tx, calloop_rx) = calloop::channel::channel();
+ let (toplevel_tx, toplevel_rx) = unbounded();
+ let _ = std::thread::spawn(move || {
+ wayland_handler(toplevel_tx, calloop_rx);
+ });
+ *guard = Some(toplevel_rx);
+ _ = output.send(WaylandUpdate::Init(calloop_tx)).await;
+ }
+ guard.as_mut().unwrap()
+ };
+ match rx.next().await {
+ Some(u) => {
+ _ = output.send(u).await;
+ State::Waiting
+ }
+ None => {
+ _ = output.send(WaylandUpdate::Finished).await;
+ tracing::error!("Wayland handler thread died");
+ State::Finished
+ }
+ }
+ }
+ State::Finished => iced::futures::future::pending().await,
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum WaylandUpdate {
+ Init(calloop::channel::Sender),
+ Finished,
+ Toplevel(ToplevelUpdate),
+ Image(ZcosmicToplevelHandleV1, WaylandImage),
+}
+
+#[derive(Debug, Clone)]
+pub struct WaylandImage {
+ pub img: Arc,
+}
+
+impl WaylandImage {
+ pub fn new(img: image::RgbaImage) -> Self {
+ Self { img: Arc::new(img) }
+ }
+}
+
+impl AsRef<[u8]> for WaylandImage {
+ fn as_ref(&self) -> &[u8] {
+ self.img.as_bytes()
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum ToplevelUpdate {
+ Add(ZcosmicToplevelHandleV1, ToplevelInfo),
+ Update(ZcosmicToplevelHandleV1, ToplevelInfo),
+ Remove(ZcosmicToplevelHandleV1),
+}
+
+#[derive(Clone, Debug)]
+pub enum WaylandRequest {
+ Toplevel(ToplevelRequest),
+}
+
+#[derive(Debug, Clone)]
+pub enum ToplevelRequest {
+ Activate(ZcosmicToplevelHandleV1),
+}
diff --git a/cosmic-applet-minimize/src/window_image.rs b/cosmic-applet-minimize/src/window_image.rs
new file mode 100644
index 00000000..d9d9f652
--- /dev/null
+++ b/cosmic-applet-minimize/src/window_image.rs
@@ -0,0 +1,289 @@
+use cosmic::{
+ desktop::IconSource,
+ iced::Limits,
+ iced_core::{layout, overlay, widget::Tree, Border, Layout, Length, Size, Vector},
+ theme::{Button, Container},
+ widget::{button, container, image::Handle, Image, Widget},
+ Element,
+};
+
+use crate::wayland_subscription::WaylandImage;
+
+pub struct WindowImage<'a, Msg> {
+ image_button: Element<'a, Msg>,
+ icon: Element<'a, Msg>,
+}
+
+impl<'a, Msg> WindowImage<'a, Msg>
+where
+ Msg: 'static + Clone,
+{
+ pub fn new(
+ img: Option,
+ icon: &IconSource,
+ size: f32,
+ on_press: Msg,
+ padding: u16,
+ ) -> Self {
+ let border = 1.0;
+ Self {
+ image_button: button(
+ container(
+ container(if let Some(img) = img {
+ let max_dim = img.img.width().max(img.img.height()).max(1);
+ let ratio = max_dim as f32 / (size - border * 2.0).max(1.0);
+ let adjusted_width = img.img.width() as f32 / ratio;
+ let adjusted_height = img.img.height() as f32 / ratio;
+
+ Element::from(
+ Image::new(Handle::from_pixels(
+ img.img.width(),
+ img.img.height(),
+ img.clone(),
+ ))
+ .width(Length::Fixed(adjusted_width))
+ .height(Length::Fixed(adjusted_height))
+ .content_fit(cosmic::iced_core::ContentFit::Contain),
+ )
+ } else {
+ Element::from(
+ icon.as_cosmic_icon()
+ .width(Length::Fixed(size))
+ .height(Length::Fixed(size)),
+ )
+ })
+ .style(Container::Custom(Box::new(move |theme| {
+ container::Appearance {
+ border: Border {
+ color: theme.cosmic().bg_divider().into(),
+ width: border,
+ radius: 0.0.into(),
+ },
+ ..Default::default()
+ }
+ })))
+ .padding(border as u16)
+ .height(Length::Shrink)
+ .width(Length::Shrink),
+ )
+ .align_x(cosmic::iced_core::alignment::Horizontal::Center)
+ .align_y(cosmic::iced_core::alignment::Vertical::Center)
+ .height(Length::Fixed(size))
+ .width(Length::Fixed(size)),
+ )
+ .on_press(on_press)
+ .width(Length::Shrink)
+ .height(Length::Shrink)
+ .style(Button::AppletIcon)
+ .padding(padding)
+ .into(),
+ icon: icon
+ .as_cosmic_icon()
+ .width(Length::Fixed(size / 3.0))
+ .height(Length::Fixed(size / 3.0))
+ .into(),
+ }
+ }
+}
+
+impl<'a, Msg> Widget for WindowImage<'a, Msg> {
+ fn children(&self) -> Vec {
+ vec![Tree::new(&self.image_button), Tree::new(&self.icon)]
+ }
+
+ fn diff(&mut self, tree: &mut cosmic::iced_core::widget::Tree) {
+ tree.diff_children(&mut [&mut self.image_button, &mut self.icon])
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ state: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &cosmic::Renderer,
+ ) -> Option> {
+ let children = [&mut self.image_button, &mut self.icon]
+ .into_iter()
+ .zip(&mut state.children)
+ .zip(layout.children())
+ .filter_map(|((child, state), layout)| {
+ child.as_widget_mut().overlay(state, layout, renderer)
+ })
+ .collect::>();
+
+ (!children.is_empty()).then(|| overlay::Group::with_children(children).overlay())
+ }
+
+ fn size(&self) -> Size {
+ Size::new(Length::Shrink, Length::Shrink)
+ }
+
+ fn layout(
+ &self,
+ tree: &mut cosmic::iced_core::widget::Tree,
+ renderer: &cosmic::Renderer,
+ limits: &cosmic::iced_core::layout::Limits,
+ ) -> cosmic::iced_core::layout::Node {
+ let children = &mut tree.children;
+ let button = &mut children[0];
+ let button_node = self
+ .image_button
+ .as_widget()
+ .layout(button, renderer, limits);
+
+ let button_bounds = button_node.size();
+ let icon_width = button_bounds.width / 3.0;
+ let icon_height = button_bounds.height / 3.0;
+ let icon = &mut children[1];
+ let icon_node = self
+ .icon
+ .as_widget()
+ .layout(
+ icon,
+ renderer,
+ &Limits::NONE.width(icon_width).height(icon_height),
+ )
+ .translate(Vector::new(2. * icon_width, 2. * icon_height));
+
+ layout::Node::with_children(
+ limits.resolve(Length::Shrink, Length::Shrink, button_node.size()),
+ vec![button_node, icon_node],
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &cosmic::iced_core::widget::Tree,
+ renderer: &mut cosmic::Renderer,
+ theme: &cosmic::Theme,
+ style: &cosmic::iced_core::renderer::Style,
+ layout: cosmic::iced_core::Layout<'_>,
+ cursor: cosmic::iced_core::mouse::Cursor,
+ viewport: &cosmic::iced_core::Rectangle,
+ ) {
+ let children = &[&self.image_button, &self.icon];
+ // draw children in order
+ for (i, (layout, child)) in layout.children().zip(children).enumerate() {
+ let tree = &tree.children[i];
+ child
+ .as_widget()
+ .draw(tree, renderer, theme, style, layout, cursor, viewport);
+ }
+ }
+
+ fn size_hint(&self) -> Size {
+ self.size()
+ }
+
+ fn tag(&self) -> cosmic::iced_core::widget::tree::Tag {
+ cosmic::iced_core::widget::tree::Tag::stateless()
+ }
+
+ fn state(&self) -> cosmic::iced_core::widget::tree::State {
+ cosmic::iced_core::widget::tree::State::None
+ }
+
+ fn operate(
+ &self,
+ tree: &mut cosmic::iced_core::widget::Tree,
+ layout: cosmic::iced_core::Layout<'_>,
+ renderer: &cosmic::Renderer,
+ operation: &mut dyn cosmic::widget::Operation<
+ cosmic::iced_core::widget::OperationOutputWrapper,
+ >,
+ ) {
+ let layout = layout.children().collect::>();
+ let children = [&self.image_button, &self.icon];
+ for (i, (layout, child)) in layout
+ .into_iter()
+ .zip(children.into_iter())
+ .enumerate()
+ .rev()
+ {
+ let tree = &mut tree.children[i];
+ child.as_widget().operate(tree, layout, renderer, operation);
+ }
+ }
+
+ fn on_event(
+ &mut self,
+ state: &mut cosmic::iced_core::widget::Tree,
+ event: cosmic::iced_core::Event,
+ layout: cosmic::iced_core::Layout<'_>,
+ cursor: cosmic::iced_core::mouse::Cursor,
+ renderer: &cosmic::Renderer,
+ clipboard: &mut dyn cosmic::iced_core::Clipboard,
+ shell: &mut cosmic::iced_core::Shell<'_, Msg>,
+ viewport: &cosmic::iced_core::Rectangle,
+ ) -> cosmic::iced_core::event::Status {
+ let children = [&mut self.image_button, &mut self.icon];
+
+ let layout = layout.children().collect::>();
+ // draw children in order
+ let mut status = cosmic::iced_core::event::Status::Ignored;
+ for (i, (layout, child)) in layout
+ .into_iter()
+ .zip(children.into_iter())
+ .enumerate()
+ .rev()
+ {
+ let tree = &mut state.children[i];
+
+ status = child.as_widget_mut().on_event(
+ tree,
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ );
+ if matches!(status, cosmic::iced_core::event::Status::Captured) {
+ return status;
+ }
+ }
+ status
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &cosmic::iced_core::widget::Tree,
+ layout: cosmic::iced_core::Layout<'_>,
+ cursor: cosmic::iced_core::mouse::Cursor,
+ viewport: &cosmic::iced_core::Rectangle,
+ renderer: &cosmic::Renderer,
+ ) -> cosmic::iced_core::mouse::Interaction {
+ let children = [&self.image_button, &self.icon];
+ let layout = layout.children().collect::>();
+ for (i, (layout, child)) in layout
+ .into_iter()
+ .zip(children.into_iter())
+ .enumerate()
+ .rev()
+ {
+ let tree = &state.children[i];
+ let interaction = child
+ .as_widget()
+ .mouse_interaction(tree, layout, cursor, viewport, renderer);
+ if cursor.is_over(layout.bounds()) {
+ return interaction;
+ }
+ }
+ cosmic::iced_core::mouse::Interaction::Idle
+ }
+
+ fn id(&self) -> Option {
+ None
+ }
+
+ fn set_id(&mut self, _id: cosmic::widget::Id) {}
+}
+
+impl<'a, Message> From> for cosmic::Element<'a, Message>
+where
+ Message: 'static + Clone,
+{
+ fn from(w: WindowImage<'a, Message>) -> cosmic::Element<'a, Message> {
+ Element::new(w)
+ }
+}
diff --git a/cosmic-applet-network/Cargo.toml b/cosmic-applet-network/Cargo.toml
index 3378556f..3cd47e01 100644
--- a/cosmic-applet-network/Cargo.toml
+++ b/cosmic-applet-network/Cargo.toml
@@ -18,7 +18,7 @@ tracing-log.workspace = true
itertools = "0.10.3"
slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] }
-anyhow = "1.0"
+anyhow.workspace = true
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"
diff --git a/cosmic-applet-notifications/Cargo.toml b/cosmic-applet-notifications/Cargo.toml
index 3c629b3e..27b3f83f 100644
--- a/cosmic-applet-notifications/Cargo.toml
+++ b/cosmic-applet-notifications/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
-anyhow = "1"
+anyhow.workspace = true
libcosmic.workspace = true
cosmic-time.workspace = true
nix = "0.26"
diff --git a/cosmic-applet-workspaces/Cargo.toml b/cosmic-applet-workspaces/Cargo.toml
index 1de199af..18c3d72f 100644
--- a/cosmic-applet-workspaces/Cargo.toml
+++ b/cosmic-applet-workspaces/Cargo.toml
@@ -15,7 +15,7 @@ tracing-log.workspace = true
once_cell = "1.9"
futures = "0.3.21"
xdg = "2.4.0"
-anyhow = "1.0"
+anyhow.workspace = true
tokio = "1.35"
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
diff --git a/justfile b/justfile
index 80c0e82c..04a4ac3c 100644
--- a/justfile
+++ b/justfile
@@ -39,6 +39,7 @@ _install_app_list: (_install 'com.system76.CosmicAppList' 'cosmic-app-list')
_install_audio: (_install 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio')
_install_battery: (_install 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery')
_install_bluetooth: (_install 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth')
+_install_minimize: (_install 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize')
_install_network: (_install 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network')
_install_notifications: (_install 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications')
_install_power: (_install 'com.system76.CosmicAppletPower' 'cosmic-applet-power')
@@ -54,7 +55,7 @@ _install_app_button: (_install_button 'com.system76.CosmicPanelAppButton' 'cosmi
_install_workspaces_button: (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button')
# Installs files into the system
-install: _install_app_list _install_audio _install_battery _install_bluetooth _install_network _install_notifications _install_power _install_workspace _install_time _install_tiling _install_panel_button _install_app_button _install_workspaces_button _install_status_area
+install: _install_app_list _install_audio _install_battery _install_bluetooth _install_minimize _install_network _install_notifications _install_power _install_workspace _install_time _install_tiling _install_panel_button _install_app_button _install_workspaces_button _install_status_area
# Extracts vendored dependencies if vendor=1
_extract_vendor: