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:
parent
8b46cc209f
commit
9ebd9b511a
48 changed files with 2841 additions and 1681 deletions
1740
Cargo.lock
generated
1740
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"applet",
|
||||
"cosmic-app-list",
|
||||
"cosmic-applet-audio",
|
||||
"cosmic-applet-battery",
|
||||
|
|
@ -15,4 +16,5 @@ members = [
|
|||
]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
lto = "thin"
|
||||
# lto = "fat"
|
||||
|
|
|
|||
12
applet/Cargo.toml
Normal file
12
applet/Cargo.toml
Normal 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
203
applet/src/lib.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,17 @@ edition = "2021"
|
|||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
|
||||
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "applet", "tokio"] }
|
||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "f0cfe09" }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "f0cfe09" }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio"] }
|
||||
ron = "0.8"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
once_cell = "1.9"
|
||||
xdg = "2.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
calloop = "0.10"
|
||||
nix = "0.26"
|
||||
shlex = "1.1.0"
|
||||
|
|
@ -29,3 +30,5 @@ i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester
|
|||
i18n-embed-fl = "0.6"
|
||||
rust-embed = "6.3"
|
||||
url = "2.3.1"
|
||||
rust-embed-utils = "7.5.0"
|
||||
rand = "0.8.5"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::config;
|
||||
use crate::config::AppListConfig;
|
||||
use crate::config::APP_ID;
|
||||
use crate::fl;
|
||||
use crate::toplevel_subscription::toplevel_subscription;
|
||||
use crate::toplevel_subscription::ToplevelRequest;
|
||||
|
|
@ -13,46 +9,56 @@ use calloop::channel::Sender;
|
|||
use cctk::toplevel_info::ToplevelInfo;
|
||||
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use cctk::wayland_client::protocol::wl_seat::WlSeat;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::applet::CosmicAppletHelper;
|
||||
use cosmic::cosmic_config;
|
||||
use cosmic::cosmic_config::Config;
|
||||
use cosmic::iced;
|
||||
use cosmic::iced::subscription::events_with;
|
||||
use cosmic::iced::wayland::actions::data_device::DataFromMimeType;
|
||||
use cosmic::iced::wayland::actions::data_device::DndIcon;
|
||||
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
|
||||
use cosmic::iced::wayland::popup::destroy_popup;
|
||||
use cosmic::iced::wayland::popup::get_popup;
|
||||
use cosmic::iced::widget::{column, dnd_source, mouse_listener, row, text, Column, Row};
|
||||
use cosmic::iced::widget::dnd_listener;
|
||||
use cosmic::iced::widget::vertical_rule;
|
||||
use cosmic::iced::widget::vertical_space;
|
||||
use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row};
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::Settings;
|
||||
use cosmic::iced::{window, Application, Command, Subscription};
|
||||
use cosmic::iced_native as native;
|
||||
use cosmic::iced_native::alignment::Horizontal;
|
||||
use cosmic::iced_native::subscription::events_with;
|
||||
use cosmic::iced_native::widget::vertical_space;
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::iced_runtime::core::event;
|
||||
use cosmic::iced_sctk::commands::data_device::accept_mime_type;
|
||||
use cosmic::iced_sctk::commands::data_device::finish_dnd;
|
||||
use cosmic::iced_sctk::commands::data_device::request_dnd_data;
|
||||
use cosmic::iced_sctk::commands::data_device::set_actions;
|
||||
use cosmic::iced_sctk::commands::data_device::start_drag;
|
||||
use cosmic::iced_sctk::layout::Limits;
|
||||
use cosmic::iced_sctk::settings::InitialSurface;
|
||||
use cosmic::iced_sctk::widget::dnd_listener;
|
||||
use cosmic::iced_sctk::widget::vertical_rule;
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::Color;
|
||||
use cosmic::theme::Button;
|
||||
use cosmic::widget::divider;
|
||||
use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription;
|
||||
use cosmic::widget::rectangle_tracker::RectangleTracker;
|
||||
use cosmic::widget::rectangle_tracker::RectangleUpdate;
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic_applet::CosmicAppletHelper;
|
||||
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||
use freedesktop_desktop_entry::DesktopEntry;
|
||||
use futures::future::pending;
|
||||
use iced::widget::container;
|
||||
use iced::Alignment;
|
||||
use iced::Background;
|
||||
use iced::Length;
|
||||
use itertools::Itertools;
|
||||
use native::event;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use url::Url;
|
||||
|
||||
static MIME_TYPE: &str = "text/uri-list";
|
||||
|
|
@ -71,15 +77,12 @@ pub fn run() -> cosmic::iced::Result {
|
|||
|
||||
CosmicAppList::run(Settings {
|
||||
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
|
||||
iced_settings: cosmic::iced_native::window::Settings {
|
||||
..Default::default()
|
||||
},
|
||||
autosize: true,
|
||||
size_limits: Limits::NONE
|
||||
.min_height(1)
|
||||
.min_width(1)
|
||||
.max_height(h)
|
||||
.max_width(w),
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_height(h as f32)
|
||||
.max_width(w as f32),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
|
@ -144,19 +147,19 @@ impl DockItem {
|
|||
let dots = (0..toplevels.len())
|
||||
.into_iter()
|
||||
.map(|_| {
|
||||
container(vertical_space(Length::Units(0)))
|
||||
container(vertical_space(Length::Fixed(0.0)))
|
||||
.padding(dot_radius)
|
||||
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(
|
||||
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(Box::new(
|
||||
|theme| container::Appearance {
|
||||
text_color: Some(Color::TRANSPARENT),
|
||||
background: Some(Background::Color(
|
||||
theme.cosmic().on_bg_color().into(),
|
||||
)),
|
||||
border_radius: 4.0,
|
||||
border_radius: 4.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
))
|
||||
)))
|
||||
.into()
|
||||
})
|
||||
.collect_vec();
|
||||
|
|
@ -179,12 +182,12 @@ impl DockItem {
|
|||
.into(),
|
||||
};
|
||||
|
||||
let mut icon_button = cosmic::widget::button(Button::Text)
|
||||
let icon_button = cosmic::widget::button(Button::Text)
|
||||
.custom(vec![icon_wrapper])
|
||||
.padding(8);
|
||||
let icon_button = if interaction_enabled {
|
||||
dnd_source(
|
||||
mouse_listener(
|
||||
mouse_area(
|
||||
icon_button
|
||||
.on_press(
|
||||
toplevels
|
||||
|
|
@ -222,7 +225,7 @@ struct DndOffer {
|
|||
struct CosmicAppList {
|
||||
theme: Theme,
|
||||
popup: Option<(window::Id, DockItem)>,
|
||||
surface_id_ctr: u32,
|
||||
surface_id_ctr: u128,
|
||||
subscription_ctr: u32,
|
||||
item_ctr: u32,
|
||||
active_list: Vec<DockItem>,
|
||||
|
|
@ -262,6 +265,8 @@ enum Message {
|
|||
DndData(PathBuf),
|
||||
StartListeningForDnd,
|
||||
StopListeningForDnd,
|
||||
IncrementSubscriptionCtr,
|
||||
ConfigUpdated(AppListConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -363,22 +368,20 @@ impl Application for CosmicAppList {
|
|||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
let config = config::AppListConfig::load().unwrap_or_default();
|
||||
let mut favorite_ctr = 0;
|
||||
let self_ = CosmicAppList {
|
||||
let mut self_ = CosmicAppList {
|
||||
favorite_list: desktop_info_for_app_ids(config.favorites.clone())
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
favorite_ctr += 1;
|
||||
DockItem {
|
||||
id: favorite_ctr,
|
||||
toplevels: Default::default(),
|
||||
desktop_info: e,
|
||||
}
|
||||
.enumerate()
|
||||
.map(|(favorite_ctr, e)| DockItem {
|
||||
id: favorite_ctr as u32,
|
||||
toplevels: Default::default(),
|
||||
desktop_info: e,
|
||||
})
|
||||
.collect(),
|
||||
config,
|
||||
..Default::default()
|
||||
};
|
||||
self_.item_ctr = self_.favorite_list.len() as u32;
|
||||
|
||||
(self_, Command::none())
|
||||
}
|
||||
|
|
@ -405,11 +408,11 @@ impl Application for CosmicAppList {
|
|||
};
|
||||
|
||||
self.surface_id_ctr += 1;
|
||||
let new_id = window::Id::new(self.surface_id_ctr);
|
||||
let new_id = window::Id(self.surface_id_ctr);
|
||||
self.popup = Some((new_id, toplevel_group.clone()));
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -440,13 +443,16 @@ impl Application for CosmicAppList {
|
|||
self.favorite_list.push(entry);
|
||||
}
|
||||
|
||||
let _ = self.config.add_favorite(id);
|
||||
self.config
|
||||
.add_favorite(id, &Config::new(APP_ID, 1).unwrap());
|
||||
if let Some((popup_id, _toplevel)) = self.popup.take() {
|
||||
return destroy_popup(popup_id);
|
||||
}
|
||||
}
|
||||
Message::UnFavorite(id) => {
|
||||
let _ = self.config.remove_favorite(id.clone());
|
||||
let _ = self
|
||||
.config
|
||||
.remove_favorite(id.clone(), &Config::new(APP_ID, 1).unwrap());
|
||||
if let Some(i) = self
|
||||
.favorite_list
|
||||
.iter()
|
||||
|
|
@ -506,7 +512,10 @@ impl Application for CosmicAppList {
|
|||
.position(|t| t.desktop_info.id == id)
|
||||
{
|
||||
let t = self.favorite_list.remove(pos);
|
||||
let _ = self.config.remove_favorite(t.desktop_info.id.clone());
|
||||
let _ = self.config.remove_favorite(
|
||||
t.desktop_info.id.clone(),
|
||||
&Config::new(APP_ID, 1).unwrap(),
|
||||
);
|
||||
Some((true, t))
|
||||
} else {
|
||||
None
|
||||
|
|
@ -514,7 +523,7 @@ impl Application for CosmicAppList {
|
|||
})
|
||||
{
|
||||
self.surface_id_ctr += 1;
|
||||
let icon_id = window::Id::new(self.surface_id_ctr);
|
||||
let icon_id = window::Id(self.surface_id_ctr);
|
||||
self.dnd_source = Some((icon_id, toplevel_group.clone(), DndAction::empty()));
|
||||
return start_drag(
|
||||
vec![MIME_TYPE.to_string()],
|
||||
|
|
@ -523,7 +532,7 @@ impl Application for CosmicAppList {
|
|||
} else {
|
||||
DndAction::Copy
|
||||
},
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
Some(DndIcon::Custom(icon_id)),
|
||||
Box::new(toplevel_group.clone()),
|
||||
);
|
||||
|
|
@ -629,7 +638,10 @@ impl Application for CosmicAppList {
|
|||
.and_then(|o| o.dock_item.map(|i| (i, o.preview_index)))
|
||||
{
|
||||
self.item_ctr += 1;
|
||||
let _ = self.config.add_favorite(dock_item.desktop_info.id.clone());
|
||||
let _ = self.config.add_favorite(
|
||||
dock_item.desktop_info.id.clone(),
|
||||
&Config::new(APP_ID, 1).unwrap(),
|
||||
);
|
||||
if let Some((pos, is_favorite)) = self
|
||||
.active_list
|
||||
.iter()
|
||||
|
|
@ -689,11 +701,26 @@ impl Application for CosmicAppList {
|
|||
self.toplevel_sender.replace(tx);
|
||||
}
|
||||
ToplevelUpdate::Finished => {
|
||||
self.subscription_ctr += 1;
|
||||
for t in &mut self.favorite_list {
|
||||
t.toplevels.clear();
|
||||
}
|
||||
self.active_list.clear();
|
||||
let subscription_ctr = self.subscription_ctr;
|
||||
let mut rng = thread_rng();
|
||||
let rand_d = rng.gen_range(0..100);
|
||||
return Command::perform(
|
||||
async move {
|
||||
if let Some(millis) = 2u64
|
||||
.checked_pow(subscription_ctr)
|
||||
.and_then(|d| d.checked_add(rand_d))
|
||||
{
|
||||
sleep(Duration::from_millis(millis)).await;
|
||||
} else {
|
||||
pending::<()>().await;
|
||||
}
|
||||
},
|
||||
|_| Message::IncrementSubscriptionCtr,
|
||||
);
|
||||
}
|
||||
ToplevelUpdate::RemoveToplevel(handle) => {
|
||||
for t in self
|
||||
|
|
@ -765,6 +792,49 @@ impl Application for CosmicAppList {
|
|||
Message::StopListeningForDnd => {
|
||||
self.is_listening_for_dnd = false;
|
||||
}
|
||||
Message::IncrementSubscriptionCtr => {
|
||||
self.subscription_ctr += 1;
|
||||
}
|
||||
Message::ConfigUpdated(config) => {
|
||||
self.config = config;
|
||||
|
||||
let mut new_list: Vec<_> = desktop_info_for_app_ids(self.config.favorites.clone())
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
self.item_ctr += 1;
|
||||
|
||||
DockItem {
|
||||
id: self.item_ctr,
|
||||
toplevels: Default::default(),
|
||||
desktop_info: e,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for item in &mut new_list {
|
||||
if let Some(old_item) = self
|
||||
.favorite_list
|
||||
.iter()
|
||||
.position(|i| i.desktop_info.id == item.desktop_info.id)
|
||||
{
|
||||
let old_item = self.favorite_list.swap_remove(old_item);
|
||||
*item = old_item;
|
||||
} else if let Some(old_item) = self
|
||||
.active_list
|
||||
.iter()
|
||||
.position(|i| i.desktop_info.id == item.desktop_info.id)
|
||||
{
|
||||
let old_item = self.active_list.remove(old_item);
|
||||
*item = old_item;
|
||||
}
|
||||
}
|
||||
|
||||
for item in self.favorite_list.drain(..) {
|
||||
self.active_list.push(item);
|
||||
}
|
||||
|
||||
self.favorite_list = new_list;
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
|
@ -970,7 +1040,7 @@ impl Application for CosmicAppList {
|
|||
),
|
||||
};
|
||||
if self.popup.is_some() {
|
||||
mouse_listener(content)
|
||||
mouse_area(content)
|
||||
.on_right_release(Message::ClosePopup)
|
||||
.on_press(Message::ClosePopup)
|
||||
.into()
|
||||
|
|
@ -981,48 +1051,58 @@ impl Application for CosmicAppList {
|
|||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event)),
|
||||
toplevel_subscription(self.subscription_ctr).map(|e| Message::Toplevel(e.1)),
|
||||
events_with(|e, _| match e {
|
||||
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||
event::wayland::Event::Seat(e, seat),
|
||||
)) => match e {
|
||||
cosmic::iced_runtime::core::Event::PlatformSpecific(
|
||||
event::PlatformSpecific::Wayland(event::wayland::Event::Seat(e, seat)),
|
||||
) => match e {
|
||||
event::wayland::SeatEvent::Enter => Some(Message::NewSeat(seat)),
|
||||
event::wayland::SeatEvent::Leave => Some(Message::RemovedSeat(seat)),
|
||||
},
|
||||
// XXX Must be done to catch a finished drag after the source is removed
|
||||
// (for now, the source is removed when the drag starts)
|
||||
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||
event::wayland::Event::DataSource(
|
||||
cosmic::iced_runtime::core::Event::PlatformSpecific(
|
||||
event::PlatformSpecific::Wayland(event::wayland::Event::DataSource(
|
||||
event::wayland::DataSourceEvent::DndFinished
|
||||
| event::wayland::DataSourceEvent::Cancelled,
|
||||
),
|
||||
)) => Some(Message::DragFinished),
|
||||
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||
event::wayland::Event::DndOffer(event::wayland::DndOfferEvent::Enter {
|
||||
mime_types,
|
||||
..
|
||||
}),
|
||||
)) => {
|
||||
)),
|
||||
) => Some(Message::DragFinished),
|
||||
cosmic::iced_runtime::core::Event::PlatformSpecific(
|
||||
event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
|
||||
event::wayland::DndOfferEvent::Enter { mime_types, .. },
|
||||
)),
|
||||
) => {
|
||||
if mime_types.iter().any(|m| m == MIME_TYPE) {
|
||||
Some(Message::StartListeningForDnd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
native::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||
event::wayland::Event::DndOffer(
|
||||
cosmic::iced_runtime::core::Event::PlatformSpecific(
|
||||
event::PlatformSpecific::Wayland(event::wayland::Event::DndOffer(
|
||||
event::wayland::DndOfferEvent::Leave
|
||||
| event::wayland::DndOfferEvent::DropPerformed,
|
||||
),
|
||||
)) => Some(Message::StopListeningForDnd),
|
||||
)),
|
||||
) => Some(Message::StopListeningForDnd),
|
||||
_ => None,
|
||||
}),
|
||||
rectangle_tracker_subscription(0).map(|(_, update)| Message::Rectangle(update)),
|
||||
rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)),
|
||||
cosmic_config::config_subscription(0, Cow::from(APP_ID), 1).map(|(_, config)| {
|
||||
match config {
|
||||
Ok(config) => Message::ConfigUpdated(config),
|
||||
Err((errors, config)) => {
|
||||
for error in errors {
|
||||
log::error!("{:?}", error);
|
||||
}
|
||||
Message::ConfigUpdated(config)
|
||||
}
|
||||
}
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -1030,9 +1110,9 @@ impl Application for CosmicAppList {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
use anyhow::anyhow;
|
||||
|
||||
use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||
use cosmic::cosmic_config::{self, Config, ConfigGet, ConfigSet, CosmicConfigEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
pub const APP_ID: &str = "com.system76.CosmicAppList";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
|
||||
pub enum TopLevelFilter {
|
||||
#[default]
|
||||
ActiveWorkspace,
|
||||
ConfiguredOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, CosmicConfigEntry)]
|
||||
pub struct AppListConfig {
|
||||
pub filter_top_levels: Option<TopLevelFilter>,
|
||||
pub favorites: Vec<String>,
|
||||
|
|
@ -42,26 +44,17 @@ impl AppListConfig {
|
|||
.map_err(|err| anyhow!("Failed to parse config file: {}", err))
|
||||
}
|
||||
|
||||
pub fn add_favorite(&mut self, id: String) -> anyhow::Result<()> {
|
||||
pub fn add_favorite(&mut self, id: String, config: &Config) {
|
||||
if !self.favorites.contains(&id) {
|
||||
self.favorites.push(id);
|
||||
let _ = self.write_entry(&config);
|
||||
}
|
||||
self.save()
|
||||
}
|
||||
|
||||
pub fn remove_favorite(&mut self, id: String) -> anyhow::Result<()> {
|
||||
self.favorites.retain(|e| e != &id);
|
||||
self.save()
|
||||
}
|
||||
|
||||
// TODO async?
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let bd = BaseDirectories::new()?;
|
||||
let mut relative_path = PathBuf::from(APP_ID);
|
||||
relative_path.push("config.ron");
|
||||
let config_path = bd.place_config_file(relative_path)?;
|
||||
let f = File::create(config_path)?;
|
||||
ron::ser::to_writer_pretty(f, self, Default::default())?;
|
||||
Ok(())
|
||||
pub fn remove_favorite(&mut self, id: String, config: &Config) {
|
||||
if let Some(pos) = self.favorites.iter().position(|e| e == &id) {
|
||||
self.favorites.remove(pos);
|
||||
let _ = self.write_entry(&config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use futures::{
|
|||
channel::mpsc::{unbounded, UnboundedReceiver},
|
||||
StreamExt,
|
||||
};
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
|
||||
|
||||
use crate::toplevel_handler::toplevel_handler;
|
||||
|
||||
|
|
@ -26,31 +26,45 @@ pub enum State {
|
|||
Waiting(
|
||||
UnboundedReceiver<ToplevelUpdate>,
|
||||
calloop::channel::Sender<ToplevelRequest>,
|
||||
JoinHandle<()>,
|
||||
),
|
||||
Finished,
|
||||
}
|
||||
|
||||
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, ToplevelUpdate)>, State) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
let (calloop_tx, calloop_rx) = calloop::channel::channel();
|
||||
let (toplevel_tx, toplevel_rx) = unbounded();
|
||||
std::thread::spawn(move || {
|
||||
toplevel_handler(toplevel_tx, calloop_rx);
|
||||
});
|
||||
(
|
||||
Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
|
||||
State::Waiting(toplevel_rx, calloop_tx),
|
||||
)
|
||||
}
|
||||
State::Waiting(mut rx, tx) => match rx.next().await {
|
||||
Some(u) => (Some((id, u)), State::Waiting(rx, tx)),
|
||||
None => {
|
||||
let _ = tx.send(ToplevelRequest::Exit);
|
||||
(Some((id, ToplevelUpdate::Finished)), State::Finished)
|
||||
async fn start_listening<I: Copy>(id: I, mut state: State) -> ((I, ToplevelUpdate), State) {
|
||||
loop {
|
||||
let (update, new_state) = match state {
|
||||
State::Ready => {
|
||||
let (calloop_tx, calloop_rx) = calloop::channel::channel();
|
||||
let (toplevel_tx, toplevel_rx) = unbounded();
|
||||
let handle = std::thread::spawn(move || {
|
||||
toplevel_handler(toplevel_tx, calloop_rx);
|
||||
});
|
||||
(
|
||||
Some((id, ToplevelUpdate::Init(calloop_tx.clone()))),
|
||||
State::Waiting(toplevel_rx, calloop_tx, handle),
|
||||
)
|
||||
}
|
||||
},
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
State::Waiting(mut rx, tx, handle) => {
|
||||
if handle.is_finished() {
|
||||
return ((id, ToplevelUpdate::Finished), State::Finished);
|
||||
}
|
||||
match rx.next().await {
|
||||
Some(u) => (Some((id, u)), State::Waiting(rx, tx, handle)),
|
||||
None => {
|
||||
let _ = tx.send(ToplevelRequest::Exit);
|
||||
(Some((id, ToplevelUpdate::Finished)), State::Finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
};
|
||||
|
||||
if let Some(update) = update {
|
||||
return (update, new_state);
|
||||
} else {
|
||||
state = new_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ icon-loader = { version = "0.3.6", features = ["gtk"] }
|
|||
libpulse-binding = "2.26.0"
|
||||
libpulse-glib-binding = "2.25.0"
|
||||
tokio = { version = "1.20.1", features=["full"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6"
|
||||
rust-embed = "6.6"
|
||||
rust-embed-utils = "7.5.0"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
|
|
|
|||
4
cosmic-applet-audio/i18n.toml
Normal file
4
cosmic-applet-audio/i18n.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
6
cosmic-applet-audio/i18n/en/cosmic_applet_audio.ftl
Normal file
6
cosmic-applet-audio/i18n/en/cosmic_applet_audio.ftl
Normal 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
|
||||
47
cosmic-applet-audio/src/localize.rs
Normal file
47
cosmic-applet-audio/src/localize.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
mod localize;
|
||||
|
||||
use cosmic::iced::widget;
|
||||
use cosmic::iced_native::alignment::Horizontal;
|
||||
use cosmic::iced_native::layout::Limits;
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::theme::Svg;
|
||||
|
||||
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
|
||||
use cosmic::widget::{button, divider, icon};
|
||||
use cosmic::Renderer;
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
|
||||
use cosmic::iced::{
|
||||
self,
|
||||
|
|
@ -20,12 +22,16 @@ use iced::widget::container;
|
|||
use iced::Color;
|
||||
|
||||
mod pulse;
|
||||
use crate::localize::localize;
|
||||
use crate::pulse::DeviceInfo;
|
||||
use libpulse_binding::volume::VolumeLinear;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
pretty_env_logger::init();
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
let helper = CosmicAppletHelper::default();
|
||||
Audio::run(helper.window_settings())
|
||||
}
|
||||
|
|
@ -43,7 +49,7 @@ struct Audio {
|
|||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
show_media_controls_in_top_panel: bool,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
|
@ -93,7 +99,7 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -101,10 +107,10 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
|
|
@ -117,21 +123,21 @@ impl Application for Audio {
|
|||
conn.send(pulse::Message::UpdateConnection);
|
||||
}
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1)
|
||||
.min_width(1)
|
||||
.max_width(400)
|
||||
.max_height(1080);
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_width(400.0)
|
||||
.max_height(1080.0);
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
|
|
@ -268,7 +274,7 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
|
|
@ -291,20 +297,18 @@ impl Application for Audio {
|
|||
.0 * 100.0;
|
||||
|
||||
let audio_content = if audio_disabled {
|
||||
column![text("PulseAudio Disconnected")
|
||||
column![text(fl!("disconnected"))
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.size(24),]
|
||||
} else {
|
||||
column![
|
||||
row![
|
||||
icon("audio-volume-high-symbolic", 32)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.style(Svg::Symbolic),
|
||||
icon("audio-volume-high-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", out_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
|
|
@ -312,13 +316,11 @@ impl Application for Audio {
|
|||
.align_items(Alignment::Center)
|
||||
.padding([8, 24]),
|
||||
row![
|
||||
icon("audio-input-microphone-symbolic", 32)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.style(Svg::Symbolic),
|
||||
icon("audio-input-microphone-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", in_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
|
|
@ -330,7 +332,7 @@ impl Application for Audio {
|
|||
.width(Length::Fill),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Output,
|
||||
"Output",
|
||||
fl!("output"),
|
||||
match &self.current_output {
|
||||
Some(output) => pretty_name(output.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
|
|
@ -348,10 +350,10 @@ impl Application for Audio {
|
|||
),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Input,
|
||||
"Input",
|
||||
fl!("input"),
|
||||
match &self.current_input {
|
||||
Some(input) => pretty_name(input.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
None => fl!("no-device"),
|
||||
},
|
||||
self.inputs
|
||||
.clone()
|
||||
|
|
@ -372,17 +374,20 @@ impl Application for Audio {
|
|||
container(divider::horizontal::light())
|
||||
.padding([12, 24])
|
||||
.width(Length::Fill),
|
||||
container(toggler(
|
||||
Some("Show Media Controls on Top Panel".into()),
|
||||
self.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
))
|
||||
container(
|
||||
toggler(
|
||||
Some(fl!("show-media-controls")),
|
||||
self.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
)
|
||||
.text_size(14)
|
||||
)
|
||||
.padding([0, 24]),
|
||||
container(divider::horizontal::light())
|
||||
.padding([12, 24])
|
||||
.width(Length::Fill),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
.text("Sound Settings...")
|
||||
button(applet_button_theme())
|
||||
.custom(vec![text(fl!("sound-settings")).size(14).into()])
|
||||
.padding([8, 24])
|
||||
.width(Length::Fill)
|
||||
]
|
||||
|
|
@ -398,19 +403,19 @@ impl Application for Audio {
|
|||
|
||||
fn revealer(
|
||||
open: bool,
|
||||
title: &str,
|
||||
title: String,
|
||||
selected: String,
|
||||
options: Vec<(String, String)>,
|
||||
toggle: Message,
|
||||
mut change: impl FnMut(String) -> Message + 'static,
|
||||
) -> widget::Column<Message, Renderer> {
|
||||
) -> widget::Column<'static, Message, Renderer> {
|
||||
if open {
|
||||
options.iter().fold(
|
||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||
|col, (id, name)| {
|
||||
col.push(
|
||||
button(APPLET_BUTTON_THEME)
|
||||
.custom(vec![text(name).into()])
|
||||
button(applet_button_theme())
|
||||
.custom(vec![text(name).size(14).into()])
|
||||
.on_press(change(id.clone()))
|
||||
.width(Length::Fill)
|
||||
.padding([8, 48]),
|
||||
|
|
@ -424,14 +429,14 @@ fn revealer(
|
|||
|
||||
fn revealer_head(
|
||||
_open: bool,
|
||||
title: &str,
|
||||
title: String,
|
||||
selected: String,
|
||||
toggle: Message,
|
||||
) -> widget::Button<Message, Renderer> {
|
||||
button(APPLET_BUTTON_THEME)
|
||||
) -> widget::Button<'static, Message, Renderer> {
|
||||
button(applet_button_theme())
|
||||
.custom(vec![
|
||||
text(title).width(Length::Fill).into(),
|
||||
text(selected).into(),
|
||||
text(title).width(Length::Fill).size(14).into(),
|
||||
text(selected).size(10).into(),
|
||||
])
|
||||
.padding([8, 24])
|
||||
.width(Length::Fill)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use cosmic::iced_native::subscription::{self, Subscription};
|
||||
use std::cell::RefCell;
|
||||
use std::{rc::Rc, thread};
|
||||
|
||||
extern crate libpulse_binding as pulse;
|
||||
use cosmic::iced::{subscription, Subscription};
|
||||
//use futures::channel::mpsc;
|
||||
use libpulse_binding::{
|
||||
callbacks::ListResult,
|
||||
|
|
@ -15,71 +15,80 @@ use libpulse_binding::{
|
|||
proplist::Proplist,
|
||||
volume::ChannelVolumes,
|
||||
};
|
||||
|
||||
pub fn connect() -> Subscription<Event> {
|
||||
struct Connect;
|
||||
|
||||
subscription::unfold(
|
||||
std::any::TypeId::of::<Connect>(),
|
||||
State::Init,
|
||||
|state| async move {
|
||||
match state {
|
||||
State::Init => {
|
||||
let PulseHandle {
|
||||
to_pulse,
|
||||
from_pulse,
|
||||
} = PulseHandle::new();
|
||||
(
|
||||
Some(Event::Init(Connection(to_pulse))),
|
||||
State::Connecting(from_pulse),
|
||||
)
|
||||
}
|
||||
// Waiting for Connection to succeed
|
||||
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
|
||||
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
|
||||
Some(Message::Connected) => {
|
||||
(Some(Event::Connected), State::Connected(from_pulse))
|
||||
}
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
Some(m) => {
|
||||
panic!("Unexpected message: {:?}", m);
|
||||
}
|
||||
None => {
|
||||
panic!("Pulse Sender dropped, something has gone wrong!");
|
||||
}
|
||||
},
|
||||
State::Connected(mut from_pulse) => {
|
||||
// This is where we match messages from the pulse server to pass to the gui
|
||||
match from_pulse.recv().await {
|
||||
Some(Message::SetSinks(sinks)) => (
|
||||
Some(Event::MessageReceived(Message::SetSinks(sinks))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetSources(sources)) => (
|
||||
Some(Event::MessageReceived(Message::SetSources(sources))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSink(sink)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSource(source)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
|
||||
_ => (None, State::Connected(from_pulse)),
|
||||
}
|
||||
|mut state| async move {
|
||||
loop {
|
||||
let (update, new_state) = connection(state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn connection(state: State) -> (Option<Event>, State) {
|
||||
match state {
|
||||
State::Init => {
|
||||
let PulseHandle {
|
||||
to_pulse,
|
||||
from_pulse,
|
||||
} = PulseHandle::new();
|
||||
(
|
||||
Some(Event::Init(Connection(to_pulse))),
|
||||
State::Connecting(from_pulse),
|
||||
)
|
||||
}
|
||||
// Waiting for Connection to succeed
|
||||
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
|
||||
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
|
||||
Some(Message::Connected) => (Some(Event::Connected), State::Connected(from_pulse)),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
Some(m) => {
|
||||
panic!("Unexpected message: {:?}", m);
|
||||
}
|
||||
None => {
|
||||
panic!("Pulse Sender dropped, something has gone wrong!");
|
||||
}
|
||||
},
|
||||
State::Connected(mut from_pulse) => {
|
||||
// This is where we match messages from the pulse server to pass to the gui
|
||||
match from_pulse.recv().await {
|
||||
Some(Message::SetSinks(sinks)) => (
|
||||
Some(Event::MessageReceived(Message::SetSinks(sinks))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetSources(sources)) => (
|
||||
Some(Event::MessageReceived(Message::SetSources(sources))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSink(sink)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSource(source)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
|
||||
_ => (None, State::Connected(from_pulse)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Debug)]
|
||||
enum State {
|
||||
Init,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1.16.0"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
futures = "0.3"
|
||||
zbus = { version = "3.5", default-features = false, features = ["tokio"] }
|
||||
zbus = { version = "3.13", default-features = false, features = ["tokio"] }
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6.4"
|
||||
|
|
|
|||
|
|
@ -10,19 +10,19 @@ use crate::upower_device::{device_subscription, DeviceDbusEvent};
|
|||
use crate::upower_kbdbacklight::{
|
||||
kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate,
|
||||
};
|
||||
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
|
||||
use cosmic::iced::alignment::Horizontal;
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::{
|
||||
widget::{column, container, row, slider, text},
|
||||
window, Alignment, Application, Command, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_native::layout::Limits;
|
||||
use cosmic::iced_runtime::core::layout::Limits;
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::Color;
|
||||
use cosmic::theme::Svg;
|
||||
use cosmic::widget::{button, divider, icon, toggler};
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
use log::error;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
|
@ -58,7 +58,7 @@ struct CosmicBatteryApplet {
|
|||
kbd_brightness: f64,
|
||||
screen_brightness: f64,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
screen_sender: Option<UnboundedSender<ScreenBacklightRequest>>,
|
||||
kbd_sender: Option<UnboundedSender<KeyboardBacklightRequest>>,
|
||||
applet_helper: CosmicAppletHelper,
|
||||
|
|
@ -144,21 +144,21 @@ impl Application for CosmicBatteryApplet {
|
|||
}
|
||||
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.max_width(372)
|
||||
.min_width(300)
|
||||
.min_height(200)
|
||||
.max_height(1080);
|
||||
.max_width(372.0)
|
||||
.min_width(300.0)
|
||||
.min_height(200.0)
|
||||
.max_height(1080.0);
|
||||
if let Some(tx) = self.power_profile_sender.as_ref() {
|
||||
let _ = tx.send(PowerProfileRequest::Get);
|
||||
}
|
||||
|
|
@ -213,13 +213,13 @@ impl Application for CosmicBatteryApplet {
|
|||
Command::none()
|
||||
}
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
} else {
|
||||
let name = text(fl!("battery")).size(18);
|
||||
let name = text(fl!("battery")).size(14);
|
||||
let description = text(
|
||||
if "battery-full-charging-symbolic" == self.icon_name
|
||||
|| "battery-full-charged-symbolic" == self.icon_name
|
||||
|
|
@ -234,15 +234,12 @@ impl Application for CosmicBatteryApplet {
|
|||
)
|
||||
},
|
||||
)
|
||||
.size(12);
|
||||
.size(10);
|
||||
self.applet_helper
|
||||
.popup_container(
|
||||
column![
|
||||
row![
|
||||
icon(&*self.icon_name, 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon(&*self.icon_name, 24).style(Svg::Symbolic),
|
||||
column![name, description]
|
||||
]
|
||||
.padding([0, 24])
|
||||
|
|
@ -251,11 +248,11 @@ impl Application for CosmicBatteryApplet {
|
|||
container(divider::horizontal::light())
|
||||
.width(Length::Fill)
|
||||
.padding([0, 12]),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(fl!("battery")).size(14),
|
||||
text(fl!("battery-desc")).size(12)
|
||||
text(fl!("battery-desc")).size(10)
|
||||
]
|
||||
.width(Length::Fill),
|
||||
icon("emblem-ok-symbolic", 12).size(12).style(
|
||||
|
|
@ -270,11 +267,11 @@ impl Application for CosmicBatteryApplet {
|
|||
.padding([8, 24])
|
||||
.on_press(Message::SelectProfile(Power::Battery))
|
||||
.width(Length::Fill),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(fl!("balanced")).size(14),
|
||||
text(fl!("balanced-desc")).size(12)
|
||||
text(fl!("balanced-desc")).size(10)
|
||||
]
|
||||
.width(Length::Fill),
|
||||
icon("emblem-ok-symbolic", 12).size(12).style(
|
||||
|
|
@ -289,11 +286,11 @@ impl Application for CosmicBatteryApplet {
|
|||
.padding([8, 24])
|
||||
.on_press(Message::SelectProfile(Power::Balanced))
|
||||
.width(Length::Fill),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(fl!("performance")).size(14),
|
||||
text(fl!("performance-desc")).size(12)
|
||||
text(fl!("performance-desc")).size(10)
|
||||
]
|
||||
.width(Length::Fill),
|
||||
icon("emblem-ok-symbolic", 12).size(12).style(
|
||||
|
|
@ -311,42 +308,42 @@ impl Application for CosmicBatteryApplet {
|
|||
container(divider::horizontal::light())
|
||||
.width(Length::Fill)
|
||||
.padding([0, 12]),
|
||||
container(toggler(fl!("max-charge"), self.charging_limit, |_| {
|
||||
Message::SetChargingLimit(!self.charging_limit)
|
||||
}))
|
||||
container(
|
||||
toggler(fl!("max-charge"), self.charging_limit, |_| {
|
||||
Message::SetChargingLimit(!self.charging_limit)
|
||||
})
|
||||
.text_size(14)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.padding([0, 24])
|
||||
.width(Length::Fill),
|
||||
container(divider::horizontal::light())
|
||||
.width(Length::Fill)
|
||||
.padding([0, 12]),
|
||||
row![
|
||||
icon("display-brightness-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("display-brightness-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(
|
||||
1..=100,
|
||||
(self.screen_brightness * 100.0) as i32,
|
||||
Message::SetScreenBrightness
|
||||
),
|
||||
text(format!("{:.0}%", self.screen_brightness * 100.0))
|
||||
.width(Length::Units(40))
|
||||
.size(16)
|
||||
.width(Length::Fixed(40.0))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.padding([0, 24])
|
||||
.spacing(12),
|
||||
row![
|
||||
icon("keyboard-brightness-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("keyboard-brightness-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(
|
||||
0..=100,
|
||||
(self.kbd_brightness * 100.0) as i32,
|
||||
Message::SetKbdBrightness
|
||||
),
|
||||
text(format!("{:.0}%", self.kbd_brightness * 100.0))
|
||||
.width(Length::Units(40))
|
||||
.size(16)
|
||||
.width(Length::Fixed(40.0))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.padding([0, 24])
|
||||
|
|
@ -354,8 +351,11 @@ impl Application for CosmicBatteryApplet {
|
|||
container(divider::horizontal::light())
|
||||
.width(Length::Fill)
|
||||
.padding([0, 12]),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
.custom(vec![text(fl!("power-settings")).width(Length::Fill).into()])
|
||||
button(applet_button_theme())
|
||||
.custom(vec![text(fl!("power-settings"))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.into()])
|
||||
.on_press(Message::OpenBatterySettings)
|
||||
.width(Length::Fill)
|
||||
.padding([8, 24])
|
||||
|
|
@ -369,35 +369,38 @@ impl Application for CosmicBatteryApplet {
|
|||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
device_subscription(0).map(|(_, event)| match event {
|
||||
DeviceDbusEvent::Update {
|
||||
icon_name,
|
||||
percent,
|
||||
time_to_empty,
|
||||
} => Message::Update {
|
||||
device_subscription(0).map(
|
||||
|(
|
||||
_,
|
||||
DeviceDbusEvent::Update {
|
||||
icon_name,
|
||||
percent,
|
||||
time_to_empty,
|
||||
},
|
||||
)| Message::Update {
|
||||
icon_name,
|
||||
percent,
|
||||
time_to_empty,
|
||||
},
|
||||
),
|
||||
kbd_backlight_subscription(0).map(|event| match event {
|
||||
(_, KeyboardBacklightUpdate::Update(b)) => Message::UpdateKbdBrightness(b),
|
||||
(_, KeyboardBacklightUpdate::Init(tx, b)) => Message::InitKbdBacklight(tx, b),
|
||||
}),
|
||||
kbd_backlight_subscription(0).map(|(_, event)| match event {
|
||||
KeyboardBacklightUpdate::Update(b) => Message::UpdateKbdBrightness(b),
|
||||
KeyboardBacklightUpdate::Init(tx, b) => Message::InitKbdBacklight(tx, b),
|
||||
screen_backlight_subscription(0).map(|e| match e {
|
||||
(_, ScreenBacklightUpdate::Update(b)) => Message::UpdateScreenBrightness(b),
|
||||
(_, ScreenBacklightUpdate::Init(tx, b)) => Message::InitScreenBacklight(tx, b),
|
||||
}),
|
||||
screen_backlight_subscription(0).map(|(_, event)| match event {
|
||||
ScreenBacklightUpdate::Update(b) => Message::UpdateScreenBrightness(b),
|
||||
ScreenBacklightUpdate::Init(tx, b) => Message::InitScreenBacklight(tx, b),
|
||||
}),
|
||||
power_profile_subscription(0).map(|(_, event)| match event {
|
||||
PowerProfileUpdate::Update { profile } => Message::Profile(profile),
|
||||
PowerProfileUpdate::Init(tx, p) => Message::InitProfile(p, tx),
|
||||
PowerProfileUpdate::Error(e) => Message::Errored(e), // TODO: handle error
|
||||
power_profile_subscription(0).map(|event| match event {
|
||||
(_, PowerProfileUpdate::Update { profile }) => Message::Profile(profile),
|
||||
(_, PowerProfileUpdate::Init(tx, p)) => Message::InitProfile(p, tx),
|
||||
(_, PowerProfileUpdate::Error(e)) => Message::Errored(e), // TODO: handle error
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Message {
|
||||
|
|
@ -405,9 +408,9 @@ impl Application for CosmicBatteryApplet {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,20 @@ pub async fn backlight() -> io::Result<Option<Backlight>> {
|
|||
pub fn screen_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, ScreenBacklightUpdate)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
|
||||
}
|
||||
|
||||
async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, ScreenBacklightUpdate), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
|
||||
use cosmic::iced;
|
||||
use cosmic::iced_native::subscription;
|
||||
use cosmic::iced::subscription;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
|
@ -114,7 +114,9 @@ pub async fn set_power_profile(daemon: PowerDaemonProxy<'_>, power: Power) -> Re
|
|||
pub fn power_profile_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, PowerProfileUpdate)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| {
|
||||
start_listening_loop(id, state)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -124,6 +126,19 @@ pub enum State {
|
|||
Finished,
|
||||
}
|
||||
|
||||
async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, PowerProfileUpdate), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, PowerProfileUpdate)>, State) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
|
|
@ -189,14 +204,11 @@ async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, PowerProfi
|
|||
}
|
||||
}
|
||||
Some(PowerProfileRequest::Set(profile)) => {
|
||||
if set_power_profile(power_proxy, profile).await.is_ok() {
|
||||
(
|
||||
Some((id, PowerProfileUpdate::Update { profile })),
|
||||
State::Waiting(conn, rx),
|
||||
)
|
||||
} else {
|
||||
(None, State::Waiting(conn, rx))
|
||||
}
|
||||
let _ = set_power_profile(power_proxy, profile).await;
|
||||
(
|
||||
Some((id, PowerProfileUpdate::Update { profile })),
|
||||
State::Waiting(conn, rx),
|
||||
)
|
||||
}
|
||||
None => (None, State::Finished),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ trait Device {
|
|||
pub fn device_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, DeviceDbusEvent)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -174,6 +174,19 @@ async fn display_device() -> zbus::Result<DeviceProxy<'static>> {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, DeviceDbusEvent), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, DeviceDbusEvent)>, State) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use iced::subscription;
|
|||
use std::{fmt::Debug, hash::Hash};
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
use zbus::dbus_proxy;
|
||||
|
||||
#[dbus_proxy(
|
||||
default_service = "org.freedesktop.UPower",
|
||||
interface = "org.freedesktop.UPower.KbdBacklight",
|
||||
|
|
@ -35,7 +36,20 @@ trait KbdBacklight {
|
|||
pub fn kbd_backlight_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, KeyboardBacklightUpdate)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
|
||||
}
|
||||
|
||||
async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, KeyboardBacklightUpdate), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ license = "GPL-3.0-or-later"
|
|||
once_cell = "1.16.0"
|
||||
bluer = { version = "0.15", features = ["bluetoothd", "id"] }
|
||||
futures-util = "0.3.21"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
itertools = "0.10.3"
|
||||
slotmap = "1.0.6"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
use crate::bluetooth::{BluerDeviceStatus, BluerRequest, BluerState};
|
||||
use cosmic::applet::APPLET_BUTTON_THEME;
|
||||
use cosmic::iced_style;
|
||||
use cosmic::{
|
||||
applet::CosmicAppletHelper,
|
||||
iced::{
|
||||
wayland::{
|
||||
popup::{destroy_popup, get_popup},
|
||||
},
|
||||
wayland::popup::{destroy_popup, get_popup},
|
||||
widget::{column, container, row, scrollable, text, Column},
|
||||
Alignment, Application, Color, Command, Length, Subscription,
|
||||
},
|
||||
iced_native::{
|
||||
iced_runtime::core::{
|
||||
alignment::{Horizontal, Vertical},
|
||||
layout::Limits,
|
||||
renderer::BorderRadius,
|
||||
window,
|
||||
},
|
||||
iced_style::{application, button::StyleSheet},
|
||||
|
|
@ -21,6 +16,7 @@ use cosmic::{
|
|||
widget::{button, divider, icon, toggler},
|
||||
Element, Theme,
|
||||
};
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
|
@ -38,7 +34,7 @@ struct CosmicBluetoothApplet {
|
|||
icon_name: String,
|
||||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
applet_helper: CosmicAppletHelper,
|
||||
bluer_state: BluerState,
|
||||
bluer_sender: Option<Sender<BluerRequest>>,
|
||||
|
|
@ -87,11 +83,11 @@ impl Application for CosmicBluetoothApplet {
|
|||
} else {
|
||||
// TODO request update of state maybe
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -99,10 +95,10 @@ impl Application for CosmicBluetoothApplet {
|
|||
);
|
||||
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1)
|
||||
.min_width(1)
|
||||
.max_height(800)
|
||||
.max_width(400);
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_height(800.0)
|
||||
.max_width(400.0);
|
||||
let tx = self.bluer_sender.as_ref().cloned();
|
||||
return Command::batch(vec![
|
||||
Command::perform(
|
||||
|
|
@ -275,17 +271,17 @@ impl Application for CosmicBluetoothApplet {
|
|||
Command::none()
|
||||
}
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
let button_style = Button::Custom {
|
||||
active: |t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
let button_style = || Button::Custom {
|
||||
active: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: 0.0,
|
||||
..t.active(&Button::Text)
|
||||
},
|
||||
hover: |t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
}),
|
||||
hover: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: 0.0,
|
||||
..t.hovered(&Button::Text)
|
||||
},
|
||||
}),
|
||||
};
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
|
|
@ -320,20 +316,15 @@ impl Application for CosmicBluetoothApplet {
|
|||
}
|
||||
BluerDeviceStatus::Paired => {}
|
||||
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
|
||||
row = row.push(
|
||||
icon("process-working-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
);
|
||||
row = row.push(icon("process-working-symbolic", 24).style(Svg::Symbolic));
|
||||
}
|
||||
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
|
||||
};
|
||||
|
||||
known_bluetooth = known_bluetooth.push(
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row.into()])
|
||||
.style(APPLET_BUTTON_THEME)
|
||||
.style(applet_button_theme())
|
||||
.on_press(match dev.status {
|
||||
BluerDeviceStatus::Connected => {
|
||||
Message::Request(BluerRequest::DisconnectDevice(dev.address))
|
||||
|
|
@ -392,25 +383,20 @@ impl Application for CosmicBluetoothApplet {
|
|||
text(fl!("other-devices"))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(24))
|
||||
.height(Length::Fixed(24.0))
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.into(),
|
||||
container(
|
||||
icon(dropdown_icon, 14)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(14))
|
||||
.height(Length::Units(14)),
|
||||
)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Fixed(24.0))
|
||||
.height(Length::Fixed(24.0))
|
||||
.into(),
|
||||
]
|
||||
.into(),
|
||||
)
|
||||
.padding([8, 24])
|
||||
.style(button_style.clone())
|
||||
.style(button_style())
|
||||
.on_press(Message::ToggleVisibleDevices(!self.show_visible_devices));
|
||||
content = content.push(available_connections_btn);
|
||||
let mut list_column: Vec<Element<'_, Message>> =
|
||||
|
|
@ -420,6 +406,7 @@ impl Application for CosmicBluetoothApplet {
|
|||
let row = column![
|
||||
icon(device.icon.as_str(), 16).style(Svg::Symbolic),
|
||||
text(&device.name)
|
||||
.size(14)
|
||||
.horizontal_alignment(Horizontal::Left)
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.width(Length::Fill),
|
||||
|
|
@ -435,20 +422,20 @@ impl Application for CosmicBluetoothApplet {
|
|||
.horizontal_alignment(Horizontal::Center)
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.width(Length::Fill)
|
||||
.size(32),
|
||||
.size(22),
|
||||
row![
|
||||
button(Button::Secondary)
|
||||
.custom(
|
||||
vec![text(fl!("cancel"))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(24))
|
||||
.height(Length::Fixed(24.0))
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.into(),]
|
||||
.into(),
|
||||
)
|
||||
.padding([8, 24])
|
||||
.style(button_style.clone())
|
||||
.style(button_style())
|
||||
.on_press(Message::Cancel)
|
||||
.width(Length::Fill),
|
||||
button(Button::Secondary)
|
||||
|
|
@ -456,13 +443,13 @@ impl Application for CosmicBluetoothApplet {
|
|||
vec![text(fl!("confirm"))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(24))
|
||||
.height(Length::Fixed(24.0))
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.into(),]
|
||||
.into(),
|
||||
)
|
||||
.padding([8, 24])
|
||||
.style(button_style.clone())
|
||||
.style(button_style())
|
||||
.on_press(Message::Confirm)
|
||||
.width(Length::Fill),
|
||||
]
|
||||
|
|
@ -494,7 +481,7 @@ impl Application for CosmicBluetoothApplet {
|
|||
.align_items(Alignment::Center)
|
||||
.spacing(12);
|
||||
visible_devices = visible_devices.push(
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row.width(Length::Fill).into()])
|
||||
.on_press(Message::Request(BluerRequest::PairDevice(
|
||||
dev.address.clone(),
|
||||
|
|
@ -516,7 +503,7 @@ impl Application for CosmicBluetoothApplet {
|
|||
|
||||
if item_counter > 10 {
|
||||
content = content.push(
|
||||
scrollable(Column::with_children(list_column)).height(Length::Units(300)),
|
||||
scrollable(Column::with_children(list_column)).height(Length::Fixed(300.0)),
|
||||
);
|
||||
} else {
|
||||
content = content.push(Column::with_children(list_column));
|
||||
|
|
@ -526,11 +513,11 @@ impl Application for CosmicBluetoothApplet {
|
|||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
bluetooth_subscription(0).map(|e| Message::BluetoothEvent(e.1))
|
||||
bluetooth_subscription(0).map(|(_, e)| Message::BluetoothEvent(e))
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -538,9 +525,11 @@ impl Application for CosmicBluetoothApplet {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
|
||||
application::Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use tokio::{
|
|||
pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, BluerEvent)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening_loop(id, state))
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
|
|
@ -30,6 +30,19 @@ pub enum State {
|
|||
Finished,
|
||||
}
|
||||
|
||||
async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, BluerEvent), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_listening<I: Copy + Debug>(id: I, state: State) -> (Option<(I, BluerEvent)>, State) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
zbus = "3.4"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
zbus = "3.13"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
once_cell = "1"
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ mod localize;
|
|||
mod window;
|
||||
|
||||
use cosmic::{
|
||||
applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper},
|
||||
iced::{wayland::InitialSurface, Application, Settings},
|
||||
iced_native::layout::Limits,
|
||||
iced_runtime::core::layout::Limits,
|
||||
};
|
||||
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
|
||||
|
||||
use window::*;
|
||||
|
||||
|
|
@ -21,10 +21,10 @@ pub fn main() -> cosmic::iced::Result {
|
|||
InitialSurface::XdgWindow(w) => {
|
||||
w.autosize = true;
|
||||
w.size_limits = Limits::NONE
|
||||
.min_height(1)
|
||||
.max_height(200)
|
||||
.min_width(1)
|
||||
.max_width(1000);
|
||||
.min_height(1.0)
|
||||
.max_height(200.0)
|
||||
.min_width(1.0)
|
||||
.max_width(1000.0);
|
||||
}
|
||||
InitialSurface::None => unimplemented!(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
use crate::dbus::{self, PowerDaemonProxy};
|
||||
use crate::fl;
|
||||
use crate::graphics::{get_current_graphics, set_graphics, Graphics};
|
||||
use cosmic::applet::CosmicAppletHelper;
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced_native::alignment::Horizontal;
|
||||
use cosmic::iced_native::Alignment;
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::iced_runtime::core::Alignment;
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::Color;
|
||||
use cosmic::theme::Button;
|
||||
use cosmic::widget::icon;
|
||||
use cosmic::{
|
||||
applet::{cosmic_panel_config::PanelAnchor, APPLET_BUTTON_THEME},
|
||||
iced::widget::{column, container, row, text},
|
||||
iced::{self, Application, Command, Length},
|
||||
iced_native::window,
|
||||
iced_runtime::core::window,
|
||||
theme::{Svg, Theme},
|
||||
widget::{button, divider},
|
||||
Element,
|
||||
};
|
||||
use cosmic_applet::{applet_button_theme, cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
|
||||
use zbus::Connection;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -41,7 +40,7 @@ impl GraphicsMode {
|
|||
pub struct Window {
|
||||
popup: Option<window::Id>,
|
||||
graphics_mode: Option<GraphicsMode>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
theme: Theme,
|
||||
dbus: Option<(Connection, PowerDaemonProxy<'static>)>,
|
||||
applet_helper: CosmicAppletHelper,
|
||||
|
|
@ -92,7 +91,7 @@ impl Application for Window {
|
|||
return destroy_popup(p);
|
||||
} else {
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
let mut commands = Vec::new();
|
||||
if let Some((_, proxy)) = self.dbus.as_ref() {
|
||||
|
|
@ -102,7 +101,7 @@ impl Application for Window {
|
|||
));
|
||||
}
|
||||
let popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -184,7 +183,7 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
match self.applet_helper.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => self
|
||||
.applet_helper
|
||||
|
|
@ -206,6 +205,7 @@ impl Application for Window {
|
|||
Some(Graphics::Hybrid) => fl!("hybrid"),
|
||||
None => "".into(),
|
||||
})
|
||||
.size(14)
|
||||
]
|
||||
.spacing(8)
|
||||
.padding([0, self.applet_helper.suggested_size().0 / 2])
|
||||
|
|
@ -220,7 +220,7 @@ impl Application for Window {
|
|||
}
|
||||
} else {
|
||||
let content_list = vec![
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14),
|
||||
|
|
@ -256,7 +256,7 @@ impl Application for Window {
|
|||
.on_press(Message::SelectGraphicsMode(Graphics::Integrated))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),]
|
||||
.width(Length::Fill),
|
||||
|
|
@ -289,7 +289,7 @@ impl Application for Window {
|
|||
.on_press(Message::SelectGraphicsMode(Graphics::Nvidia))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14),
|
||||
|
|
@ -325,7 +325,7 @@ impl Application for Window {
|
|||
.on_press(Message::SelectGraphicsMode(Graphics::Hybrid))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![row![
|
||||
column![
|
||||
text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14),
|
||||
|
|
@ -369,7 +369,7 @@ impl Application for Window {
|
|||
text(fl!("graphics-mode"))
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.size(24)
|
||||
.size(14)
|
||||
.into(),
|
||||
container(divider::horizontal::light())
|
||||
.padding([0, 12])
|
||||
|
|
@ -385,7 +385,7 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn close_requested(&self, id: window::Id) -> Self::Message {
|
||||
if id != window::Id::new(0) {
|
||||
if id != window::Id(0) {
|
||||
Message::PopupClosed(id)
|
||||
} else {
|
||||
unimplemented!();
|
||||
|
|
@ -393,10 +393,10 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().background.on.into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_exit(&self) -> bool {
|
||||
|
|
@ -404,6 +404,6 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ once_cell = "1.16.0"
|
|||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "main" }
|
||||
# cosmic-dbus-networkmanager = { path = "../../../dbus-settings-bindings/networkmanager" }
|
||||
futures-util = "0.3.21"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet", "tokio"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["wayland", "tokio"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
futures = "0.3"
|
||||
zbus = { version = "3.7", default-features = false }
|
||||
zbus = { version = "3.13", default-features = false }
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
itertools = "0.10.3"
|
||||
slotmap = "1.0.6"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
|
@ -22,3 +23,5 @@ anyhow = "1.0"
|
|||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6.4"
|
||||
rust-embed = "6.3.0"
|
||||
rust-embed-utils = "7.5.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
use cosmic::iced_style;
|
||||
use cosmic::iced_widget::Row;
|
||||
use cosmic::{
|
||||
applet::CosmicAppletHelper,
|
||||
iced::{
|
||||
wayland::popup::{destroy_popup, get_popup},
|
||||
widget::{column, container, row, scrollable, text, text_input, Column},
|
||||
Alignment, Application, Color, Command, Length, Subscription,
|
||||
},
|
||||
iced_native::{
|
||||
iced_runtime::core::{
|
||||
alignment::{Horizontal, Vertical},
|
||||
layout::Limits,
|
||||
renderer::BorderRadius,
|
||||
window,
|
||||
},
|
||||
iced_style::{application, button::StyleSheet},
|
||||
|
|
@ -17,9 +16,14 @@ use cosmic::{
|
|||
widget::{button, divider, icon, toggler},
|
||||
Element, Theme,
|
||||
};
|
||||
use cosmic_applet::CosmicAppletHelper;
|
||||
use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState};
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::network_manager::active_conns::active_conns_subscription;
|
||||
use crate::network_manager::devices::devices_subscription;
|
||||
use crate::network_manager::wireless_enabled::wireless_enabled_subscription;
|
||||
use crate::network_manager::NetworkManagerState;
|
||||
use crate::{
|
||||
config, fl,
|
||||
|
|
@ -77,13 +81,14 @@ struct CosmicNetworkApplet {
|
|||
icon_name: String,
|
||||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
applet_helper: CosmicAppletHelper,
|
||||
nm_state: NetworkManagerState,
|
||||
// UI state
|
||||
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
|
||||
show_visible_networks: bool,
|
||||
new_connection: Option<NewConnectionState>,
|
||||
conn: Option<Connection>,
|
||||
}
|
||||
|
||||
impl CosmicNetworkApplet {
|
||||
|
|
@ -110,7 +115,7 @@ impl CosmicNetworkApplet {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
pub(crate) enum Message {
|
||||
ActivateKnownWifi(String),
|
||||
Disconnect(String),
|
||||
TogglePopup,
|
||||
|
|
@ -155,11 +160,11 @@ impl Application for CosmicNetworkApplet {
|
|||
} else {
|
||||
// TODO request update of state maybe
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -167,10 +172,10 @@ impl Application for CosmicNetworkApplet {
|
|||
);
|
||||
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1)
|
||||
.min_width(1)
|
||||
.max_height(800)
|
||||
.max_width(400);
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_height(800.0)
|
||||
.max_width(400.0);
|
||||
return get_popup(popup_settings);
|
||||
}
|
||||
}
|
||||
|
|
@ -193,10 +198,15 @@ impl Application for CosmicNetworkApplet {
|
|||
}
|
||||
}
|
||||
Message::NetworkManagerEvent(event) => match event {
|
||||
NetworkManagerEvent::Init { sender, state } => {
|
||||
NetworkManagerEvent::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
} => {
|
||||
self.nm_sender.replace(sender);
|
||||
self.nm_state = state;
|
||||
self.update_icon_name();
|
||||
self.conn = Some(conn);
|
||||
}
|
||||
NetworkManagerEvent::WiFiEnabled(state) => {
|
||||
self.nm_state = state;
|
||||
|
|
@ -337,17 +347,17 @@ impl Application for CosmicNetworkApplet {
|
|||
Command::none()
|
||||
}
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
let button_style = Button::Custom {
|
||||
active: |t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
let button_style = || Button::Custom {
|
||||
active: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: 0.0,
|
||||
..t.active(&Button::Text)
|
||||
},
|
||||
hover: |t| iced_style::button::Appearance {
|
||||
border_radius: BorderRadius::from(0.0),
|
||||
}),
|
||||
hover: Box::new(|t| iced_style::button::Appearance {
|
||||
border_radius: 0.0,
|
||||
..t.hovered(&Button::Text)
|
||||
},
|
||||
}),
|
||||
};
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
|
|
@ -362,7 +372,7 @@ impl Application for CosmicNetworkApplet {
|
|||
for addr in ip_addresses {
|
||||
ipv4.push(
|
||||
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
|
||||
.size(12)
|
||||
.size(10)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
|
@ -412,8 +422,6 @@ impl Application for CosmicNetworkApplet {
|
|||
let mut btn_content = vec![
|
||||
icon("network-wireless-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
column![text(name).size(14), Column::with_children(ipv4)]
|
||||
.width(Length::Fill)
|
||||
|
|
@ -425,8 +433,6 @@ impl Application for CosmicNetworkApplet {
|
|||
btn_content.push(
|
||||
icon("process-working-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
|
@ -441,9 +447,12 @@ impl Application for CosmicNetworkApplet {
|
|||
};
|
||||
known_wifi = known_wifi.push(
|
||||
column![button(Button::Secondary)
|
||||
.custom(btn_content)
|
||||
.custom(vec![Row::with_children(btn_content)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.into()])
|
||||
.padding([8, 24])
|
||||
.style(button_style.clone())
|
||||
.style(button_style())
|
||||
.on_press(Message::Disconnect(name.clone()))]
|
||||
.align_items(Alignment::Center),
|
||||
);
|
||||
|
|
@ -454,8 +463,6 @@ impl Application for CosmicNetworkApplet {
|
|||
let mut btn_content = vec![
|
||||
icon("network-wireless-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
text(&known.ssid).size(14).width(Length::Fill).into(),
|
||||
];
|
||||
|
|
@ -464,17 +471,18 @@ impl Application for CosmicNetworkApplet {
|
|||
btn_content.push(
|
||||
icon("process-working-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut btn = button(Button::Secondary)
|
||||
.custom(btn_content)
|
||||
.custom(vec![Row::with_children(btn_content)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.into()])
|
||||
.padding([8, 24])
|
||||
.width(Length::Fill)
|
||||
.style(button_style.clone());
|
||||
.style(button_style());
|
||||
btn = match known.state {
|
||||
DeviceState::Failed
|
||||
| DeviceState::Unknown
|
||||
|
|
@ -495,6 +503,7 @@ impl Application for CosmicNetworkApplet {
|
|||
toggler(fl!("airplane-mode"), self.nm_state.airplane_mode, |m| {
|
||||
Message::ToggleAirplaneMode(m)
|
||||
})
|
||||
.text_size(14)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.padding([0, 12]),
|
||||
|
|
@ -503,6 +512,7 @@ impl Application for CosmicNetworkApplet {
|
|||
toggler(fl!("wifi"), self.nm_state.wifi_enabled, |m| {
|
||||
Message::ToggleWiFi(m)
|
||||
})
|
||||
.text_size(14)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.padding([0, 12]),
|
||||
|
|
@ -523,25 +533,20 @@ impl Application for CosmicNetworkApplet {
|
|||
text(fl!("visible-wireless-networks"))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(24))
|
||||
.height(Length::Fixed(24.0))
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.into(),
|
||||
container(
|
||||
icon(dropdown_icon, 14)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(14))
|
||||
.height(Length::Units(14)),
|
||||
)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.into(),
|
||||
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Fixed(24.0))
|
||||
.height(Length::Fixed(24.0))
|
||||
.into(),
|
||||
]
|
||||
.into(),
|
||||
)
|
||||
.padding([8, 24])
|
||||
.style(button_style.clone())
|
||||
.style(button_style())
|
||||
.on_press(Message::ToggleVisibleNetworks);
|
||||
content = content.push(available_connections_btn);
|
||||
if self.show_visible_networks {
|
||||
|
|
@ -552,10 +557,7 @@ impl Application for CosmicNetworkApplet {
|
|||
password,
|
||||
} => {
|
||||
let id = row![
|
||||
icon("network-wireless-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
|
||||
text(&access_point.ssid).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
|
|
@ -565,7 +567,9 @@ impl Application for CosmicNetworkApplet {
|
|||
content = content.push(id);
|
||||
let col = column![
|
||||
text(fl!("enter-password")),
|
||||
text_input("", password, Message::Password)
|
||||
text_input("", password)
|
||||
.on_input(Message::Password)
|
||||
.on_paste(Message::Password)
|
||||
.on_submit(Message::SubmitPassword)
|
||||
.password(),
|
||||
container(text(fl!("router-wps-button"))).padding(8),
|
||||
|
|
@ -590,10 +594,7 @@ impl Application for CosmicNetworkApplet {
|
|||
}
|
||||
NewConnectionState::Waiting(access_point) => {
|
||||
let id = row![
|
||||
icon("network-wireless-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
|
||||
text(&access_point.ssid).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
|
|
@ -601,10 +602,7 @@ impl Application for CosmicNetworkApplet {
|
|||
.spacing(12);
|
||||
let connecting = row![
|
||||
id,
|
||||
icon("process-working-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("process-working-symbolic", 24).style(Svg::Symbolic),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding([0, 24]);
|
||||
|
|
@ -612,10 +610,7 @@ impl Application for CosmicNetworkApplet {
|
|||
}
|
||||
NewConnectionState::Failure(access_point) => {
|
||||
let id = row![
|
||||
icon("network-wireless-symbolic", 24)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24)),
|
||||
icon("network-wireless-symbolic", 24).style(Svg::Symbolic),
|
||||
text(&access_point.ssid).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
|
|
@ -660,15 +655,12 @@ impl Application for CosmicNetworkApplet {
|
|||
{
|
||||
continue;
|
||||
}
|
||||
let button = button(button_style)
|
||||
let button = button(button_style())
|
||||
.custom(vec![row![
|
||||
icon("network-wireless-symbolic", 16)
|
||||
.style(Svg::Symbolic)
|
||||
.width(Length::Units(16))
|
||||
.height(Length::Units(16)),
|
||||
icon("network-wireless-symbolic", 16).style(Svg::Symbolic),
|
||||
text(&ap.ssid)
|
||||
.size(14)
|
||||
.height(Length::Units(24))
|
||||
.height(Length::Fixed(24.0))
|
||||
.vertical_alignment(Vertical::Center)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
|
|
@ -680,7 +672,7 @@ impl Application for CosmicNetworkApplet {
|
|||
list_col.push(button.into());
|
||||
}
|
||||
content = content.push(
|
||||
scrollable(Column::with_children(list_col)).height(Length::Units(300)),
|
||||
scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -689,11 +681,25 @@ impl Application for CosmicNetworkApplet {
|
|||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
network_manager_subscription(0).map(|(_, event)| Message::NetworkManagerEvent(event))
|
||||
let network_sub =
|
||||
network_manager_subscription(0).map(|e| Message::NetworkManagerEvent(e.1));
|
||||
|
||||
if let Some(conn) = self.conn.as_ref() {
|
||||
Subscription::batch(vec![
|
||||
network_sub,
|
||||
active_conns_subscription(0, conn.clone())
|
||||
.map(|e| Message::NetworkManagerEvent(e.1)),
|
||||
devices_subscription(0, conn.clone()).map(|e| Message::NetworkManagerEvent(e.1)),
|
||||
wireless_enabled_subscription(0, conn.clone())
|
||||
.map(|e| Message::NetworkManagerEvent(e.1)),
|
||||
])
|
||||
} else {
|
||||
network_sub
|
||||
}
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -701,9 +707,11 @@ impl Application for CosmicNetworkApplet {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
|
||||
application::Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
cosmic-applet-network/src/network_manager/active_conns.rs
Normal file
56
cosmic-applet-network/src/network_manager/active_conns.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
59
cosmic-applet-network/src/network_manager/devices.rs
Normal file
59
cosmic-applet-network/src/network_manager/devices.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
pub mod active_conns;
|
||||
pub mod available_wifi;
|
||||
pub mod current_networks;
|
||||
pub mod devices;
|
||||
pub mod wireless_enabled;
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::Deref, time::Duration};
|
||||
|
||||
|
|
@ -32,7 +35,9 @@ use self::{
|
|||
pub fn network_manager_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> iced::Subscription<(I, NetworkManagerEvent)> {
|
||||
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
|
||||
subscription::unfold(id, State::Ready, move |state| {
|
||||
start_listening_loop(id, state)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -42,6 +47,19 @@ pub enum State {
|
|||
Finished,
|
||||
}
|
||||
|
||||
pub async fn start_listening_loop<I: Copy + Debug>(
|
||||
id: I,
|
||||
mut state: State,
|
||||
) -> ((I, NetworkManagerEvent), State) {
|
||||
loop {
|
||||
let (update, new_state) = start_listening(id, state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_listening<I: Copy + Debug>(
|
||||
id: I,
|
||||
state: State,
|
||||
|
|
@ -59,6 +77,7 @@ async fn start_listening<I: Copy + Debug>(
|
|||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::Init {
|
||||
conn: conn.clone(),
|
||||
sender: tx,
|
||||
state: nm_state,
|
||||
},
|
||||
|
|
@ -72,289 +91,402 @@ async fn start_listening<I: Copy + Debug>(
|
|||
Err(_) => return (None, State::Finished),
|
||||
};
|
||||
|
||||
let mut active_conns_changed = tokio::time::sleep(Duration::from_secs(5))
|
||||
.then(|_| async { network_manager.receive_active_connections_changed().await })
|
||||
.await;
|
||||
let mut devices_changed = network_manager.receive_devices_changed().await;
|
||||
let mut wireless_enabled_changed =
|
||||
network_manager.receive_wireless_enabled_changed().await;
|
||||
let mut req = rx.next().boxed().fuse();
|
||||
|
||||
let (update, should_exit) = futures::select! {
|
||||
req = req => {
|
||||
match req {
|
||||
Some(NetworkManagerRequest::Disconnect(ssid)) => {
|
||||
let mut success = false;
|
||||
for c in network_manager.active_connections().await.unwrap_or_default() {
|
||||
if c.id().await.unwrap_or_default() == ssid {
|
||||
if let Ok(_) = network_manager.deactivate_connection(&c).await {
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
let (update, should_exit) = match rx.next().await {
|
||||
Some(NetworkManagerRequest::Disconnect(ssid)) => {
|
||||
let mut success = false;
|
||||
for c in network_manager
|
||||
.active_connections()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if c.id().await.unwrap_or_default() == ssid {
|
||||
if let Ok(_) = network_manager.deactivate_connection(&c).await {
|
||||
success = true;
|
||||
}
|
||||
(Some((id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
}
|
||||
}
|
||||
(
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Disconnect(ssid.clone()),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false)
|
||||
},
|
||||
)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
|
||||
// wifi
|
||||
let mut success = network_manager
|
||||
.set_wireless_enabled(!airplane_mode)
|
||||
.await
|
||||
.is_ok();
|
||||
// bluetooth
|
||||
success = success
|
||||
&& Command::new("rfkill")
|
||||
.arg(if airplane_mode { "block" } else { "unblock" })
|
||||
.arg("bluetooth")
|
||||
.output()
|
||||
.await
|
||||
.is_ok();
|
||||
let response = NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
};
|
||||
(Some((id, response)), false)
|
||||
}
|
||||
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
|
||||
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
|
||||
let response = NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetAirplaneMode(enabled),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
};
|
||||
(Some((id, response)), false)
|
||||
}
|
||||
Some(NetworkManagerRequest::Password(ssid, password)) => {
|
||||
let s = match NetworkManagerSettings::new(&conn).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return (None, State::Finished),
|
||||
};
|
||||
|
||||
let mut status = (None, false);
|
||||
|
||||
// First try known connections
|
||||
// TODO more convenient methods of managing settings
|
||||
for c in s.list_connections().await.unwrap_or_default() {
|
||||
let mut settings = match c.get_settings().await.ok() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let cur_ssid = settings
|
||||
.get("802-11-wireless")
|
||||
.and_then(|w| w.get("ssid"))
|
||||
.cloned()
|
||||
.and_then(|ssid| ssid.try_into().ok())
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok());
|
||||
if cur_ssid.as_ref() != Some(&ssid) {
|
||||
continue;
|
||||
}
|
||||
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
|
||||
// wifi
|
||||
let mut success = network_manager.set_wireless_enabled(!airplane_mode).await.is_ok();
|
||||
// bluetooth
|
||||
success = success && Command::new("rfkill")
|
||||
.arg(if airplane_mode { "block" } else { "unblock" })
|
||||
.arg("bluetooth")
|
||||
.output()
|
||||
.await
|
||||
.is_ok();
|
||||
let response = NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
};
|
||||
(Some((id, response)), false)
|
||||
}
|
||||
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
|
||||
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
|
||||
let response = NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SetAirplaneMode(enabled),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
};
|
||||
(Some((id, response)), false)
|
||||
}
|
||||
Some(NetworkManagerRequest::Password(ssid, password)) => {
|
||||
let s = match NetworkManagerSettings::new(&conn).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return (None, State::Finished),
|
||||
};
|
||||
|
||||
let mut status = (None, false);
|
||||
|
||||
// First try known connections
|
||||
// TODO more convenient methods of managing settings
|
||||
for c in s.list_connections().await.unwrap_or_default() {
|
||||
let mut settings = match c.get_settings().await.ok() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let cur_ssid = settings
|
||||
.get("802-11-wireless")
|
||||
.and_then(|w| w.get("ssid"))
|
||||
.cloned()
|
||||
.and_then(|ssid| ssid.try_into().ok())
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok());
|
||||
if cur_ssid.as_ref() != Some(&ssid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut secrets = match
|
||||
c.get_secrets("802-11-wireless-security")
|
||||
.await {
|
||||
Ok(s) => s,
|
||||
_ => HashMap::from([("802-11-wireless-security".into(), HashMap::from([
|
||||
("psk".into(), Value::Str(password.as_str().into()).to_owned()),
|
||||
("key-mgmt".into(), Value::Str("wpa-psk".into()).to_owned())
|
||||
]))]),
|
||||
};
|
||||
if let Some(s) = secrets.get_mut("802-11-wireless-security") {
|
||||
s.insert("psk".into(), Value::Str(password.clone().into()).to_owned());
|
||||
drop(s);
|
||||
settings.extend(secrets.into_iter());
|
||||
let settings: HashMap<_, _> = settings.iter().map(|(k, v)| {
|
||||
let map = (k.as_str(), v.iter()
|
||||
let mut secrets = match c.get_secrets("802-11-wireless-security").await {
|
||||
Ok(s) => s,
|
||||
_ => HashMap::from([(
|
||||
"802-11-wireless-security".into(),
|
||||
HashMap::from([
|
||||
(
|
||||
"psk".into(),
|
||||
Value::Str(password.as_str().into()).to_owned(),
|
||||
),
|
||||
("key-mgmt".into(), Value::Str("wpa-psk".into()).to_owned()),
|
||||
]),
|
||||
)]),
|
||||
};
|
||||
if let Some(s) = secrets.get_mut("802-11-wireless-security") {
|
||||
s.insert("psk".into(), Value::Str(password.clone().into()).to_owned());
|
||||
drop(s);
|
||||
settings.extend(secrets.into_iter());
|
||||
let settings: HashMap<_, _> = settings
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let map = (
|
||||
k.as_str(),
|
||||
v.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.into()))
|
||||
.collect::<HashMap<_, _>>());
|
||||
map
|
||||
}).collect();
|
||||
let updated = c.update(settings).await;
|
||||
if updated.is_ok() {
|
||||
let success = if let Ok(path) = network_manager.deref().activate_connection(c.deref().path(), &ObjectPath::try_from("/").unwrap(), &ObjectPath::try_from("/").unwrap()).await {
|
||||
// let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1));
|
||||
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
|
||||
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
|
||||
let state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
|
||||
let s = if let enums::ActiveConnectionState::Activating = state {
|
||||
if let Ok(Some(s)) = timeout(Duration::from_secs(10), active.receive_state_changed().await.next()).await {
|
||||
s.get().await.unwrap_or_default().into()
|
||||
} else {
|
||||
state
|
||||
}
|
||||
} else {
|
||||
state
|
||||
};
|
||||
matches!(s, enums::ActiveConnectionState::Activated)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
status = (Some((id, NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(ssid.clone(), password.clone()),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create a connection
|
||||
if status.0.is_none() {
|
||||
for device in network_manager.devices().await.ok().unwrap_or_default() {
|
||||
if matches!(device.device_type().await.unwrap_or(DeviceType::Other), DeviceType::Wifi) {
|
||||
let conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([
|
||||
("802-11-wireless".into(), HashMap::from([
|
||||
("ssid".into(), Value::Array(ssid.as_bytes().into())),
|
||||
])),
|
||||
("connection".into(), HashMap::from([
|
||||
("id".into(), Value::Str(ssid.as_str().into())),
|
||||
("type".into(), Value::Str("802-11-wireless".into())),
|
||||
])),
|
||||
("802-11-wireless-security".into(), HashMap::from([
|
||||
("psk".into(), Value::Str(password.as_str().into())),
|
||||
("key-mgmt".into(), Value::Str("wpa-psk".into()))
|
||||
]))
|
||||
]);
|
||||
let success = if let Ok((_, path)) = network_manager.add_and_activate_connection(conn_settings, device.path(), &ObjectPath::try_from("/").unwrap()).await {
|
||||
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
|
||||
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
|
||||
let state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
|
||||
let s = if let enums::ActiveConnectionState::Activating = state {
|
||||
if let Ok(Some(s)) = timeout(Duration::from_secs(10), active.receive_state_changed().await.next()).await {
|
||||
s.get().await.unwrap_or_default().into()
|
||||
} else {
|
||||
state
|
||||
}
|
||||
} else {
|
||||
state
|
||||
};
|
||||
matches!(s, enums::ActiveConnectionState::Activated)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
status = (Some((id, NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(ssid.clone(), password.clone()),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if status.0.is_none() {
|
||||
status = (Some((id, NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(ssid, password),
|
||||
success: false,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false);
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
Some(NetworkManagerRequest::SelectAccessPoint(ssid)) => {
|
||||
let s = match NetworkManagerSettings::new(&conn).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return (None, State::Finished),
|
||||
};
|
||||
// find known connection with matching ssid and activate
|
||||
let mut status = (None, false);
|
||||
|
||||
for c in s.list_connections().await.unwrap_or_default() {
|
||||
let settings = match c.get_settings().await.ok() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let cur_ssid = settings
|
||||
.get("802-11-wireless")
|
||||
.and_then(|w| w.get("ssid"))
|
||||
.cloned()
|
||||
.and_then(|ssid| ssid.try_into().ok())
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok());
|
||||
|
||||
if cur_ssid.as_ref() != Some(&ssid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let success = if let Ok(path) = network_manager.deref().activate_connection(c.deref().path(), &ObjectPath::try_from("/").unwrap(), &ObjectPath::try_from("/").unwrap()).await {
|
||||
.collect::<HashMap<_, _>>(),
|
||||
);
|
||||
map
|
||||
})
|
||||
.collect();
|
||||
let updated = c.update(settings).await;
|
||||
if updated.is_ok() {
|
||||
let success = if let Ok(path) = network_manager
|
||||
.deref()
|
||||
.activate_connection(
|
||||
c.deref().path(),
|
||||
&ObjectPath::try_from("/").unwrap(),
|
||||
&ObjectPath::try_from("/").unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
// let active_conn = ActiveConnection::from(ActiveConnectionProxy::from(conn.1));
|
||||
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
|
||||
let active = ActiveConnectionProxy::builder(&conn).path(path).unwrap().destination(dummy.destination()).unwrap().interface(dummy.interface()).unwrap().build().await.unwrap();
|
||||
let mut state = enums::ActiveConnectionState::from(active.state().await.unwrap_or_default());
|
||||
while let enums::ActiveConnectionState::Activating = state {
|
||||
if let Ok(Some(s)) = timeout(Duration::from_secs(20), active.receive_state_changed().await.next()).await {
|
||||
state = s.get().await.unwrap_or_default().into();
|
||||
let active = ActiveConnectionProxy::builder(&conn)
|
||||
.path(path)
|
||||
.unwrap()
|
||||
.destination(dummy.destination())
|
||||
.unwrap()
|
||||
.interface(dummy.interface())
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
let state = enums::ActiveConnectionState::from(
|
||||
active.state().await.unwrap_or_default(),
|
||||
);
|
||||
let s = if let enums::ActiveConnectionState::Activating = state
|
||||
{
|
||||
if let Ok(Some(s)) = timeout(
|
||||
Duration::from_secs(10),
|
||||
active.receive_state_changed().await.next(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
s.get().await.unwrap_or_default().into()
|
||||
} else {
|
||||
break;
|
||||
state
|
||||
}
|
||||
} else {
|
||||
state
|
||||
};
|
||||
matches!(state, enums::ActiveConnectionState::Activated)
|
||||
matches!(s, enums::ActiveConnectionState::Activated)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
status = (Some((id, NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false);
|
||||
status = (
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(
|
||||
ssid.clone(),
|
||||
password.clone(),
|
||||
),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create a connection
|
||||
if status.0.is_none() {
|
||||
for device in network_manager.devices().await.ok().unwrap_or_default() {
|
||||
if matches!(
|
||||
device.device_type().await.unwrap_or(DeviceType::Other),
|
||||
DeviceType::Wifi
|
||||
) {
|
||||
let conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> =
|
||||
HashMap::from([
|
||||
(
|
||||
"802-11-wireless".into(),
|
||||
HashMap::from([(
|
||||
"ssid".into(),
|
||||
Value::Array(ssid.as_bytes().into()),
|
||||
)]),
|
||||
),
|
||||
(
|
||||
"connection".into(),
|
||||
HashMap::from([
|
||||
("id".into(), Value::Str(ssid.as_str().into())),
|
||||
(
|
||||
"type".into(),
|
||||
Value::Str("802-11-wireless".into()),
|
||||
),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"802-11-wireless-security".into(),
|
||||
HashMap::from([
|
||||
(
|
||||
"psk".into(),
|
||||
Value::Str(password.as_str().into()),
|
||||
),
|
||||
("key-mgmt".into(), Value::Str("wpa-psk".into())),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
let success = if let Ok((_, path)) = network_manager
|
||||
.add_and_activate_connection(
|
||||
conn_settings,
|
||||
device.path(),
|
||||
&ObjectPath::try_from("/").unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
|
||||
let active = ActiveConnectionProxy::builder(&conn)
|
||||
.path(path)
|
||||
.unwrap()
|
||||
.destination(dummy.destination())
|
||||
.unwrap()
|
||||
.interface(dummy.interface())
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
let state = enums::ActiveConnectionState::from(
|
||||
active.state().await.unwrap_or_default(),
|
||||
);
|
||||
let s = if let enums::ActiveConnectionState::Activating = state
|
||||
{
|
||||
if let Ok(Some(s)) = timeout(
|
||||
Duration::from_secs(10),
|
||||
active.receive_state_changed().await.next(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
s.get().await.unwrap_or_default().into()
|
||||
} else {
|
||||
state
|
||||
}
|
||||
} else {
|
||||
state
|
||||
};
|
||||
matches!(s, enums::ActiveConnectionState::Activated)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
status = (
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(
|
||||
ssid.clone(),
|
||||
password.clone(),
|
||||
),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
false,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if status.0.is_none() {
|
||||
status = (Some((id, NetworkManagerEvent::RequestResponse {
|
||||
if status.0.is_none() {
|
||||
status = (
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::Password(ssid, password),
|
||||
success: false,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
Some(NetworkManagerRequest::SelectAccessPoint(ssid)) => {
|
||||
let s = match NetworkManagerSettings::new(&conn).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return (None, State::Finished),
|
||||
};
|
||||
// find known connection with matching ssid and activate
|
||||
let mut status = (None, false);
|
||||
|
||||
for c in s.list_connections().await.unwrap_or_default() {
|
||||
let settings = match c.get_settings().await.ok() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let cur_ssid = settings
|
||||
.get("802-11-wireless")
|
||||
.and_then(|w| w.get("ssid"))
|
||||
.cloned()
|
||||
.and_then(|ssid| ssid.try_into().ok())
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok());
|
||||
|
||||
if cur_ssid.as_ref() != Some(&ssid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let success = if let Ok(path) = network_manager
|
||||
.deref()
|
||||
.activate_connection(
|
||||
c.deref().path(),
|
||||
&ObjectPath::try_from("/").unwrap(),
|
||||
&ObjectPath::try_from("/").unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let dummy = ActiveConnectionProxy::new(&conn).await.unwrap();
|
||||
let active = ActiveConnectionProxy::builder(&conn)
|
||||
.path(path)
|
||||
.unwrap()
|
||||
.destination(dummy.destination())
|
||||
.unwrap()
|
||||
.interface(dummy.interface())
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
let mut state = enums::ActiveConnectionState::from(
|
||||
active.state().await.unwrap_or_default(),
|
||||
);
|
||||
while let enums::ActiveConnectionState::Activating = state {
|
||||
if let Ok(Some(s)) = timeout(
|
||||
Duration::from_secs(20),
|
||||
active.receive_state_changed().await.next(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
state = s.get().await.unwrap_or_default().into();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
matches!(state, enums::ActiveConnectionState::Activated)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
status = (
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
|
||||
success,
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
false,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if status.0.is_none() {
|
||||
status = (
|
||||
Some((
|
||||
id,
|
||||
NetworkManagerEvent::RequestResponse {
|
||||
req: NetworkManagerRequest::SelectAccessPoint(ssid.clone()),
|
||||
success: false,
|
||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||
})), false);
|
||||
}
|
||||
status
|
||||
}
|
||||
None => {
|
||||
(None, true)
|
||||
}
|
||||
}}
|
||||
_ = active_conns_changed.next().boxed().fuse() => {
|
||||
(Some((id, NetworkManagerEvent::ActiveConns(NetworkManagerState::new(&conn).await.unwrap_or_default()))), false)
|
||||
}
|
||||
_ = devices_changed.next().boxed().fuse() => {
|
||||
let devices = network_manager.devices().await.ok().unwrap_or_default();
|
||||
let wireless_access_point_futures: Vec<_> = devices.into_iter().map(|device| async move {
|
||||
if let Ok(Some(SpecificDevice::Wireless(wireless_device))) =
|
||||
device.downcast_to_device().await
|
||||
{
|
||||
handle_wireless_device(wireless_device).await.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}).collect();
|
||||
let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len());
|
||||
for f in wireless_access_point_futures {
|
||||
wireless_access_points.append(&mut f.await);
|
||||
state: NetworkManagerState::new(&conn)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
(Some((id, NetworkManagerEvent::WirelessAccessPoints(NetworkManagerState::new(&conn).await.unwrap_or_default()))), false)
|
||||
}
|
||||
enabled = wireless_enabled_changed.next().boxed().fuse() => {
|
||||
let update = if let Some(update) = enabled {
|
||||
if let Ok(_) = update.get().await {
|
||||
Some((id, NetworkManagerEvent::WiFiEnabled(NetworkManagerState::new(&conn).await.unwrap_or_default())))
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(update, false)
|
||||
status
|
||||
}
|
||||
None => (None, true),
|
||||
};
|
||||
drop(active_conns_changed);
|
||||
drop(wireless_enabled_changed);
|
||||
drop(req);
|
||||
(
|
||||
update,
|
||||
if should_exit {
|
||||
|
|
@ -385,6 +517,7 @@ pub enum NetworkManagerEvent {
|
|||
success: bool,
|
||||
},
|
||||
Init {
|
||||
conn: Connection,
|
||||
sender: UnboundedSender<NetworkManagerRequest>,
|
||||
state: NetworkManagerState,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -6,5 +6,6 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
icon-loader = { version = "0.3.6", features = ["gtk"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
nix = "0.24.1"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
|
||||
use cosmic::iced::wayland::{
|
||||
popup::{destroy_popup, get_popup},
|
||||
};
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced::{
|
||||
widget::{button, column, row, text, Row, Space},
|
||||
window, Alignment, Application, Color, Command, Length, Subscription,
|
||||
};
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
|
||||
use cosmic::iced_widget::Button;
|
||||
use cosmic::theme::Svg;
|
||||
use cosmic::widget::{divider, icon, toggler};
|
||||
use cosmic::Renderer;
|
||||
|
|
@ -27,7 +26,7 @@ struct Notifications {
|
|||
theme: Theme,
|
||||
icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
do_not_disturb: bool,
|
||||
notifications: Vec<Vec<String>>,
|
||||
}
|
||||
|
|
@ -61,7 +60,7 @@ impl Application for Notifications {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -69,10 +68,10 @@ impl Application for Notifications {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
|
|
@ -86,11 +85,11 @@ impl Application for Notifications {
|
|||
destroy_popup(p)
|
||||
} else {
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -112,7 +111,7 @@ impl Application for Notifications {
|
|||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
|
|
@ -164,11 +163,9 @@ impl Application for Notifications {
|
|||
}
|
||||
|
||||
// todo put into libcosmic doing so will fix the row_button's boarder radius
|
||||
fn row_button(
|
||||
mut content: Vec<Element<Message>>,
|
||||
) -> cosmic::iced_native::widget::Button<Message, Renderer> {
|
||||
content.insert(0, Space::with_width(Length::Units(24)).into());
|
||||
content.push(Space::with_width(Length::Units(24)).into());
|
||||
fn row_button(mut content: Vec<Element<Message>>) -> Button<Message, Renderer> {
|
||||
content.insert(0, Space::with_width(Length::Fixed(24.0)).into());
|
||||
content.push(Space::with_width(Length::Fixed(24.0)).into());
|
||||
|
||||
button(
|
||||
Row::with_children(content)
|
||||
|
|
@ -176,8 +173,8 @@ fn row_button(
|
|||
.align_items(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(36))
|
||||
.style(APPLET_BUTTON_THEME)
|
||||
.height(Length::Fixed(36.0))
|
||||
.style(applet_button_theme())
|
||||
}
|
||||
|
||||
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@ icon-loader = { version = "0.3.6", features = ["gtk"] }
|
|||
libpulse-binding = "2.26.0"
|
||||
libpulse-glib-binding = "2.25.0"
|
||||
tokio = { version = "1.20.1", features=["full"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
nix = "0.26.1"
|
||||
zbus = "3.7"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
nix = "0.26.2"
|
||||
zbus = "3.13"
|
||||
logind-zbus = "3.1"
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6"
|
||||
rust-embed = "6.6"
|
||||
rust-embed-utils = "7.5.0"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
|
|
|
|||
4
cosmic-applet-power/i18n.toml
Normal file
4
cosmic-applet-power/i18n.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
21
cosmic-applet-power/i18n/en/cosmic_applet_power.ftl
Normal file
21
cosmic-applet-power/i18n/en/cosmic_applet_power.ftl
Normal 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.
|
||||
|
||||
47
cosmic-applet-power/src/localize.rs
Normal file
47
cosmic-applet-power/src/localize.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,28 @@
|
|||
use std::collections::HashMap;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::event::wayland::{self, LayerEvent};
|
||||
use cosmic::iced::event::PlatformSpecific;
|
||||
use cosmic::iced::subscription::events_with;
|
||||
use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings;
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced_native::layout::Limits;
|
||||
use cosmic::iced_native::widget::Space;
|
||||
use cosmic::iced_runtime::core::layout::Limits;
|
||||
use cosmic::iced_sctk::commands::layer_surface::{
|
||||
destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity,
|
||||
};
|
||||
use cosmic::iced_widget::mouse_area;
|
||||
use cosmic::widget::{button, divider, icon};
|
||||
use cosmic::Renderer;
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::{
|
||||
widget::{self, column, container, row, Row},
|
||||
widget::{self, column, container, row, space::Space, text, Row},
|
||||
window, Alignment, Application, Command, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::Color;
|
||||
use cosmic::theme::{self, Svg};
|
||||
use cosmic::{Element, Theme};
|
||||
|
||||
|
|
@ -20,9 +30,11 @@ use logind_zbus::manager::ManagerProxy;
|
|||
use logind_zbus::session::{SessionProxy, SessionType};
|
||||
use logind_zbus::user::UserProxy;
|
||||
use nix::unistd::getuid;
|
||||
use tokio::time::sleep;
|
||||
use zbus::Connection;
|
||||
|
||||
pub mod cosmic_session;
|
||||
mod localize;
|
||||
pub mod session_manager;
|
||||
|
||||
use crate::cosmic_session::CosmicSessionProxy;
|
||||
|
|
@ -39,19 +51,28 @@ struct Power {
|
|||
icon_name: String,
|
||||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
action_to_confirm: Option<(window::Id, PowerAction)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum PowerAction {
|
||||
Lock,
|
||||
LogOut,
|
||||
Suspend,
|
||||
Restart,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Timeout(window::Id),
|
||||
Action(PowerAction),
|
||||
TogglePopup,
|
||||
Settings,
|
||||
Ignore,
|
||||
Confirm,
|
||||
Cancel,
|
||||
Closed(window::Id),
|
||||
Zbus(Result<(), zbus::Error>),
|
||||
}
|
||||
|
||||
|
|
@ -72,26 +93,34 @@ impl Application for Power {
|
|||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Power")
|
||||
fl!("power")
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
Message::Ignore
|
||||
fn close_requested(&self, id: window::Id) -> Self::Message {
|
||||
Message::Closed(id)
|
||||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::none()
|
||||
events_with(|e, _status| match e {
|
||||
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Layer(LayerEvent::Unfocused, ..),
|
||||
)) => Some(Message::Cancel),
|
||||
// cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
// wayland::Event::Seat(wayland::SeatEvent::Leave, _),
|
||||
// )) => Some(Message::Cancel),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
|
|
@ -101,21 +130,21 @@ impl Application for Power {
|
|||
destroy_popup(p)
|
||||
} else {
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(100)
|
||||
.min_height(100)
|
||||
.max_height(400)
|
||||
.max_width(500);
|
||||
.min_width(100.0)
|
||||
.min_height(100.0)
|
||||
.max_height(400.0)
|
||||
.max_width(500.0);
|
||||
get_popup(popup_settings)
|
||||
}
|
||||
}
|
||||
|
|
@ -123,51 +152,117 @@ impl Application for Power {
|
|||
let _ = process::Command::new("cosmic-settings").spawn();
|
||||
Command::none()
|
||||
}
|
||||
Message::Lock => Command::perform(lock(), Message::Zbus),
|
||||
Message::LogOut => Command::perform(log_out(), Message::Zbus),
|
||||
Message::Suspend => Command::perform(suspend(), Message::Zbus),
|
||||
Message::Restart => Command::perform(restart(), Message::Zbus),
|
||||
Message::Shutdown => Command::perform(shutdown(), Message::Zbus),
|
||||
Message::Action(action) => {
|
||||
self.id_ctr += 1;
|
||||
let id = window::Id(self.id_ctr);
|
||||
self.action_to_confirm = Some((id, action));
|
||||
return Command::batch(vec![
|
||||
Command::perform(sleep(Duration::from_secs(60)), move |_| {
|
||||
Message::Timeout(id)
|
||||
}),
|
||||
get_layer_surface(SctkLayerSurfaceSettings {
|
||||
id,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::all(),
|
||||
namespace: "dialog".into(),
|
||||
size: Some((None, None)),
|
||||
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Message::Zbus(result) => {
|
||||
if let Err(e) = result {
|
||||
eprintln!("cosmic-applet-power ERROR: '{}'", e);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Ignore => Command::none(),
|
||||
Message::Confirm => {
|
||||
if let Some((id, a)) = self.action_to_confirm.take() {
|
||||
Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => Command::perform(lock(), Message::Zbus),
|
||||
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus),
|
||||
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus),
|
||||
PowerAction::Restart => Command::perform(restart(), Message::Zbus),
|
||||
PowerAction::Shutdown => Command::perform(shutdown(), Message::Zbus),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
Message::Cancel => {
|
||||
if let Some((id, _)) = self.action_to_confirm.take() {
|
||||
return destroy_layer_surface(id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Closed(id) => {
|
||||
if let Some((surface_id, _)) = self.action_to_confirm {
|
||||
if id == surface_id {
|
||||
self.action_to_confirm = None;
|
||||
return destroy_layer_surface(id);
|
||||
}
|
||||
}
|
||||
if id == window::Id(0) {
|
||||
process::exit(0);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Timeout(id) => {
|
||||
if let Some((surface_id, a)) = self.action_to_confirm {
|
||||
if id == surface_id {
|
||||
self.action_to_confirm = None;
|
||||
return Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => Command::perform(lock(), Message::Zbus),
|
||||
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus),
|
||||
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus),
|
||||
PowerAction::Restart => Command::perform(restart(), Message::Zbus),
|
||||
PowerAction::Shutdown => {
|
||||
Command::perform(shutdown(), Message::Zbus)
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
} else {
|
||||
let settings = row_button(vec!["Settings...".into()]).on_press(Message::Settings);
|
||||
if matches!(self.popup, Some(p) if p == id) {
|
||||
let settings =
|
||||
row_button(vec![text(fl!("settings")).size(14).into()]).on_press(Message::Settings);
|
||||
|
||||
let session = column![
|
||||
row_button(vec![
|
||||
text_icon("system-lock-screen-symbolic", 24).into(),
|
||||
"Lock Screen".into(),
|
||||
text(fl!("lock-screen")).size(14).into(),
|
||||
Space::with_width(Length::Fill).into(),
|
||||
"Super + Escape".into(),
|
||||
text(fl!("lock-screen-shortcut")).size(14).into(),
|
||||
])
|
||||
.on_press(Message::Lock),
|
||||
.on_press(Message::Action(PowerAction::Lock)),
|
||||
row_button(vec![
|
||||
text_icon("system-log-out-symbolic", 24).into(),
|
||||
"Log Out".into(),
|
||||
text(fl!("log-out")).size(14).into(),
|
||||
Space::with_width(Length::Fill).into(),
|
||||
"Ctrl + Alt + Delete".into(),
|
||||
text(fl!("log-out-shortcut")).size(14).into(),
|
||||
])
|
||||
.on_press(Message::LogOut),
|
||||
.on_press(Message::Action(PowerAction::LogOut)),
|
||||
];
|
||||
|
||||
let power = row![
|
||||
power_buttons("system-lock-screen-symbolic", "Suspend").on_press(Message::Suspend),
|
||||
power_buttons("system-restart-symbolic", "Restart").on_press(Message::Restart),
|
||||
power_buttons("system-shutdown-symbolic", "Shutdown").on_press(Message::Shutdown),
|
||||
power_buttons("system-lock-screen-symbolic", fl!("suspend"))
|
||||
.on_press(Message::Action(PowerAction::Suspend)),
|
||||
power_buttons("system-restart-symbolic", fl!("restart"))
|
||||
.on_press(Message::Action(PowerAction::Restart)),
|
||||
power_buttons("system-shutdown-symbolic", fl!("shutdown"))
|
||||
.on_press(Message::Action(PowerAction::Shutdown)),
|
||||
]
|
||||
.spacing(24)
|
||||
.padding([0, 24]);
|
||||
|
|
@ -188,6 +283,65 @@ impl Application for Power {
|
|||
.padding([8, 0]);
|
||||
|
||||
self.applet_helper.popup_container(content).into()
|
||||
} else if matches!(self.action_to_confirm, Some((c_id, _)) if c_id == id) {
|
||||
let action = match self.action_to_confirm.as_ref().unwrap().1 {
|
||||
PowerAction::Lock => "lock-screen",
|
||||
PowerAction::LogOut => "log-out",
|
||||
PowerAction::Suspend => "suspend",
|
||||
PowerAction::Restart => "restart",
|
||||
PowerAction::Shutdown => "shutdown",
|
||||
};
|
||||
// TODO actual countdown
|
||||
let content = column![
|
||||
text(fl!(
|
||||
"confirm-question",
|
||||
HashMap::from_iter(vec![("action", action), ("countdown", "60")])
|
||||
))
|
||||
.size(16),
|
||||
row![
|
||||
button(theme::Button::Primary)
|
||||
.custom(vec![text(fl!("confirm")).size(14).into()])
|
||||
.on_press(Message::Confirm),
|
||||
button(theme::Button::Primary)
|
||||
.custom(vec![text(fl!("cancel")).size(14).into()])
|
||||
.on_press(Message::Cancel),
|
||||
]
|
||||
.spacing(24)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(12)
|
||||
.padding(24);
|
||||
mouse_area(
|
||||
container(
|
||||
container(content)
|
||||
.style(cosmic::theme::Container::custom(|theme| {
|
||||
cosmic::iced_style::container::Appearance {
|
||||
text_color: Some(theme.cosmic().background.on.into()),
|
||||
background: Some(
|
||||
Color::from(theme.cosmic().background.base).into(),
|
||||
),
|
||||
border_radius: 12.0.into(),
|
||||
border_width: 2.0,
|
||||
border_color: theme.cosmic().bg_divider().into(),
|
||||
}
|
||||
}))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.on_press(Message::Cancel)
|
||||
.on_right_press(Message::Cancel)
|
||||
.on_middle_press(Message::Cancel)
|
||||
.into()
|
||||
} else {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +349,7 @@ impl Application for Power {
|
|||
// ### UI Helplers
|
||||
|
||||
fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Renderer> {
|
||||
button(APPLET_BUTTON_THEME)
|
||||
button(applet_button_theme())
|
||||
.custom(vec![Row::with_children(content)
|
||||
.spacing(4)
|
||||
.align_items(Alignment::Center)
|
||||
|
|
@ -204,14 +358,14 @@ fn row_button(content: Vec<Element<Message>>) -> widget::Button<Message, Rendere
|
|||
.padding([8, 24])
|
||||
}
|
||||
|
||||
fn power_buttons<'a>(name: &'a str, text: &'a str) -> widget::Button<'a, Message, Renderer> {
|
||||
fn power_buttons<'a>(name: &'a str, msg: String) -> widget::Button<'a, Message, Renderer> {
|
||||
widget::button(
|
||||
column![text_icon(name, 40), text]
|
||||
column![text_icon(name, 40), text(msg).size(14)]
|
||||
.spacing(4)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Units(76))
|
||||
.height(Length::Fixed(76.0))
|
||||
.style(theme::Button::Text)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
icon-loader = { version = "0.3.6", features = ["gtk"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
nix = "0.24.1"
|
||||
chrono = { version = "0.4.23", features = ["clock"] }
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
use cosmic::applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::{
|
||||
time,
|
||||
wayland::InitialSurface,
|
||||
widget::{button, column, text, vertical_space},
|
||||
window, Alignment, Application, Color, Command, Length, Rectangle, Subscription,
|
||||
};
|
||||
use cosmic::iced_sctk::layout::Limits;
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::theme;
|
||||
use cosmic::{
|
||||
widget::{icon, rectangle_tracker::*},
|
||||
Element, Theme,
|
||||
};
|
||||
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
|
||||
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use std::time::Duration;
|
||||
|
|
@ -22,10 +22,8 @@ pub fn main() -> cosmic::iced::Result {
|
|||
let mut settings = helper.window_settings();
|
||||
match &mut settings.initial_surface {
|
||||
InitialSurface::XdgWindow(s) => {
|
||||
s.iced_settings.min_size = Some((1, 1));
|
||||
s.iced_settings.max_size = None;
|
||||
s.autosize = true;
|
||||
s.size_limits = Limits::NONE.min_height(1).min_width(1);
|
||||
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
|
@ -36,7 +34,7 @@ struct Time {
|
|||
applet_helper: CosmicAppletHelper,
|
||||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
update_at: Every,
|
||||
now: DateTime<Local>,
|
||||
msg: String,
|
||||
|
|
@ -90,7 +88,7 @@ impl Application for Time {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -98,10 +96,10 @@ impl Application for Time {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
|
|
@ -120,7 +118,7 @@ impl Application for Time {
|
|||
.expect("Setting nanoseconds to 0 should always be possible.");
|
||||
let wait = 1.max((next - now).num_milliseconds());
|
||||
Subscription::batch(vec![
|
||||
rectangle_tracker_subscription(0).map(|(_, update)| Message::Rectangle(update)),
|
||||
rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)),
|
||||
time::every(Duration::from_millis(
|
||||
wait.try_into().unwrap_or(FALLBACK_DELAY),
|
||||
))
|
||||
|
|
@ -149,11 +147,11 @@ impl Application for Time {
|
|||
.to_string();
|
||||
self.msg = calendar;
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -194,13 +192,13 @@ impl Application for Time {
|
|||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
let button = button(
|
||||
if matches!(
|
||||
self.applet_helper.anchor,
|
||||
PanelAnchor::Top | PanelAnchor::Bottom
|
||||
) {
|
||||
column![text(self.now.format("%b %-d %-I:%M %p").to_string())]
|
||||
column![text(self.now.format("%b %-d %-I:%M %p").to_string()).size(14)]
|
||||
} else {
|
||||
let mut date_time_col = column![
|
||||
icon(
|
||||
|
|
@ -208,10 +206,10 @@ impl Application for Time {
|
|||
self.applet_helper.suggested_size().0
|
||||
)
|
||||
.style(theme::Svg::Symbolic),
|
||||
text(self.now.format("%I").to_string()),
|
||||
text(self.now.format("%M").to_string()),
|
||||
text(self.now.format("%p").to_string()),
|
||||
vertical_space(Length::Units(4)),
|
||||
text(self.now.format("%I").to_string()).size(14),
|
||||
text(self.now.format("%M").to_string()).size(14),
|
||||
text(self.now.format("%p").to_string()).size(14),
|
||||
vertical_space(Length::Fixed(4.0)),
|
||||
// TODO better calendar icon?
|
||||
icon(
|
||||
"calendar-go-today-symbolic",
|
||||
|
|
@ -222,7 +220,7 @@ impl Application for Time {
|
|||
.align_items(Alignment::Center)
|
||||
.spacing(4);
|
||||
for d in self.now.format("%x").to_string().split("/") {
|
||||
date_time_col = date_time_col.push(text(d.to_string()));
|
||||
date_time_col = date_time_col.push(text(d.to_string()).size(14));
|
||||
}
|
||||
date_time_col
|
||||
},
|
||||
|
|
@ -240,7 +238,7 @@ impl Application for Time {
|
|||
.align_items(Alignment::Start)
|
||||
.spacing(12)
|
||||
.padding([24, 0])
|
||||
.push(text(&self.msg))
|
||||
.push(text(&self.msg).size(14))
|
||||
.padding(8);
|
||||
|
||||
self.applet_helper.popup_container(content).into()
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@ authors = ["Ashley Wulber <ashley@system76.com>"]
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, rev = "f0cfe09" }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "f0cfe09" }
|
||||
wayland-backend = {version = "0.1.0", features = ["client_system"]}
|
||||
wayland-client = {version = "0.30.0"}
|
||||
calloop = "0.10.1"
|
||||
nix = "0.26.1"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
once_cell = "1.9"
|
||||
futures = "0.3.21"
|
||||
xdg = "2.4.0"
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
use calloop::channel::SyncSender;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::applet::CosmicAppletHelper;
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::mouse::{self, ScrollDelta};
|
||||
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
|
||||
use cosmic::iced::wayland::{window::resize_window, InitialSurface};
|
||||
use cosmic::iced::widget::{column, container, row, text};
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::{
|
||||
subscription, widget::button, window, Application, Command, Event::Mouse, Length, Settings,
|
||||
Subscription,
|
||||
};
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::Color;
|
||||
use cosmic::theme::Button;
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic_applet::CosmicAppletHelper;
|
||||
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
|
||||
use std::cmp::Ordering;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
|
@ -25,10 +25,7 @@ use crate::wayland_subscription::{workspaces, WorkspacesUpdate};
|
|||
pub fn run() -> cosmic::iced::Result {
|
||||
let settings = Settings {
|
||||
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
|
||||
iced_settings: cosmic::iced_native::window::Settings {
|
||||
size: (32, 32),
|
||||
..Default::default()
|
||||
},
|
||||
size: (32, 32),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
|
@ -103,7 +100,7 @@ impl Application for IcedWorkspacesApplet {
|
|||
Layout::Row => (unit * self.workspaces.len().max(1) as u32, unit),
|
||||
Layout::Column => (unit, unit * self.workspaces.len().max(1) as u32),
|
||||
};
|
||||
return resize_window(window::Id::new(0), w, h);
|
||||
return resize_window(window::Id(0), w, h);
|
||||
}
|
||||
WorkspacesUpdate::Started(tx) => {
|
||||
self.workspace_tx.replace(tx);
|
||||
|
|
@ -141,6 +138,7 @@ impl Application for IcedWorkspacesApplet {
|
|||
.filter_map(|w| {
|
||||
let btn = button(
|
||||
text(w.0.clone())
|
||||
.size(14)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.vertical_alignment(Vertical::Center)
|
||||
.width(Length::Fill)
|
||||
|
|
@ -184,7 +182,7 @@ impl Application for IcedWorkspacesApplet {
|
|||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(
|
||||
vec![
|
||||
workspaces(0).map(|(_, msg)| Message::WorkspaceUpdate(msg)),
|
||||
workspaces(0).map(|e| Message::WorkspaceUpdate(e.1)),
|
||||
subscription::events_with(|e, _| match e {
|
||||
Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||
Some(Message::WheelScrolled(delta))
|
||||
|
|
@ -197,7 +195,7 @@ impl Application for IcedWorkspacesApplet {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -205,9 +203,9 @@ impl Application for IcedWorkspacesApplet {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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@',
|
||||
]
|
||||
)
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
use calloop::channel::*;
|
||||
use cosmic::applet::cosmic_panel_config::CosmicPanelOuput;
|
||||
use cosmic_client_toolkit::{
|
||||
sctk::{
|
||||
self,
|
||||
|
|
@ -11,7 +10,7 @@ use cosmic_client_toolkit::{
|
|||
};
|
||||
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
|
||||
use futures::{channel::mpsc, executor::block_on, SinkExt};
|
||||
use std::{env, os::unix::net::UnixStream, path::PathBuf, str::FromStr, time::Duration};
|
||||
use std::{env, os::unix::net::UnixStream, path::PathBuf, time::Duration};
|
||||
use wayland_backend::client::ObjectId;
|
||||
use wayland_client::{
|
||||
globals::registry_queue_init,
|
||||
|
|
@ -45,10 +44,6 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
|
|||
std::thread::spawn(move || {
|
||||
let configured_output = std::env::var("COSMIC_PANEL_OUTPUT")
|
||||
.ok()
|
||||
.map(|output_str| match CosmicPanelOuput::from_str(&output_str) {
|
||||
Ok(CosmicPanelOuput::Name(name)) => name,
|
||||
_ => "".to_string(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap();
|
||||
let loop_handle = event_loop.handle();
|
||||
|
|
@ -97,7 +92,11 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
|
|||
.workspace_groups()
|
||||
.iter()
|
||||
.find_map(|g| {
|
||||
if !g.outputs.iter().any(|o| Some(o) == state.expected_output.as_ref()) {
|
||||
if !g
|
||||
.outputs
|
||||
.iter()
|
||||
.any(|o| Some(o) == state.expected_output.as_ref())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
g.workspaces
|
||||
|
|
@ -179,7 +178,10 @@ impl State {
|
|||
.workspace_groups()
|
||||
.iter()
|
||||
.filter_map(|g| {
|
||||
if g.outputs.iter().any(|o| Some(o) == self.expected_output.as_ref()) {
|
||||
if g.outputs
|
||||
.iter()
|
||||
.any(|o| Some(o) == self.expected_output.as_ref())
|
||||
{
|
||||
Some(g.workspaces.iter().map(|w| {
|
||||
(
|
||||
w.name.clone(),
|
||||
|
|
|
|||
|
|
@ -18,29 +18,36 @@ pub fn workspaces<I: 'static + Hash + Copy + Send + Sync>(
|
|||
subscription::unfold(id, State::Ready, move |state| _workspaces(id, state))
|
||||
}
|
||||
|
||||
async fn _workspaces<I: Copy>(id: I, state: State) -> (Option<(I, WorkspacesUpdate)>, State) {
|
||||
match state {
|
||||
State::Ready => {
|
||||
if let Ok(watcher) = WorkspacesWatcher::new() {
|
||||
(
|
||||
Some((id, WorkspacesUpdate::Started(watcher.get_sender()))),
|
||||
State::Waiting(watcher),
|
||||
)
|
||||
} else {
|
||||
(Some((id, WorkspacesUpdate::Errored)), State::Error)
|
||||
async fn _workspaces<I: Copy>(id: I, mut state: State) -> ((I, WorkspacesUpdate), State) {
|
||||
loop {
|
||||
let (update, new_state) = match state {
|
||||
State::Ready => {
|
||||
if let Ok(watcher) = WorkspacesWatcher::new() {
|
||||
(
|
||||
Some((id, WorkspacesUpdate::Started(watcher.get_sender()))),
|
||||
State::Waiting(watcher),
|
||||
)
|
||||
} else {
|
||||
(Some((id, WorkspacesUpdate::Errored)), State::Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Waiting(mut t) => {
|
||||
if let Some(w) = t.workspaces().await {
|
||||
(
|
||||
Some((id, WorkspacesUpdate::Workspaces(w))),
|
||||
State::Waiting(t),
|
||||
)
|
||||
} else {
|
||||
(Some((id, WorkspacesUpdate::Errored)), State::Error)
|
||||
State::Waiting(mut t) => {
|
||||
if let Some(w) = t.workspaces().await {
|
||||
(
|
||||
Some((id, WorkspacesUpdate::Workspaces(w))),
|
||||
State::Waiting(t),
|
||||
)
|
||||
} else {
|
||||
(Some((id, WorkspacesUpdate::Errored)), State::Error)
|
||||
}
|
||||
}
|
||||
State::Error => cosmic::iced::futures::future::pending().await,
|
||||
};
|
||||
state = new_state;
|
||||
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
State::Error => cosmic::iced::futures::future::pending().await,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
freedesktop-desktop-entry = "0.5.0"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", rev = "31f7e97", default-features = false, features = ["tokio", "wayland"] }
|
||||
cosmic-applet = { path = "../applet" }
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
use cosmic::{
|
||||
applet::CosmicAppletHelper,
|
||||
iced::{
|
||||
self,
|
||||
wayland::InitialSurface,
|
||||
Application,
|
||||
},
|
||||
iced_sctk::layout::Limits,
|
||||
iced::Limits,
|
||||
iced::{self, wayland::InitialSurface, Application},
|
||||
iced_runtime::core::window,
|
||||
iced_style::application,
|
||||
iced_native::window,
|
||||
};
|
||||
use cosmic_applet::CosmicAppletHelper;
|
||||
use freedesktop_desktop_entry::DesktopEntry;
|
||||
use std::{env, fs, process::Command};
|
||||
|
||||
|
|
@ -47,10 +43,12 @@ impl iced::Application for Button {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
|
||||
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
|
||||
application::Appearance {
|
||||
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> iced::Subscription<Msg> {
|
||||
|
|
@ -112,10 +110,8 @@ pub fn main() -> iced::Result {
|
|||
};
|
||||
match &mut settings.initial_surface {
|
||||
InitialSurface::XdgWindow(s) => {
|
||||
s.iced_settings.min_size = Some((1, 1));
|
||||
s.iced_settings.max_size = None;
|
||||
s.autosize = true;
|
||||
s.size_limits = Limits::NONE.min_height(1).min_width(1);
|
||||
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue