wip: update libcosmic (#93)

* wip: update libcosmic

* fix: damge issue resolved by updating iced

* fix: high cpu usage by time applet and app-list

* refactor subscriptions to produce fewer events

* refactor network applet to use less cpu

* fix: text size

* refactor: i18n for audio applet

* refactor: power applet i18n setup

* fix (battery): always send profile update

* fix (battery): set toggler width to layout correctly

* fix (app-list): backoff for restarts of toplevel subscription

* fix (network): alignment

* feat: ask for comfirmation before applying power applet actions

* wip: integrate cosmic-config

* update zbus

* feat: update to use latest libcosmic

* update iced

* udpate deps

* update deps

* refactor: move applet helpers to this repo, outside of libcosmic.

this should help alleviate some dependency hell

* chore update deps

* update deps

* cleanup
This commit is contained in:
Ashley Wulber 2023-06-01 12:23:12 -04:00 committed by GitHub
parent 8b46cc209f
commit 9ebd9b511a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2841 additions and 1681 deletions

1740
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
[workspace]
members = [
"applet",
"cosmic-app-list",
"cosmic-applet-audio",
"cosmic-applet-battery",
@ -15,4 +16,5 @@ members = [
]
[profile.release]
lto = "fat"
lto = "thin"
# lto = "fat"

12
applet/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "cosmic-applet"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmic = { package = "libcosmic", git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
ron = { version = "0.8" }
serde = { version = "1.0" }
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", rev = "11cfff0" }

203
applet/src/lib.rs Normal file
View file

@ -0,0 +1,203 @@
use cosmic::{
iced::{self, window, Limits},
iced_style, iced_widget, sctk,
theme::Button,
Renderer,
};
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
use iced::{
alignment::{Horizontal, Vertical},
wayland::InitialSurface,
widget::{self, Container},
Color, Element, Length, Rectangle, Settings,
};
use iced_style::{button::StyleSheet, container::Appearance};
use iced_widget::runtime::command::platform_specific::wayland::{
popup::{SctkPopupSettings, SctkPositioner},
window::SctkWindowSettings,
};
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
pub use cosmic_panel_config;
const APPLET_PADDING: u32 = 8;
#[must_use]
pub fn applet_button_theme() -> Button {
Button::Custom {
active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.active(&Button::Text)
}),
hover: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.hovered(&Button::Text)
}),
}
}
#[derive(Debug, Clone)]
pub struct CosmicAppletHelper {
pub size: Size,
pub anchor: PanelAnchor,
pub background: CosmicPanelBackground,
pub output_name: String,
}
#[derive(Clone, Debug)]
pub enum Size {
PanelSize(PanelSize),
// (width, height)
Hardcoded((u16, u16)),
}
impl Default for CosmicAppletHelper {
fn default() -> Self {
Self {
size: Size::PanelSize(
std::env::var("COSMIC_PANEL_SIZE")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(PanelSize::S),
),
anchor: std::env::var("COSMIC_PANEL_ANCHOR")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(PanelAnchor::Top),
background: std::env::var("COSMIC_PANEL_BACKGROUND")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(CosmicPanelBackground::ThemeDefault),
output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(),
}
}
}
impl CosmicAppletHelper {
#[must_use]
pub fn suggested_size(&self) -> (u16, u16) {
match &self.size {
Size::PanelSize(size) => match size {
PanelSize::XL => (64, 64),
PanelSize::L => (36, 36),
PanelSize::M => (24, 24),
PanelSize::S => (16, 16),
PanelSize::XS => (12, 12),
},
Size::Hardcoded((width, height)) => (*width, *height),
}
}
// Set the default window size. Helper for application init with hardcoded size.
pub fn window_size(&mut self, width: u16, height: u16) {
self.size = Size::Hardcoded((width, height));
}
#[must_use]
pub fn window_settings<F: Default>(&self) -> Settings<F> {
self.window_settings_with_flags(F::default())
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn window_settings_with_flags<F>(&self, flags: F) -> Settings<F> {
let (width, height) = self.suggested_size();
let width = u32::from(width);
let height = u32::from(height);
Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2),
size_limits: Limits::NONE
.min_height(height as f32 + APPLET_PADDING as f32 * 2.0)
.max_height(height as f32 + APPLET_PADDING as f32 * 2.0)
.min_width(width as f32 + APPLET_PADDING as f32 * 2.0)
.max_width(width as f32 + APPLET_PADDING as f32 * 2.0),
resizable: None,
..Default::default()
}),
..cosmic::settings_with_flags(flags)
}
}
#[must_use]
pub fn icon_button<'a, Message: 'static>(
&self,
icon_name: &'a str,
) -> widget::Button<'a, Message, Renderer> {
cosmic::widget::button(cosmic::theme::Button::Text)
.icon(
cosmic::theme::Svg::Symbolic,
icon_name,
self.suggested_size().0,
)
.padding(8)
}
// TODO popup container which tracks the size of itself and requests the popup to resize to match
pub fn popup_container<'a, Message: 'static>(
&self,
content: impl Into<Element<'a, Message, Renderer>>,
) -> Container<'a, Message, Renderer> {
let (vertical_align, horizontal_align) = match self.anchor {
PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
PanelAnchor::Top => (Vertical::Top, Horizontal::Center),
PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center),
};
Container::<Message, Renderer>::new(Container::<Message, Renderer>::new(content).style(
cosmic::theme::Container::custom(|theme| Appearance {
text_color: Some(theme.cosmic().background.on.into()),
background: Some(Color::from(theme.cosmic().background.base).into()),
border_radius: 12.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
}),
))
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(horizontal_align)
.align_y(vertical_align)
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn get_popup_settings(
&self,
parent: window::Id,
id: window::Id,
size: Option<(u32, u32)>,
width_padding: Option<i32>,
height_padding: Option<i32>,
) -> SctkPopupSettings {
let (width, height) = self.suggested_size();
let pixel_offset = 8;
let (offset, anchor, gravity) = match self.anchor {
PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right),
PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left),
PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom),
PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top),
};
SctkPopupSettings {
parent,
id,
positioner: SctkPositioner {
anchor,
gravity,
offset,
size,
anchor_rect: Rectangle {
x: 0,
y: 0,
width: width_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(width),
height: height_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(height),
},
reactive: true,
constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y
..Default::default()
},
parent_size: None,
grab: true,
}
}
}

View file

@ -5,16 +5,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
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"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "applet", "tokio"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "f0cfe09" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "f0cfe09" }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
cosmic-applet = { path = "../applet" }
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio"] }
ron = "0.8"
futures = "0.3"
futures-util = "0.3"
once_cell = "1.9"
xdg = "2.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
calloop = "0.10"
nix = "0.26"
shlex = "1.1.0"
@ -29,3 +30,5 @@ i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester
i18n-embed-fl = "0.6"
rust-embed = "6.3"
url = "2.3.1"
rust-embed-utils = "7.5.0"
rand = "0.8.5"

View file

@ -1,10 +1,6 @@
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use crate::config;
use crate::config::AppListConfig;
use crate::config::APP_ID;
use crate::fl;
use crate::toplevel_subscription::toplevel_subscription;
use crate::toplevel_subscription::ToplevelRequest;
@ -13,46 +9,56 @@ use calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cosmic::applet::cosmic_panel_config::PanelAnchor;
use cosmic::applet::CosmicAppletHelper;
use cosmic::cosmic_config;
use cosmic::cosmic_config::Config;
use cosmic::iced;
use cosmic::iced::subscription::events_with;
use cosmic::iced::wayland::actions::data_device::DataFromMimeType;
use cosmic::iced::wayland::actions::data_device::DndIcon;
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_popup;
use cosmic::iced::widget::{column, dnd_source, mouse_listener, row, text, Column, Row};
use cosmic::iced::widget::dnd_listener;
use cosmic::iced::widget::vertical_rule;
use cosmic::iced::widget::vertical_space;
use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row};
use cosmic::iced::Color;
use cosmic::iced::Limits;
use cosmic::iced::Settings;
use cosmic::iced::{window, Application, Command, Subscription};
use cosmic::iced_native as native;
use cosmic::iced_native::alignment::Horizontal;
use cosmic::iced_native::subscription::events_with;
use cosmic::iced_native::widget::vertical_space;
use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_runtime::core::event;
use cosmic::iced_sctk::commands::data_device::accept_mime_type;
use cosmic::iced_sctk::commands::data_device::finish_dnd;
use cosmic::iced_sctk::commands::data_device::request_dnd_data;
use cosmic::iced_sctk::commands::data_device::set_actions;
use cosmic::iced_sctk::commands::data_device::start_drag;
use cosmic::iced_sctk::layout::Limits;
use cosmic::iced_sctk::settings::InitialSurface;
use cosmic::iced_sctk::widget::dnd_listener;
use cosmic::iced_sctk::widget::vertical_rule;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::widget::divider;
use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription;
use cosmic::widget::rectangle_tracker::RectangleTracker;
use cosmic::widget::rectangle_tracker::RectangleUpdate;
use cosmic::{Element, Theme};
use cosmic_applet::cosmic_panel_config::PanelAnchor;
use cosmic_applet::CosmicAppletHelper;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry;
use futures::future::pending;
use iced::widget::container;
use iced::Alignment;
use iced::Background;
use iced::Length;
use itertools::Itertools;
use native::event;
use rand::{thread_rng, Rng};
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use tokio::time::sleep;
use url::Url;
static MIME_TYPE: &str = "text/uri-list";
@ -71,15 +77,12 @@ pub fn run() -> cosmic::iced::Result {
CosmicAppList::run(Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
iced_settings: cosmic::iced_native::window::Settings {
..Default::default()
},
autosize: true,
size_limits: Limits::NONE
.min_height(1)
.min_width(1)
.max_height(h)
.max_width(w),
.min_height(1.0)
.min_width(1.0)
.max_height(h as f32)
.max_width(w as f32),
..Default::default()
}),
..Default::default()
@ -144,19 +147,19 @@ impl DockItem {
let dots = (0..toplevels.len())
.into_iter()
.map(|_| {
container(vertical_space(Length::Units(0)))
container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius)
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(Box::new(
|theme| container::Appearance {
text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color(
theme.cosmic().on_bg_color().into(),
)),
border_radius: 4.0,
border_radius: 4.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
))
)))
.into()
})
.collect_vec();
@ -179,12 +182,12 @@ impl DockItem {
.into(),
};
let mut icon_button = cosmic::widget::button(Button::Text)
let icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper])
.padding(8);
let icon_button = if interaction_enabled {
dnd_source(
mouse_listener(
mouse_area(
icon_button
.on_press(
toplevels
@ -222,7 +225,7 @@ struct DndOffer {
struct CosmicAppList {
theme: Theme,
popup: Option<(window::Id, DockItem)>,
surface_id_ctr: u32,
surface_id_ctr: u128,
subscription_ctr: u32,
item_ctr: u32,
active_list: Vec<DockItem>,
@ -262,6 +265,8 @@ enum Message {
DndData(PathBuf),
StartListeningForDnd,
StopListeningForDnd,
IncrementSubscriptionCtr,
ConfigUpdated(AppListConfig),
}
#[derive(Debug, Clone, Default)]
@ -363,22 +368,20 @@ impl Application for CosmicAppList {
fn new(_flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default();
let mut favorite_ctr = 0;
let self_ = CosmicAppList {
let mut self_ = CosmicAppList {
favorite_list: desktop_info_for_app_ids(config.favorites.clone())
.into_iter()
.map(|e| {
favorite_ctr += 1;
DockItem {
id: favorite_ctr,
toplevels: Default::default(),
desktop_info: e,
}
.enumerate()
.map(|(favorite_ctr, e)| DockItem {
id: favorite_ctr as u32,
toplevels: Default::default(),
desktop_info: e,
})
.collect(),
config,
..Default::default()
};
self_.item_ctr = self_.favorite_list.len() as u32;
(self_, Command::none())
}
@ -405,11 +408,11 @@ impl Application for CosmicAppList {
};
self.surface_id_ctr += 1;
let new_id = window::Id::new(self.surface_id_ctr);
let new_id = window::Id(self.surface_id_ctr);
self.popup = Some((new_id, toplevel_group.clone()));
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -440,13 +443,16 @@ impl Application for CosmicAppList {
self.favorite_list.push(entry);
}
let _ = self.config.add_favorite(id);
self.config
.add_favorite(id, &Config::new(APP_ID, 1).unwrap());
if let Some((popup_id, _toplevel)) = self.popup.take() {
return destroy_popup(popup_id);
}
}
Message::UnFavorite(id) => {
let _ = self.config.remove_favorite(id.clone());
let _ = self
.config
.remove_favorite(id.clone(), &Config::new(APP_ID, 1).unwrap());
if let Some(i) = self
.favorite_list
.iter()
@ -506,7 +512,10 @@ impl Application for CosmicAppList {
.position(|t| t.desktop_info.id == id)
{
let t = self.favorite_list.remove(pos);
let _ = self.config.remove_favorite(t.desktop_info.id.clone());
let _ = self.config.remove_favorite(
t.desktop_info.id.clone(),
&Config::new(APP_ID, 1).unwrap(),
);
Some((true, t))
} else {
None
@ -514,7 +523,7 @@ impl Application for CosmicAppList {
})
{
self.surface_id_ctr += 1;
let icon_id = window::Id::new(self.surface_id_ctr);
let icon_id = window::Id(self.surface_id_ctr);
self.dnd_source = Some((icon_id, toplevel_group.clone(), DndAction::empty()));
return start_drag(
vec![MIME_TYPE.to_string()],
@ -523,7 +532,7 @@ impl Application for CosmicAppList {
} else {
DndAction::Copy
},
window::Id::new(0),
window::Id(0),
Some(DndIcon::Custom(icon_id)),
Box::new(toplevel_group.clone()),
);
@ -629,7 +638,10 @@ impl Application for CosmicAppList {
.and_then(|o| o.dock_item.map(|i| (i, o.preview_index)))
{
self.item_ctr += 1;
let _ = self.config.add_favorite(dock_item.desktop_info.id.clone());
let _ = self.config.add_favorite(
dock_item.desktop_info.id.clone(),
&Config::new(APP_ID, 1).unwrap(),
);
if let Some((pos, is_favorite)) = self
.active_list
.iter()
@ -689,11 +701,26 @@ impl Application for CosmicAppList {
self.toplevel_sender.replace(tx);
}
ToplevelUpdate::Finished => {
self.subscription_ctr += 1;
for t in &mut self.favorite_list {
t.toplevels.clear();
}
self.active_list.clear();
let subscription_ctr = self.subscription_ctr;
let mut rng = thread_rng();
let rand_d = rng.gen_range(0..100);
return Command::perform(
async move {
if let Some(millis) = 2u64
.checked_pow(subscription_ctr)
.and_then(|d| d.checked_add(rand_d))
{
sleep(Duration::from_millis(millis)).await;
} else {
pending::<()>().await;
}
},
|_| Message::IncrementSubscriptionCtr,
);
}
ToplevelUpdate::RemoveToplevel(handle) => {
for t in self
@ -765,6 +792,49 @@ impl Application for CosmicAppList {
Message::StopListeningForDnd => {
self.is_listening_for_dnd = false;
}
Message::IncrementSubscriptionCtr => {
self.subscription_ctr += 1;
}
Message::ConfigUpdated(config) => {
self.config = config;
let mut new_list: Vec<_> = desktop_info_for_app_ids(self.config.favorites.clone())
.into_iter()
.map(|e| {
self.item_ctr += 1;
DockItem {
id: self.item_ctr,
toplevels: Default::default(),
desktop_info: e,
}
})
.collect();
for item in &mut new_list {
if let Some(old_item) = self
.favorite_list
.iter()
.position(|i| i.desktop_info.id == item.desktop_info.id)
{
let old_item = self.favorite_list.swap_remove(old_item);
*item = old_item;
} else if let Some(old_item) = self
.active_list
.iter()
.position(|i| i.desktop_info.id == item.desktop_info.id)
{
let old_item = self.active_list.remove(old_item);
*item = old_item;
}
}
for item in self.favorite_list.drain(..) {
self.active_list.push(item);
}
self.favorite_list = new_list;
}
}
Command::none()
}
@ -970,7 +1040,7 @@ impl Application for CosmicAppList {
),
};
if self.popup.is_some() {
mouse_listener(content)
mouse_area(content)
.on_right_release(Message::ClosePopup)
.on_press(Message::ClosePopup)
.into()
@ -981,48 +1051,58 @@ impl Application for CosmicAppList {
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event)),
toplevel_subscription(self.subscription_ctr).map(|e| Message::Toplevel(e.1)),
events_with(|e, _| match e {
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
event::wayland::Event::Seat(e, seat),
)) => match e {
cosmic::iced_runtime::core::Event::PlatformSpecific(
event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)),
) => match e {
event::wayland::SeatEvent::Enter => Some(Message::NewSeat(seat)),
event::wayland::SeatEvent::Leave => Some(Message::RemovedSeat(seat)),
},
// XXX Must be done to catch a finished drag after the source is removed
// (for now, the source is removed when the drag starts)
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
event::wayland::Event::DataSource(
cosmic::iced_runtime::core::Event::PlatformSpecific(
event::PlatformSpecific::Wayland(event::wayland::Event::DataSource(
event::wayland::DataSourceEvent::DndFinished
| event::wayland::DataSourceEvent::Cancelled,
),
)) => Some(Message::DragFinished),
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
event::wayland::Event::DndOffer(event::wayland::DndOfferEvent::Enter {
mime_types,
..
}),
)) => {
)),
) => Some(Message::DragFinished),
cosmic::iced_runtime::core::Event::PlatformSpecific(
event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
event::wayland::DndOfferEvent::Enter { mime_types, .. },
)),
) => {
if mime_types.iter().any(|m| m == MIME_TYPE) {
Some(Message::StartListeningForDnd)
} else {
None
}
}
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
event::wayland::Event::DndOffer(
cosmic::iced_runtime::core::Event::PlatformSpecific(
event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
event::wayland::DndOfferEvent::Leave
| event::wayland::DndOfferEvent::DropPerformed,
),
)) => Some(Message::StopListeningForDnd),
)),
) => Some(Message::StopListeningForDnd),
_ => None,
}),
rectangle_tracker_subscription(0).map(|(_, update)| Message::Rectangle(update)),
rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)),
cosmic_config::config_subscription(0, Cow::from(APP_ID), 1).map(|(_, config)| {
match config {
Ok(config) => Message::ConfigUpdated(config),
Err((errors, config)) => {
for error in errors {
log::error!("{:?}", error);
}
Message::ConfigUpdated(config)
}
}
}),
])
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -1030,9 +1110,9 @@ impl Application for CosmicAppList {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|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,21 +1,23 @@
use anyhow::anyhow;
use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
use cosmic::cosmic_config::{self, Config, ConfigGet, ConfigSet, CosmicConfigEntry};
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)]
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
pub enum TopLevelFilter {
#[default]
ActiveWorkspace,
ConfiguredOutput,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, CosmicConfigEntry)]
pub struct AppListConfig {
pub filter_top_levels: Option<TopLevelFilter>,
pub favorites: Vec<String>,
@ -42,26 +44,17 @@ impl AppListConfig {
.map_err(|err| anyhow!("Failed to parse config file: {}", err))
}
pub fn add_favorite(&mut self, id: String) -> anyhow::Result<()> {
pub fn add_favorite(&mut self, id: String, config: &Config) {
if !self.favorites.contains(&id) {
self.favorites.push(id);
let _ = self.write_entry(&config);
}
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(())
pub fn remove_favorite(&mut self, id: String, config: &Config) {
if let Some(pos) = self.favorites.iter().position(|e| e == &id) {
self.favorites.remove(pos);
let _ = self.write_entry(&config);
}
}
}

View file

@ -11,7 +11,7 @@ use futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
StreamExt,
};
use std::{fmt::Debug, hash::Hash};
use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
use crate::toplevel_handler::toplevel_handler;
@ -26,31 +26,45 @@ pub enum State {
Waiting(
UnboundedReceiver<ToplevelUpdate>,
calloop::channel::Sender<ToplevelRequest>,
JoinHandle<()>,
),
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);
});
(
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)
async fn start_listening<I: Copy>(id: I, mut state: State) -> ((I, ToplevelUpdate), State) {
loop {
let (update, new_state) = match state {
State::Ready => {
let (calloop_tx, calloop_rx) = calloop::channel::channel();
let (toplevel_tx, toplevel_rx) = unbounded();
let handle = std::thread::spawn(move || {
toplevel_handler(toplevel_tx, calloop_rx);
});
(
Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
State::Waiting(toplevel_rx, calloop_tx, handle),
)
}
},
State::Finished => iced::futures::future::pending().await,
State::Waiting(mut rx, tx, handle) => {
if handle.is_finished() {
return ((id, ToplevelUpdate::Finished), State::Finished);
}
match rx.next().await {
Some(u) => (Some((id, u)), State::Waiting(rx, tx, handle)),
None => {
let _ = tx.send(ToplevelRequest::Exit);
(Some((id, ToplevelUpdate::Finished)), State::Finished)
}
}
}
State::Finished => iced::futures::future::pending().await,
};
if let Some(update) = update {
return (update, new_state);
} else {
state = new_state;
}
}
}

View file

@ -10,6 +10,14 @@ icon-loader = { version = "0.3.6", features = ["gtk"] }
libpulse-binding = "2.26.0"
libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
log = "0.4.14"
pretty_env_logger = "0.4.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.17.1"

View file

@ -0,0 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"

View file

@ -0,0 +1,6 @@
output = Output
input = Input
show-media-controls = Show Media Controls on Top Panel
sound-settings = Sound Settings...
disconnected = PulseAudio Disconnected
no-device = No device selected

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 = 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,11 +1,13 @@
mod localize;
use cosmic::iced::widget;
use cosmic::iced_native::alignment::Horizontal;
use cosmic::iced_native::layout::Limits;
use cosmic::iced::Limits;
use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::theme::Svg;
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
use cosmic::widget::{button, divider, icon};
use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::{
self,
@ -20,12 +22,16 @@ use iced::widget::container;
use iced::Color;
mod pulse;
use crate::localize::localize;
use crate::pulse::DeviceInfo;
use libpulse_binding::volume::VolumeLinear;
pub fn main() -> cosmic::iced::Result {
pretty_env_logger::init();
// Prepare i18n
localize();
let helper = CosmicAppletHelper::default();
Audio::run(helper.window_settings())
}
@ -43,7 +49,7 @@ struct Audio {
theme: Theme,
popup: Option<window::Id>,
show_media_controls_in_top_panel: bool,
id_ctr: u32,
id_ctr: u128,
}
#[derive(Debug, PartialEq, Eq)]
@ -93,7 +99,7 @@ impl Application for Audio {
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -101,10 +107,10 @@ impl Application for Audio {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}))
}
fn update(&mut self, message: Message) -> Command<Message> {
@ -117,21 +123,21 @@ impl Application for Audio {
conn.send(pulse::Message::UpdateConnection);
}
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
None,
);
popup_settings.positioner.size_limits = Limits::NONE
.min_height(1)
.min_width(1)
.max_width(400)
.max_height(1080);
.min_height(1.0)
.min_width(1.0)
.max_width(400.0)
.max_height(1080.0);
if let Some(conn) = self.pulse_state.connection() {
conn.send(pulse::Message::GetDefaultSink);
@ -268,7 +274,7 @@ impl Application for Audio {
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
if id == window::Id(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
@ -291,20 +297,18 @@ impl Application for Audio {
.0 * 100.0;
let audio_content = if audio_disabled {
column![text("PulseAudio Disconnected")
column![text(fl!("disconnected"))
.width(Length::Fill)
.horizontal_alignment(Horizontal::Center)
.size(24),]
} else {
column![
row![
icon("audio-volume-high-symbolic", 32)
.width(Length::Units(24))
.height(Length::Units(24))
.style(Svg::Symbolic),
icon("audio-volume-high-symbolic", 24).style(Svg::Symbolic),
slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
.width(Length::FillPortion(5)),
text(format!("{}%", out_f64.round()))
.size(16)
.width(Length::FillPortion(1))
.horizontal_alignment(Horizontal::Right)
]
@ -312,13 +316,11 @@ impl Application for Audio {
.align_items(Alignment::Center)
.padding([8, 24]),
row![
icon("audio-input-microphone-symbolic", 32)
.width(Length::Units(24))
.height(Length::Units(24))
.style(Svg::Symbolic),
icon("audio-input-microphone-symbolic", 24).style(Svg::Symbolic),
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
.width(Length::FillPortion(5)),
text(format!("{}%", in_f64.round()))
.size(16)
.width(Length::FillPortion(1))
.horizontal_alignment(Horizontal::Right)
]
@ -330,7 +332,7 @@ impl Application for Audio {
.width(Length::Fill),
revealer(
self.is_open == IsOpen::Output,
"Output",
fl!("output"),
match &self.current_output {
Some(output) => pretty_name(output.description.clone()),
None => String::from("No device selected"),
@ -348,10 +350,10 @@ impl Application for Audio {
),
revealer(
self.is_open == IsOpen::Input,
"Input",
fl!("input"),
match &self.current_input {
Some(input) => pretty_name(input.description.clone()),
None => String::from("No device selected"),
None => fl!("no-device"),
},
self.inputs
.clone()
@ -372,17 +374,20 @@ impl Application for Audio {
container(divider::horizontal::light())
.padding([12, 24])
.width(Length::Fill),
container(toggler(
Some("Show Media Controls on Top Panel".into()),
self.show_media_controls_in_top_panel,
Message::ToggleMediaControlsInTopPanel,
))
container(
toggler(
Some(fl!("show-media-controls")),
self.show_media_controls_in_top_panel,
Message::ToggleMediaControlsInTopPanel,
)
.text_size(14)
)
.padding([0, 24]),
container(divider::horizontal::light())
.padding([12, 24])
.width(Length::Fill),
button(APPLET_BUTTON_THEME)
.text("Sound Settings...")
button(applet_button_theme())
.custom(vec![text(fl!("sound-settings")).size(14).into()])
.padding([8, 24])
.width(Length::Fill)
]
@ -398,19 +403,19 @@ impl Application for Audio {
fn revealer(
open: bool,
title: &str,
title: String,
selected: String,
options: Vec<(String, String)>,
toggle: Message,
mut change: impl FnMut(String) -> Message + 'static,
) -> widget::Column<Message, Renderer> {
) -> widget::Column<'static, Message, Renderer> {
if open {
options.iter().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|col, (id, name)| {
col.push(
button(APPLET_BUTTON_THEME)
.custom(vec![text(name).into()])
button(applet_button_theme())
.custom(vec![text(name).size(14).into()])
.on_press(change(id.clone()))
.width(Length::Fill)
.padding([8, 48]),
@ -424,14 +429,14 @@ fn revealer(
fn revealer_head(
_open: bool,
title: &str,
title: String,
selected: String,
toggle: Message,
) -> widget::Button<Message, Renderer> {
button(APPLET_BUTTON_THEME)
) -> widget::Button<'static, Message, Renderer> {
button(applet_button_theme())
.custom(vec![
text(title).width(Length::Fill).into(),
text(selected).into(),
text(title).width(Length::Fill).size(14).into(),
text(selected).size(10).into(),
])
.padding([8, 24])
.width(Length::Fill)

View file

@ -1,8 +1,8 @@
use cosmic::iced_native::subscription::{self, Subscription};
use std::cell::RefCell;
use std::{rc::Rc, thread};
extern crate libpulse_binding as pulse;
use cosmic::iced::{subscription, Subscription};
//use futures::channel::mpsc;
use libpulse_binding::{
callbacks::ListResult,
@ -15,71 +15,80 @@ use libpulse_binding::{
proplist::Proplist,
volume::ChannelVolumes,
};
pub fn connect() -> Subscription<Event> {
struct Connect;
subscription::unfold(
std::any::TypeId::of::<Connect>(),
State::Init,
|state| async move {
match state {
State::Init => {
let PulseHandle {
to_pulse,
from_pulse,
} = PulseHandle::new();
(
Some(Event::Init(Connection(to_pulse))),
State::Connecting(from_pulse),
)
}
// Waiting for Connection to succeed
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
Some(Message::Connected) => {
(Some(Event::Connected), State::Connected(from_pulse))
}
Some(Message::Disconnected) => {
(Some(Event::Disconnected), State::Connecting(from_pulse))
}
Some(m) => {
panic!("Unexpected message: {:?}", m);
}
None => {
panic!("Pulse Sender dropped, something has gone wrong!");
}
},
State::Connected(mut from_pulse) => {
// This is where we match messages from the pulse server to pass to the gui
match from_pulse.recv().await {
Some(Message::SetSinks(sinks)) => (
Some(Event::MessageReceived(Message::SetSinks(sinks))),
State::Connected(from_pulse),
),
Some(Message::SetSources(sources)) => (
Some(Event::MessageReceived(Message::SetSources(sources))),
State::Connected(from_pulse),
),
Some(Message::SetDefaultSink(sink)) => (
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
State::Connected(from_pulse),
),
Some(Message::SetDefaultSource(source)) => (
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
State::Connected(from_pulse),
),
Some(Message::Disconnected) => {
(Some(Event::Disconnected), State::Connecting(from_pulse))
}
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
_ => (None, State::Connected(from_pulse)),
}
|mut state| async move {
loop {
let (update, new_state) = connection(state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
},
)
}
async fn connection(state: State) -> (Option<Event>, State) {
match state {
State::Init => {
let PulseHandle {
to_pulse,
from_pulse,
} = PulseHandle::new();
(
Some(Event::Init(Connection(to_pulse))),
State::Connecting(from_pulse),
)
}
// Waiting for Connection to succeed
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
Some(Message::Connected) => (Some(Event::Connected), State::Connected(from_pulse)),
Some(Message::Disconnected) => {
(Some(Event::Disconnected), State::Connecting(from_pulse))
}
Some(m) => {
panic!("Unexpected message: {:?}", m);
}
None => {
panic!("Pulse Sender dropped, something has gone wrong!");
}
},
State::Connected(mut from_pulse) => {
// This is where we match messages from the pulse server to pass to the gui
match from_pulse.recv().await {
Some(Message::SetSinks(sinks)) => (
Some(Event::MessageReceived(Message::SetSinks(sinks))),
State::Connected(from_pulse),
),
Some(Message::SetSources(sources)) => (
Some(Event::MessageReceived(Message::SetSources(sources))),
State::Connected(from_pulse),
),
Some(Message::SetDefaultSink(sink)) => (
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
State::Connected(from_pulse),
),
Some(Message::SetDefaultSource(source)) => (
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
State::Connected(from_pulse),
),
Some(Message::Disconnected) => {
(Some(Event::Disconnected), State::Connecting(from_pulse))
}
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
_ => (None, State::Connected(from_pulse)),
}
}
}
}
// #[derive(Debug)]
enum State {
Init,

View file

@ -5,11 +5,12 @@ edition = "2021"
[dependencies]
once_cell = "1.16.0"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
futures = "0.3"
zbus = { version = "3.5", default-features = false, features = ["tokio"] }
zbus = { version = "3.13", default-features = false, features = ["tokio"] }
log = "0.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"

View file

@ -10,19 +10,19 @@ use crate::upower_device::{device_subscription, DeviceDbusEvent};
use crate::upower_kbdbacklight::{
kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate,
};
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
use cosmic::iced::alignment::Horizontal;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Color;
use cosmic::iced::{
widget::{column, container, row, slider, text},
window, Alignment, Application, Command, Length, Subscription,
};
use cosmic::iced_native::layout::Limits;
use cosmic::iced_runtime::core::layout::Limits;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Svg;
use cosmic::widget::{button, divider, icon, toggler};
use cosmic::{Element, Theme};
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use log::error;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
@ -58,7 +58,7 @@ struct CosmicBatteryApplet {
kbd_brightness: f64,
screen_brightness: f64,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
screen_sender: Option<UnboundedSender<ScreenBacklightRequest>>,
kbd_sender: Option<UnboundedSender<KeyboardBacklightRequest>>,
applet_helper: CosmicAppletHelper,
@ -144,21 +144,21 @@ impl Application for CosmicBatteryApplet {
}
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
None,
);
popup_settings.positioner.size_limits = Limits::NONE
.max_width(372)
.min_width(300)
.min_height(200)
.max_height(1080);
.max_width(372.0)
.min_width(300.0)
.min_height(200.0)
.max_height(1080.0);
if let Some(tx) = self.power_profile_sender.as_ref() {
let _ = tx.send(PowerProfileRequest::Get);
}
@ -213,13 +213,13 @@ impl Application for CosmicBatteryApplet {
Command::none()
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
if id == window::Id(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
} else {
let name = text(fl!("battery")).size(18);
let name = text(fl!("battery")).size(14);
let description = text(
if "battery-full-charging-symbolic" == self.icon_name
|| "battery-full-charged-symbolic" == self.icon_name
@ -234,15 +234,12 @@ impl Application for CosmicBatteryApplet {
)
},
)
.size(12);
.size(10);
self.applet_helper
.popup_container(
column![
row![
icon(&*self.icon_name, 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon(&*self.icon_name, 24).style(Svg::Symbolic),
column![name, description]
]
.padding([0, 24])
@ -251,11 +248,11 @@ impl Application for CosmicBatteryApplet {
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("battery")).size(14),
text(fl!("battery-desc")).size(12)
text(fl!("battery-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
@ -270,11 +267,11 @@ impl Application for CosmicBatteryApplet {
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Battery))
.width(Length::Fill),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("balanced")).size(14),
text(fl!("balanced-desc")).size(12)
text(fl!("balanced-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
@ -289,11 +286,11 @@ impl Application for CosmicBatteryApplet {
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Balanced))
.width(Length::Fill),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("performance")).size(14),
text(fl!("performance-desc")).size(12)
text(fl!("performance-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
@ -311,42 +308,42 @@ impl Application for CosmicBatteryApplet {
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
container(toggler(fl!("max-charge"), self.charging_limit, |_| {
Message::SetChargingLimit(!self.charging_limit)
}))
container(
toggler(fl!("max-charge"), self.charging_limit, |_| {
Message::SetChargingLimit(!self.charging_limit)
})
.text_size(14)
.width(Length::Fill)
)
.padding([0, 24])
.width(Length::Fill),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
row![
icon("display-brightness-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("display-brightness-symbolic", 24).style(Svg::Symbolic),
slider(
1..=100,
(self.screen_brightness * 100.0) as i32,
Message::SetScreenBrightness
),
text(format!("{:.0}%", self.screen_brightness * 100.0))
.width(Length::Units(40))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
.spacing(12),
row![
icon("keyboard-brightness-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("keyboard-brightness-symbolic", 24).style(Svg::Symbolic),
slider(
0..=100,
(self.kbd_brightness * 100.0) as i32,
Message::SetKbdBrightness
),
text(format!("{:.0}%", self.kbd_brightness * 100.0))
.width(Length::Units(40))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
@ -354,8 +351,11 @@ impl Application for CosmicBatteryApplet {
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
button(APPLET_BUTTON_THEME)
.custom(vec![text(fl!("power-settings")).width(Length::Fill).into()])
button(applet_button_theme())
.custom(vec![text(fl!("power-settings"))
.size(14)
.width(Length::Fill)
.into()])
.on_press(Message::OpenBatterySettings)
.width(Length::Fill)
.padding([8, 24])
@ -369,35 +369,38 @@ impl Application for CosmicBatteryApplet {
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
device_subscription(0).map(|(_, event)| match event {
DeviceDbusEvent::Update {
icon_name,
percent,
time_to_empty,
} => Message::Update {
device_subscription(0).map(
|(
_,
DeviceDbusEvent::Update {
icon_name,
percent,
time_to_empty,
},
)| Message::Update {
icon_name,
percent,
time_to_empty,
},
),
kbd_backlight_subscription(0).map(|event| match event {
(_, KeyboardBacklightUpdate::Update(b)) => Message::UpdateKbdBrightness(b),
(_, KeyboardBacklightUpdate::Init(tx, b)) => Message::InitKbdBacklight(tx, b),
}),
kbd_backlight_subscription(0).map(|(_, event)| match event {
KeyboardBacklightUpdate::Update(b) => Message::UpdateKbdBrightness(b),
KeyboardBacklightUpdate::Init(tx, b) => Message::InitKbdBacklight(tx, b),
screen_backlight_subscription(0).map(|e| match e {
(_, ScreenBacklightUpdate::Update(b)) => Message::UpdateScreenBrightness(b),
(_, ScreenBacklightUpdate::Init(tx, b)) => Message::InitScreenBacklight(tx, b),
}),
screen_backlight_subscription(0).map(|(_, event)| match event {
ScreenBacklightUpdate::Update(b) => Message::UpdateScreenBrightness(b),
ScreenBacklightUpdate::Init(tx, b) => Message::InitScreenBacklight(tx, b),
}),
power_profile_subscription(0).map(|(_, event)| match event {
PowerProfileUpdate::Update { profile } => Message::Profile(profile),
PowerProfileUpdate::Init(tx, p) => Message::InitProfile(p, tx),
PowerProfileUpdate::Error(e) => Message::Errored(e), // TODO: handle error
power_profile_subscription(0).map(|event| match event {
(_, PowerProfileUpdate::Update { profile }) => Message::Profile(profile),
(_, PowerProfileUpdate::Init(tx, p)) => Message::InitProfile(p, tx),
(_, PowerProfileUpdate::Error(e)) => Message::Errored(e), // TODO: handle error
}),
])
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Message {
@ -405,9 +408,9 @@ impl Application for CosmicBatteryApplet {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|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

@ -78,7 +78,20 @@ pub async fn backlight() -> io::Result<Option<Backlight>> {
pub fn screen_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, ScreenBacklightUpdate)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
}
async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, ScreenBacklightUpdate), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
pub enum State {

View file

@ -19,7 +19,7 @@
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
use cosmic::iced;
use cosmic::iced_native::subscription;
use cosmic::iced::subscription;
use std::fmt::Debug;
use std::hash::Hash;
use tokio::sync::mpsc::UnboundedReceiver;
@ -114,7 +114,9 @@ pub async fn set_power_profile(daemon: PowerDaemonProxy<'_>, power: Power) -> Re
pub fn power_profile_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, PowerProfileUpdate)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| {
start_listening_loop(id, state)
})
}
#[derive(Debug)]
@ -124,6 +126,19 @@ pub enum State {
Finished,
}
async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, PowerProfileUpdate), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, PowerProfileUpdate)>, State) {
match state {
State::Ready => {
@ -189,14 +204,11 @@ async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, PowerProfi
}
}
Some(PowerProfileRequest::Set(profile)) => {
if set_power_profile(power_proxy, profile).await.is_ok() {
(
Some((id, PowerProfileUpdate::Update { profile })),
State::Waiting(conn, rx),
)
} else {
(None, State::Waiting(conn, rx))
}
let _ = set_power_profile(power_proxy, profile).await;
(
Some((id, PowerProfileUpdate::Update { profile })),
State::Waiting(conn, rx),
)
}
None => (None, State::Finished),
}

View file

@ -153,7 +153,7 @@ trait Device {
pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, DeviceDbusEvent)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
}
#[derive(Debug)]
@ -174,6 +174,19 @@ async fn display_device() -> zbus::Result<DeviceProxy<'static>> {
.await
}
async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, DeviceDbusEvent), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, DeviceDbusEvent)>, State) {
match state {
State::Ready => {

View file

@ -8,6 +8,7 @@ use iced::subscription;
use std::{fmt::Debug, hash::Hash};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use zbus::dbus_proxy;
#[dbus_proxy(
default_service = "org.freedesktop.UPower",
interface = "org.freedesktop.UPower.KbdBacklight",
@ -35,7 +36,20 @@ trait KbdBacklight {
pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, KeyboardBacklightUpdate)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
}
async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, KeyboardBacklightUpdate), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
#[derive(Debug)]

View file

@ -8,10 +8,11 @@ license = "GPL-3.0-or-later"
once_cell = "1.16.0"
bluer = { version = "0.15", features = ["bluetoothd", "id"] }
futures-util = "0.3.21"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
cosmic-applet = { path = "../applet" }
futures = "0.3"
log = "0.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
itertools = "0.10.3"
slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] }

View file

@ -1,19 +1,14 @@
use crate::bluetooth::{BluerDeviceStatus, BluerRequest, BluerState};
use cosmic::applet::APPLET_BUTTON_THEME;
use cosmic::iced_style;
use cosmic::{
applet::CosmicAppletHelper,
iced::{
wayland::{
popup::{destroy_popup, get_popup},
},
wayland::popup::{destroy_popup, get_popup},
widget::{column, container, row, scrollable, text, Column},
Alignment, Application, Color, Command, Length, Subscription,
},
iced_native::{
iced_runtime::core::{
alignment::{Horizontal, Vertical},
layout::Limits,
renderer::BorderRadius,
window,
},
iced_style::{application, button::StyleSheet},
@ -21,6 +16,7 @@ use cosmic::{
widget::{button, divider, icon, toggler},
Element, Theme,
};
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
@ -38,7 +34,7 @@ struct CosmicBluetoothApplet {
icon_name: String,
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
applet_helper: CosmicAppletHelper,
bluer_state: BluerState,
bluer_sender: Option<Sender<BluerRequest>>,
@ -87,11 +83,11 @@ impl Application for CosmicBluetoothApplet {
} else {
// TODO request update of state maybe
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -99,10 +95,10 @@ impl Application for CosmicBluetoothApplet {
);
popup_settings.positioner.size_limits = Limits::NONE
.min_height(1)
.min_width(1)
.max_height(800)
.max_width(400);
.min_height(1.0)
.min_width(1.0)
.max_height(800.0)
.max_width(400.0);
let tx = self.bluer_sender.as_ref().cloned();
return Command::batch(vec![
Command::perform(
@ -275,17 +271,17 @@ impl Application for CosmicBluetoothApplet {
Command::none()
}
fn view(&self, id: window::Id) -> Element<Message> {
let button_style = Button::Custom {
active: |t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0),
let button_style = || Button::Custom {
active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.active(&Button::Text)
},
hover: |t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0),
}),
hover: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.hovered(&Button::Text)
},
}),
};
if id == window::Id::new(0) {
if id == window::Id(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
@ -320,20 +316,15 @@ impl Application for CosmicBluetoothApplet {
}
BluerDeviceStatus::Paired => {}
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
row = row.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
);
row = row.push(icon("process-working-symbolic", 24).style(Svg::Symbolic));
}
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
};
known_bluetooth = known_bluetooth.push(
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row.into()])
.style(APPLET_BUTTON_THEME)
.style(applet_button_theme())
.on_press(match dev.status {
BluerDeviceStatus::Connected => {
Message::Request(BluerRequest::DisconnectDevice(dev.address))
@ -392,25 +383,20 @@ impl Application for CosmicBluetoothApplet {
text(fl!("other-devices"))
.size(14)
.width(Length::Fill)
.height(Length::Units(24))
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(
icon(dropdown_icon, 14)
.style(Svg::Symbolic)
.width(Length::Units(14))
.height(Length::Units(14)),
)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(),
]
.into(),
)
.padding([8, 24])
.style(button_style.clone())
.style(button_style())
.on_press(Message::ToggleVisibleDevices(!self.show_visible_devices));
content = content.push(available_connections_btn);
let mut list_column: Vec<Element<'_, Message>> =
@ -420,6 +406,7 @@ impl Application for CosmicBluetoothApplet {
let row = column![
icon(device.icon.as_str(), 16).style(Svg::Symbolic),
text(&device.name)
.size(14)
.horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center)
.width(Length::Fill),
@ -435,20 +422,20 @@ impl Application for CosmicBluetoothApplet {
.horizontal_alignment(Horizontal::Center)
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
.size(32),
.size(22),
row![
button(Button::Secondary)
.custom(
vec![text(fl!("cancel"))
.size(14)
.width(Length::Fill)
.height(Length::Units(24))
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style.clone())
.style(button_style())
.on_press(Message::Cancel)
.width(Length::Fill),
button(Button::Secondary)
@ -456,13 +443,13 @@ impl Application for CosmicBluetoothApplet {
vec![text(fl!("confirm"))
.size(14)
.width(Length::Fill)
.height(Length::Units(24))
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style.clone())
.style(button_style())
.on_press(Message::Confirm)
.width(Length::Fill),
]
@ -494,7 +481,7 @@ impl Application for CosmicBluetoothApplet {
.align_items(Alignment::Center)
.spacing(12);
visible_devices = visible_devices.push(
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row.width(Length::Fill).into()])
.on_press(Message::Request(BluerRequest::PairDevice(
dev.address.clone(),
@ -516,7 +503,7 @@ impl Application for CosmicBluetoothApplet {
if item_counter > 10 {
content = content.push(
scrollable(Column::with_children(list_column)).height(Length::Units(300)),
scrollable(Column::with_children(list_column)).height(Length::Fixed(300.0)),
);
} else {
content = content.push(Column::with_children(list_column));
@ -526,11 +513,11 @@ impl Application for CosmicBluetoothApplet {
}
fn subscription(&self) -> Subscription<Message> {
bluetooth_subscription(0).map(|e| Message::BluetoothEvent(e.1))
bluetooth_subscription(0).map(|(_, e)| Message::BluetoothEvent(e))
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -538,9 +525,11 @@ impl Application for CosmicBluetoothApplet {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}
}))
}
}

View file

@ -21,7 +21,7 @@ use tokio::{
pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, BluerEvent)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
}
pub enum State {
@ -30,6 +30,19 @@ pub enum State {
Finished,
}
async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, BluerEvent), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
async fn start_listening<I: Copy + Debug>(id: I, state: State) -> (Option<(I, BluerEvent)>, State) {
match state {
State::Ready => {

View file

@ -6,8 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
zbus = "3.4"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
zbus = "3.13"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
once_cell = "1"
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }

View file

@ -4,10 +4,10 @@ mod localize;
mod window;
use cosmic::{
applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper},
iced::{wayland::InitialSurface, Application, Settings},
iced_native::layout::Limits,
iced_runtime::core::layout::Limits,
};
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use window::*;
@ -21,10 +21,10 @@ pub fn main() -> cosmic::iced::Result {
InitialSurface::XdgWindow(w) => {
w.autosize = true;
w.size_limits = Limits::NONE
.min_height(1)
.max_height(200)
.min_width(1)
.max_width(1000);
.min_height(1.0)
.max_height(200.0)
.min_width(1.0)
.max_width(1000.0);
}
InitialSurface::None => unimplemented!(),
};

View file

@ -1,23 +1,22 @@
use crate::dbus::{self, PowerDaemonProxy};
use crate::fl;
use crate::graphics::{get_current_graphics, set_graphics, Graphics};
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced_native::alignment::Horizontal;
use cosmic::iced_native::Alignment;
use cosmic::iced::Color;
use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_runtime::core::Alignment;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::widget::icon;
use cosmic::{
applet::{cosmic_panel_config::PanelAnchor, APPLET_BUTTON_THEME},
iced::widget::{column, container, row, text},
iced::{self, Application, Command, Length},
iced_native::window,
iced_runtime::core::window,
theme::{Svg, Theme},
widget::{button, divider},
Element,
};
use cosmic_applet::{applet_button_theme, cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use zbus::Connection;
#[derive(Clone, Copy)]
@ -41,7 +40,7 @@ impl GraphicsMode {
pub struct Window {
popup: Option<window::Id>,
graphics_mode: Option<GraphicsMode>,
id_ctr: u32,
id_ctr: u128,
theme: Theme,
dbus: Option<(Connection, PowerDaemonProxy<'static>)>,
applet_helper: CosmicAppletHelper,
@ -92,7 +91,7 @@ impl Application for Window {
return destroy_popup(p);
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut commands = Vec::new();
if let Some((_, proxy)) = self.dbus.as_ref() {
@ -102,7 +101,7 @@ impl Application for Window {
));
}
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -184,7 +183,7 @@ impl Application for Window {
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
if id == window::Id(0) {
match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => self
.applet_helper
@ -206,6 +205,7 @@ impl Application for Window {
Some(Graphics::Hybrid) => fl!("hybrid"),
None => "".into(),
})
.size(14)
]
.spacing(8)
.padding([0, self.applet_helper.suggested_size().0 / 2])
@ -220,7 +220,7 @@ impl Application for Window {
}
} else {
let content_list = vec![
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14),
@ -256,7 +256,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Integrated))
.width(Length::Fill)
.into(),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),]
.width(Length::Fill),
@ -289,7 +289,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Nvidia))
.width(Length::Fill)
.into(),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14),
@ -325,7 +325,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Hybrid))
.width(Length::Fill)
.into(),
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14),
@ -369,7 +369,7 @@ impl Application for Window {
text(fl!("graphics-mode"))
.width(Length::Fill)
.horizontal_alignment(Horizontal::Center)
.size(24)
.size(14)
.into(),
container(divider::horizontal::light())
.padding([0, 12])
@ -385,7 +385,7 @@ impl Application for Window {
}
fn close_requested(&self, id: window::Id) -> Self::Message {
if id != window::Id::new(0) {
if id != window::Id(0) {
Message::PopupClosed(id)
} else {
unimplemented!();
@ -393,10 +393,10 @@ impl Application for Window {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().background.on.into(),
})
}))
}
fn should_exit(&self) -> bool {
@ -404,6 +404,6 @@ impl Application for Window {
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
}

View file

@ -9,11 +9,12 @@ once_cell = "1.16.0"
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "main" }
# cosmic-dbus-networkmanager = { path = "../../../dbus-settings-bindings/networkmanager" }
futures-util = "0.3.21"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
cosmic-applet = { path = "../applet" }
futures = "0.3"
zbus = { version = "3.7", default-features = false }
zbus = { version = "3.13", default-features = false }
log = "0.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
itertools = "0.10.3"
slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] }
@ -22,3 +23,5 @@ anyhow = "1.0"
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"
rust-embed = "6.3.0"
rust-embed-utils = "7.5.0"

View file

@ -1,15 +1,14 @@
use cosmic::iced_style;
use cosmic::iced_widget::Row;
use cosmic::{
applet::CosmicAppletHelper,
iced::{
wayland::popup::{destroy_popup, get_popup},
widget::{column, container, row, scrollable, text, text_input, Column},
Alignment, Application, Color, Command, Length, Subscription,
},
iced_native::{
iced_runtime::core::{
alignment::{Horizontal, Vertical},
layout::Limits,
renderer::BorderRadius,
window,
},
iced_style::{application, button::StyleSheet},
@ -17,9 +16,14 @@ use cosmic::{
widget::{button, divider, icon, toggler},
Element, Theme,
};
use cosmic_applet::CosmicAppletHelper;
use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState};
use futures::channel::mpsc::UnboundedSender;
use zbus::Connection;
use crate::network_manager::active_conns::active_conns_subscription;
use crate::network_manager::devices::devices_subscription;
use crate::network_manager::wireless_enabled::wireless_enabled_subscription;
use crate::network_manager::NetworkManagerState;
use crate::{
config, fl,
@ -77,13 +81,14 @@ struct CosmicNetworkApplet {
icon_name: String,
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
applet_helper: CosmicAppletHelper,
nm_state: NetworkManagerState,
// UI state
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
show_visible_networks: bool,
new_connection: Option<NewConnectionState>,
conn: Option<Connection>,
}
impl CosmicNetworkApplet {
@ -110,7 +115,7 @@ impl CosmicNetworkApplet {
}
#[derive(Debug, Clone)]
enum Message {
pub(crate) enum Message {
ActivateKnownWifi(String),
Disconnect(String),
TogglePopup,
@ -155,11 +160,11 @@ impl Application for CosmicNetworkApplet {
} else {
// TODO request update of state maybe
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -167,10 +172,10 @@ impl Application for CosmicNetworkApplet {
);
popup_settings.positioner.size_limits = Limits::NONE
.min_height(1)
.min_width(1)
.max_height(800)
.max_width(400);
.min_height(1.0)
.min_width(1.0)
.max_height(800.0)
.max_width(400.0);
return get_popup(popup_settings);
}
}
@ -193,10 +198,15 @@ impl Application for CosmicNetworkApplet {
}
}
Message::NetworkManagerEvent(event) => match event {
NetworkManagerEvent::Init { sender, state } => {
NetworkManagerEvent::Init {
conn,
sender,
state,
} => {
self.nm_sender.replace(sender);
self.nm_state = state;
self.update_icon_name();
self.conn = Some(conn);
}
NetworkManagerEvent::WiFiEnabled(state) => {
self.nm_state = state;
@ -337,17 +347,17 @@ impl Application for CosmicNetworkApplet {
Command::none()
}
fn view(&self, id: window::Id) -> Element<Message> {
let button_style = Button::Custom {
active: |t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0),
let button_style = || Button::Custom {
active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.active(&Button::Text)
},
hover: |t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0),
}),
hover: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0,
..t.hovered(&Button::Text)
},
}),
};
if id == window::Id::new(0) {
if id == window::Id(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
@ -362,7 +372,7 @@ impl Application for CosmicNetworkApplet {
for addr in ip_addresses {
ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(12)
.size(10)
.into(),
);
}
@ -412,8 +422,6 @@ impl Application for CosmicNetworkApplet {
let mut btn_content = vec![
icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
column![text(name).size(14), Column::with_children(ipv4)]
.width(Length::Fill)
@ -425,8 +433,6 @@ impl Application for CosmicNetworkApplet {
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
);
}
@ -441,9 +447,12 @@ impl Application for CosmicNetworkApplet {
};
known_wifi = known_wifi.push(
column![button(Button::Secondary)
.custom(btn_content)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.style(button_style.clone())
.style(button_style())
.on_press(Message::Disconnect(name.clone()))]
.align_items(Alignment::Center),
);
@ -454,8 +463,6 @@ impl Application for CosmicNetworkApplet {
let mut btn_content = vec![
icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
text(&known.ssid).size(14).width(Length::Fill).into(),
];
@ -464,17 +471,18 @@ impl Application for CosmicNetworkApplet {
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
);
}
let mut btn = button(Button::Secondary)
.custom(btn_content)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.width(Length::Fill)
.style(button_style.clone());
.style(button_style());
btn = match known.state {
DeviceState::Failed
| DeviceState::Unknown
@ -495,6 +503,7 @@ impl Application for CosmicNetworkApplet {
toggler(fl!("airplane-mode"), self.nm_state.airplane_mode, |m| {
Message::ToggleAirplaneMode(m)
})
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
@ -503,6 +512,7 @@ impl Application for CosmicNetworkApplet {
toggler(fl!("wifi"), self.nm_state.wifi_enabled, |m| {
Message::ToggleWiFi(m)
})
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
@ -523,25 +533,20 @@ impl Application for CosmicNetworkApplet {
text(fl!("visible-wireless-networks"))
.size(14)
.width(Length::Fill)
.height(Length::Units(24))
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(
icon(dropdown_icon, 14)
.style(Svg::Symbolic)
.width(Length::Units(14))
.height(Length::Units(14)),
)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(),
]
.into(),
)
.padding([8, 24])
.style(button_style.clone())
.style(button_style())
.on_press(Message::ToggleVisibleNetworks);
content = content.push(available_connections_btn);
if self.show_visible_networks {
@ -552,10 +557,7 @@ impl Application for CosmicNetworkApplet {
password,
} => {
let id = row![
icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
@ -565,7 +567,9 @@ impl Application for CosmicNetworkApplet {
content = content.push(id);
let col = column![
text(fl!("enter-password")),
text_input("", password, Message::Password)
text_input("", password)
.on_input(Message::Password)
.on_paste(Message::Password)
.on_submit(Message::SubmitPassword)
.password(),
container(text(fl!("router-wps-button"))).padding(8),
@ -590,10 +594,7 @@ impl Application for CosmicNetworkApplet {
}
NewConnectionState::Waiting(access_point) => {
let id = row![
icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
@ -601,10 +602,7 @@ impl Application for CosmicNetworkApplet {
.spacing(12);
let connecting = row![
id,
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("process-working-symbolic", 24).style(Svg::Symbolic),
]
.spacing(8)
.padding([0, 24]);
@ -612,10 +610,7 @@ impl Application for CosmicNetworkApplet {
}
NewConnectionState::Failure(access_point) => {
let id = row![
icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
@ -660,15 +655,12 @@ impl Application for CosmicNetworkApplet {
{
continue;
}
let button = button(button_style)
let button = button(button_style())
.custom(vec![row![
icon("network-wireless-symbolic", 16)
.style(Svg::Symbolic)
.width(Length::Units(16))
.height(Length::Units(16)),
icon("network-wireless-symbolic", 16).style(Svg::Symbolic),
text(&ap.ssid)
.size(14)
.height(Length::Units(24))
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
]
.align_items(Alignment::Center)
@ -680,7 +672,7 @@ impl Application for CosmicNetworkApplet {
list_col.push(button.into());
}
content = content.push(
scrollable(Column::with_children(list_col)).height(Length::Units(300)),
scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)),
);
}
}
@ -689,11 +681,25 @@ impl Application for CosmicNetworkApplet {
}
fn subscription(&self) -> Subscription<Message> {
network_manager_subscription(0).map(|(_, event)| Message::NetworkManagerEvent(event))
let network_sub =
network_manager_subscription(0).map(|e| Message::NetworkManagerEvent(e.1));
if let Some(conn) = self.conn.as_ref() {
Subscription::batch(vec![
network_sub,
active_conns_subscription(0, conn.clone())
.map(|e| Message::NetworkManagerEvent(e.1)),
devices_subscription(0, conn.clone()).map(|e| Message::NetworkManagerEvent(e.1)),
wireless_enabled_subscription(0, conn.clone())
.map(|e| Message::NetworkManagerEvent(e.1)),
])
} else {
network_sub
}
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -701,9 +707,11 @@ impl Application for CosmicNetworkApplet {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
application::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,56 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::iced::{self, subscription};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::StreamExt;
use log::error;
use std::fmt::Debug;
use std::hash::Hash;
use zbus::Connection;
pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
conn: Connection,
) -> iced::Subscription<(I, NetworkManagerEvent)> {
subscription::unfold(id, State::Continue(conn), move |mut state| async move {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
})
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening<I: Copy + Debug>(
id: I,
state: State,
) -> (Option<(I, NetworkManagerEvent)>, State) {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(e) => {
error!("Failed to connect to NetworkManager: {}", e);
return (None, State::Error);
}
};
let mut active_conns_changed = network_manager.receive_active_connections_changed().await;
active_conns_changed.next().await;
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
(
Some((id, NetworkManagerEvent::ActiveConns(new_state))),
State::Continue(conn),
)
}

View file

@ -0,0 +1,59 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::iced::{self, subscription};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use log::error;
use std::fmt::Debug;
use std::hash::Hash;
use zbus::Connection;
use futures::StreamExt;
pub fn devices_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
conn: Connection,
) -> iced::Subscription<(I, NetworkManagerEvent)> {
subscription::unfold(id, State::Continue(conn), move |mut state| async move {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
})
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening<I: Copy + Debug>(
id: I,
state: State,
) -> (Option<(I, NetworkManagerEvent)>, State) {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(e) => {
error!("Failed to connect to NetworkManager: {}", e);
return (None, State::Error);
}
};
let mut devices_changed = network_manager.receive_devices_changed().await;
devices_changed.next().await;
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
(
Some((
id,
NetworkManagerEvent::WirelessAccessPoints(new_state),
)),
State::Continue(conn),
)
}

View file

@ -1,5 +1,8 @@
pub mod active_conns;
pub mod available_wifi;
pub mod current_networks;
pub mod devices;
pub mod wireless_enabled;
use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::Deref, time::Duration};
@ -32,7 +35,9 @@ use self::{
pub fn network_manager_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<(I, NetworkManagerEvent)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
subscription::unfold(id, State::Ready, move |state| {
start_listening_loop(id, state)
})
}
#[derive(Debug)]
@ -42,6 +47,19 @@ pub enum State {
Finished,
}
pub async fn start_listening_loop<I: Copy + Debug>(
id: I,
mut state: State,
) -> ((I, NetworkManagerEvent), State) {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
}
async fn start_listening<I: Copy + Debug>(
id: I,
state: State,
@ -59,6 +77,7 @@ async fn start_listening<I: Copy + Debug>(
Some((
id,
NetworkManagerEvent::Init {
conn: conn.clone(),
sender: tx,
state: nm_state,
},
@ -72,289 +91,402 @@ async fn start_listening<I: Copy + Debug>(
Err(_) => return (None, State::Finished),
};
let mut active_conns_changed = tokio::time::sleep(Duration::from_secs(5))
.then(|_| async { network_manager.receive_active_connections_changed().await })
.await;
let mut devices_changed = network_manager.receive_devices_changed().await;
let mut wireless_enabled_changed =
network_manager.receive_wireless_enabled_changed().await;
let mut req = rx.next().boxed().fuse();
let (update, should_exit) = futures::select! {
req = req => {
match req {
Some(NetworkManagerRequest::Disconnect(ssid)) => {
let mut success = false;
for c in network_manager.active_connections().await.unwrap_or_default() {
if c.id().await.unwrap_or_default() == ssid {
if let Ok(_) = network_manager.deactivate_connection(&c).await {
success = true;
}
}
let (update, should_exit) = match rx.next().await {
Some(NetworkManagerRequest::Disconnect(ssid)) => {
let mut success = false;
for c in network_manager
.active_connections()
.await
.unwrap_or_default()
{
if c.id().await.unwrap_or_default() == ssid {
if let Ok(_) = network_manager.deactivate_connection(&c).await {
success = true;
}
(Some((id,
NetworkManagerEvent::RequestResponse {
}
}
(
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Disconnect(ssid.clone()),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false)
},
)),
false,
)
}
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
// wifi
let mut success = network_manager
.set_wireless_enabled(!airplane_mode)
.await
.is_ok();
// bluetooth
success = success
&& Command::new("rfkill")
.arg(if airplane_mode { "block" } else { "unblock" })
.arg("bluetooth")
.output()
.await
.is_ok();
let response = NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
};
(Some((id, response)), false)
}
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
let response = NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetAirplaneMode(enabled),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
};
(Some((id, response)), false)
}
Some(NetworkManagerRequest::Password(ssid, password)) => {
let s = match NetworkManagerSettings::new(&conn).await {
Ok(s) => s,
Err(_) => return (None, State::Finished),
};
let mut status = (None, false);
// First try known connections
// TODO more convenient methods of managing settings
for c in s.list_connections().await.unwrap_or_default() {
let mut settings = match c.get_settings().await.ok() {
Some(s) => s,
None => continue,
};
let cur_ssid = settings
.get("802-11-wireless")
.and_then(|w| w.get("ssid"))
.cloned()
.and_then(|ssid| ssid.try_into().ok())
.and_then(|ssid| String::from_utf8(ssid).ok());
if cur_ssid.as_ref() != Some(&ssid) {
continue;
}
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
// wifi
let mut success = network_manager.set_wireless_enabled(!airplane_mode).await.is_ok();
// bluetooth
success = success && Command::new("rfkill")
.arg(if airplane_mode { "block" } else { "unblock" })
.arg("bluetooth")
.output()
.await
.is_ok();
let response = NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
};
(Some((id, response)), false)
}
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
let response = NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetAirplaneMode(enabled),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
};
(Some((id, response)), false)
}
Some(NetworkManagerRequest::Password(ssid, password)) => {
let s = match NetworkManagerSettings::new(&conn).await {
Ok(s) => s,
Err(_) => return (None, State::Finished),
};
let mut status = (None, false);
// First try known connections
// TODO more convenient methods of managing settings
for c in s.list_connections().await.unwrap_or_default() {
let mut settings = match c.get_settings().await.ok() {
Some(s) => s,
None => continue,
};
let cur_ssid = settings
.get("802-11-wireless")
.and_then(|w| w.get("ssid"))
.cloned()
.and_then(|ssid| ssid.try_into().ok())
.and_then(|ssid| String::from_utf8(ssid).ok());
if cur_ssid.as_ref() != Some(&ssid) {
continue;
}
let mut secrets = match
c.get_secrets("802-11-wireless-security")
.await {
Ok(s) => s,
_ => HashMap::from([("802-11-wireless-security".into(), HashMap::from([
("psk".into(), Value::Str(password.as_str().into()).to_owned()),
("key-mgmt".into(), Value::Str("wpa-psk".into()).to_owned())
]))]),
};
if let Some(s) = secrets.get_mut("802-11-wireless-security") {
s.insert("psk".into(), Value::Str(password.clone().into()).to_owned());
drop(s);
settings.extend(secrets.into_iter());
let settings: HashMap<_, _> = settings.iter().map(|(k, v)| {
let map = (k.as_str(), v.iter()
let mut secrets = match c.get_secrets("802-11-wireless-security").await {
Ok(s) => s,
_ => HashMap::from([(
"802-11-wireless-security".into(),
HashMap::from([
(
"psk".into(),
Value::Str(password.as_str().into()).to_owned(),
),
("key-mgmt".into(), Value::Str("wpa-psk".into()).to_owned()),
]),
)]),
};
if let Some(s) = secrets.get_mut("802-11-wireless-security") {
s.insert("psk".into(), Value::Str(password.clone().into()).to_owned());
drop(s);
settings.extend(secrets.into_iter());
let settings: HashMap<_, _> = settings
.iter()
.map(|(k, v)| {
let map = (
k.as_str(),
v.iter()
.map(|(k, v)| (k.as_str(), v.into()))
.collect::<HashMap<_, _>>());
map
}).collect();
let updated = c.update(settings).await;
if updated.is_ok() {
let success = if let Ok(path) = network_manager.deref().activate_connection(c.deref().path(), &ObjectPath::try_from("/").unwrap(), &ObjectPath::try_from("/").unwrap()).await {
// let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1));
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
let state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
let s = if let enums::ActiveConnectionState::Activating = state {
if let Ok(Some(s)) = timeout(Duration::from_secs(10), active.receive_state_changed().await.next()).await {
s.get().await.unwrap_or_default().into()
} else {
state
}
} else {
state
};
matches!(s, enums::ActiveConnectionState::Activated)
} else {
false
};
status = (Some((id, NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(ssid.clone(), password.clone()),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false);
}
break;
}
}
// create a connection
if status.0.is_none() {
for device in network_manager.devices().await.ok().unwrap_or_default() {
if matches!(device.device_type().await.unwrap_or(DeviceType::Other), DeviceType::Wifi) {
let conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([
("802-11-wireless".into(), HashMap::from([
("ssid".into(), Value::Array(ssid.as_bytes().into())),
])),
("connection".into(), HashMap::from([
("id".into(), Value::Str(ssid.as_str().into())),
("type".into(), Value::Str("802-11-wireless".into())),
])),
("802-11-wireless-security".into(), HashMap::from([
("psk".into(), Value::Str(password.as_str().into())),
("key-mgmt".into(), Value::Str("wpa-psk".into()))
]))
]);
let success = if let Ok((_, path)) = network_manager.add_and_activate_connection(conn_settings, device.path(), &ObjectPath::try_from("/").unwrap()).await {
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
let state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
let s = if let enums::ActiveConnectionState::Activating = state {
if let Ok(Some(s)) = timeout(Duration::from_secs(10), active.receive_state_changed().await.next()).await {
s.get().await.unwrap_or_default().into()
} else {
state
}
} else {
state
};
matches!(s, enums::ActiveConnectionState::Activated)
} else {
false
};
status = (Some((id, NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(ssid.clone(), password.clone()),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false);
break;
}
}
}
if status.0.is_none() {
status = (Some((id, NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(ssid, password),
success: false,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false);
}
status
}
Some(NetworkManagerRequest::SelectAccessPoint(ssid)) => {
let s = match NetworkManagerSettings::new(&conn).await {
Ok(s) => s,
Err(_) => return (None, State::Finished),
};
// find known connection with matching ssid and activate
let mut status = (None, false);
for c in s.list_connections().await.unwrap_or_default() {
let settings = match c.get_settings().await.ok() {
Some(s) => s,
None => continue,
};
let cur_ssid = settings
.get("802-11-wireless")
.and_then(|w| w.get("ssid"))
.cloned()
.and_then(|ssid| ssid.try_into().ok())
.and_then(|ssid| String::from_utf8(ssid).ok());
if cur_ssid.as_ref() != Some(&ssid) {
continue;
}
let success = if let Ok(path) = network_manager.deref().activate_connection(c.deref().path(), &ObjectPath::try_from("/").unwrap(), &ObjectPath::try_from("/").unwrap()).await {
.collect::<HashMap<_, _>>(),
);
map
})
.collect();
let updated = c.update(settings).await;
if updated.is_ok() {
let success = if let Ok(path) = network_manager
.deref()
.activate_connection(
c.deref().path(),
&ObjectPath::try_from("/").unwrap(),
&ObjectPath::try_from("/").unwrap(),
)
.await
{
// let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1));
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
let mut state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
while let enums::ActiveConnectionState::Activating = state {
if let Ok(Some(s)) = timeout(Duration::from_secs(20), active.receive_state_changed().await.next()).await {
state = s.get().await.unwrap_or_default().into();
let active = ActiveConnectionProxy::builder(&conn)
.path(path)
.unwrap()
.destination(dummy.destination())
.unwrap()
.interface(dummy.interface())
.unwrap()
.build()
.await
.unwrap();
let state = enums::ActiveConnectionState::from(
active.state().await.unwrap_or_default(),
);
let s = if let enums::ActiveConnectionState::Activating = state
{
if let Ok(Some(s)) = timeout(
Duration::from_secs(10),
active.receive_state_changed().await.next(),
)
.await
{
s.get().await.unwrap_or_default().into()
} else {
break;
state
}
} else {
state
};
matches!(state, enums::ActiveConnectionState::Activated)
matches!(s, enums::ActiveConnectionState::Activated)
} else {
false
};
status = (Some((id, NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false);
status = (
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(
ssid.clone(),
password.clone(),
),
success,
state: NetworkManagerState::new(&conn)
.await
.unwrap_or_default(),
},
)),
false,
);
}
break;
}
}
// create a connection
if status.0.is_none() {
for device in network_manager.devices().await.ok().unwrap_or_default() {
if matches!(
device.device_type().await.unwrap_or(DeviceType::Other),
DeviceType::Wifi
) {
let conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> =
HashMap::from([
(
"802-11-wireless".into(),
HashMap::from([(
"ssid".into(),
Value::Array(ssid.as_bytes().into()),
)]),
),
(
"connection".into(),
HashMap::from([
("id".into(), Value::Str(ssid.as_str().into())),
(
"type".into(),
Value::Str("802-11-wireless".into()),
),
]),
),
(
"802-11-wireless-security".into(),
HashMap::from([
(
"psk".into(),
Value::Str(password.as_str().into()),
),
("key-mgmt".into(), Value::Str("wpa-psk".into())),
]),
),
]);
let success = if let Ok((_, path)) = network_manager
.add_and_activate_connection(
conn_settings,
device.path(),
&ObjectPath::try_from("/").unwrap(),
)
.await
{
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
let active = ActiveConnectionProxy::builder(&conn)
.path(path)
.unwrap()
.destination(dummy.destination())
.unwrap()
.interface(dummy.interface())
.unwrap()
.build()
.await
.unwrap();
let state = enums::ActiveConnectionState::from(
active.state().await.unwrap_or_default(),
);
let s = if let enums::ActiveConnectionState::Activating = state
{
if let Ok(Some(s)) = timeout(
Duration::from_secs(10),
active.receive_state_changed().await.next(),
)
.await
{
s.get().await.unwrap_or_default().into()
} else {
state
}
} else {
state
};
matches!(s, enums::ActiveConnectionState::Activated)
} else {
false
};
status = (
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(
ssid.clone(),
password.clone(),
),
success,
state: NetworkManagerState::new(&conn)
.await
.unwrap_or_default(),
},
)),
false,
);
break;
}
}
}
if status.0.is_none() {
status = (Some((id, NetworkManagerEvent::RequestResponse {
if status.0.is_none() {
status = (
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Password(ssid, password),
success: false,
state: NetworkManagerState::new(&conn)
.await
.unwrap_or_default(),
},
)),
false,
);
}
status
}
Some(NetworkManagerRequest::SelectAccessPoint(ssid)) => {
let s = match NetworkManagerSettings::new(&conn).await {
Ok(s) => s,
Err(_) => return (None, State::Finished),
};
// find known connection with matching ssid and activate
let mut status = (None, false);
for c in s.list_connections().await.unwrap_or_default() {
let settings = match c.get_settings().await.ok() {
Some(s) => s,
None => continue,
};
let cur_ssid = settings
.get("802-11-wireless")
.and_then(|w| w.get("ssid"))
.cloned()
.and_then(|ssid| ssid.try_into().ok())
.and_then(|ssid| String::from_utf8(ssid).ok());
if cur_ssid.as_ref() != Some(&ssid) {
continue;
}
let success = if let Ok(path) = network_manager
.deref()
.activate_connection(
c.deref().path(),
&ObjectPath::try_from("/").unwrap(),
&ObjectPath::try_from("/").unwrap(),
)
.await
{
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
let active = ActiveConnectionProxy::builder(&conn)
.path(path)
.unwrap()
.destination(dummy.destination())
.unwrap()
.interface(dummy.interface())
.unwrap()
.build()
.await
.unwrap();
let mut state = enums::ActiveConnectionState::from(
active.state().await.unwrap_or_default(),
);
while let enums::ActiveConnectionState::Activating = state {
if let Ok(Some(s)) = timeout(
Duration::from_secs(20),
active.receive_state_changed().await.next(),
)
.await
{
state = s.get().await.unwrap_or_default().into();
} else {
break;
}
}
matches!(state, enums::ActiveConnectionState::Activated)
} else {
false
};
status = (
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
success,
state: NetworkManagerState::new(&conn)
.await
.unwrap_or_default(),
},
)),
false,
);
break;
}
if status.0.is_none() {
status = (
Some((
id,
NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
success: false,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})), false);
}
status
}
None => {
(None, true)
}
}}
_ = active_conns_changed.next().boxed().fuse() => {
(Some((id, NetworkManagerEvent::ActiveConns(NetworkManagerState::new(&conn).await.unwrap_or_default()))), false)
}
_ = devices_changed.next().boxed().fuse() => {
let devices = network_manager.devices().await.ok().unwrap_or_default();
let wireless_access_point_futures: Vec<_> = devices.into_iter().map(|device| async move {
if let Ok(Some(SpecificDevice::Wireless(wireless_device))) =
device.downcast_to_device().await
{
handle_wireless_device(wireless_device).await.unwrap_or_default()
} else {
Vec::new()
}
}).collect();
let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len());
for f in wireless_access_point_futures {
wireless_access_points.append(&mut f.await);
state: NetworkManagerState::new(&conn)
.await
.unwrap_or_default(),
},
)),
false,
);
}
(Some((id, NetworkManagerEvent::WirelessAccessPoints(NetworkManagerState::new(&conn).await.unwrap_or_default()))), false)
}
enabled = wireless_enabled_changed.next().boxed().fuse() => {
let update = if let Some(update) = enabled {
if let Ok(_) = update.get().await {
Some((id, NetworkManagerEvent::WiFiEnabled(NetworkManagerState::new(&conn).await.unwrap_or_default())))
}
else {
None
}
} else {
None
};
(update, false)
status
}
None => (None, true),
};
drop(active_conns_changed);
drop(wireless_enabled_changed);
drop(req);
(
update,
if should_exit {
@ -385,6 +517,7 @@ pub enum NetworkManagerEvent {
success: bool,
},
Init {
conn: Connection,
sender: UnboundedSender<NetworkManagerRequest>,
state: NetworkManagerState,
},

View file

@ -0,0 +1,56 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::iced::{self, subscription};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::StreamExt;
use log::error;
use std::fmt::Debug;
use std::hash::Hash;
use zbus::Connection;
pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
conn: Connection,
) -> iced::Subscription<(I, NetworkManagerEvent)> {
subscription::unfold(id, State::Continue(conn), move |mut state| async move {
loop {
let (update, new_state) = start_listening(id, state).await;
state = new_state;
if let Some(update) = update {
return (update, state);
}
}
})
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening<I: Copy + Debug>(
id: I,
state: State,
) -> (Option<(I, NetworkManagerEvent)>, State) {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(e) => {
error!("Failed to connect to NetworkManager: {}", e);
return (None, State::Error);
}
};
let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await;
wireless_enabled_changed.next().await;
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
(
Some((id, NetworkManagerEvent::WiFiEnabled(new_state))),
State::Continue(conn),
)
}

View file

@ -6,5 +6,6 @@ license = "GPL-3.0-or-later"
[dependencies]
icon-loader = { version = "0.3.6", features = ["gtk"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
nix = "0.24.1"

View file

@ -1,14 +1,13 @@
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
use cosmic::iced::wayland::{
popup::{destroy_popup, get_popup},
};
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::{
widget::{button, column, row, text, Row, Space},
window, Alignment, Application, Color, Command, Length, Subscription,
};
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_widget::Button;
use cosmic::theme::Svg;
use cosmic::widget::{divider, icon, toggler};
use cosmic::Renderer;
@ -27,7 +26,7 @@ struct Notifications {
theme: Theme,
icon_name: String,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
do_not_disturb: bool,
notifications: Vec<Vec<String>>,
}
@ -61,7 +60,7 @@ impl Application for Notifications {
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -69,10 +68,10 @@ impl Application for Notifications {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}))
}
fn subscription(&self) -> Subscription<Message> {
@ -86,11 +85,11 @@ impl Application for Notifications {
destroy_popup(p)
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -112,7 +111,7 @@ impl Application for Notifications {
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
if id == window::Id(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
@ -164,11 +163,9 @@ impl Application for Notifications {
}
// todo put into libcosmic doing so will fix the row_button's boarder radius
fn row_button(
mut content: Vec<Element<Message>>,
) -> cosmic::iced_native::widget::Button<Message, Renderer> {
content.insert(0, Space::with_width(Length::Units(24)).into());
content.push(Space::with_width(Length::Units(24)).into());
fn row_button(mut content: Vec<Element<Message>>) -> Button<Message, Renderer> {
content.insert(0, Space::with_width(Length::Fixed(24.0)).into());
content.push(Space::with_width(Length::Fixed(24.0)).into());
button(
Row::with_children(content)
@ -176,8 +173,8 @@ fn row_button(
.align_items(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Units(36))
.style(APPLET_BUTTON_THEME)
.height(Length::Fixed(36.0))
.style(applet_button_theme())
}
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {

View file

@ -9,7 +9,15 @@ icon-loader = { version = "0.3.6", features = ["gtk"] }
libpulse-binding = "2.26.0"
libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
nix = "0.26.1"
zbus = "3.7"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
nix = "0.26.2"
zbus = "3.13"
logind-zbus = "3.1"
# 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.17.1"

View file

@ -0,0 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"

View file

@ -0,0 +1,21 @@
power = Power
settings = Settings...
lock-screen = Lock Screen
lock-screen-shortcut = Super + Escape
log-out = Log Out
log-out-shortcut = Ctrl + Alt + Delete
suspend = Suspend
restart = Restart
shutdown = Shutdown
confirm = Confirm
cancel = Cancel
confirm-question =
Are you sure? { $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] { shutdown }
[lock-screen] Locking the screen
[log-out] Logging out
*[other] The selected action
} will continue in { $countdown } seconds.

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 = 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,18 +1,28 @@
use std::collections::HashMap;
use std::process;
use std::time::Duration;
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::event::wayland::{self, LayerEvent};
use cosmic::iced::event::PlatformSpecific;
use cosmic::iced::subscription::events_with;
use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced_native::layout::Limits;
use cosmic::iced_native::widget::Space;
use cosmic::iced_runtime::core::layout::Limits;
use cosmic::iced_sctk::commands::layer_surface::{
destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity,
};
use cosmic::iced_widget::mouse_area;
use cosmic::widget::{button, divider, icon};
use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::Color;
use cosmic::iced::{
widget::{self, column, container, row, Row},
widget::{self, column, container, row, space::Space, text, Row},
window, Alignment, Application, Command, Length, Subscription,
};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::{self, Svg};
use cosmic::{Element, Theme};
@ -20,9 +30,11 @@ use logind_zbus::manager::ManagerProxy;
use logind_zbus::session::{SessionProxy, SessionType};
use logind_zbus::user::UserProxy;
use nix::unistd::getuid;
use tokio::time::sleep;
use zbus::Connection;
pub mod cosmic_session;
mod localize;
pub mod session_manager;
use crate::cosmic_session::CosmicSessionProxy;
@ -39,19 +51,28 @@ struct Power {
icon_name: String,
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
action_to_confirm: Option<(window::Id, PowerAction)>,
}
#[derive(Debug, Clone)]
enum Message {
#[derive(Debug, Clone, Copy)]
enum PowerAction {
Lock,
LogOut,
Suspend,
Restart,
Shutdown,
}
#[derive(Debug, Clone)]
enum Message {
Timeout(window::Id),
Action(PowerAction),
TogglePopup,
Settings,
Ignore,
Confirm,
Cancel,
Closed(window::Id),
Zbus(Result<(), zbus::Error>),
}
@ -72,26 +93,34 @@ impl Application for Power {
}
fn title(&self) -> String {
String::from("Power")
fl!("power")
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
Message::Ignore
fn close_requested(&self, id: window::Id) -> Self::Message {
Message::Closed(id)
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}))
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
events_with(|e, _status| match e {
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
wayland::Event::Layer(LayerEvent::Unfocused, ..),
)) => Some(Message::Cancel),
// cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
// wayland::Event::Seat(wayland::SeatEvent::Leave, _),
// )) => Some(Message::Cancel),
_ => None,
})
}
fn update(&mut self, message: Message) -> Command<Message> {
@ -101,21 +130,21 @@ impl Application for Power {
destroy_popup(p)
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
None,
);
popup_settings.positioner.size_limits = Limits::NONE
.min_width(100)
.min_height(100)
.max_height(400)
.max_width(500);
.min_width(100.0)
.min_height(100.0)
.max_height(400.0)
.max_width(500.0);
get_popup(popup_settings)
}
}
@ -123,51 +152,117 @@ impl Application for Power {
let _ = process::Command::new("cosmic-settings").spawn();
Command::none()
}
Message::Lock => Command::perform(lock(), Message::Zbus),
Message::LogOut => Command::perform(log_out(), Message::Zbus),
Message::Suspend => Command::perform(suspend(), Message::Zbus),
Message::Restart => Command::perform(restart(), Message::Zbus),
Message::Shutdown => Command::perform(shutdown(), Message::Zbus),
Message::Action(action) => {
self.id_ctr += 1;
let id = window::Id(self.id_ctr);
self.action_to_confirm = Some((id, action));
return Command::batch(vec![
Command::perform(sleep(Duration::from_secs(60)), move |_| {
Message::Timeout(id)
}),
get_layer_surface(SctkLayerSurfaceSettings {
id,
keyboard_interactivity: KeyboardInteractivity::None,
anchor: Anchor::all(),
namespace: "dialog".into(),
size: Some((None, None)),
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
..Default::default()
}),
]);
}
Message::Zbus(result) => {
if let Err(e) = result {
eprintln!("cosmic-applet-power ERROR: '{}'", e);
}
Command::none()
}
Message::Ignore => Command::none(),
Message::Confirm => {
if let Some((id, a)) = self.action_to_confirm.take() {
Command::batch(vec![
destroy_layer_surface(id),
match a {
PowerAction::Lock => Command::perform(lock(), Message::Zbus),
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus),
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus),
PowerAction::Restart => Command::perform(restart(), Message::Zbus),
PowerAction::Shutdown => Command::perform(shutdown(), Message::Zbus),
},
])
} else {
Command::none()
}
}
Message::Cancel => {
if let Some((id, _)) = self.action_to_confirm.take() {
return destroy_layer_surface(id);
}
Command::none()
}
Message::Closed(id) => {
if let Some((surface_id, _)) = self.action_to_confirm {
if id == surface_id {
self.action_to_confirm = None;
return destroy_layer_surface(id);
}
}
if id == window::Id(0) {
process::exit(0);
}
Command::none()
}
Message::Timeout(id) => {
if let Some((surface_id, a)) = self.action_to_confirm {
if id == surface_id {
self.action_to_confirm = None;
return Command::batch(vec![
destroy_layer_surface(id),
match a {
PowerAction::Lock => Command::perform(lock(), Message::Zbus),
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus),
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus),
PowerAction::Restart => Command::perform(restart(), Message::Zbus),
PowerAction::Shutdown => {
Command::perform(shutdown(), Message::Zbus)
}
},
]);
}
}
Command::none()
}
}
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
} else {
let settings = row_button(vec!["Settings...".into()]).on_press(Message::Settings);
if matches!(self.popup, Some(p) if p == id) {
let settings =
row_button(vec![text(fl!("settings")).size(14).into()]).on_press(Message::Settings);
let session = column![
row_button(vec![
text_icon("system-lock-screen-symbolic", 24).into(),
"Lock Screen".into(),
text(fl!("lock-screen")).size(14).into(),
Space::with_width(Length::Fill).into(),
"Super + Escape".into(),
text(fl!("lock-screen-shortcut")).size(14).into(),
])
.on_press(Message::Lock),
.on_press(Message::Action(PowerAction::Lock)),
row_button(vec![
text_icon("system-log-out-symbolic", 24).into(),
"Log Out".into(),
text(fl!("log-out")).size(14).into(),
Space::with_width(Length::Fill).into(),
"Ctrl + Alt + Delete".into(),
text(fl!("log-out-shortcut")).size(14).into(),
])
.on_press(Message::LogOut),
.on_press(Message::Action(PowerAction::LogOut)),
];
let power = row![
power_buttons("system-lock-screen-symbolic", "Suspend").on_press(Message::Suspend),
power_buttons("system-restart-symbolic", "Restart").on_press(Message::Restart),
power_buttons("system-shutdown-symbolic", "Shutdown").on_press(Message::Shutdown),
power_buttons("system-lock-screen-symbolic", fl!("suspend"))
.on_press(Message::Action(PowerAction::Suspend)),
power_buttons("system-restart-symbolic", fl!("restart"))
.on_press(Message::Action(PowerAction::Restart)),
power_buttons("system-shutdown-symbolic", fl!("shutdown"))
.on_press(Message::Action(PowerAction::Shutdown)),
]
.spacing(24)
.padding([0, 24]);
@ -188,6 +283,65 @@ impl Application for Power {
.padding([8, 0]);
self.applet_helper.popup_container(content).into()
} else if matches!(self.action_to_confirm, Some((c_id, _)) if c_id == id) {
let action = match self.action_to_confirm.as_ref().unwrap().1 {
PowerAction::Lock => "lock-screen",
PowerAction::LogOut => "log-out",
PowerAction::Suspend => "suspend",
PowerAction::Restart => "restart",
PowerAction::Shutdown => "shutdown",
};
// TODO actual countdown
let content = column![
text(fl!(
"confirm-question",
HashMap::from_iter(vec![("action", action), ("countdown", "60")])
))
.size(16),
row![
button(theme::Button::Primary)
.custom(vec![text(fl!("confirm")).size(14).into()])
.on_press(Message::Confirm),
button(theme::Button::Primary)
.custom(vec![text(fl!("cancel")).size(14).into()])
.on_press(Message::Cancel),
]
.spacing(24)
]
.align_items(Alignment::Center)
.spacing(12)
.padding(24);
mouse_area(
container(
container(content)
.style(cosmic::theme::Container::custom(|theme| {
cosmic::iced_style::container::Appearance {
text_color: Some(theme.cosmic().background.on.into()),
background: Some(
Color::from(theme.cosmic().background.base).into(),
),
border_radius: 12.0.into(),
border_width: 2.0,
border_color: theme.cosmic().bg_divider().into(),
}
}))
.width(Length::Shrink)
.height(Length::Shrink),
)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fill)
.height(Length::Fill),
)
.on_press(Message::Cancel)
.on_right_press(Message::Cancel)
.on_middle_press(Message::Cancel)
.into()
} else {
self.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
}
}
}
@ -195,7 +349,7 @@ impl Application for Power {
// ### UI Helplers
fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Renderer> {
button(APPLET_BUTTON_THEME)
button(applet_button_theme())
.custom(vec![Row::with_children(content)
.spacing(4)
.align_items(Alignment::Center)
@ -204,14 +358,14 @@ fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Rendere
.padding([8, 24])
}
fn power_buttons<'a>(name: &'a str, text: &'a str) -> widget::Button<'a, Message, Renderer> {
fn power_buttons<'a>(name: &'a str, msg: String) -> widget::Button<'a, Message, Renderer> {
widget::button(
column![text_icon(name, 40), text]
column![text_icon(name, 40), text(msg).size(14)]
.spacing(4)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Units(76))
.height(Length::Fixed(76.0))
.style(theme::Button::Text)
}

View file

@ -6,6 +6,7 @@ license = "GPL-3.0-or-later"
[dependencies]
icon-loader = { version = "0.3.6", features = ["gtk"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
nix = "0.24.1"
chrono = { version = "0.4.23", features = ["clock"] }

View file

@ -1,18 +1,18 @@
use cosmic::applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Limits;
use cosmic::iced::{
time,
wayland::InitialSurface,
widget::{button, column, text, vertical_space},
window, Alignment, Application, Color, Command, Length, Rectangle, Subscription,
};
use cosmic::iced_sctk::layout::Limits;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::theme;
use cosmic::{
widget::{icon, rectangle_tracker::*},
Element, Theme,
};
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use chrono::{DateTime, Local, Timelike};
use std::time::Duration;
@ -22,10 +22,8 @@ pub fn main() -> cosmic::iced::Result {
let mut settings = helper.window_settings();
match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => {
s.iced_settings.min_size = Some((1, 1));
s.iced_settings.max_size = None;
s.autosize = true;
s.size_limits = Limits::NONE.min_height(1).min_width(1);
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
}
_ => {}
};
@ -36,7 +34,7 @@ struct Time {
applet_helper: CosmicAppletHelper,
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
id_ctr: u128,
update_at: Every,
now: DateTime<Local>,
msg: String,
@ -90,7 +88,7 @@ impl Application for Time {
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -98,10 +96,10 @@ impl Application for Time {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}))
}
fn subscription(&self) -> Subscription<Message> {
@ -120,7 +118,7 @@ impl Application for Time {
.expect("Setting nanoseconds to 0 should always be possible.");
let wait = 1.max((next - now).num_milliseconds());
Subscription::batch(vec![
rectangle_tracker_subscription(0).map(|(_, update)| Message::Rectangle(update)),
rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)),
time::every(Duration::from_millis(
wait.try_into().unwrap_or(FALLBACK_DELAY),
))
@ -149,11 +147,11 @@ impl Application for Time {
.to_string();
self.msg = calendar;
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
window::Id(0),
new_id,
None,
None,
@ -194,13 +192,13 @@ impl Application for Time {
}
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) {
if id == window::Id(0) {
let button = button(
if matches!(
self.applet_helper.anchor,
PanelAnchor::Top | PanelAnchor::Bottom
) {
column![text(self.now.format("%b %-d %-I:%M %p").to_string())]
column![text(self.now.format("%b %-d %-I:%M %p").to_string()).size(14)]
} else {
let mut date_time_col = column![
icon(
@ -208,10 +206,10 @@ impl Application for Time {
self.applet_helper.suggested_size().0
)
.style(theme::Svg::Symbolic),
text(self.now.format("%I").to_string()),
text(self.now.format("%M").to_string()),
text(self.now.format("%p").to_string()),
vertical_space(Length::Units(4)),
text(self.now.format("%I").to_string()).size(14),
text(self.now.format("%M").to_string()).size(14),
text(self.now.format("%p").to_string()).size(14),
vertical_space(Length::Fixed(4.0)),
// TODO better calendar icon?
icon(
"calendar-go-today-symbolic",
@ -222,7 +220,7 @@ impl Application for Time {
.align_items(Alignment::Center)
.spacing(4);
for d in self.now.format("%x").to_string().split("/") {
date_time_col = date_time_col.push(text(d.to_string()));
date_time_col = date_time_col.push(text(d.to_string()).size(14));
}
date_time_col
},
@ -240,7 +238,7 @@ impl Application for Time {
.align_items(Alignment::Start)
.spacing(12)
.padding([24, 0])
.push(text(&self.msg))
.push(text(&self.msg).size(14))
.padding(8);
self.applet_helper.popup_container(content).into()

View file

@ -5,15 +5,16 @@ authors = ["Ashley Wulber <ashley@system76.com>"]
edition = "2021"
[dependencies]
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, rev = "f0cfe09" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "f0cfe09" }
wayland-backend = {version = "0.1.0", features = ["client_system"]}
wayland-client = {version = "0.30.0"}
calloop = "0.10.1"
nix = "0.26.1"
log = "0.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
once_cell = "1.9"
futures = "0.3.21"
xdg = "2.4.0"

View file

@ -1,19 +1,19 @@
use calloop::channel::SyncSender;
use cosmic::applet::cosmic_panel_config::PanelAnchor;
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::mouse::{self, ScrollDelta};
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::{window::resize_window, InitialSurface};
use cosmic::iced::widget::{column, container, row, text};
use cosmic::iced::Color;
use cosmic::iced::{
subscription, widget::button, window, Application, Command, Event::Mouse, Length, Settings,
Subscription,
};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::{Element, Theme};
use cosmic_applet::cosmic_panel_config::PanelAnchor;
use cosmic_applet::CosmicAppletHelper;
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use std::cmp::Ordering;
use wayland_backend::client::ObjectId;
@ -25,10 +25,7 @@ use crate::wayland_subscription::{workspaces, WorkspacesUpdate};
pub fn run() -> cosmic::iced::Result {
let settings = Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
iced_settings: cosmic::iced_native::window::Settings {
size: (32, 32),
..Default::default()
},
size: (32, 32),
..Default::default()
}),
..Default::default()
@ -103,7 +100,7 @@ impl Application for IcedWorkspacesApplet {
Layout::Row => (unit * self.workspaces.len().max(1) as u32, unit),
Layout::Column => (unit, unit * self.workspaces.len().max(1) as u32),
};
return resize_window(window::Id::new(0), w, h);
return resize_window(window::Id(0), w, h);
}
WorkspacesUpdate::Started(tx) => {
self.workspace_tx.replace(tx);
@ -141,6 +138,7 @@ impl Application for IcedWorkspacesApplet {
.filter_map(|w| {
let btn = button(
text(w.0.clone())
.size(14)
.horizontal_alignment(Horizontal::Center)
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
@ -184,7 +182,7 @@ impl Application for IcedWorkspacesApplet {
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(
vec![
workspaces(0).map(|(_, msg)| Message::WorkspaceUpdate(msg)),
workspaces(0).map(|e| Message::WorkspaceUpdate(e.1)),
subscription::events_with(|e, _| match e {
Mouse(mouse::Event::WheelScrolled { delta }) => {
Some(Message::WheelScrolled(delta))
@ -197,7 +195,7 @@ impl Application for IcedWorkspacesApplet {
}
fn theme(&self) -> Theme {
self.theme
self.theme.clone()
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
@ -205,9 +203,9 @@ impl Application for IcedWorkspacesApplet {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|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,53 +0,0 @@
global_conf = configuration_data()
global_conf.set_quoted('APP_ID', application_id)
global_conf.set_quoted('PROFILE', profile)
global_conf.set_quoted('VERSION', version + version_suffix)
config = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf
)
# Copy the config.rs output to the source directory.
run_command(
'cp',
meson.project_build_root() / 'src' / 'config.rs',
meson.project_source_root() / 'src' / 'config.rs',
check: true
)
cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
if get_option('profile') == 'default'
cargo_options += [ '--release' ]
rust_target = 'release'
message('Building in release mode')
else
rust_target = 'debug'
message('Building in debug mode')
endif
if get_option('vendor') == true
cargo_options += [ '--locked' ]
message('Building with vendoring')
endif
cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
cargo_build = custom_target(
'cargo-build',
build_by_default: true,
build_always_stale: true,
output: meson.project_name(),
console: true,
install: true,
install_dir: bindir,
command: [
'env',
cargo_env,
cargo, 'build',
cargo_options,
'&&',
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
]
)

View file

@ -1,5 +1,4 @@
use calloop::channel::*;
use cosmic::applet::cosmic_panel_config::CosmicPanelOuput;
use cosmic_client_toolkit::{
sctk::{
self,
@ -11,7 +10,7 @@ use cosmic_client_toolkit::{
};
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use futures::{channel::mpsc, executor::block_on, SinkExt};
use std::{env, os::unix::net::UnixStream, path::PathBuf, str::FromStr, time::Duration};
use std::{env, os::unix::net::UnixStream, path::PathBuf, time::Duration};
use wayland_backend::client::ObjectId;
use wayland_client::{
globals::registry_queue_init,
@ -45,10 +44,6 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
std::thread::spawn(move || {
let configured_output = std::env::var("COSMIC_PANEL_OUTPUT")
.ok()
.map(|output_str| match CosmicPanelOuput::from_str(&output_str) {
Ok(CosmicPanelOuput::Name(name)) => name,
_ => "".to_string(),
})
.unwrap_or_default();
let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap();
let loop_handle = event_loop.handle();
@ -97,7 +92,11 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
.workspace_groups()
.iter()
.find_map(|g| {
if !g.outputs.iter().any(|o| Some(o) == state.expected_output.as_ref()) {
if !g
.outputs
.iter()
.any(|o| Some(o) == state.expected_output.as_ref())
{
return None;
}
g.workspaces
@ -179,7 +178,10 @@ impl State {
.workspace_groups()
.iter()
.filter_map(|g| {
if g.outputs.iter().any(|o| Some(o) == self.expected_output.as_ref()) {
if g.outputs
.iter()
.any(|o| Some(o) == self.expected_output.as_ref())
{
Some(g.workspaces.iter().map(|w| {
(
w.name.clone(),

View file

@ -18,29 +18,36 @@ pub fn workspaces<I: 'static + Hash + Copy + Send + Sync>(
subscription::unfold(id, State::Ready, move |state| _workspaces(id, state))
}
async fn _workspaces<I: Copy>(id: I, state: State) -> (Option<(I, WorkspacesUpdate)>, State) {
match state {
State::Ready => {
if let Ok(watcher) = WorkspacesWatcher::new() {
(
Some((id, WorkspacesUpdate::Started(watcher.get_sender()))),
State::Waiting(watcher),
)
} else {
(Some((id, WorkspacesUpdate::Errored)), State::Error)
async fn _workspaces<I: Copy>(id: I, mut state: State) -> ((I, WorkspacesUpdate), State) {
loop {
let (update, new_state) = match state {
State::Ready => {
if let Ok(watcher) = WorkspacesWatcher::new() {
(
Some((id, WorkspacesUpdate::Started(watcher.get_sender()))),
State::Waiting(watcher),
)
} else {
(Some((id, WorkspacesUpdate::Errored)), State::Error)
}
}
}
State::Waiting(mut t) => {
if let Some(w) = t.workspaces().await {
(
Some((id, WorkspacesUpdate::Workspaces(w))),
State::Waiting(t),
)
} else {
(Some((id, WorkspacesUpdate::Errored)), State::Error)
State::Waiting(mut t) => {
if let Some(w) = t.workspaces().await {
(
Some((id, WorkspacesUpdate::Workspaces(w))),
State::Waiting(t),
)
} else {
(Some((id, WorkspacesUpdate::Errored)), State::Error)
}
}
State::Error => cosmic::iced::futures::future::pending().await,
};
state = new_state;
if let Some(update) = update {
return (update, state);
}
State::Error => cosmic::iced::futures::future::pending().await,
}
}

View file

@ -6,4 +6,5 @@ license = "GPL-3.0-or-later"
[dependencies]
freedesktop-desktop-entry = "0.5.0"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
cosmic-applet = { path = "../applet" }

View file

@ -1,14 +1,10 @@
use cosmic::{
applet::CosmicAppletHelper,
iced::{
self,
wayland::InitialSurface,
Application,
},
iced_sctk::layout::Limits,
iced::Limits,
iced::{self, wayland::InitialSurface, Application},
iced_runtime::core::window,
iced_style::application,
iced_native::window,
};
use cosmic_applet::CosmicAppletHelper;
use freedesktop_desktop_entry::DesktopEntry;
use std::{env, fs, process::Command};
@ -47,10 +43,12 @@ impl iced::Application for Button {
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
application::Appearance {
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}
}))
}
fn subscription(&self) -> iced::Subscription<Msg> {
@ -112,10 +110,8 @@ pub fn main() -> iced::Result {
};
match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => {
s.iced_settings.min_size = Some((1, 1));
s.iced_settings.max_size = None;
s.autosize = true;
s.size_limits = Limits::NONE.min_height(1).min_width(1);
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
}
_ => unreachable!(),
};