feat: app list is displayed and updated

This commit is contained in:
Ashley Wulber 2022-12-12 19:48:31 -05:00 committed by Ashley Wulber
parent ceff811072
commit 8223c76361
17 changed files with 4317 additions and 1256 deletions

97
Cargo.lock generated
View file

@ -230,19 +230,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "calloop"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a22a6a8f622f797120d452c630b0ab12e1331a1a753e2039ce7868d4ac77b4ee"
dependencies = [
"log",
"nix 0.24.2",
"slotmap",
"thiserror",
"vec_map",
]
[[package]]
name = "cascade"
version = "1.0.0"
@ -300,39 +287,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cosmic-app-list"
version = "0.1.0"
dependencies = [
"anyhow",
"calloop",
"cascade",
"cosmic-panel-config",
"cosmic-protocols",
"futures",
"futures-util",
"gio",
"glib-build-tools",
"gsk4",
"gtk4",
"i18n-embed",
"i18n-embed-fl",
"libadwaita",
"libcosmic",
"log",
"nix 0.25.0",
"once_cell",
"pretty_env_logger",
"relm4-macros",
"ron 0.8.0",
"rust-embed",
"serde",
"serde_json",
"wayland-backend",
"wayland-client",
"xdg",
]
[[package]]
name = "cosmic-applet-notifications"
version = "0.1.0"
@ -420,18 +374,6 @@ dependencies = [
"xdg-shell-wrapper-config",
]
[[package]]
name = "cosmic-protocols"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-protocols#3ff11df30ef551e1ccbdcb091930fe0d72266195"
dependencies = [
"bitflags",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "cpufeatures"
version = "0.2.2"
@ -1148,12 +1090,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "itoa"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "js-sys"
version = "0.3.59"
@ -1331,7 +1267,6 @@ dependencies = [
"cfg-if",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
@ -1806,12 +1741,6 @@ dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "same-file"
version = "1.0.6"
@ -1877,17 +1806,6 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.9"
@ -1948,15 +1866,6 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
[[package]]
name = "slotmap"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.9.0"
@ -2166,12 +2075,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version-compare"
version = "0.1.0"

View file

@ -2,7 +2,6 @@
members = [
"applets/cosmic-applet-notifications",
"applets/cosmic-applet-status-area",
"applets/cosmic-app-list",
"applets/cosmic-panel-button",
"libcosmic-applet",
]
@ -14,6 +13,7 @@ exclude = [
"applets/cosmic-applet-audio",
"applets/cosmic-applet-power",
"applets/cosmic-applet-time",
"applets/cosmic-app-list",
]
[patch.crates-io]

3677
applets/cosmic-app-list/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,33 +5,33 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"] }
cctk = {git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit"}
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
cascade = "1.0.0"
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_4"] }
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core" }
relm4-macros = { git = "https://github.com/Relm4/Relm4.git", branch = "next" }
serde_json = "1.0.78"
futures = "0.3.19"
futures-util = "0.3.19"
once_cell = "1.9.0"
xdg = "2.4.0"
gsk4 = { git = "https://github.com/gtk-rs/gtk4-rs" }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
ron = "0.8"
futures = "0.3"
futures-util = "0.3"
once_cell = "1.9"
xdg = "2.4"
pretty_env_logger = "0.4"
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"
rust-embed = "6.3.0"
calloop = "0.10.1"
wayland-backend = { git = "https://github.com/Smithay/wayland-rs", version = "0.1.0-beta.9"}
wayland-client = { git = "https://github.com/Smithay/wayland-rs", version = "0.30.0-beta.9"}
nix = "0.25"
# config
anyhow = "1.0.53"
ron = "0.8.0"
serde = { version = "1.0.136", features = ["derive"] }
calloop = "0.10"
nix = "0.26"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "macros"] }
itertools = "*"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", no-default-features = true}
freedesktop-desktop-entry = "0.5.0"
freedesktop-icons = {git = "https://github.com/wash2/freedestkop-icons"}
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6"
rust-embed = "6.3"
[build-dependencies]
glib-build-tools = { git = "https://github.com/gtk-rs/gtk-rs-core" }
[dependencies.iced]
git = "https://github.com/pop-os/iced.git"
branch = "sctk-cosmic"
# path = "../iced"
default-features = false
features = ["image", "svg", "tokio", "wayland"]

View file

@ -1,7 +0,0 @@
fn main() {
glib_build_tools::compile_resources(
"data/resources",
"data/resources/resources.gresource.xml",
"compiled.gresource",
);
}

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema path="/com/System76/CosmicAppList/" id="com.System76.CosmicAppList">
<key name="window-width" type="i">
<default>600</default>
<summary>Default window width</summary>
<description>Default window width</description>
</key>
<key name="window-height" type="i">
<default>400</default>
<summary>Default window height</summary>
<description>Default window height</description>
</key>
<key name="is-maximized" type="b">
<default>false</default>
<summary>Default window maximized behaviour</summary>
<description></description>
</key>
</schema>
</schemalist>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/System76/CosmicAppList/">
<!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
</gresource>
</gresources>

View file

@ -0,0 +1,363 @@
use std::ffi::OsStr;
use std::path::PathBuf;
use crate::config;
use crate::config::AppListConfig;
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 cosmic::applet::CosmicAppletHelper;
use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_popup;
use cosmic::iced::wayland::SurfaceIdWrapper;
use cosmic::iced::widget::{column, row};
use cosmic::iced::{executor, window, Application, Command, Subscription};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::widget::{horizontal_rule, vertical_rule};
use cosmic::{Element, Theme};
use cosmic_panel_config::PanelAnchor;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry;
use iced::Alignment;
use iced::Background;
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::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 {
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
app_id: String,
icon_path: PathBuf,
}
#[derive(Clone, Default)]
struct CosmicAppList {
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
subscription_ctr: u32,
toplevel_list: Vec<Toplevel>,
config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>,
applet_helper: CosmicAppletHelper,
}
// TODO DnD after sctk merges DnD
#[derive(Debug, Clone)]
enum Message {
Toplevel(ToplevelUpdate),
Favorite(String),
UnFavorite(String),
TogglePopup(usize),
Activate(Option<ZcosmicToplevelHandleV1>),
Quit(ZcosmicToplevelHandleV1),
Errored(String),
Ignore,
}
fn icon_for_app_ids(mut app_ids: Vec<String>) -> Vec<(String, PathBuf)> {
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) {
let id = app_ids.remove(i);
freedesktop_icons::lookup(de.icon().unwrap_or(de.appid))
.with_size(128)
.with_cache()
.find()
.map(|buf| (id, buf))
} else {
None
}
})
})
})
.collect_vec();
ret.append(
&mut app_ids
.into_iter()
.map(|id| (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();
(
CosmicAppList {
toplevel_list: icon_for_app_ids(config.favorites.clone())
.into_iter()
.map(|e| Toplevel {
toplevels: Default::default(),
app_id: e.0,
icon_path: e.1,
})
.collect(),
config,
..Default::default()
},
Command::none(),
)
}
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::TogglePopup(_) => {
if let Some(p) = self.popup.take() {
return destroy_popup(p);
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
new_id,
(400, 240),
None,
None,
);
return get_popup(popup_settings);
}
}
Message::Favorite(id) => {
let _ = self.config.add_favorite(id);
}
Message::UnFavorite(id) => {
let _ = self.config.remove_favorite(id);
}
Message::Activate(handle) => {
if let (Some(tx), Some(handle)) = (self.toplevel_sender.as_ref(), handle) {
let _ = tx.send(ToplevelRequest::Activate(handle));
}
}
Message::Quit(handle) => {
if let Some(tx) = self.toplevel_sender.as_ref() {
let _ = tx.send(ToplevelRequest::Quit(handle));
}
}
Message::Toplevel(event) => {
// dbg!(&self.toplevel_list);
match event {
ToplevelUpdate::AddToplevel(handle, info) => {
if let Some(i) = self
.toplevel_list
.iter()
.position(|Toplevel { app_id, .. }| app_id == &info.app_id)
{
self.toplevel_list[i].toplevels.push((handle, info));
} else {
let (app_id, icon_name) =
icon_for_app_ids(vec![info.app_id.clone()]).remove(0);
self.toplevel_list.push(Toplevel {
toplevels: vec![(handle, info)],
app_id,
icon_path: icon_name,
});
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
let 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();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
}
ToplevelUpdate::Init(tx) => {
self.toplevel_sender.replace(tx);
}
ToplevelUpdate::Finished => {
self.subscription_ctr += 1;
}
ToplevelUpdate::RemoveToplevel(handle) => {
if let Some(i) = self.toplevel_list.iter_mut().position(
|Toplevel {
toplevels, app_id, ..
}| {
if let Some(ret) = toplevels.iter().position(|t| &t.0 == &handle) {
toplevels.remove(ret);
toplevels.is_empty() && self.config.favorites.contains(app_id)
} else {
false
}
},
) {
self.toplevel_list.remove(i);
}
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
let 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();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
ToplevelUpdate::UpdateToplevel(handle, info) => {
'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;
}
}
}
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
let 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();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
}
}
Message::Ignore => {}
}
Command::none()
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => {
let (favorites, running) = self.toplevel_list.iter().enumerate().fold(
(Vec::new(), Vec::new()),
|(mut favorites, mut running),
(
i,
Toplevel {
toplevels,
app_id,
icon_path,
},
)| {
let icon = if icon_path.extension() == Some(&OsStr::new("svg")) {
let handle = svg::Handle::from_path(icon_path);
svg::Svg::new(handle)
.width(Length::Units(self.applet_helper.suggested_icon_size()))
.height(Length::Units(self.applet_helper.suggested_icon_size()))
.into()
} else {
Image::new(icon_path)
.width(Length::Units(self.applet_helper.suggested_icon_size()))
.height(Length::Units(self.applet_helper.suggested_icon_size()))
.into()
};
let dot_size = (self.applet_helper.suggested_icon_size() / 8).max(2);
let dots = (0..toplevels.len())
.into_iter()
.map(|_| {
container(horizontal_space(Length::Units(0)))
.padding(dot_size)
.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(2).into(), icon]).align_items(iced::Alignment::Center).spacing(2).into(),
PanelAnchor::Right => row(vec![icon, column(dots).spacing(2).into()]).align_items(iced::Alignment::Center).spacing(2).into(),
PanelAnchor::Top => column(vec![row(dots).spacing(2).into(), icon]).align_items(iced::Alignment::Center).spacing(2).into(),
PanelAnchor::Bottom => column(vec![icon, row(dots).spacing(2).into()]).align_items(iced::Alignment::Center).spacing(2).into(),
};
// TODO tooltip on hover
let icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper])
.on_press(Message::Activate(toplevels.first().map(|t| t.0.clone())))
.padding(8).into();
if self.config.favorites.contains(&app_id) {
favorites.push(icon_button)
} else {
running.push(icon_button);
}
(favorites, running)
},
);
match &self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => {
column![column(favorites), horizontal_rule(1), column(running)].spacing(4).align_items(Alignment::Center).height(Length::Fill).width(Length::Fill).into()
}
PanelAnchor::Top | PanelAnchor::Bottom => {
row![row(favorites), vertical_rule(1), row(running)].spacing(4).align_items(Alignment::Center).height(Length::Fill).width(Length::Fill).into()
}
}
}
SurfaceIdWrapper::Popup(_) => {
todo!();
}
}
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event))
])
}
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

@ -1,12 +1,15 @@
use crate::ID;
use anyhow::anyhow;
use serde::Deserialize;
use std::fmt::Debug;
use std::fs::File;
use xdg::BaseDirectories;
#[derive(Debug, Clone, Deserialize)]
pub const APP_ID: &str = "com.system76.CosmicAppList";
pub const VERSION: &str = "0.1.0";
#[derive(Debug, Clone, Deserialize, Default)]
pub enum TopLevelFilter {
#[default]
ActiveWorkspace,
ConfiguredOutput,
}
@ -14,6 +17,7 @@ pub enum TopLevelFilter {
#[derive(Debug, Clone, Default, Deserialize)]
pub struct AppListConfig {
pub filter_top_levels: Option<TopLevelFilter>,
pub favorites: Vec<String>,
}
impl AppListConfig {
@ -21,7 +25,7 @@ impl AppListConfig {
pub fn load() -> anyhow::Result<AppListConfig> {
let file = match BaseDirectories::new()
.ok()
.and_then(|dirs| dirs.find_config_file(format!("{ID}/config.ron")))
.and_then(|dirs| dirs.find_config_file(format!("{APP_ID}/config.ron")))
.and_then(|p| File::open(p).ok())
{
Some(path) => path,
@ -33,4 +37,20 @@ impl AppListConfig {
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);
}
todo!()
}
pub fn remove_favorite(&mut self, id: String) -> anyhow::Result<()> {
self.favorites.retain(|e| e != &id);
todo!()
}
pub fn save() -> anyhow::Result<()> {
todo!()
}
}

View file

@ -36,3 +36,12 @@ macro_rules! fl {
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

@ -1,292 +1,24 @@
// SPDX-License-Identifier: MPL-2.0-only
use apps_window::CosmicAppListWindow;
use calloop::channel::SyncSender;
use dock_list::DockListType;
use dock_object::DockObject;
use gio::{ApplicationFlags, DesktopAppInfo};
use gtk4::gdk::Display;
use gtk4::{glib, prelude::*, CssProvider, StyleContext};
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use utils::{AppListEvent, BoxedWindowList};
use wayland::{Toplevel, ToplevelEvent};
mod apps_container;
mod apps_window;
mod app;
mod config;
mod dock_item;
mod dock_list;
mod dock_object;
mod dock_popover;
mod localize;
mod utils;
mod wayland;
mod wayland_source;
mod toplevel_handler;
mod toplevel_subscription;
const ID: &str = "com.system76.CosmicAppList";
static TX: OnceCell<glib::Sender<AppListEvent>> = OnceCell::new();
static WAYLAND_TX: OnceCell<SyncSender<ToplevelEvent>> = OnceCell::new();
use log::info;
pub fn localize() {
let localizer = crate::localize::localizer();
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
use localize::localize;
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for App List {}", error);
}
}
fn load_css() {
let provider = CssProvider::new();
provider.load_from_data(include_bytes!("style.css"));
StyleContext::add_provider_for_display(
&Display::default().unwrap(),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn main() {
let _monitors = libcosmic::init();
use crate::config::{APP_ID, VERSION};
fn main() -> cosmic::iced::Result {
// Initialize logger
pretty_env_logger::init();
glib::set_application_name("Cosmic Dock App List");
info!("Iced Workspaces Applet ({})", APP_ID);
info!("Version: {}", VERSION);
// Prepare i18n
localize();
gio::resources_register_include!("compiled.gresource").unwrap();
let app = gtk4::Application::new(None, ApplicationFlags::default());
app.connect_activate(|app| {
load_css();
let (tx, rx) = glib::MainContext::channel(glib::Priority::default());
let window = CosmicAppListWindow::new(app);
let wayland_tx = wayland::spawn_toplevels();
WAYLAND_TX.set(wayland_tx).unwrap();
let mut cached_results = Vec::new();
// let zbus_conn = spawn_zbus(tx.clone(), Arc::clone(&cached_results));
TX.set(tx.clone()).unwrap();
rx.attach(None, glib::clone!(@weak window => @default-return glib::prelude::Continue(true), move |event| {
let apps_container = window.apps_container();
let should_apply_changes = match event {
AppListEvent::Favorite((name, should_favorite)) => {
let saved_app_model = apps_container.model(DockListType::Saved);
let active_app_model = apps_container.model(DockListType::Active);
if should_favorite {
let mut cur: u32 = 0;
let mut index: Option<u32> = None;
while let Some(item) = active_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
if cur_dock_object.get_name() == Some(name.clone()) {
cur_dock_object.set_saved(true);
index = Some(cur);
}
}
cur += 1;
}
if let Some(index) = index {
let object = active_app_model.item(index).unwrap();
active_app_model.remove(index);
saved_app_model.append(&object);
}
} else {
let mut cur: u32 = 0;
let mut index: Option<u32> = None;
while let Some(item) = saved_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
if cur_dock_object.get_name() == Some(name.clone()) {
cur_dock_object.set_saved(false);
index = Some(cur);
}
}
cur += 1;
}
if let Some(index) = index {
let object = saved_app_model.item(index).unwrap();
saved_app_model.remove(index);
active_app_model.append(&object);
}
}
let _ = tx.send(AppListEvent::Refresh);
false
}
AppListEvent::Refresh => {
// println!("refreshing model from cache");
let stack_active = cached_results.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<String, BoxedWindowList>, elem: &Toplevel| {
if let Some(v) = acc.get_mut(&elem.app_id) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.app_id.clone(),
BoxedWindowList(vec![elem.clone()]),
);
}
acc
},
);
let mut stack_active: Vec<BoxedWindowList> =
stack_active.into_values().collect();
// update active app stacks for saved apps into the saved app model
// then put the rest in the active app model (which doesn't include saved apps)
let saved_app_model = apps_container.model(DockListType::Saved);
let mut saved_i: u32 = 0;
while let Some(item) = saved_app_model.item(saved_i) {
if let Ok(dock_obj) = item.downcast::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
{
if let Some((i, _s)) = stack_active
.iter()
.enumerate()
.find(|(_i, s)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
// println!(
// "found active saved app {} at {}",
// _s.0[0].name, i
// );
let active = stack_active.remove(i);
dock_obj.set_property("active", active.to_value());
saved_app_model.items_changed(saved_i, 0, 0);
} else if cached_results
.iter()
.any(|s| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
dock_obj.set_property(
"active",
BoxedWindowList(Vec::new()).to_value(),
);
saved_app_model.items_changed(saved_i, 0, 0);
}
}
}
saved_i += 1;
}
let active_app_model = apps_container.model(DockListType::Active);
let model_len = active_app_model.n_items();
let new_results: Vec<glib::Object> = stack_active
.into_iter()
.filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast()))
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
true
}
AppListEvent::WindowList(toplevels) => {
cached_results = toplevels;
true
}
AppListEvent::Remove(top_level) => {
if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) {
cached_results.swap_remove(i);
}
true
}
AppListEvent::Add(top_level) => {
// sort to make comparison with cache easier
if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) {
cached_results[i] = top_level;
} else {
cached_results.push(top_level);
}
true
}
};
if should_apply_changes {
// dbg!(&cached_results);
// build active app stacks for each app
let stack_active = cached_results.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
if let Some(v) = acc.get_mut(&elem.app_id) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.app_id.clone(),
BoxedWindowList(vec![elem.clone()]),
);
}
acc
},
);
let mut stack_active: Vec<BoxedWindowList> =
stack_active.into_values().collect();
// update active app stacks for saved apps into the saved app model
// then put the rest in the active app model (which doesn't include saved apps)
let saved_app_model = apps_container.model(DockListType::Saved);
let mut saved_i: u32 = 0;
while let Some(item) = saved_app_model.item(saved_i) {
if let Ok(dock_obj) = item.downcast::<DockObject>() {
// clear active if it has some, they will be updated back if they still exist
let prev_active: BoxedWindowList = dock_obj.property("active");
if !prev_active.0.is_empty() {
dock_obj.set_property("active", BoxedWindowList::default().to_value());
saved_app_model.items_changed(saved_i, 0, 0);
}
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
{
if let Some((i, _s)) = stack_active
.iter()
.enumerate()
.find(|(_i, s)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
// println!("found active saved app {} at {}", s.0[0].name, i);
let active = stack_active.remove(i);
dock_obj.set_property("active", active.to_value());
saved_app_model.items_changed(saved_i, 0, 0);
} else if cached_results
.iter()
.any(|s| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
dock_obj.set_property(
"active",
BoxedWindowList(Vec::new()).to_value(),
);
saved_app_model.items_changed(saved_i, 0, 0);
}
}
}
saved_i += 1;
}
let active_app_model = apps_container.model(DockListType::Active);
let model_len = active_app_model.n_items();
let new_results: Vec<glib::Object> = stack_active
.into_iter()
.filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast()))
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
}
glib::prelude::Continue(true)
}));
window.show();
});
app.run();
app::run()
}

View file

@ -0,0 +1,124 @@
use cctk::{
sctk::{self, event_loop::WaylandSource},
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
wayland_client,
};
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1;
use futures::channel::mpsc::UnboundedSender;
use sctk::registry::{ProvidesRegistryState, RegistryState};
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
use crate::toplevel_subscription::{ToplevelRequest, ToplevelUpdate};
struct AppData {
exit: bool,
tx: UnboundedSender<ToplevelUpdate>,
registry_state: RegistryState,
toplevel_info_state: ToplevelInfoState,
}
impl ProvidesRegistryState for AppData {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
sctk::registry_handlers!();
}
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(_) => {} // TODO
ToplevelRequest::Quit(_) => {} // TODO
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,
toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh),
registry_state,
};
loop {
if app_data.exit {
break;
}
event_loop.dispatch(None, &mut app_data).unwrap();
}
}
sctk::delegate_registry!(AppData);
cctk::delegate_toplevel_info!(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::toplevel_info::ToplevelInfo;
use cosmic::iced;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
StreamExt,
};
use iced::subscription;
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),
Quit(ZcosmicToplevelHandleV1),
Exit,
}

View file

@ -1,597 +0,0 @@
use crate::config::AppListConfig;
use crate::{config::TopLevelFilter, utils::AppListEvent, wayland_source::WaylandSource, TX};
use calloop::channel::*;
use cosmic_panel_config::CosmicPanelOuput;
use cosmic_protocols::{
toplevel_info::v1::client::{
zcosmic_toplevel_handle_v1::{self, ZcosmicToplevelHandleV1},
zcosmic_toplevel_info_v1::{self, ZcosmicToplevelInfoV1},
},
toplevel_management::v1::client::zcosmic_toplevel_manager_v1::{
self, ZcosmicToplevelManagerV1,
},
workspace::v1::client::{
zcosmic_workspace_group_handle_v1::{self, ZcosmicWorkspaceGroupHandleV1},
zcosmic_workspace_handle_v1::{self, ZcosmicWorkspaceHandleV1},
zcosmic_workspace_manager_v1::{self, ZcosmicWorkspaceManagerV1},
},
};
use std::{env, os::unix::net::UnixStream, path::PathBuf, time::Duration};
use wayland_client::protocol::wl_seat::{self, WlSeat};
use wayland_client::{
event_created_child,
protocol::{
wl_output::{self, WlOutput},
wl_registry,
},
ConnectError, Proxy,
};
use wayland_client::{Connection, Dispatch, QueueHandle};
#[derive(Debug, Clone)]
pub enum ToplevelEvent {
Activate(ZcosmicToplevelHandleV1),
Close(ZcosmicToplevelHandleV1),
}
pub fn spawn_toplevels() -> SyncSender<ToplevelEvent> {
let config = AppListConfig::load().unwrap_or_default();
let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100);
if let Ok(Ok(conn)) = std::env::var("WAYLAND_DISPLAY")
.map_err(anyhow::Error::msg)
.map(|display_str| {
let mut socket_path = env::var_os("XDG_RUNTIME_DIR")
.map(Into::<PathBuf>::into)
.ok_or(ConnectError::NoCompositor)?;
socket_path.push(display_str);
Ok(UnixStream::connect(socket_path).map_err(|_| ConnectError::NoCompositor)?)
})
.and_then(|s| s.map(|s| Connection::from_socket(s).map_err(anyhow::Error::msg)))
{
std::thread::spawn(move || {
let output = std::env::var("COSMIC_PANEL_OUTPUT").ok().and_then(|size| {
match size.parse::<CosmicPanelOuput>() {
Ok(CosmicPanelOuput::Name(n)) => Some(n),
// TODO handle Active & panic if the space is still configured for All instead of being assigned a named output
_ => None,
}
});
let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap();
let loop_handle = event_loop.handle();
let event_queue = conn.new_event_queue::<State>();
let qhandle = event_queue.handle();
WaylandSource::new(event_queue)
.expect("Failed to create wayland source")
.insert(loop_handle)
.unwrap();
let display = conn.display();
display.get_registry(&qhandle, ());
let mut state = State {
workspace_manager: None,
workspace_groups: Vec::new(),
toplevel_info: None,
toplevel_manager: None,
config,
configured_output: output,
expected_output: None,
running: true,
toplevels: vec![],
seats: vec![],
};
let loop_handle = event_loop.handle();
loop_handle
.insert_source(workspaces_rx, |e, _, state| match e {
Event::Msg(ToplevelEvent::Activate(toplevel)) => {
if let Some(manager) = &state.toplevel_manager {
for seat in &state.seats {
manager.activate(&toplevel, seat)
}
}
}
Event::Msg(ToplevelEvent::Close(t)) => {
if let Some(manager) = &state.toplevel_manager {
manager.close(&t);
}
}
Event::Closed => {
if let Some(workspace_manager) = &mut state.workspace_manager {
for g in &mut state.workspace_groups {
g.workspace_group_handle.destroy();
}
workspace_manager.stop();
}
if let Some(toplevel_manager) = &mut state.toplevel_manager {
toplevel_manager.destroy();
}
if let Some(toplevel_info) = &mut state.toplevel_info {
for toplevel in &state.toplevels {
toplevel.toplevel_handle.destroy();
}
toplevel_info.stop();
}
}
})
.unwrap();
while state.running {
event_loop
.dispatch(Duration::from_millis(16), &mut state)
.unwrap();
}
});
} else {
eprintln!("ENV variable WAYLAND_DISPLAY is missing. Exiting...");
std::process::exit(1);
}
workspaces_tx
}
#[derive(Debug, Clone)]
pub struct State {
running: bool,
config: AppListConfig,
configured_output: Option<String>,
expected_output: Option<WlOutput>,
workspace_manager: Option<ZcosmicWorkspaceManagerV1>,
workspace_groups: Vec<WorkspaceGroup>,
toplevel_info: Option<ZcosmicToplevelInfoV1>,
toplevel_manager: Option<ZcosmicToplevelManagerV1>,
toplevels: Vec<Toplevel>,
seats: Vec<WlSeat>,
}
#[derive(Debug, Clone)]
pub struct Toplevel {
pub name: String,
pub app_id: String,
pub toplevel_handle: ZcosmicToplevelHandleV1,
pub states: Vec<zcosmic_toplevel_handle_v1::State>,
pub output: Option<WlOutput>,
pub workspace: Option<ZcosmicWorkspaceHandleV1>,
}
#[derive(Debug, Clone)]
struct WorkspaceGroup {
workspace_group_handle: ZcosmicWorkspaceGroupHandleV1,
output: Option<WlOutput>,
workspaces: Vec<Workspace>,
}
#[derive(Debug, Clone)]
struct Workspace {
workspace_handle: ZcosmicWorkspaceHandleV1,
name: String,
coordinates: Vec<u32>,
states: Vec<zcosmic_workspace_handle_v1::State>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match &interface[..] {
"zcosmic_toplevel_info_v1" => {
let ti = registry
.bind::<ZcosmicToplevelInfoV1, _, _>(name, 1, qh, ());
state.toplevel_info = Some(ti);
}
"zcosmic_toplevel_manager_v1" => {
let tm = registry
.bind::<ZcosmicToplevelManagerV1, _, _>(name, 1, qh, ());
state.toplevel_manager = Some(tm);
}
"zcosmic_workspace_manager_v1" => {
let workspace_manager = registry
.bind::<ZcosmicWorkspaceManagerV1, _, _>(name, 1, qh, ());
state.workspace_manager = Some(workspace_manager);
}
"wl_seat" => {
registry.bind::<WlSeat, _, _>(name, 1, qh, ());
}
"wl_output" => {
registry.bind::<WlOutput, _, _>(name, 1, qh, ());
}
_ => {}
}
}
}
}
impl Dispatch<ZcosmicToplevelInfoV1, ()> for State {
fn event(
state: &mut Self,
_: &ZcosmicToplevelInfoV1,
event: <ZcosmicToplevelInfoV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_toplevel_info_v1::Event::Toplevel { toplevel } => {
state.toplevels.push(Toplevel {
name: "".into(),
app_id: "".into(),
toplevel_handle: toplevel,
states: vec![],
output: None,
workspace: None,
});
}
zcosmic_toplevel_info_v1::Event::Finished => {
todo!()
}
_ => {}
}
}
event_created_child!(State, ZcosmicWorkspaceManagerV1, [
0 => (ZcosmicToplevelHandleV1, ())
]);
}
impl Dispatch<ZcosmicToplevelManagerV1, ()> for State {
fn event(
_: &mut Self,
_: &ZcosmicToplevelManagerV1,
event: <ZcosmicToplevelManagerV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_toplevel_manager_v1::Event::Capabilities { .. } => {
// TODO capabilities affect what is shown to user in applet
}
_ => {}
}
}
}
impl Dispatch<ZcosmicToplevelHandleV1, ()> for State {
fn event(
state: &mut Self,
p: &ZcosmicToplevelHandleV1,
event: <ZcosmicToplevelHandleV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_toplevel_handle_v1::Event::Closed => {
if let Some(i) = state.toplevels.iter().position(|t| &t.toplevel_handle == p) {
let removed_toplevel = state.toplevels.remove(i);
if match state.config.filter_top_levels {
Some(TopLevelFilter::ActiveWorkspace) => state
.workspace_groups
.iter()
.find(|g| {
g.workspaces
.iter()
.find(|w| {
w.states
.contains(&zcosmic_workspace_handle_v1::State::Active)
&& Some(&w.workspace_handle)
== removed_toplevel.workspace.as_ref()
})
.is_some()
})
.is_some(),
Some(TopLevelFilter::ConfiguredOutput) => {
state.expected_output == removed_toplevel.output
}
_ => true,
} {
let tx = TX.get().unwrap().clone();
let _ = tx.send(AppListEvent::Remove(removed_toplevel));
}
}
}
zcosmic_toplevel_handle_v1::Event::Done => {
let to_send = match state.config.filter_top_levels {
Some(TopLevelFilter::ActiveWorkspace) => state.toplevels.iter_mut().find(|t| {
if &t.toplevel_handle == p {
state
.workspace_groups
.iter()
.find(|g| {
g.workspaces
.iter()
.find(|w| {
w.states.contains(
&zcosmic_workspace_handle_v1::State::Active,
) && Some(&w.workspace_handle) == t.workspace.as_ref()
})
.is_some()
})
.is_some()
} else {
false
}
}),
Some(TopLevelFilter::ConfiguredOutput) => state
.toplevels
.iter_mut()
.find(|t| &t.toplevel_handle == p && state.expected_output == t.output),
_ => state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p),
};
if let Some(toplevel) = to_send.cloned() {
let tx = TX.get().unwrap().clone();
let _ = tx.send(AppListEvent::Add(toplevel));
}
}
zcosmic_toplevel_handle_v1::Event::Title { title } => {
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
i.name = title;
}
}
zcosmic_toplevel_handle_v1::Event::AppId { app_id } => {
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
i.app_id = app_id;
}
}
zcosmic_toplevel_handle_v1::Event::OutputEnter { output } => {
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
i.output.replace(output);
}
}
zcosmic_toplevel_handle_v1::Event::OutputLeave { output } => {
if let Some(i) = state
.toplevels
.iter_mut()
.find(|t| &t.toplevel_handle == p && t.output.as_ref() == Some(&output))
{
i.output.take();
}
}
zcosmic_toplevel_handle_v1::Event::WorkspaceEnter { workspace } => {
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
i.workspace.replace(workspace);
}
}
zcosmic_toplevel_handle_v1::Event::WorkspaceLeave { workspace } => {
if let Some(i) = state
.toplevels
.iter_mut()
.find(|t| &t.toplevel_handle == p && t.workspace.as_ref() == Some(&workspace))
{
i.workspace.take();
}
}
zcosmic_toplevel_handle_v1::Event::State { state: t_state } => {
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
i.states = t_state
.chunks(4)
.map(|chunk| {
zcosmic_toplevel_handle_v1::State::try_from(u32::from_ne_bytes(
chunk.try_into().unwrap(),
))
.unwrap()
})
.collect();
}
}
_ => todo!(),
}
}
}
impl Dispatch<ZcosmicWorkspaceManagerV1, ()> for State {
fn event(
state: &mut Self,
_: &ZcosmicWorkspaceManagerV1,
event: zcosmic_workspace_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => {
state.workspace_groups.push(WorkspaceGroup {
workspace_group_handle: workspace_group,
output: None,
workspaces: Vec::new(),
});
}
zcosmic_workspace_manager_v1::Event::Done => {
for group in &mut state.workspace_groups {
group.workspaces.sort_by(|w1, w2| {
w1.coordinates
.iter()
.zip(w2.coordinates.iter())
.skip_while(|(coord1, coord2)| coord1 == coord2)
.next()
.map(|(coord1, coord2)| coord1.cmp(coord2))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
zcosmic_workspace_manager_v1::Event::Finished => {
state.workspace_manager.take();
}
_ => {}
}
}
event_created_child!(State, ZcosmicWorkspaceManagerV1, [
0 => (ZcosmicWorkspaceGroupHandleV1, ())
]);
}
impl Dispatch<ZcosmicWorkspaceGroupHandleV1, ()> for State {
fn event(
state: &mut Self,
group: &ZcosmicWorkspaceGroupHandleV1,
event: zcosmic_workspace_group_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_workspace_group_handle_v1::Event::OutputEnter { output } => {
if let Some(group) = state
.workspace_groups
.iter_mut()
.find(|g| &g.workspace_group_handle == group)
{
group.output = Some(output);
}
}
zcosmic_workspace_group_handle_v1::Event::OutputLeave { output } => {
if let Some(group) = state.workspace_groups.iter_mut().find(|g| {
&g.workspace_group_handle == group && g.output.as_ref() == Some(&output)
}) {
group.output = None;
}
}
zcosmic_workspace_group_handle_v1::Event::Workspace { workspace } => {
if let Some(group) = state
.workspace_groups
.iter_mut()
.find(|g| &g.workspace_group_handle == group)
{
group.workspaces.push(Workspace {
workspace_handle: workspace,
name: String::new(),
coordinates: Vec::new(),
states: Vec::new(),
})
}
}
zcosmic_workspace_group_handle_v1::Event::Remove => {
if let Some(group) = state
.workspace_groups
.iter()
.position(|g| &g.workspace_group_handle == group)
{
state.workspace_groups.remove(group);
}
}
_ => {}
}
}
event_created_child!(State, ZcosmicWorkspaceGroupHandleV1, [
3 => (ZcosmicWorkspaceHandleV1, ())
]);
}
impl Dispatch<ZcosmicWorkspaceHandleV1, ()> for State {
fn event(
state: &mut Self,
workspace: &ZcosmicWorkspaceHandleV1,
event: zcosmic_workspace_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zcosmic_workspace_handle_v1::Event::Name { name } => {
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
g.workspaces
.iter_mut()
.find(|w| &w.workspace_handle == workspace)
}) {
w.name = name;
}
}
zcosmic_workspace_handle_v1::Event::Coordinates { coordinates } => {
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
g.workspaces
.iter_mut()
.find(|w| &w.workspace_handle == workspace)
}) {
// wayland is host byte order
w.coordinates = coordinates
.chunks(4)
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
.collect();
}
}
zcosmic_workspace_handle_v1::Event::State {
state: workspace_state,
} => {
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
g.workspaces
.iter_mut()
.find(|w| &w.workspace_handle == workspace)
}) {
// wayland is host byte order
w.states = workspace_state
.chunks(4)
.map(|chunk| {
zcosmic_workspace_handle_v1::State::try_from(u32::from_ne_bytes(
chunk.try_into().unwrap(),
))
.unwrap()
})
.collect();
// TODO if workspace active status changes while configured to only show active workspace, clear the list
}
}
zcosmic_workspace_handle_v1::Event::Remove => {
if let Some((g, w_i)) = state.workspace_groups.iter_mut().find_map(|g| {
g.workspaces
.iter_mut()
.position(|w| &w.workspace_handle == workspace)
.map(|p| (g, p))
}) {
g.workspaces.remove(w_i);
}
}
_ => {}
}
}
}
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
o: &WlOutput,
e: wl_output::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match e {
wl_output::Event::Name { name } if Some(&name) == state.configured_output.as_ref() => {
state.expected_output.replace(o.clone());
}
_ => {} // ignored
}
}
}
impl Dispatch<WlSeat, ()> for State {
fn event(
state: &mut Self,
seat: &WlSeat,
_: wl_seat::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if state.seats.iter().find(|s| s == &seat).is_none() {
state.seats.push(seat.clone());
}
}
}

View file

@ -1,219 +0,0 @@
//! Utilities for using an [`EventQueue`] from wayland-client with an event loop that performs polling with
//! [`calloop`](https://crates.io/crates/calloop).
use std::{io, os::unix::prelude::{RawFd, AsRawFd}};
use calloop::{
generic::Generic, EventSource, InsertError, Interest, LoopHandle, Mode, Poll, PostAction,
Readiness, RegistrationToken, Token, TokenFactory,
};
use nix::errno::Errno;
use wayland_backend::client::{ReadEventsGuard, WaylandError};
use wayland_client::{DispatchError, EventQueue};
/// An adapter to insert an [`EventQueue`] into a calloop [`EventLoop`](calloop::EventLoop).
///
/// This type implements [`EventSource`] which generates an event whenever events on the display need to be
/// dispatched. The event queue available in the callback calloop registers may be used to dispatch pending
/// events using [`EventQueue::dispatch_pending`].
///
/// [`WaylandSource::insert`] can be used to insert this source into an event loop and automatically dispatch
/// pending events on the display.
#[derive(Debug)]
pub struct WaylandSource<D> {
queue: EventQueue<D>,
fd: Generic<RawFd>,
read_guard: Option<ReadEventsGuard>,
}
impl<D> WaylandSource<D> {
/// Wrap an [`EventQueue`] as a [`WaylandSource`].
pub fn new(queue: EventQueue<D>) -> Result<WaylandSource<D>, WaylandError> {
let guard = queue.prepare_read()?;
let fd = Generic::new(guard.connection_fd().as_raw_fd(), Interest::READ, Mode::Level);
drop(guard);
Ok(WaylandSource {
queue,
fd,
read_guard: None,
})
}
/// Access the underlying event queue
///
/// Note that you should be careful when interacting with it if you invoke methods that
/// interact with the wayland socket (such as `dispatch()` or `prepare_read()`). These may
/// interfere with the proper waking up of this event source in the event loop.
pub fn queue(&mut self) -> &mut EventQueue<D> {
&mut self.queue
}
/// Insert this source into the given event loop.
///
/// This adapter will pass the event loop's shared data as the `D` type for the event loop.
pub fn insert(self, handle: LoopHandle<D>) -> Result<RegistrationToken, InsertError<Self>>
where
D: 'static,
{
handle.insert_source(self, |_, queue, data| queue.dispatch_pending(data))
}
}
impl<D> EventSource for WaylandSource<D> {
type Event = ();
/// The underlying event queue.
///
/// You should call [`EventQueue::dispatch_pending`] inside your callback using this queue.
type Metadata = EventQueue<D>;
type Ret = Result<usize, DispatchError>;
type Error = calloop::Error;
fn process_events<F>(
&mut self,
readiness: Readiness,
token: Token,
mut callback: F,
) -> Result<PostAction, Self::Error>
where
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
{
let queue = &mut self.queue;
let read_guard = &mut self.read_guard;
let action = self.fd.process_events(readiness, token, |_, _| {
// 1. read events from the socket if any are available
if let Some(guard) = read_guard.take() {
// might be None if some other thread read events before us, concurrently
if let Err(WaylandError::Io(err)) = guard.read() {
if err.kind() != io::ErrorKind::WouldBlock {
return Err(err);
}
}
}
// 2. dispatch any pending events in the queue
// This is done to ensure we are not waiting for messages that are already in the buffer.
Self::loop_callback_pending(queue, &mut callback)?;
*read_guard = Some(Self::prepare_read(queue)?);
// 3. Once dispatching is finished, flush the responses to the compositor
if let Err(WaylandError::Io(e)) = queue.flush() {
if e.kind() != io::ErrorKind::WouldBlock {
// in case of error, forward it and fast-exit
return Err(e);
}
// WouldBlock error means the compositor could not process all our messages
// quickly. Either it is slowed down or we are a spammer.
// Should not really happen, if it does we do nothing and will flush again later
}
Ok(PostAction::Continue)
})?;
Ok(action)
}
fn register(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.fd.register(poll, token_factory)
}
fn reregister(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.fd.reregister(poll, token_factory)
}
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
self.fd.unregister(poll)
}
fn pre_run<F>(&mut self, mut callback: F) -> calloop::Result<()>
where
F: FnMut((), &mut Self::Metadata) -> Self::Ret,
{
debug_assert!(self.read_guard.is_none());
// flush the display before starting to poll
if let Err(WaylandError::Io(err)) = self.queue.flush() {
if err.kind() != io::ErrorKind::WouldBlock {
// in case of error, don't prepare a read, if the error is persistent, it'll trigger in other
// wayland methods anyway
log::error!("Error trying to flush the wayland display: {}", err);
return Err(err.into());
}
}
// ensure we are not waiting for messages that are already in the buffer.
Self::loop_callback_pending(&mut self.queue, &mut callback)?;
self.read_guard = Some(Self::prepare_read(&mut self.queue)?);
Ok(())
}
fn post_run<F>(&mut self, _: F) -> calloop::Result<()>
where
F: FnMut((), &mut Self::Metadata) -> Self::Ret,
{
// Drop implementation of ReadEventsGuard will do cleanup
self.read_guard.take();
Ok(())
}
}
impl<D> WaylandSource<D> {
/// Loop over the callback until all pending messages have been dispatched.
fn loop_callback_pending<F>(queue: &mut EventQueue<D>, callback: &mut F) -> io::Result<()>
where
F: FnMut((), &mut EventQueue<D>) -> Result<usize, DispatchError>,
{
// Loop on the callback until no pending events are left.
loop {
match callback((), queue) {
// No more pending events.
Ok(0) => break Ok(()),
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(err))) => {
return Err(err);
}
Err(DispatchError::Backend(WaylandError::Protocol(err))) => {
log::error!("Protocol error received on display: {}", err);
break Err(Errno::EPROTO.into());
}
Err(DispatchError::BadMessage { sender_id, interface, opcode }) => {
log::error!(
"Bad message on interface \"{}\": (opcode: {}, sender_id: {:?})",
interface,
opcode,
sender_id,
);
break Err(Errno::EPROTO.into());
}
}
}
}
fn prepare_read(queue: &mut EventQueue<D>) -> io::Result<ReadEventsGuard> {
queue.prepare_read().map_err(|err| match err {
WaylandError::Io(err) => err,
WaylandError::Protocol(err) => {
log::error!("Protocol error received on display: {}", err);
Errno::EPROTO.into()
}
})
}
}

7
debian/rules vendored
View file

@ -71,6 +71,13 @@ override_dh_auto_clean:
echo 'directory = "vendor"' >> .cargo/config; \
tar pcf vendor.tar vendor; \
rm -rf vendor; \
cd ../..; \
cd applets/cosmic-app-list/; \
mkdir -p .cargo; \
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config; \
echo 'directory = "vendor"' >> .cargo/config; \
tar pcf vendor.tar vendor; \
rm -rf vendor; \
fi
override_dh_auto_build:

View file

@ -28,6 +28,9 @@ workspaces_button_id := 'com.system76.CosmicPanelWorkspacesButton'
build: _extract_vendor
#!/usr/bin/env bash
pushd applets/cosmic-app-list/
cargo build {{cargo_args}}
popd
pushd applets/cosmic-applet-audio/
cargo build {{cargo_args}}
popd
@ -63,7 +66,7 @@ install:
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.Devel.svg {{iconsdir}}/{{app_list_id}}.Devel.svg
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.svg {{iconsdir}}/{{app_list_id}}.svg
install -Dm0644 applets/cosmic-app-list/data/{{app_list_id}}.desktop {{sharedir}}/applications/{{app_list_id}}.desktop
install -Dm0755 target/release/cosmic-app-list {{bindir}}/cosmic-app-list
install -Dm0755 applets/cosmic-app-list/target/release/cosmic-app-list {{bindir}}/cosmic-app-list
# network
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg
@ -128,4 +131,5 @@ _extract_vendor:
rm -rf applets/cosmic-applet-power/vendor; tar xf applets/cosmic-applet-power/vendor.tar --directory applets/cosmic-applet-power
rm -rf applets/cosmic-applet-time/vendor; tar xf applets/cosmic-applet-time/vendor.tar --directory applets/cosmic-applet-time
rm -rf applets/cosmic-applet-network/vendor; tar xf applets/cosmic-applet-network/vendor.tar --directory applets/cosmic-applet-network
rm -rf applets/cosmic-app-list/vendor; tar xf applets/cosmic-app-list/vendor.tar --directory applets/cosmic-app-list
fi