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] [workspace]
members = [ members = [
"applet",
"cosmic-app-list", "cosmic-app-list",
"cosmic-applet-audio", "cosmic-applet-audio",
"cosmic-applet-battery", "cosmic-applet-battery",
@ -15,4 +16,5 @@ members = [
] ]
[profile.release] [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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" } 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"] } 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/", 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"] }
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "applet", "tokio"] } cosmic-applet = { path = "../applet" }
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio"] }
ron = "0.8" ron = "0.8"
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
once_cell = "1.9" once_cell = "1.9"
xdg = "2.4" xdg = "2.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.5"
calloop = "0.10" calloop = "0.10"
nix = "0.26" nix = "0.26"
shlex = "1.1.0" shlex = "1.1.0"
@ -29,3 +30,5 @@ i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester
i18n-embed-fl = "0.6" i18n-embed-fl = "0.6"
rust-embed = "6.3" rust-embed = "6.3"
url = "2.3.1" 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;
use crate::config::AppListConfig; use crate::config::AppListConfig;
use crate::config::APP_ID;
use crate::fl; use crate::fl;
use crate::toplevel_subscription::toplevel_subscription; use crate::toplevel_subscription::toplevel_subscription;
use crate::toplevel_subscription::ToplevelRequest; use crate::toplevel_subscription::ToplevelRequest;
@ -13,46 +9,56 @@ use calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo; use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
use cctk::wayland_client::protocol::wl_seat::WlSeat; use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cosmic::applet::cosmic_panel_config::PanelAnchor; use cosmic::cosmic_config;
use cosmic::applet::CosmicAppletHelper; use cosmic::cosmic_config::Config;
use cosmic::iced; use cosmic::iced;
use cosmic::iced::subscription::events_with;
use cosmic::iced::wayland::actions::data_device::DataFromMimeType; use cosmic::iced::wayland::actions::data_device::DataFromMimeType;
use cosmic::iced::wayland::actions::data_device::DndIcon; use cosmic::iced::wayland::actions::data_device::DndIcon;
use cosmic::iced::wayland::actions::window::SctkWindowSettings; use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::popup::destroy_popup; use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_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::Settings;
use cosmic::iced::{window, Application, Command, Subscription}; use cosmic::iced::{window, Application, Command, Subscription};
use cosmic::iced_native as native; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_native::alignment::Horizontal; use cosmic::iced_runtime::core::event;
use cosmic::iced_native::subscription::events_with;
use cosmic::iced_native::widget::vertical_space;
use cosmic::iced_sctk::commands::data_device::accept_mime_type; 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::finish_dnd;
use cosmic::iced_sctk::commands::data_device::request_dnd_data; 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::set_actions;
use cosmic::iced_sctk::commands::data_device::start_drag; 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::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::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::widget::divider; use cosmic::widget::divider;
use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription; use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription;
use cosmic::widget::rectangle_tracker::RectangleTracker; use cosmic::widget::rectangle_tracker::RectangleTracker;
use cosmic::widget::rectangle_tracker::RectangleUpdate; use cosmic::widget::rectangle_tracker::RectangleUpdate;
use cosmic::{Element, Theme}; 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 cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry; use freedesktop_desktop_entry::DesktopEntry;
use futures::future::pending;
use iced::widget::container; use iced::widget::container;
use iced::Alignment; use iced::Alignment;
use iced::Background; use iced::Background;
use iced::Length; use iced::Length;
use itertools::Itertools; 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; use url::Url;
static MIME_TYPE: &str = "text/uri-list"; static MIME_TYPE: &str = "text/uri-list";
@ -71,15 +77,12 @@ pub fn run() -> cosmic::iced::Result {
CosmicAppList::run(Settings { CosmicAppList::run(Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings { initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
iced_settings: cosmic::iced_native::window::Settings {
..Default::default()
},
autosize: true, autosize: true,
size_limits: Limits::NONE size_limits: Limits::NONE
.min_height(1) .min_height(1.0)
.min_width(1) .min_width(1.0)
.max_height(h) .max_height(h as f32)
.max_width(w), .max_width(w as f32),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -144,19 +147,19 @@ impl DockItem {
let dots = (0..toplevels.len()) let dots = (0..toplevels.len())
.into_iter() .into_iter()
.map(|_| { .map(|_| {
container(vertical_space(Length::Units(0))) container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius) .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 { |theme| container::Appearance {
text_color: Some(Color::TRANSPARENT), text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color( background: Some(Background::Color(
theme.cosmic().on_bg_color().into(), theme.cosmic().on_bg_color().into(),
)), )),
border_radius: 4.0, border_radius: 4.0.into(),
border_width: 0.0, border_width: 0.0,
border_color: Color::TRANSPARENT, border_color: Color::TRANSPARENT,
}, },
)) )))
.into() .into()
}) })
.collect_vec(); .collect_vec();
@ -179,12 +182,12 @@ impl DockItem {
.into(), .into(),
}; };
let mut icon_button = cosmic::widget::button(Button::Text) let icon_button = cosmic::widget::button(Button::Text)
.custom(vec![icon_wrapper]) .custom(vec![icon_wrapper])
.padding(8); .padding(8);
let icon_button = if interaction_enabled { let icon_button = if interaction_enabled {
dnd_source( dnd_source(
mouse_listener( mouse_area(
icon_button icon_button
.on_press( .on_press(
toplevels toplevels
@ -222,7 +225,7 @@ struct DndOffer {
struct CosmicAppList { struct CosmicAppList {
theme: Theme, theme: Theme,
popup: Option<(window::Id, DockItem)>, popup: Option<(window::Id, DockItem)>,
surface_id_ctr: u32, surface_id_ctr: u128,
subscription_ctr: u32, subscription_ctr: u32,
item_ctr: u32, item_ctr: u32,
active_list: Vec<DockItem>, active_list: Vec<DockItem>,
@ -262,6 +265,8 @@ enum Message {
DndData(PathBuf), DndData(PathBuf),
StartListeningForDnd, StartListeningForDnd,
StopListeningForDnd, StopListeningForDnd,
IncrementSubscriptionCtr,
ConfigUpdated(AppListConfig),
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -363,22 +368,20 @@ impl Application for CosmicAppList {
fn new(_flags: ()) -> (Self, Command<Message>) { fn new(_flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default(); let config = config::AppListConfig::load().unwrap_or_default();
let mut favorite_ctr = 0; let mut self_ = CosmicAppList {
let self_ = CosmicAppList {
favorite_list: desktop_info_for_app_ids(config.favorites.clone()) favorite_list: desktop_info_for_app_ids(config.favorites.clone())
.into_iter() .into_iter()
.map(|e| { .enumerate()
favorite_ctr += 1; .map(|(favorite_ctr, e)| DockItem {
DockItem { id: favorite_ctr as u32,
id: favorite_ctr, toplevels: Default::default(),
toplevels: Default::default(), desktop_info: e,
desktop_info: e,
}
}) })
.collect(), .collect(),
config, config,
..Default::default() ..Default::default()
}; };
self_.item_ctr = self_.favorite_list.len() as u32;
(self_, Command::none()) (self_, Command::none())
} }
@ -405,11 +408,11 @@ impl Application for CosmicAppList {
}; };
self.surface_id_ctr += 1; 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())); self.popup = Some((new_id, toplevel_group.clone()));
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
@ -440,13 +443,16 @@ impl Application for CosmicAppList {
self.favorite_list.push(entry); 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() { if let Some((popup_id, _toplevel)) = self.popup.take() {
return destroy_popup(popup_id); return destroy_popup(popup_id);
} }
} }
Message::UnFavorite(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 if let Some(i) = self
.favorite_list .favorite_list
.iter() .iter()
@ -506,7 +512,10 @@ impl Application for CosmicAppList {
.position(|t| t.desktop_info.id == id) .position(|t| t.desktop_info.id == id)
{ {
let t = self.favorite_list.remove(pos); 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)) Some((true, t))
} else { } else {
None None
@ -514,7 +523,7 @@ impl Application for CosmicAppList {
}) })
{ {
self.surface_id_ctr += 1; 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())); self.dnd_source = Some((icon_id, toplevel_group.clone(), DndAction::empty()));
return start_drag( return start_drag(
vec![MIME_TYPE.to_string()], vec![MIME_TYPE.to_string()],
@ -523,7 +532,7 @@ impl Application for CosmicAppList {
} else { } else {
DndAction::Copy DndAction::Copy
}, },
window::Id::new(0), window::Id(0),
Some(DndIcon::Custom(icon_id)), Some(DndIcon::Custom(icon_id)),
Box::new(toplevel_group.clone()), 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))) .and_then(|o| o.dock_item.map(|i| (i, o.preview_index)))
{ {
self.item_ctr += 1; 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 if let Some((pos, is_favorite)) = self
.active_list .active_list
.iter() .iter()
@ -689,11 +701,26 @@ impl Application for CosmicAppList {
self.toplevel_sender.replace(tx); self.toplevel_sender.replace(tx);
} }
ToplevelUpdate::Finished => { ToplevelUpdate::Finished => {
self.subscription_ctr += 1;
for t in &mut self.favorite_list { for t in &mut self.favorite_list {
t.toplevels.clear(); t.toplevels.clear();
} }
self.active_list.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) => { ToplevelUpdate::RemoveToplevel(handle) => {
for t in self for t in self
@ -765,6 +792,49 @@ impl Application for CosmicAppList {
Message::StopListeningForDnd => { Message::StopListeningForDnd => {
self.is_listening_for_dnd = false; 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() Command::none()
} }
@ -970,7 +1040,7 @@ impl Application for CosmicAppList {
), ),
}; };
if self.popup.is_some() { if self.popup.is_some() {
mouse_listener(content) mouse_area(content)
.on_right_release(Message::ClosePopup) .on_right_release(Message::ClosePopup)
.on_press(Message::ClosePopup) .on_press(Message::ClosePopup)
.into() .into()
@ -981,48 +1051,58 @@ impl Application for CosmicAppList {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ 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 { events_with(|e, _| match e {
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland( cosmic::iced_runtime::core::Event::PlatformSpecific(
event::wayland::Event::Seat(e, seat), event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)),
)) => match e { ) => match e {
event::wayland::SeatEvent::Enter => Some(Message::NewSeat(seat)), event::wayland::SeatEvent::Enter => Some(Message::NewSeat(seat)),
event::wayland::SeatEvent::Leave => Some(Message::RemovedSeat(seat)), event::wayland::SeatEvent::Leave => Some(Message::RemovedSeat(seat)),
}, },
// XXX Must be done to catch a finished drag after the source is removed // XXX Must be done to catch a finished drag after the source is removed
// (for now, the source is removed when the drag starts) // (for now, the source is removed when the drag starts)
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland( cosmic::iced_runtime::core::Event::PlatformSpecific(
event::wayland::Event::DataSource( event::PlatformSpecific::Wayland(event::wayland::Event::DataSource(
event::wayland::DataSourceEvent::DndFinished event::wayland::DataSourceEvent::DndFinished
| event::wayland::DataSourceEvent::Cancelled, | event::wayland::DataSourceEvent::Cancelled,
), )),
)) => Some(Message::DragFinished), ) => Some(Message::DragFinished),
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland( cosmic::iced_runtime::core::Event::PlatformSpecific(
event::wayland::Event::DndOffer(event::wayland::DndOfferEvent::Enter { event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
mime_types, event::wayland::DndOfferEvent::Enter { mime_types, .. },
.. )),
}), ) => {
)) => {
if mime_types.iter().any(|m| m == MIME_TYPE) { if mime_types.iter().any(|m| m == MIME_TYPE) {
Some(Message::StartListeningForDnd) Some(Message::StartListeningForDnd)
} else { } else {
None None
} }
} }
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland( cosmic::iced_runtime::core::Event::PlatformSpecific(
event::wayland::Event::DndOffer( event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
event::wayland::DndOfferEvent::Leave event::wayland::DndOfferEvent::Leave
| event::wayland::DndOfferEvent::DropPerformed, | event::wayland::DndOfferEvent::DropPerformed,
), )),
)) => Some(Message::StopListeningForDnd), ) => Some(Message::StopListeningForDnd),
_ => None, _ => 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 { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { 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 { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(),
}) }))
} }
} }

View file

@ -1,21 +1,23 @@
use anyhow::anyhow; 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 serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
use xdg::BaseDirectories; use xdg::BaseDirectories;
pub const APP_ID: &str = "com.system76.CosmicAppList"; pub const APP_ID: &str = "com.system76.CosmicAppList";
pub const VERSION: &str = "0.1.0"; pub const VERSION: &str = "0.1.0";
#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
pub enum TopLevelFilter { pub enum TopLevelFilter {
#[default] #[default]
ActiveWorkspace, ActiveWorkspace,
ConfiguredOutput, ConfiguredOutput,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, CosmicConfigEntry)]
pub struct AppListConfig { pub struct AppListConfig {
pub filter_top_levels: Option<TopLevelFilter>, pub filter_top_levels: Option<TopLevelFilter>,
pub favorites: Vec<String>, pub favorites: Vec<String>,
@ -42,26 +44,17 @@ impl AppListConfig {
.map_err(|err| anyhow!("Failed to parse config file: {}", err)) .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) { if !self.favorites.contains(&id) {
self.favorites.push(id); self.favorites.push(id);
let _ = self.write_entry(&config);
} }
self.save()
} }
pub fn remove_favorite(&mut self, id: String) -> anyhow::Result<()> { pub fn remove_favorite(&mut self, id: String, config: &Config) {
self.favorites.retain(|e| e != &id); if let Some(pos) = self.favorites.iter().position(|e| e == &id) {
self.save() self.favorites.remove(pos);
} let _ = self.write_entry(&config);
}
// TODO async?
pub fn save(&self) -> anyhow::Result<()> {
let bd = BaseDirectories::new()?;
let mut relative_path = PathBuf::from(APP_ID);
relative_path.push("config.ron");
let config_path = bd.place_config_file(relative_path)?;
let f = File::create(config_path)?;
ron::ser::to_writer_pretty(f, self, Default::default())?;
Ok(())
} }
} }

View file

@ -11,7 +11,7 @@ use futures::{
channel::mpsc::{unbounded, UnboundedReceiver}, channel::mpsc::{unbounded, UnboundedReceiver},
StreamExt, StreamExt,
}; };
use std::{fmt::Debug, hash::Hash}; use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
use crate::toplevel_handler::toplevel_handler; use crate::toplevel_handler::toplevel_handler;
@ -26,31 +26,45 @@ pub enum State {
Waiting( Waiting(
UnboundedReceiver<ToplevelUpdate>, UnboundedReceiver<ToplevelUpdate>,
calloop::channel::Sender<ToplevelRequest>, calloop::channel::Sender<ToplevelRequest>,
JoinHandle<()>,
), ),
Finished, Finished,
} }
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, ToplevelUpdate)>, State) { async fn start_listening<I: Copy>(id: I, mut state: State) -> ((I, ToplevelUpdate), State) {
match state { loop {
State::Ready => { let (update, new_state) = match state {
let (calloop_tx, calloop_rx) = calloop::channel::channel(); State::Ready => {
let (toplevel_tx, toplevel_rx) = unbounded(); let (calloop_tx, calloop_rx) = calloop::channel::channel();
std::thread::spawn(move || { let (toplevel_tx, toplevel_rx) = unbounded();
toplevel_handler(toplevel_tx, calloop_rx); 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), Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
) State::Waiting(toplevel_rx, calloop_tx, handle),
} )
State::Waiting(mut rx, tx) => match rx.next().await {
Some(u) => (Some((id, u)), State::Waiting(rx, tx)),
None => {
let _ = tx.send(ToplevelRequest::Exit);
(Some((id, ToplevelUpdate::Finished)), State::Finished)
} }
}, State::Waiting(mut rx, tx, handle) => {
State::Finished => iced::futures::future::pending().await, 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-binding = "2.26.0"
libpulse-glib-binding = "2.25.0" libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] } 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" log = "0.4.14"
pretty_env_logger = "0.4.0" 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::widget;
use cosmic::iced_native::alignment::Horizontal; use cosmic::iced::Limits;
use cosmic::iced_native::layout::Limits; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::theme::Svg; use cosmic::theme::Svg;
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
use cosmic::widget::{button, divider, icon}; use cosmic::widget::{button, divider, icon};
use cosmic::Renderer; use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::{ use cosmic::iced::{
self, self,
@ -20,12 +22,16 @@ use iced::widget::container;
use iced::Color; use iced::Color;
mod pulse; mod pulse;
use crate::localize::localize;
use crate::pulse::DeviceInfo; use crate::pulse::DeviceInfo;
use libpulse_binding::volume::VolumeLinear; use libpulse_binding::volume::VolumeLinear;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
pretty_env_logger::init(); pretty_env_logger::init();
// Prepare i18n
localize();
let helper = CosmicAppletHelper::default(); let helper = CosmicAppletHelper::default();
Audio::run(helper.window_settings()) Audio::run(helper.window_settings())
} }
@ -43,7 +49,7 @@ struct Audio {
theme: Theme, theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
show_media_controls_in_top_panel: bool, show_media_controls_in_top_panel: bool,
id_ctr: u32, id_ctr: u128,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -93,7 +99,7 @@ impl Application for Audio {
} }
fn theme(&self) -> Theme { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { 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 { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(),
}) }))
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -117,21 +123,21 @@ impl Application for Audio {
conn.send(pulse::Message::UpdateConnection); conn.send(pulse::Message::UpdateConnection);
} }
self.id_ctr += 1; 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); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
None, None,
); );
popup_settings.positioner.size_limits = Limits::NONE popup_settings.positioner.size_limits = Limits::NONE
.min_height(1) .min_height(1.0)
.min_width(1) .min_width(1.0)
.max_width(400) .max_width(400.0)
.max_height(1080); .max_height(1080.0);
if let Some(conn) = self.pulse_state.connection() { if let Some(conn) = self.pulse_state.connection() {
conn.send(pulse::Message::GetDefaultSink); conn.send(pulse::Message::GetDefaultSink);
@ -268,7 +274,7 @@ impl Application for Audio {
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) { if id == window::Id(0) {
self.applet_helper self.applet_helper
.icon_button(&self.icon_name) .icon_button(&self.icon_name)
.on_press(Message::TogglePopup) .on_press(Message::TogglePopup)
@ -291,20 +297,18 @@ impl Application for Audio {
.0 * 100.0; .0 * 100.0;
let audio_content = if audio_disabled { let audio_content = if audio_disabled {
column![text("PulseAudio Disconnected") column![text(fl!("disconnected"))
.width(Length::Fill) .width(Length::Fill)
.horizontal_alignment(Horizontal::Center) .horizontal_alignment(Horizontal::Center)
.size(24),] .size(24),]
} else { } else {
column![ column![
row![ row![
icon("audio-volume-high-symbolic", 32) icon("audio-volume-high-symbolic", 24).style(Svg::Symbolic),
.width(Length::Units(24))
.height(Length::Units(24))
.style(Svg::Symbolic),
slider(0.0..=100.0, out_f64, Message::SetOutputVolume) slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
.width(Length::FillPortion(5)), .width(Length::FillPortion(5)),
text(format!("{}%", out_f64.round())) text(format!("{}%", out_f64.round()))
.size(16)
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
.horizontal_alignment(Horizontal::Right) .horizontal_alignment(Horizontal::Right)
] ]
@ -312,13 +316,11 @@ impl Application for Audio {
.align_items(Alignment::Center) .align_items(Alignment::Center)
.padding([8, 24]), .padding([8, 24]),
row![ row![
icon("audio-input-microphone-symbolic", 32) icon("audio-input-microphone-symbolic", 24).style(Svg::Symbolic),
.width(Length::Units(24))
.height(Length::Units(24))
.style(Svg::Symbolic),
slider(0.0..=100.0, in_f64, Message::SetInputVolume) slider(0.0..=100.0, in_f64, Message::SetInputVolume)
.width(Length::FillPortion(5)), .width(Length::FillPortion(5)),
text(format!("{}%", in_f64.round())) text(format!("{}%", in_f64.round()))
.size(16)
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
.horizontal_alignment(Horizontal::Right) .horizontal_alignment(Horizontal::Right)
] ]
@ -330,7 +332,7 @@ impl Application for Audio {
.width(Length::Fill), .width(Length::Fill),
revealer( revealer(
self.is_open == IsOpen::Output, self.is_open == IsOpen::Output,
"Output", fl!("output"),
match &self.current_output { match &self.current_output {
Some(output) => pretty_name(output.description.clone()), Some(output) => pretty_name(output.description.clone()),
None => String::from("No device selected"), None => String::from("No device selected"),
@ -348,10 +350,10 @@ impl Application for Audio {
), ),
revealer( revealer(
self.is_open == IsOpen::Input, self.is_open == IsOpen::Input,
"Input", fl!("input"),
match &self.current_input { match &self.current_input {
Some(input) => pretty_name(input.description.clone()), Some(input) => pretty_name(input.description.clone()),
None => String::from("No device selected"), None => fl!("no-device"),
}, },
self.inputs self.inputs
.clone() .clone()
@ -372,17 +374,20 @@ impl Application for Audio {
container(divider::horizontal::light()) container(divider::horizontal::light())
.padding([12, 24]) .padding([12, 24])
.width(Length::Fill), .width(Length::Fill),
container(toggler( container(
Some("Show Media Controls on Top Panel".into()), toggler(
self.show_media_controls_in_top_panel, Some(fl!("show-media-controls")),
Message::ToggleMediaControlsInTopPanel, self.show_media_controls_in_top_panel,
)) Message::ToggleMediaControlsInTopPanel,
)
.text_size(14)
)
.padding([0, 24]), .padding([0, 24]),
container(divider::horizontal::light()) container(divider::horizontal::light())
.padding([12, 24]) .padding([12, 24])
.width(Length::Fill), .width(Length::Fill),
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.text("Sound Settings...") .custom(vec![text(fl!("sound-settings")).size(14).into()])
.padding([8, 24]) .padding([8, 24])
.width(Length::Fill) .width(Length::Fill)
] ]
@ -398,19 +403,19 @@ impl Application for Audio {
fn revealer( fn revealer(
open: bool, open: bool,
title: &str, title: String,
selected: String, selected: String,
options: Vec<(String, String)>, options: Vec<(String, String)>,
toggle: Message, toggle: Message,
mut change: impl FnMut(String) -> Message + 'static, mut change: impl FnMut(String) -> Message + 'static,
) -> widget::Column<Message, Renderer> { ) -> widget::Column<'static, Message, Renderer> {
if open { if open {
options.iter().fold( options.iter().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill), column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|col, (id, name)| { |col, (id, name)| {
col.push( col.push(
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![text(name).into()]) .custom(vec![text(name).size(14).into()])
.on_press(change(id.clone())) .on_press(change(id.clone()))
.width(Length::Fill) .width(Length::Fill)
.padding([8, 48]), .padding([8, 48]),
@ -424,14 +429,14 @@ fn revealer(
fn revealer_head( fn revealer_head(
_open: bool, _open: bool,
title: &str, title: String,
selected: String, selected: String,
toggle: Message, toggle: Message,
) -> widget::Button<Message, Renderer> { ) -> widget::Button<'static, Message, Renderer> {
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![ .custom(vec![
text(title).width(Length::Fill).into(), text(title).width(Length::Fill).size(14).into(),
text(selected).into(), text(selected).size(10).into(),
]) ])
.padding([8, 24]) .padding([8, 24])
.width(Length::Fill) .width(Length::Fill)

View file

@ -1,8 +1,8 @@
use cosmic::iced_native::subscription::{self, Subscription};
use std::cell::RefCell; use std::cell::RefCell;
use std::{rc::Rc, thread}; use std::{rc::Rc, thread};
extern crate libpulse_binding as pulse; extern crate libpulse_binding as pulse;
use cosmic::iced::{subscription, Subscription};
//use futures::channel::mpsc; //use futures::channel::mpsc;
use libpulse_binding::{ use libpulse_binding::{
callbacks::ListResult, callbacks::ListResult,
@ -15,71 +15,80 @@ use libpulse_binding::{
proplist::Proplist, proplist::Proplist,
volume::ChannelVolumes, volume::ChannelVolumes,
}; };
pub fn connect() -> Subscription<Event> { pub fn connect() -> Subscription<Event> {
struct Connect; struct Connect;
subscription::unfold( subscription::unfold(
std::any::TypeId::of::<Connect>(), std::any::TypeId::of::<Connect>(),
State::Init, State::Init,
|state| async move { |mut state| async move {
match state { loop {
State::Init => { let (update, new_state) = connection(state).await;
let PulseHandle { state = new_state;
to_pulse, if let Some(update) = update {
from_pulse, return (update, state);
} = 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)),
}
} }
} }
}, },
) )
} }
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)] // #[derive(Debug)]
enum State { enum State {
Init, Init,

View file

@ -5,11 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
once_cell = "1.16.0" 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" futures = "0.3"
zbus = { version = "3.5", default-features = false, features = ["tokio"] } zbus = { version = "3.13", default-features = false, features = ["tokio"] }
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.5"
# Application i18n # Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4" i18n-embed-fl = "0.6.4"

View file

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

View file

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

View file

@ -153,7 +153,7 @@ trait Device {
pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced::Subscription<(I, DeviceDbusEvent)> { ) -> 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)] #[derive(Debug)]
@ -174,6 +174,19 @@ async fn display_device() -> zbus::Result<DeviceProxy<'static>> {
.await .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) { async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, DeviceDbusEvent)>, State) {
match state { match state {
State::Ready => { State::Ready => {

View file

@ -8,6 +8,7 @@ use iced::subscription;
use std::{fmt::Debug, hash::Hash}; use std::{fmt::Debug, hash::Hash};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use zbus::dbus_proxy; use zbus::dbus_proxy;
#[dbus_proxy( #[dbus_proxy(
default_service = "org.freedesktop.UPower", default_service = "org.freedesktop.UPower",
interface = "org.freedesktop.UPower.KbdBacklight", interface = "org.freedesktop.UPower.KbdBacklight",
@ -35,7 +36,20 @@ trait KbdBacklight {
pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced::Subscription<(I, KeyboardBacklightUpdate)> { ) -> 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)] #[derive(Debug)]

View file

@ -8,10 +8,11 @@ license = "GPL-3.0-or-later"
once_cell = "1.16.0" once_cell = "1.16.0"
bluer = { version = "0.15", features = ["bluetoothd", "id"] } bluer = { version = "0.15", features = ["bluetoothd", "id"] }
futures-util = "0.3.21" 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" futures = "0.3"
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.5"
itertools = "0.10.3" itertools = "0.10.3"
slotmap = "1.0.6" slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] } tokio = { version = "1.15.0", features = ["full"] }

View file

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

View file

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

View file

@ -1,23 +1,22 @@
use crate::dbus::{self, PowerDaemonProxy}; use crate::dbus::{self, PowerDaemonProxy};
use crate::fl; use crate::fl;
use crate::graphics::{get_current_graphics, set_graphics, Graphics}; 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::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced_native::alignment::Horizontal; use cosmic::iced::Color;
use cosmic::iced_native::Alignment; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_runtime::core::Alignment;
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::widget::icon; use cosmic::widget::icon;
use cosmic::{ use cosmic::{
applet::{cosmic_panel_config::PanelAnchor, APPLET_BUTTON_THEME},
iced::widget::{column, container, row, text}, iced::widget::{column, container, row, text},
iced::{self, Application, Command, Length}, iced::{self, Application, Command, Length},
iced_native::window, iced_runtime::core::window,
theme::{Svg, Theme}, theme::{Svg, Theme},
widget::{button, divider}, widget::{button, divider},
Element, Element,
}; };
use cosmic_applet::{applet_button_theme, cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use zbus::Connection; use zbus::Connection;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -41,7 +40,7 @@ impl GraphicsMode {
pub struct Window { pub struct Window {
popup: Option<window::Id>, popup: Option<window::Id>,
graphics_mode: Option<GraphicsMode>, graphics_mode: Option<GraphicsMode>,
id_ctr: u32, id_ctr: u128,
theme: Theme, theme: Theme,
dbus: Option<(Connection, PowerDaemonProxy<'static>)>, dbus: Option<(Connection, PowerDaemonProxy<'static>)>,
applet_helper: CosmicAppletHelper, applet_helper: CosmicAppletHelper,
@ -92,7 +91,7 @@ impl Application for Window {
return destroy_popup(p); return destroy_popup(p);
} else { } else {
self.id_ctr += 1; 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); self.popup.replace(new_id);
let mut commands = Vec::new(); let mut commands = Vec::new();
if let Some((_, proxy)) = self.dbus.as_ref() { 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( let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
@ -184,7 +183,7 @@ impl Application for Window {
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) { if id == window::Id(0) {
match self.applet_helper.anchor { match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => self PanelAnchor::Left | PanelAnchor::Right => self
.applet_helper .applet_helper
@ -206,6 +205,7 @@ impl Application for Window {
Some(Graphics::Hybrid) => fl!("hybrid"), Some(Graphics::Hybrid) => fl!("hybrid"),
None => "".into(), None => "".into(),
}) })
.size(14)
] ]
.spacing(8) .spacing(8)
.padding([0, self.applet_helper.suggested_size().0 / 2]) .padding([0, self.applet_helper.suggested_size().0 / 2])
@ -220,7 +220,7 @@ impl Application for Window {
} }
} else { } else {
let content_list = vec![ let content_list = vec![
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![row![ .custom(vec![row![
column![ column![
text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14), text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14),
@ -256,7 +256,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Integrated)) .on_press(Message::SelectGraphicsMode(Graphics::Integrated))
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![row![ .custom(vec![row![
column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),] column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),]
.width(Length::Fill), .width(Length::Fill),
@ -289,7 +289,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Nvidia)) .on_press(Message::SelectGraphicsMode(Graphics::Nvidia))
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![row![ .custom(vec![row![
column![ column![
text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14), text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14),
@ -325,7 +325,7 @@ impl Application for Window {
.on_press(Message::SelectGraphicsMode(Graphics::Hybrid)) .on_press(Message::SelectGraphicsMode(Graphics::Hybrid))
.width(Length::Fill) .width(Length::Fill)
.into(), .into(),
button(APPLET_BUTTON_THEME) button(applet_button_theme())
.custom(vec![row![ .custom(vec![row![
column![ column![
text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14), text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14),
@ -369,7 +369,7 @@ impl Application for Window {
text(fl!("graphics-mode")) text(fl!("graphics-mode"))
.width(Length::Fill) .width(Length::Fill)
.horizontal_alignment(Horizontal::Center) .horizontal_alignment(Horizontal::Center)
.size(24) .size(14)
.into(), .into(),
container(divider::horizontal::light()) container(divider::horizontal::light())
.padding([0, 12]) .padding([0, 12])
@ -385,7 +385,7 @@ impl Application for Window {
} }
fn close_requested(&self, id: window::Id) -> Self::Message { fn close_requested(&self, id: window::Id) -> Self::Message {
if id != window::Id::new(0) { if id != window::Id(0) {
Message::PopupClosed(id) Message::PopupClosed(id)
} else { } else {
unimplemented!(); unimplemented!();
@ -393,10 +393,10 @@ impl Application for Window {
} }
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().background.on.into(), text_color: theme.cosmic().background.on.into(),
}) }))
} }
fn should_exit(&self) -> bool { fn should_exit(&self) -> bool {
@ -404,6 +404,6 @@ impl Application for Window {
} }
fn theme(&self) -> Theme { 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 = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "main" }
# cosmic-dbus-networkmanager = { path = "../../../dbus-settings-bindings/networkmanager" } # cosmic-dbus-networkmanager = { path = "../../../dbus-settings-bindings/networkmanager" }
futures-util = "0.3.21" 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" futures = "0.3"
zbus = { version = "3.7", default-features = false } zbus = { version = "3.13", default-features = false }
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.5"
itertools = "0.10.3" itertools = "0.10.3"
slotmap = "1.0.6" slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] } 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 = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4" i18n-embed-fl = "0.6.4"
rust-embed = "6.3.0" rust-embed = "6.3.0"
rust-embed-utils = "7.5.0"

View file

@ -1,15 +1,14 @@
use cosmic::iced_style; use cosmic::iced_style;
use cosmic::iced_widget::Row;
use cosmic::{ use cosmic::{
applet::CosmicAppletHelper,
iced::{ iced::{
wayland::popup::{destroy_popup, get_popup}, wayland::popup::{destroy_popup, get_popup},
widget::{column, container, row, scrollable, text, text_input, Column}, widget::{column, container, row, scrollable, text, text_input, Column},
Alignment, Application, Color, Command, Length, Subscription, Alignment, Application, Color, Command, Length, Subscription,
}, },
iced_native::{ iced_runtime::core::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
layout::Limits, layout::Limits,
renderer::BorderRadius,
window, window,
}, },
iced_style::{application, button::StyleSheet}, iced_style::{application, button::StyleSheet},
@ -17,9 +16,14 @@ use cosmic::{
widget::{button, divider, icon, toggler}, widget::{button, divider, icon, toggler},
Element, Theme, Element, Theme,
}; };
use cosmic_applet::CosmicAppletHelper;
use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState}; use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState};
use futures::channel::mpsc::UnboundedSender; 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::network_manager::NetworkManagerState;
use crate::{ use crate::{
config, fl, config, fl,
@ -77,13 +81,14 @@ struct CosmicNetworkApplet {
icon_name: String, icon_name: String,
theme: Theme, theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u32, id_ctr: u128,
applet_helper: CosmicAppletHelper, applet_helper: CosmicAppletHelper,
nm_state: NetworkManagerState, nm_state: NetworkManagerState,
// UI state // UI state
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>, nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
show_visible_networks: bool, show_visible_networks: bool,
new_connection: Option<NewConnectionState>, new_connection: Option<NewConnectionState>,
conn: Option<Connection>,
} }
impl CosmicNetworkApplet { impl CosmicNetworkApplet {
@ -110,7 +115,7 @@ impl CosmicNetworkApplet {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { pub(crate) enum Message {
ActivateKnownWifi(String), ActivateKnownWifi(String),
Disconnect(String), Disconnect(String),
TogglePopup, TogglePopup,
@ -155,11 +160,11 @@ impl Application for CosmicNetworkApplet {
} else { } else {
// TODO request update of state maybe // TODO request update of state maybe
self.id_ctr += 1; 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); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
@ -167,10 +172,10 @@ impl Application for CosmicNetworkApplet {
); );
popup_settings.positioner.size_limits = Limits::NONE popup_settings.positioner.size_limits = Limits::NONE
.min_height(1) .min_height(1.0)
.min_width(1) .min_width(1.0)
.max_height(800) .max_height(800.0)
.max_width(400); .max_width(400.0);
return get_popup(popup_settings); return get_popup(popup_settings);
} }
} }
@ -193,10 +198,15 @@ impl Application for CosmicNetworkApplet {
} }
} }
Message::NetworkManagerEvent(event) => match event { Message::NetworkManagerEvent(event) => match event {
NetworkManagerEvent::Init { sender, state } => { NetworkManagerEvent::Init {
conn,
sender,
state,
} => {
self.nm_sender.replace(sender); self.nm_sender.replace(sender);
self.nm_state = state; self.nm_state = state;
self.update_icon_name(); self.update_icon_name();
self.conn = Some(conn);
} }
NetworkManagerEvent::WiFiEnabled(state) => { NetworkManagerEvent::WiFiEnabled(state) => {
self.nm_state = state; self.nm_state = state;
@ -337,17 +347,17 @@ impl Application for CosmicNetworkApplet {
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
let button_style = Button::Custom { let button_style = || Button::Custom {
active: |t| iced_style::button::Appearance { active: Box::new(|t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0), border_radius: 0.0,
..t.active(&Button::Text) ..t.active(&Button::Text)
}, }),
hover: |t| iced_style::button::Appearance { hover: Box::new(|t| iced_style::button::Appearance {
border_radius: BorderRadius::from(0.0), border_radius: 0.0,
..t.hovered(&Button::Text) ..t.hovered(&Button::Text)
}, }),
}; };
if id == window::Id::new(0) { if id == window::Id(0) {
self.applet_helper self.applet_helper
.icon_button(&self.icon_name) .icon_button(&self.icon_name)
.on_press(Message::TogglePopup) .on_press(Message::TogglePopup)
@ -362,7 +372,7 @@ impl Application for CosmicNetworkApplet {
for addr in ip_addresses { for addr in ip_addresses {
ipv4.push( ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string())) text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(12) .size(10)
.into(), .into(),
); );
} }
@ -412,8 +422,6 @@ impl Application for CosmicNetworkApplet {
let mut btn_content = vec![ let mut btn_content = vec![
icon("network-wireless-symbolic", 24) icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic) .style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(), .into(),
column![text(name).size(14), Column::with_children(ipv4)] column![text(name).size(14), Column::with_children(ipv4)]
.width(Length::Fill) .width(Length::Fill)
@ -425,8 +433,6 @@ impl Application for CosmicNetworkApplet {
btn_content.push( btn_content.push(
icon("process-working-symbolic", 24) icon("process-working-symbolic", 24)
.style(Svg::Symbolic) .style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(), .into(),
); );
} }
@ -441,9 +447,12 @@ impl Application for CosmicNetworkApplet {
}; };
known_wifi = known_wifi.push( known_wifi = known_wifi.push(
column![button(Button::Secondary) column![button(Button::Secondary)
.custom(btn_content) .custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24]) .padding([8, 24])
.style(button_style.clone()) .style(button_style())
.on_press(Message::Disconnect(name.clone()))] .on_press(Message::Disconnect(name.clone()))]
.align_items(Alignment::Center), .align_items(Alignment::Center),
); );
@ -454,8 +463,6 @@ impl Application for CosmicNetworkApplet {
let mut btn_content = vec![ let mut btn_content = vec![
icon("network-wireless-symbolic", 24) icon("network-wireless-symbolic", 24)
.style(Svg::Symbolic) .style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(), .into(),
text(&known.ssid).size(14).width(Length::Fill).into(), text(&known.ssid).size(14).width(Length::Fill).into(),
]; ];
@ -464,17 +471,18 @@ impl Application for CosmicNetworkApplet {
btn_content.push( btn_content.push(
icon("process-working-symbolic", 24) icon("process-working-symbolic", 24)
.style(Svg::Symbolic) .style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24))
.into(), .into(),
); );
} }
let mut btn = button(Button::Secondary) 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]) .padding([8, 24])
.width(Length::Fill) .width(Length::Fill)
.style(button_style.clone()); .style(button_style());
btn = match known.state { btn = match known.state {
DeviceState::Failed DeviceState::Failed
| DeviceState::Unknown | DeviceState::Unknown
@ -495,6 +503,7 @@ impl Application for CosmicNetworkApplet {
toggler(fl!("airplane-mode"), self.nm_state.airplane_mode, |m| { toggler(fl!("airplane-mode"), self.nm_state.airplane_mode, |m| {
Message::ToggleAirplaneMode(m) Message::ToggleAirplaneMode(m)
}) })
.text_size(14)
.width(Length::Fill) .width(Length::Fill)
) )
.padding([0, 12]), .padding([0, 12]),
@ -503,6 +512,7 @@ impl Application for CosmicNetworkApplet {
toggler(fl!("wifi"), self.nm_state.wifi_enabled, |m| { toggler(fl!("wifi"), self.nm_state.wifi_enabled, |m| {
Message::ToggleWiFi(m) Message::ToggleWiFi(m)
}) })
.text_size(14)
.width(Length::Fill) .width(Length::Fill)
) )
.padding([0, 12]), .padding([0, 12]),
@ -523,25 +533,20 @@ impl Application for CosmicNetworkApplet {
text(fl!("visible-wireless-networks")) text(fl!("visible-wireless-networks"))
.size(14) .size(14)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Units(24)) .height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center) .vertical_alignment(Vertical::Center)
.into(), .into(),
container( container(icon(dropdown_icon, 14).style(Svg::Symbolic))
icon(dropdown_icon, 14) .align_x(Horizontal::Center)
.style(Svg::Symbolic) .align_y(Vertical::Center)
.width(Length::Units(14)) .width(Length::Fixed(24.0))
.height(Length::Units(14)), .height(Length::Fixed(24.0))
) .into(),
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Units(24))
.height(Length::Units(24))
.into(),
] ]
.into(), .into(),
) )
.padding([8, 24]) .padding([8, 24])
.style(button_style.clone()) .style(button_style())
.on_press(Message::ToggleVisibleNetworks); .on_press(Message::ToggleVisibleNetworks);
content = content.push(available_connections_btn); content = content.push(available_connections_btn);
if self.show_visible_networks { if self.show_visible_networks {
@ -552,10 +557,7 @@ impl Application for CosmicNetworkApplet {
password, password,
} => { } => {
let id = row![ let id = row![
icon("network-wireless-symbolic", 24) icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
text(&access_point.ssid).size(14), text(&access_point.ssid).size(14),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -565,7 +567,9 @@ impl Application for CosmicNetworkApplet {
content = content.push(id); content = content.push(id);
let col = column![ let col = column![
text(fl!("enter-password")), text(fl!("enter-password")),
text_input("", password, Message::Password) text_input("", password)
.on_input(Message::Password)
.on_paste(Message::Password)
.on_submit(Message::SubmitPassword) .on_submit(Message::SubmitPassword)
.password(), .password(),
container(text(fl!("router-wps-button"))).padding(8), container(text(fl!("router-wps-button"))).padding(8),
@ -590,10 +594,7 @@ impl Application for CosmicNetworkApplet {
} }
NewConnectionState::Waiting(access_point) => { NewConnectionState::Waiting(access_point) => {
let id = row![ let id = row![
icon("network-wireless-symbolic", 24) icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
text(&access_point.ssid).size(14), text(&access_point.ssid).size(14),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -601,10 +602,7 @@ impl Application for CosmicNetworkApplet {
.spacing(12); .spacing(12);
let connecting = row![ let connecting = row![
id, id,
icon("process-working-symbolic", 24) icon("process-working-symbolic", 24).style(Svg::Symbolic),
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
] ]
.spacing(8) .spacing(8)
.padding([0, 24]); .padding([0, 24]);
@ -612,10 +610,7 @@ impl Application for CosmicNetworkApplet {
} }
NewConnectionState::Failure(access_point) => { NewConnectionState::Failure(access_point) => {
let id = row![ let id = row![
icon("network-wireless-symbolic", 24) icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
.style(Svg::Symbolic)
.width(Length::Units(24))
.height(Length::Units(24)),
text(&access_point.ssid).size(14), text(&access_point.ssid).size(14),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -660,15 +655,12 @@ impl Application for CosmicNetworkApplet {
{ {
continue; continue;
} }
let button = button(button_style) let button = button(button_style())
.custom(vec![row![ .custom(vec![row![
icon("network-wireless-symbolic", 16) icon("network-wireless-symbolic", 16).style(Svg::Symbolic),
.style(Svg::Symbolic)
.width(Length::Units(16))
.height(Length::Units(16)),
text(&ap.ssid) text(&ap.ssid)
.size(14) .size(14)
.height(Length::Units(24)) .height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center) .vertical_alignment(Vertical::Center)
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -680,7 +672,7 @@ impl Application for CosmicNetworkApplet {
list_col.push(button.into()); list_col.push(button.into());
} }
content = content.push( 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> { 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 { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { 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 { fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance { <Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), application::Appearance {
text_color: theme.cosmic().on_bg_color().into(), 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 available_wifi;
pub mod current_networks; pub mod current_networks;
pub mod devices;
pub mod wireless_enabled;
use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::Deref, time::Duration}; 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>( pub fn network_manager_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced::Subscription<(I, NetworkManagerEvent)> { ) -> 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)] #[derive(Debug)]
@ -42,6 +47,19 @@ pub enum State {
Finished, 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>( async fn start_listening<I: Copy + Debug>(
id: I, id: I,
state: State, state: State,
@ -59,6 +77,7 @@ async fn start_listening<I: Copy + Debug>(
Some(( Some((
id, id,
NetworkManagerEvent::Init { NetworkManagerEvent::Init {
conn: conn.clone(),
sender: tx, sender: tx,
state: nm_state, state: nm_state,
}, },
@ -72,289 +91,402 @@ async fn start_listening<I: Copy + Debug>(
Err(_) => return (None, State::Finished), Err(_) => return (None, State::Finished),
}; };
let mut active_conns_changed = tokio::time::sleep(Duration::from_secs(5)) let (update, should_exit) = match rx.next().await {
.then(|_| async { network_manager.receive_active_connections_changed().await }) Some(NetworkManagerRequest::Disconnect(ssid)) => {
.await; let mut success = false;
let mut devices_changed = network_manager.receive_devices_changed().await; for c in network_manager
let mut wireless_enabled_changed = .active_connections()
network_manager.receive_wireless_enabled_changed().await; .await
let mut req = rx.next().boxed().fuse(); .unwrap_or_default()
{
let (update, should_exit) = futures::select! { if c.id().await.unwrap_or_default() == ssid {
req = req => { if let Ok(_) = network_manager.deactivate_connection(&c).await {
match req { success = true;
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()), req: NetworkManagerRequest::Disconnect(ssid.clone()),
success, success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(), 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); let mut secrets = match c.get_secrets("802-11-wireless-security").await {
Ok(s) => s,
// First try known connections _ => HashMap::from([(
// TODO more convenient methods of managing settings "802-11-wireless-security".into(),
for c in s.list_connections().await.unwrap_or_default() { HashMap::from([
let mut settings = match c.get_settings().await.ok() { (
Some(s) => s, "psk".into(),
None => continue, Value::Str(password.as_str().into()).to_owned(),
}; ),
("key-mgmt".into(), Value::Str("wpa-psk".into()).to_owned()),
let cur_ssid = settings ]),
.get("802-11-wireless") )]),
.and_then(|w| w.get("ssid")) };
.cloned() if let Some(s) = secrets.get_mut("802-11-wireless-security") {
.and_then(|ssid| ssid.try_into().ok()) s.insert("psk".into(), Value::Str(password.clone().into()).to_owned());
.and_then(|ssid| String::from_utf8(ssid).ok()); drop(s);
if cur_ssid.as_ref() != Some(&ssid) { settings.extend(secrets.into_iter());
continue; let settings: HashMap<_, _> = settings
} .iter()
.map(|(k, v)| {
let mut secrets = match let map = (
c.get_secrets("802-11-wireless-security") k.as_str(),
.await { v.iter()
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())) .map(|(k, v)| (k.as_str(), v.into()))
.collect::<HashMap<_, _>>()); .collect::<HashMap<_, _>>(),
map );
}).collect(); map
let updated = c.update(settings).await; })
if updated.is_ok() { .collect();
let success = if let Ok(path) = network_manager.deref().activate_connection(c.deref().path(), &ObjectPath::try_from("/").unwrap(), &ObjectPath::try_from("/").unwrap()).await { let updated = c.update(settings).await;
// let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1)); if updated.is_ok() {
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap(); let success = if let Ok(path) = network_manager
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap(); .deref()
let state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default()); .activate_connection(
let s = if let enums::ActiveConnectionState::Activating = state { c.deref().path(),
if let Ok(Some(s)) = timeout(Duration::from_secs(10), active.receive_state_changed().await.next()).await { &ObjectPath::try_from("/").unwrap(),
s.get().await.unwrap_or_default().into() &ObjectPath::try_from("/").unwrap(),
} else { )
state .await
} {
} else { // let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1));
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 {
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap(); 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 active = ActiveConnectionProxy::builder(&conn)
let mut state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default()); .path(path)
while let enums::ActiveConnectionState::Activating = state { .unwrap()
if let Ok(Some(s)) = timeout(Duration::from_secs(20), active.receive_state_changed().await.next()).await { .destination(dummy.destination())
state = s.get().await.unwrap_or_default().into(); .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 { } else {
break; state
} }
} else {
state
}; };
matches!(state, enums::ActiveConnectionState::Activated) matches!(s, enums::ActiveConnectionState::Activated)
} else { } else {
false false
}; };
status = (Some((id, NetworkManagerEvent::RequestResponse { status = (
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()), Some((
success, id,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(), NetworkManagerEvent::RequestResponse {
})), false); 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; break;
} }
}
}
if status.0.is_none() { if status.0.is_none() {
status = (Some((id, NetworkManagerEvent::RequestResponse { 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()), req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
success: false, success: false,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(), state: NetworkManagerState::new(&conn)
})), false); .await
} .unwrap_or_default(),
status },
} )),
None => { false,
(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);
} }
(Some((id, NetworkManagerEvent::WirelessAccessPoints(NetworkManagerState::new(&conn).await.unwrap_or_default()))), false) status
}
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)
} }
None => (None, true),
}; };
drop(active_conns_changed);
drop(wireless_enabled_changed);
drop(req);
( (
update, update,
if should_exit { if should_exit {
@ -385,6 +517,7 @@ pub enum NetworkManagerEvent {
success: bool, success: bool,
}, },
Init { Init {
conn: Connection,
sender: UnboundedSender<NetworkManagerRequest>, sender: UnboundedSender<NetworkManagerRequest>,
state: NetworkManagerState, 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] [dependencies]
icon-loader = { version = "0.3.6", features = ["gtk"] } 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" 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::{ use cosmic::iced::{
widget::{button, column, row, text, Row, Space}, widget::{button, column, row, text, Row, Space},
window, Alignment, Application, Color, Command, Length, Subscription, window, Alignment, Application, Color, Command, Length, Subscription,
}; };
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_widget::Button;
use cosmic::theme::Svg; use cosmic::theme::Svg;
use cosmic::widget::{divider, icon, toggler}; use cosmic::widget::{divider, icon, toggler};
use cosmic::Renderer; use cosmic::Renderer;
@ -27,7 +26,7 @@ struct Notifications {
theme: Theme, theme: Theme,
icon_name: String, icon_name: String,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u32, id_ctr: u128,
do_not_disturb: bool, do_not_disturb: bool,
notifications: Vec<Vec<String>>, notifications: Vec<Vec<String>>,
} }
@ -61,7 +60,7 @@ impl Application for Notifications {
} }
fn theme(&self) -> Theme { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { 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 { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(),
}) }))
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
@ -86,11 +85,11 @@ impl Application for Notifications {
destroy_popup(p) destroy_popup(p)
} else { } else {
self.id_ctr += 1; 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); self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings( let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
@ -112,7 +111,7 @@ impl Application for Notifications {
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) { if id == window::Id(0) {
self.applet_helper self.applet_helper
.icon_button(&self.icon_name) .icon_button(&self.icon_name)
.on_press(Message::TogglePopup) .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 // todo put into libcosmic doing so will fix the row_button's boarder radius
fn row_button( fn row_button(mut content: Vec<Element<Message>>) -> Button<Message, Renderer> {
mut content: Vec<Element<Message>>, content.insert(0, Space::with_width(Length::Fixed(24.0)).into());
) -> cosmic::iced_native::widget::Button<Message, Renderer> { content.push(Space::with_width(Length::Fixed(24.0)).into());
content.insert(0, Space::with_width(Length::Units(24)).into());
content.push(Space::with_width(Length::Units(24)).into());
button( button(
Row::with_children(content) Row::with_children(content)
@ -176,8 +173,8 @@ fn row_button(
.align_items(Alignment::Center), .align_items(Alignment::Center),
) )
.width(Length::Fill) .width(Length::Fill)
.height(Length::Units(36)) .height(Length::Fixed(36.0))
.style(APPLET_BUTTON_THEME) .style(applet_button_theme())
} }
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon { 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-binding = "2.26.0"
libpulse-glib-binding = "2.25.0" libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] } 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"] }
nix = "0.26.1" cosmic-applet = { path = "../applet" }
zbus = "3.7" nix = "0.26.2"
zbus = "3.13"
logind-zbus = "3.1" 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::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::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced_native::layout::Limits; use cosmic::iced_runtime::core::layout::Limits;
use cosmic::iced_native::widget::Space; 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::widget::{button, divider, icon};
use cosmic::Renderer; use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::Color;
use cosmic::iced::{ use cosmic::iced::{
widget::{self, column, container, row, Row}, widget::{self, column, container, row, space::Space, text, Row},
window, Alignment, Application, Command, Length, Subscription, window, Alignment, Application, Command, Length, Subscription,
}; };
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::{self, Svg}; use cosmic::theme::{self, Svg};
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
@ -20,9 +30,11 @@ use logind_zbus::manager::ManagerProxy;
use logind_zbus::session::{SessionProxy, SessionType}; use logind_zbus::session::{SessionProxy, SessionType};
use logind_zbus::user::UserProxy; use logind_zbus::user::UserProxy;
use nix::unistd::getuid; use nix::unistd::getuid;
use tokio::time::sleep;
use zbus::Connection; use zbus::Connection;
pub mod cosmic_session; pub mod cosmic_session;
mod localize;
pub mod session_manager; pub mod session_manager;
use crate::cosmic_session::CosmicSessionProxy; use crate::cosmic_session::CosmicSessionProxy;
@ -39,19 +51,28 @@ struct Power {
icon_name: String, icon_name: String,
theme: Theme, theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u32, id_ctr: u128,
action_to_confirm: Option<(window::Id, PowerAction)>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
enum Message { enum PowerAction {
Lock, Lock,
LogOut, LogOut,
Suspend, Suspend,
Restart, Restart,
Shutdown, Shutdown,
}
#[derive(Debug, Clone)]
enum Message {
Timeout(window::Id),
Action(PowerAction),
TogglePopup, TogglePopup,
Settings, Settings,
Ignore, Confirm,
Cancel,
Closed(window::Id),
Zbus(Result<(), zbus::Error>), Zbus(Result<(), zbus::Error>),
} }
@ -72,26 +93,34 @@ impl Application for Power {
} }
fn title(&self) -> String { fn title(&self) -> String {
String::from("Power") fl!("power")
} }
fn theme(&self) -> Theme { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { fn close_requested(&self, id: window::Id) -> Self::Message {
Message::Ignore Message::Closed(id)
} }
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(),
}) }))
} }
fn subscription(&self) -> Subscription<Message> { 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> { fn update(&mut self, message: Message) -> Command<Message> {
@ -101,21 +130,21 @@ impl Application for Power {
destroy_popup(p) destroy_popup(p)
} else { } else {
self.id_ctr += 1; 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); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0), window::Id(0),
new_id, new_id,
None, None,
None, None,
None, None,
); );
popup_settings.positioner.size_limits = Limits::NONE popup_settings.positioner.size_limits = Limits::NONE
.min_width(100) .min_width(100.0)
.min_height(100) .min_height(100.0)
.max_height(400) .max_height(400.0)
.max_width(500); .max_width(500.0);
get_popup(popup_settings) get_popup(popup_settings)
} }
} }
@ -123,51 +152,117 @@ impl Application for Power {
let _ = process::Command::new("cosmic-settings").spawn(); let _ = process::Command::new("cosmic-settings").spawn();
Command::none() Command::none()
} }
Message::Lock => Command::perform(lock(), Message::Zbus), Message::Action(action) => {
Message::LogOut => Command::perform(log_out(), Message::Zbus), self.id_ctr += 1;
Message::Suspend => Command::perform(suspend(), Message::Zbus), let id = window::Id(self.id_ctr);
Message::Restart => Command::perform(restart(), Message::Zbus), self.action_to_confirm = Some((id, action));
Message::Shutdown => Command::perform(shutdown(), Message::Zbus), 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) => { Message::Zbus(result) => {
if let Err(e) = result { if let Err(e) = result {
eprintln!("cosmic-applet-power ERROR: '{}'", e); eprintln!("cosmic-applet-power ERROR: '{}'", e);
} }
Command::none() 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> { fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id::new(0) { if matches!(self.popup, Some(p) if p == id) {
self.applet_helper let settings =
.icon_button(&self.icon_name) row_button(vec![text(fl!("settings")).size(14).into()]).on_press(Message::Settings);
.on_press(Message::TogglePopup)
.into()
} else {
let settings = row_button(vec!["Settings...".into()]).on_press(Message::Settings);
let session = column![ let session = column![
row_button(vec![ row_button(vec![
text_icon("system-lock-screen-symbolic", 24).into(), text_icon("system-lock-screen-symbolic", 24).into(),
"Lock Screen".into(), text(fl!("lock-screen")).size(14).into(),
Space::with_width(Length::Fill).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![ row_button(vec![
text_icon("system-log-out-symbolic", 24).into(), text_icon("system-log-out-symbolic", 24).into(),
"Log Out".into(), text(fl!("log-out")).size(14).into(),
Space::with_width(Length::Fill).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![ let power = row![
power_buttons("system-lock-screen-symbolic", "Suspend").on_press(Message::Suspend), power_buttons("system-lock-screen-symbolic", fl!("suspend"))
power_buttons("system-restart-symbolic", "Restart").on_press(Message::Restart), .on_press(Message::Action(PowerAction::Suspend)),
power_buttons("system-shutdown-symbolic", "Shutdown").on_press(Message::Shutdown), 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) .spacing(24)
.padding([0, 24]); .padding([0, 24]);
@ -188,6 +283,65 @@ impl Application for Power {
.padding([8, 0]); .padding([8, 0]);
self.applet_helper.popup_container(content).into() 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 // ### UI Helplers
fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Renderer> { 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) .custom(vec![Row::with_children(content)
.spacing(4) .spacing(4)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -204,14 +358,14 @@ fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Rendere
.padding([8, 24]) .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( widget::button(
column![text_icon(name, 40), text] column![text_icon(name, 40), text(msg).size(14)]
.spacing(4) .spacing(4)
.align_items(Alignment::Center), .align_items(Alignment::Center),
) )
.width(Length::Fill) .width(Length::Fill)
.height(Length::Units(76)) .height(Length::Fixed(76.0))
.style(theme::Button::Text) .style(theme::Button::Text)
} }

View file

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

View file

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

View file

@ -1,19 +1,19 @@
use calloop::channel::SyncSender; use calloop::channel::SyncSender;
use cosmic::applet::cosmic_panel_config::PanelAnchor;
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::mouse::{self, ScrollDelta}; use cosmic::iced::mouse::{self, ScrollDelta};
use cosmic::iced::wayland::actions::window::SctkWindowSettings; use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::{window::resize_window, InitialSurface}; use cosmic::iced::wayland::{window::resize_window, InitialSurface};
use cosmic::iced::widget::{column, container, row, text}; use cosmic::iced::widget::{column, container, row, text};
use cosmic::iced::Color;
use cosmic::iced::{ use cosmic::iced::{
subscription, widget::button, window, Application, Command, Event::Mouse, Length, Settings, subscription, widget::button, window, Application, Command, Event::Mouse, Length, Settings,
Subscription, Subscription,
}; };
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::{Element, Theme}; 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 cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use std::cmp::Ordering; use std::cmp::Ordering;
use wayland_backend::client::ObjectId; use wayland_backend::client::ObjectId;
@ -25,10 +25,7 @@ use crate::wayland_subscription::{workspaces, WorkspacesUpdate};
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let settings = Settings { let settings = Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings { initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
iced_settings: cosmic::iced_native::window::Settings { size: (32, 32),
size: (32, 32),
..Default::default()
},
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -103,7 +100,7 @@ impl Application for IcedWorkspacesApplet {
Layout::Row => (unit * self.workspaces.len().max(1) as u32, unit), Layout::Row => (unit * self.workspaces.len().max(1) as u32, unit),
Layout::Column => (unit, unit * self.workspaces.len().max(1) as u32), 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) => { WorkspacesUpdate::Started(tx) => {
self.workspace_tx.replace(tx); self.workspace_tx.replace(tx);
@ -141,6 +138,7 @@ impl Application for IcedWorkspacesApplet {
.filter_map(|w| { .filter_map(|w| {
let btn = button( let btn = button(
text(w.0.clone()) text(w.0.clone())
.size(14)
.horizontal_alignment(Horizontal::Center) .horizontal_alignment(Horizontal::Center)
.vertical_alignment(Vertical::Center) .vertical_alignment(Vertical::Center)
.width(Length::Fill) .width(Length::Fill)
@ -184,7 +182,7 @@ impl Application for IcedWorkspacesApplet {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch( Subscription::batch(
vec![ vec![
workspaces(0).map(|(_, msg)| Message::WorkspaceUpdate(msg)), workspaces(0).map(|e| Message::WorkspaceUpdate(e.1)),
subscription::events_with(|e, _| match e { subscription::events_with(|e, _| match e {
Mouse(mouse::Event::WheelScrolled { delta }) => { Mouse(mouse::Event::WheelScrolled { delta }) => {
Some(Message::WheelScrolled(delta)) Some(Message::WheelScrolled(delta))
@ -197,7 +195,7 @@ impl Application for IcedWorkspacesApplet {
} }
fn theme(&self) -> Theme { fn theme(&self) -> Theme {
self.theme self.theme.clone()
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { 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 { 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), background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(), 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 calloop::channel::*;
use cosmic::applet::cosmic_panel_config::CosmicPanelOuput;
use cosmic_client_toolkit::{ use cosmic_client_toolkit::{
sctk::{ sctk::{
self, self,
@ -11,7 +10,7 @@ use cosmic_client_toolkit::{
}; };
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1; use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use futures::{channel::mpsc, executor::block_on, SinkExt}; 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_backend::client::ObjectId;
use wayland_client::{ use wayland_client::{
globals::registry_queue_init, globals::registry_queue_init,
@ -45,10 +44,6 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
std::thread::spawn(move || { std::thread::spawn(move || {
let configured_output = std::env::var("COSMIC_PANEL_OUTPUT") let configured_output = std::env::var("COSMIC_PANEL_OUTPUT")
.ok() .ok()
.map(|output_str| match CosmicPanelOuput::from_str(&output_str) {
Ok(CosmicPanelOuput::Name(name)) => name,
_ => "".to_string(),
})
.unwrap_or_default(); .unwrap_or_default();
let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap(); let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap();
let loop_handle = event_loop.handle(); let loop_handle = event_loop.handle();
@ -97,7 +92,11 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
.workspace_groups() .workspace_groups()
.iter() .iter()
.find_map(|g| { .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; return None;
} }
g.workspaces g.workspaces
@ -179,7 +178,10 @@ impl State {
.workspace_groups() .workspace_groups()
.iter() .iter()
.filter_map(|g| { .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| { Some(g.workspaces.iter().map(|w| {
( (
w.name.clone(), 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)) subscription::unfold(id, State::Ready, move |state| _workspaces(id, state))
} }
async fn _workspaces<I: Copy>(id: I, state: State) -> (Option<(I, WorkspacesUpdate)>, State) { async fn _workspaces<I: Copy>(id: I, mut state: State) -> ((I, WorkspacesUpdate), State) {
match state { loop {
State::Ready => { let (update, new_state) = match state {
if let Ok(watcher) = WorkspacesWatcher::new() { State::Ready => {
( if let Ok(watcher) = WorkspacesWatcher::new() {
Some((id, WorkspacesUpdate::Started(watcher.get_sender()))), (
State::Waiting(watcher), Some((id, WorkspacesUpdate::Started(watcher.get_sender()))),
) State::Waiting(watcher),
} else { )
(Some((id, WorkspacesUpdate::Errored)), State::Error) } else {
(Some((id, WorkspacesUpdate::Errored)), State::Error)
}
} }
} State::Waiting(mut t) => {
State::Waiting(mut t) => { if let Some(w) = t.workspaces().await {
if let Some(w) = t.workspaces().await { (
( Some((id, WorkspacesUpdate::Workspaces(w))),
Some((id, WorkspacesUpdate::Workspaces(w))), State::Waiting(t),
State::Waiting(t), )
) } else {
} else { (Some((id, WorkspacesUpdate::Errored)), State::Error)
(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] [dependencies]
freedesktop-desktop-entry = "0.5.0" 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::{ use cosmic::{
applet::CosmicAppletHelper, iced::Limits,
iced::{ iced::{self, wayland::InitialSurface, Application},
self, iced_runtime::core::window,
wayland::InitialSurface,
Application,
},
iced_sctk::layout::Limits,
iced_style::application, iced_style::application,
iced_native::window,
}; };
use cosmic_applet::CosmicAppletHelper;
use freedesktop_desktop_entry::DesktopEntry; use freedesktop_desktop_entry::DesktopEntry;
use std::{env, fs, process::Command}; use std::{env, fs, process::Command};
@ -47,10 +43,12 @@ impl iced::Application for Button {
} }
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style { fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance { <Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0), application::Appearance {
text_color: theme.cosmic().on_bg_color().into(), 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> { fn subscription(&self) -> iced::Subscription<Msg> {
@ -112,10 +110,8 @@ pub fn main() -> iced::Result {
}; };
match &mut settings.initial_surface { match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => { InitialSurface::XdgWindow(s) => {
s.iced_settings.min_size = Some((1, 1));
s.iced_settings.max_size = None;
s.autosize = true; 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!(), _ => unreachable!(),
}; };