Move Cosmic Applets into new Dir & remove old applets

This commit is contained in:
13r0ck 2022-12-22 19:56:42 -07:00 committed by Ashley Wulber
parent 813e6c0aff
commit a682b8deb0
134 changed files with 0 additions and 1354 deletions

613
cosmic-app-list/src/app.rs Normal file
View file

@ -0,0 +1,613 @@
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::PathBuf;
use crate::config;
use crate::config::AppListConfig;
use crate::fl;
use crate::toplevel_subscription::toplevel_subscription;
use crate::toplevel_subscription::ToplevelRequest;
use crate::toplevel_subscription::ToplevelUpdate;
use calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cosmic::applet::cosmic_panel_config::PanelAnchor;
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced;
use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_popup;
use cosmic::iced::wayland::SurfaceIdWrapper;
use cosmic::iced::widget::mouse_listener;
use cosmic::iced::widget::{column, row};
use cosmic::iced::{executor, window, Application, Command, Subscription};
use cosmic::iced_native::alignment::Horizontal;
use cosmic::iced_native::subscription::events_with;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription;
use cosmic::widget::rectangle_tracker::RectangleTracker;
use cosmic::widget::rectangle_tracker::RectangleUpdate;
use cosmic::widget::{horizontal_rule, vertical_rule};
use cosmic::{Element, Theme};
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry;
use iced::wayland::window::resize_window;
use iced::widget::container;
use iced::widget::horizontal_space;
use iced::widget::svg;
use iced::widget::Image;
use iced::Alignment;
use iced::Background;
use iced::Length;
use itertools::Itertools;
pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default();
CosmicAppList::run(helper.window_settings())
}
#[derive(Debug, Clone, Default)]
struct Toplevel {
id: u32,
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
desktop_info: DesktopInfo,
popup: Option<window::Id>,
}
#[derive(Clone, Default)]
struct CosmicAppList {
theme: Theme,
popup: Option<window::Id>,
surface_id_ctr: u32,
subscription_ctr: u32,
toplevel_ctr: u32,
toplevel_list: Vec<Toplevel>,
config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>,
applet_helper: CosmicAppletHelper,
seat: Option<WlSeat>,
rectangle_tracker: Option<RectangleTracker<u32>>,
rectangles: HashMap<u32, iced::Rectangle>,
}
impl CosmicAppList {
fn window_size(&self) -> (u32, u32) {
let pixel_size = self.applet_helper.suggested_size().0;
let padding = 8;
let dot_size = 4;
let spacing = 4;
let mut length = self
.toplevel_list
.iter()
.map(|t| {
(pixel_size + 2 * padding).max((dot_size + spacing) * t.toplevels.len() as u16)
as u32
+ spacing as u32
})
.sum();
length += spacing as u32 * 2 + 2;
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
}
}
}
// TODO DnD after sctk merges DnD
#[derive(Debug, Clone)]
enum Message {
Toplevel(ToplevelUpdate),
Favorite(String),
UnFavorite(String),
Popup(String),
ClosePopup,
Activate(ZcosmicToplevelHandleV1),
Exec(String),
Quit(String),
Errored(String),
Ignore,
NewSeat(WlSeat),
RemovedSeat(WlSeat),
Rectangle(RectangleUpdate<u32>),
}
#[derive(Debug, Clone, Default)]
struct DesktopInfo {
id: String,
icon: PathBuf,
exec: String,
name: String,
}
fn desktop_info_for_app_ids(mut app_ids: Vec<String>) -> Vec<DesktopInfo> {
let mut ret = freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.filter_map(|path| {
std::fs::read_to_string(&path).ok().and_then(|input| {
DesktopEntry::decode(&path, &input).ok().and_then(|de| {
if let Some(i) = app_ids
.iter()
.position(|s| s == de.appid || s.eq(&de.name(None).unwrap_or_default()))
{
let id = app_ids.remove(i);
freedesktop_icons::lookup(de.icon().unwrap_or(de.appid))
.with_size(128)
.with_cache()
.find()
.map(|buf| DesktopInfo {
id,
icon: buf,
exec: de.exec().unwrap_or_default().to_string(),
name: de.name(None).unwrap_or_default().to_string(),
})
} else {
None
}
})
})
})
.collect_vec();
ret.append(
&mut app_ids
.into_iter()
.map(|id| DesktopInfo {
id,
..Default::default()
})
.collect_vec(),
);
ret
}
impl Application for CosmicAppList {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default();
let mut toplevel_ctr = 0;
let self_ = CosmicAppList {
toplevel_list: desktop_info_for_app_ids(config.favorites.clone())
.into_iter()
.map(|e| {
toplevel_ctr += 1;
Toplevel {
id: toplevel_ctr,
toplevels: Default::default(),
desktop_info: e,
popup: None,
}
})
.collect(),
config,
toplevel_ctr,
..Default::default()
};
let (w, h) = self_.window_size();
(self_, resize_window(window::Id::new(0), w, h))
}
fn title(&self) -> String {
config::APP_ID.to_string()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Errored(_) => {
// TODO log errors
}
Message::Popup(id) => {
if let Some(toplevel_group) = self
.toplevel_list
.iter_mut()
.find(|t| t.desktop_info.id == id)
{
if let Some(p) = self.popup.take() {
toplevel_group.popup.take();
return destroy_popup(p);
}
let rectangle = match self.rectangles.get(&toplevel_group.id) {
Some(r) => r,
None => return Command::none(),
};
self.surface_id_ctr += 1;
let new_id = window::Id::new(self.surface_id_ctr);
self.popup.replace(new_id);
toplevel_group.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
new_id,
(240, 240 + toplevel_group.toplevels.len() as u32 * 24),
None,
None,
);
let iced::Rectangle {
x,
y,
width,
height,
} = *rectangle;
popup_settings.positioner.anchor_rect = iced::Rectangle::<i32> {
x: x as i32,
y: y as i32,
width: width as i32,
height: height as i32,
};
return get_popup(popup_settings);
}
}
Message::Favorite(id) => {
let _ = self.config.add_favorite(id);
}
Message::UnFavorite(id) => {
let _ = self.config.remove_favorite(id);
self.toplevel_list.retain(|t| {
self.config.favorites.contains(&t.desktop_info.id)
|| self.config.favorites.contains(&t.desktop_info.name)
})
}
Message::Activate(handle) => {
if let (Some(tx), Some(seat)) = (self.toplevel_sender.as_ref(), self.seat.as_ref())
{
let _ = tx.send(ToplevelRequest::Activate(handle, seat.clone()));
}
}
Message::Quit(id) => {
if let Some(toplevel_group) =
self.toplevel_list.iter().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()));
}
}
}
}
Message::Toplevel(event) => {
match event {
ToplevelUpdate::AddToplevel(handle, info) => {
if info.app_id == "" {
return Command::none();
}
if let Some(i) = self.toplevel_list.iter().position(
|Toplevel { desktop_info, .. }| &desktop_info.id == &info.app_id,
) {
self.toplevel_list[i].toplevels.push((handle, info));
} else {
let desktop_info =
desktop_info_for_app_ids(vec![info.app_id.clone()]).remove(0);
self.toplevel_ctr += 1;
self.toplevel_list.push(Toplevel {
id: self.toplevel_ctr,
toplevels: vec![(handle, info)],
desktop_info,
popup: None,
});
let (w, h) = self.window_size();
return resize_window(window::Id::new(0), w, h);
}
}
ToplevelUpdate::Init(tx) => {
self.toplevel_sender.replace(tx);
}
ToplevelUpdate::Finished => {
self.subscription_ctr += 1;
for t in &mut self.toplevel_list {
t.toplevels.clear();
}
}
ToplevelUpdate::RemoveToplevel(handle) => {
if let Some(i) = self.toplevel_list.iter_mut().position(
|Toplevel {
toplevels,
desktop_info,
..
}| {
if let Some(ret) = toplevels.iter().position(|t| &t.0 == &handle) {
toplevels.remove(ret);
toplevels.is_empty()
&& !self.config.favorites.contains(&desktop_info.id)
&& !self.config.favorites.contains(&desktop_info.name)
} else {
false
}
},
) {
self.toplevel_list.remove(i);
}
let (w, h) = self.window_size();
return resize_window(window::Id::new(0), w, h);
}
ToplevelUpdate::UpdateToplevel(handle, info) => {
// TODO probably want to make sure it is removed
if info.app_id == "" {
return Command::none();
}
'toplevel_loop: for toplevel_list in &mut self.toplevel_list {
for (t_handle, t_info) in &mut toplevel_list.toplevels {
if &handle == t_handle {
*t_info = info;
break 'toplevel_loop;
}
}
}
let (w, h) = self.window_size();
return resize_window(window::Id::new(0), w, h);
}
}
}
Message::NewSeat(s) => {
self.seat.replace(s);
}
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);
}
}
let _ = cmd.spawn();
}
Message::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => {
self.rectangles.insert(r.0, r.1);
}
RectangleUpdate::Init(tracker) => {
self.rectangle_tracker.replace(tracker);
}
},
Message::Ignore => {}
Message::ClosePopup => {
if let Some(p) = self.popup.take() {
if let Some(toplevel_group) =
self.toplevel_list.iter_mut().find(|t| t.popup == Some(p))
{
toplevel_group.popup.take();
}
return destroy_popup(p);
}
}
}
Command::none()
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => {
let (favorites, running) = self.toplevel_list.iter().fold(
(Vec::new(), Vec::new()),
|(mut favorites, mut running),
Toplevel {
id,
toplevels,
desktop_info,
..
}| {
let icon = if desktop_info.icon.extension() == Some(&OsStr::new("svg")) {
let handle = svg::Handle::from_path(&desktop_info.icon);
svg::Svg::new(handle)
.width(Length::Units(self.applet_helper.suggested_size().0))
.height(Length::Units(self.applet_helper.suggested_size().0))
.into()
} else {
Image::new(&desktop_info.icon)
.width(Length::Units(self.applet_helper.suggested_size().0))
.height(Length::Units(self.applet_helper.suggested_size().0))
.into()
};
let dot_radius = 2;
let dots = (0..toplevels.len())
.into_iter()
.map(|_| {
container(horizontal_space(Length::Units(0)))
.padding(dot_radius)
.style(<Self::Theme as container::StyleSheet>::Style::Custom(
|theme| container::Appearance {
text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color(
theme.cosmic().on_bg_color().into(),
)),
border_radius: 4.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
))
.into()
})
.collect_vec();
let icon_wrapper = match &self.applet_helper.anchor {
PanelAnchor::Left => row(vec![column(dots).spacing(4).into(), icon])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Right => row(vec![icon, column(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Top => column(vec![row(dots).spacing(4).into(), icon])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Bottom => column(vec![icon, row(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
};
let mut icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper])
.padding(8);
if self.popup.is_none() {
icon_button = icon_button.on_press(
toplevels
.first()
.map(|t| Message::Activate(t.0.clone()))
.unwrap_or_else(|| Message::Exec(desktop_info.exec.clone())),
);
}
// TODO tooltip on hover
let icon_button = mouse_listener(icon_button.width(Length::Shrink).height(Length::Shrink))
.on_right_release(Message::Popup(desktop_info.id.clone()));
let icon_button = if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(*id, icon_button).into()
} else {
icon_button.into()
};
if self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name)
{
favorites.push(icon_button)
} else {
running.push(icon_button);
}
(favorites, running)
},
);
let content = match &self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => container(
column![column(favorites), horizontal_rule(1), column(running)]
.spacing(4)
.align_items(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill),
),
PanelAnchor::Top | PanelAnchor::Bottom => container(
row![row(favorites), vertical_rule(1), row(running)]
.spacing(4)
.align_items(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill),
).height(Length::Fill).width(Length::Fill),
};
if self.popup.is_some() {
mouse_listener(content)
.on_right_press(Message::ClosePopup)
.on_press(Message::ClosePopup)
.into()
} else {
content.into()
}
}
SurfaceIdWrapper::Popup(p) => {
if let Some(Toplevel {
toplevels,
desktop_info,
..
}) = self.toplevel_list.iter().find(|t| t.popup == Some(p))
{
let is_favorite = self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name);
let mut content = column![
iced::widget::text(&desktop_info.name)
.horizontal_alignment(Horizontal::Center),
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("new-window")).into()])
.on_press(Message::Exec(desktop_info.exec.clone())),
]
.padding(8)
.spacing(4)
.align_items(Alignment::Center);
if !toplevels.is_empty() {
let mut list_col = column![];
for (handle, info) in toplevels {
let title = if info.title.len() > 20 {
format!("{:.24}...", &info.title)
} else {
info.title.clone()
};
list_col = list_col.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(title).into()])
.on_press(Message::Activate(handle.clone())),
);
}
content = content.push(horizontal_rule(1));
content = content.push(list_col);
content = content.push(horizontal_rule(1));
}
content = content.push(if is_favorite {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("unfavorite")).into()])
.on_press(Message::UnFavorite(desktop_info.id.clone()))
} else {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("favorite")).into()])
.on_press(Message::Favorite(desktop_info.id.clone()))
});
if toplevels.len() == 1 {
content = content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("quit")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
)
} else if toplevels.len() > 1 {
content = content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(&fl!("quit-all")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
)
}
return self.applet_helper.popup_container(content).into();
}
return horizontal_space(Length::Units(1)).into();
}
}
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event)),
events_with(|e, _| match e {
cosmic::iced_native::Event::PlatformSpecific(
cosmic::iced_native::event::PlatformSpecific::Wayland(
cosmic::iced_native::event::wayland::Event::Seat(e, seat),
),
) => match e {
cosmic::iced_native::event::wayland::SeatEvent::Enter => {
Some(Message::NewSeat(seat))
}
cosmic::iced_native::event::wayland::SeatEvent::Leave => {
Some(Message::RemovedSeat(seat))
}
},
_ => None,
}),
rectangle_tracker_subscription(0).map(|(_, update)| Message::Rectangle(update)),
])
}
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}
}

View file

@ -0,0 +1,4 @@
(
filter_top_levels: None,
favorites: [],
)

View file

@ -0,0 +1,67 @@
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::fs::File;
use std::path::PathBuf;
use xdg::BaseDirectories;
pub const APP_ID: &str = "com.system76.CosmicAppList";
pub const VERSION: &str = "0.1.0";
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub enum TopLevelFilter {
#[default]
ActiveWorkspace,
ConfiguredOutput,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AppListConfig {
pub filter_top_levels: Option<TopLevelFilter>,
pub favorites: Vec<String>,
}
impl AppListConfig {
// TODO async?
/// load config with the provided name
pub fn load() -> anyhow::Result<AppListConfig> {
let mut relative_path = PathBuf::from(APP_ID);
relative_path.push("config.ron");
let file = match BaseDirectories::new()
.ok()
.and_then(|dirs| dirs.find_config_file(relative_path))
.and_then(|p| File::open(p).ok())
{
Some(path) => path,
_ => {
anyhow::bail!("Failed to load config");
}
};
ron::de::from_reader::<_, AppListConfig>(file)
.map_err(|err| anyhow!("Failed to parse config file: {}", err))
}
pub fn add_favorite(&mut self, id: String) -> anyhow::Result<()> {
if !self.favorites.contains(&id) {
self.favorites.push(id);
}
self.save()
}
pub fn remove_favorite(&mut self, id: String) -> anyhow::Result<()> {
self.favorites.retain(|e| e != &id);
self.save()
}
// TODO async?
pub fn save(&self) -> anyhow::Result<()> {
let bd = BaseDirectories::new()?;
let mut relative_path = PathBuf::from(APP_ID);
relative_path.push("config.ron");
let config_path = bd.place_config_file(relative_path)?;
let f = File::create(config_path)?;
ron::ser::to_writer_pretty(f, self, Default::default())?;
Ok(())
}
}

View file

@ -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<FluentLanguageLoader> = 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<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}
pub fn localize() {
let localizer = crate::localize::localizer();
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for App List {}", error);
}
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MPL-2.0-only
mod app;
mod config;
mod localize;
mod toplevel_handler;
mod toplevel_subscription;
use log::info;
use localize::localize;
use crate::config::{APP_ID, VERSION};
fn main() -> cosmic::iced::Result {
// Initialize logger
pretty_env_logger::init();
info!("Iced Workspaces Applet ({})", APP_ID);
info!("Version: {}", VERSION);
// Prepare i18n
localize();
app::run()
}

View file

@ -0,0 +1,187 @@
use crate::toplevel_subscription::{ToplevelRequest, ToplevelUpdate};
use cctk::{
sctk::{
self,
event_loop::WaylandSource,
reexports::client::protocol::wl_seat::WlSeat,
seat::{SeatHandler, SeatState},
},
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
toplevel_management::{ToplevelManagerHandler, ToplevelManagerState},
wayland_client::{self, 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 wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
struct AppData {
exit: bool,
tx: UnboundedSender<ToplevelUpdate>,
registry_state: RegistryState,
toplevel_info_state: ToplevelInfoState,
toplevel_manager_state: ToplevelManagerState,
seat_state: SeatState,
}
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<Self>, _: WlSeat) {}
fn new_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: WlSeat,
_: sctk::seat::Capability,
) {
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: WlSeat,
_: sctk::seat::Capability,
) {
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: 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<Self>,
_: Vec<WEnum<zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1>>,
) {
// TODO capabilities could affect the options in the applet
}
}
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<Self>,
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) {
if let Some(info) = self.toplevel_info_state.info(toplevel) {
let _ = self
.tx
.unbounded_send(ToplevelUpdate::AddToplevel(toplevel.clone(), info.clone()));
}
}
fn update_toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
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(),
));
}
}
fn toplevel_closed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) {
let _ = self
.tx
.unbounded_send(ToplevelUpdate::RemoveToplevel(toplevel.clone()));
}
}
pub(crate) fn toplevel_handler(
tx: UnboundedSender<ToplevelUpdate>,
rx: calloop::channel::Channel<ToplevelRequest>,
) {
let conn = Connection::connect_to_env().unwrap();
let (globals, event_queue) = registry_queue_init(&conn).unwrap();
let mut event_loop = calloop::EventLoop::<AppData>::try_new().unwrap();
let qh = event_queue.handle();
let wayland_source = WaylandSource::new(event_queue).unwrap();
let handle = event_loop.handle();
if handle
.insert_source(wayland_source, |_, q, state| q.dispatch_pending(state))
.is_err()
{
return;
};
if handle
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(req) => match req {
ToplevelRequest::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;
}
},
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),
toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh),
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
registry_state,
};
loop {
if app_data.exit {
break;
}
event_loop.dispatch(None, &mut app_data).unwrap();
}
}
sctk::delegate_seat!(AppData);
sctk::delegate_registry!(AppData);
cctk::delegate_toplevel_info!(AppData);
cctk::delegate_toplevel_manager!(AppData);

View file

@ -0,0 +1,71 @@
//! # 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::client::protocol::wl_seat::WlSeat;
use cctk::toplevel_info::ToplevelInfo;
use cosmic::iced;
use cosmic::iced::subscription;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
StreamExt,
};
use std::{fmt::Debug, hash::Hash};
use crate::toplevel_handler::toplevel_handler;
pub fn toplevel_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, ToplevelUpdate)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
}
pub enum State {
Ready,
Waiting(
UnboundedReceiver<ToplevelUpdate>,
calloop::channel::Sender<ToplevelRequest>,
),
Finished,
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, ToplevelUpdate)>, State) {
match state {
State::Ready => {
let (calloop_tx, calloop_rx) = calloop::channel::channel();
let (toplevel_tx, toplevel_rx) = unbounded();
std::thread::spawn(move || {
toplevel_handler(toplevel_tx, calloop_rx);
});
return (
Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
State::Waiting(toplevel_rx, calloop_tx),
);
}
State::Waiting(mut rx, tx) => match rx.next().await {
Some(u) => (Some((id, u)), State::Waiting(rx, tx)),
None => {
let _ = tx.send(ToplevelRequest::Exit);
(Some((id, ToplevelUpdate::Finished)), State::Finished)
}
},
State::Finished => iced::futures::future::pending().await,
}
}
#[derive(Clone, Debug)]
pub enum ToplevelUpdate {
Finished,
AddToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
UpdateToplevel(ZcosmicToplevelHandleV1, ToplevelInfo),
RemoveToplevel(ZcosmicToplevelHandleV1),
Init(calloop::channel::Sender<ToplevelRequest>),
}
#[derive(Debug, Clone)]
pub enum ToplevelRequest {
Activate(ZcosmicToplevelHandleV1, WlSeat),
Quit(ZcosmicToplevelHandleV1),
Exit,
}

View file

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MPL-2.0-only
use std::path::PathBuf;
use gtk4::glib;
use std::future::Future;
use crate::wayland::Toplevel;
pub const DEST: &str = "com.System76.PopShell";
pub const PATH: &str = "/com/System76/PopShell";
#[derive(Debug)]
pub enum AppListEvent {
WindowList(Vec<Toplevel>),
Add(Toplevel),
Remove(Toplevel),
Favorite((String, bool)),
Refresh,
}
#[derive(Clone, Debug, Default, glib::Boxed)]
#[boxed_type(name = "BoxedWindowList")]
pub struct BoxedWindowList(pub Vec<Toplevel>);
pub fn data_path() -> PathBuf {
let mut path = glib::user_data_dir();
path.push(crate::ID);
std::fs::create_dir_all(&path).expect("Could not create directory.");
path.push("data.json");
path
}
pub fn thread_context() -> glib::MainContext {
glib::MainContext::thread_default().unwrap_or_else(|| {
let ctx = glib::MainContext::new();
ctx
})
}
pub fn block_on<F>(future: F) -> F::Output
where
F: Future,
{
let ctx = thread_context();
ctx.with_thread_default(|| ctx.block_on(future)).unwrap()
}