feat: iced battery applet

This commit is contained in:
Ashley Wulber 2022-12-02 20:24:52 -05:00
parent a7b099b4b3
commit 6a98d7f7c8
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
17 changed files with 3520 additions and 964 deletions

13
Cargo.lock generated
View file

@ -371,19 +371,6 @@ dependencies = [
"zbus 2.3.2",
]
[[package]]
name = "cosmic-applet-battery"
version = "0.1.0"
dependencies = [
"futures",
"gtk4",
"libadwaita",
"libcosmic",
"libcosmic-applet",
"relm4",
"zbus 2.3.2",
]
[[package]]
name = "cosmic-applet-network"
version = "0.1.0"

View file

@ -1,7 +1,6 @@
[workspace]
members = [
"applets/cosmic-applet-audio",
"applets/cosmic-applet-battery",
"applets/cosmic-applet-network",
"applets/cosmic-applet-notifications",
"applets/cosmic-applet-power",
@ -14,6 +13,8 @@ members = [
exclude = [
"applets/cosmic-applet-graphics",
"applets/cosmic-applet-workspaces",
"applets/cosmic-applet-battery",
]
[patch.crates-io]

File diff suppressed because it is too large Load diff

View file

@ -4,10 +4,24 @@ version = "0.1.0"
edition = "2021"
[dependencies]
once_cell = "1.16.0"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "sctk-cosmic-design-system", default-features = false, features = ["wayland", "applet"] }
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", default-features = false }
iced_sctk = { git = "https://github.com/pop-os/iced-sctk" }
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", version = "0.16" }
futures = "0.3"
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs" }
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
libcosmic-applet = { path = "../../libcosmic-applet" }
relm4 = { git = "https://github.com/relm4/relm4", branch = "next", features = ["macros"] }
zbus = { version = "2", no-default-features = true }
zbus = { version = "3.5", no-default-features = true }
log = "0.4"
pretty_env_logger = "0.4"
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"
rust-embed = "6.3.0"
tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "fs"] }
[dependencies.iced]
git = "https://github.com/pop-os/iced.git"
branch = "sctk-cosmic"
# path = "../iced"
default-features = false
features = ["image", "svg", "tokio", "wayland"]

View file

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

View file

@ -0,0 +1 @@
cosmic-applet-button = Cosmic Button

View file

@ -0,0 +1,326 @@
use cosmic::applet::{get_popup_settings, icon_button, popup_container};
use cosmic::iced::alignment::Horizontal;
use cosmic::iced::{
executor,
widget::{button, column, row, text},
window, Alignment, Application, Command, Length, Subscription,
};
use cosmic::iced_native::window::Settings;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::svg;
use cosmic::separator;
use cosmic::theme::{self, Button, Svg};
use cosmic::widget::{icon, widget};
use cosmic::{iced_style, settings, Element, Theme};
use cosmic_panel_config::{PanelAnchor, PanelSize};
use iced_sctk::application::SurfaceIdWrapper;
use iced_sctk::command::platform_specific::wayland::window::SctkWindowSettings;
use iced_sctk::commands::popup::{destroy_popup, get_popup};
use iced_sctk::settings::InitialSurface;
use iced_sctk::{Color};
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use crate::backlight::{ScreenBacklightRequest, screen_backlight_subscription, ScreenBacklightUpdate};
use crate::config;
use crate::upower_device::{device_subscription, DeviceDbusEvent};
use crate::upower_kbdbacklight::{KeyboardBacklightRequest, kbd_backlight_subscription, KeyboardBacklightUpdate};
// XXX improve
// TODO: time to empty varies? needs averaging?
fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs();
if secs > 60 {
let min = secs / 60;
if min > 60 {
format!("{}:{:02}", min / 60, min % 60)
} else {
format!("{}m", min)
}
} else {
format!("{}s", secs)
}
}
pub fn run() -> cosmic::iced::Result {
let mut settings = settings();
let pixels = std::env::var("COSMIC_PANEL_SIZE")
.ok()
.and_then(|size| match size.parse::<PanelSize>() {
Ok(PanelSize::XL) => Some(64),
Ok(PanelSize::L) => Some(48),
Ok(PanelSize::M) => Some(36),
Ok(PanelSize::S) => Some(24),
Ok(PanelSize::XS) => Some(18),
Err(_) => Some(36),
})
.unwrap_or(36);
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
iced_settings: Settings {
size: (pixels + 32, pixels + 16),
min_size: Some((pixels + 32, pixels + 16)),
max_size: Some((pixels + 32, pixels + 16)),
..Default::default()
},
..Default::default()
});
CosmicBatteryApplet::run(settings)
}
#[derive(Clone, Default)]
struct CosmicBatteryApplet {
icon_name: String,
theme: Theme,
charging_limit: bool,
battery_percent: f64,
time_remaining: Duration,
kbd_brightness: f64,
screen_brightness: f64,
popup: Option<window::Id>,
id_ctr: u32,
anchor: PanelAnchor,
screen_sender: Option<UnboundedSender<ScreenBacklightRequest>>,
kbd_sender: Option<UnboundedSender<KeyboardBacklightRequest>>,
}
#[derive(Debug, Clone)]
enum Message {
TogglePopup,
Update {
icon_name: String,
percent: f64,
time_to_empty: i64,
},
SetKbdBrightness(i32),
SetScreenBrightness(i32),
SetChargingLimit(bool),
UpdateKbdBrightness(f64),
UpdateScreenBrightness(f64),
OpenBatterySettings,
InitKbdBacklight(UnboundedSender<KeyboardBacklightRequest>, f64),
InitScreenBacklight(UnboundedSender<ScreenBacklightRequest>, f64),
Errored(String),
Ignore,
}
impl Application for CosmicBatteryApplet {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
(
CosmicBatteryApplet {
icon_name: "battery-symbolic".to_string(),
anchor: std::env::var("COSMIC_PANEL_ANCHOR")
.ok()
.map(|size| match size.parse::<PanelAnchor>() {
Ok(p) => p,
Err(_) => PanelAnchor::Top,
})
.unwrap_or(PanelAnchor::Top),
..Default::default()
},
Command::none(),
)
}
fn title(&self) -> String {
config::APP_ID.to_string()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::SetKbdBrightness(brightness) => {
self.kbd_brightness = (brightness as f64 / 100.0).clamp(0., 1.);
if let Some(tx) = &self.kbd_sender {
let _ = tx.send(KeyboardBacklightRequest::Set(self.kbd_brightness));
}
}
Message::SetScreenBrightness(brightness) => {
self.screen_brightness = brightness as f64 / 100.0;
if let Some(tx) = &self.screen_sender {
let _ = tx.send(ScreenBacklightRequest::Set(self.screen_brightness));
}
}
Message::SetChargingLimit(enable_charging_limit) => {
self.charging_limit = enable_charging_limit;
}
Message::OpenBatterySettings => {
// TODO Ashley
}
Message::Errored(_) => {
// TODO log errors
}
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
return destroy_popup(p);
} else {
if let Some(tx) = &self.kbd_sender {
let _ = tx.send(KeyboardBacklightRequest::Get);
}
if let Some(tx) = &self.screen_sender {
let _ = tx.send(ScreenBacklightRequest::Get);
}
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
self.popup.replace(new_id);
let mut popup_settings =
get_popup_settings(window::Id::new(0), new_id, (400, 240), None, None);
// popup_settings.positioner.anchor_rect.x = 200;
return get_popup(popup_settings);
}
}
Message::Update {
icon_name,
percent,
time_to_empty,
} => {
self.icon_name = icon_name;
self.battery_percent = percent;
self.time_remaining = Duration::from_secs(time_to_empty as u64);
}
Message::UpdateKbdBrightness(b) => {
self.kbd_brightness = b;
},
Message::Ignore => {},
Message::InitKbdBacklight(tx, brightness) => {
let _ = tx.send(KeyboardBacklightRequest::Get);
self.kbd_sender = Some(tx);
self.kbd_brightness = brightness;
},
Message::InitScreenBacklight(tx, brightness) => {
let _ = tx.send(ScreenBacklightRequest::Get);
self.screen_sender = Some(tx);
self.screen_brightness = brightness;
},
Message::UpdateScreenBrightness(b) => {
self.screen_brightness = b;
},
}
Command::none()
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => icon_button(
&self.icon_name,
Svg::Custom(|theme| svg::Appearance {
fill: Some(theme.palette().text),
}),
)
.on_press(Message::TogglePopup)
.style(Button::Text)
.into(),
SurfaceIdWrapper::Popup(_) => {
let name = text("Battery").size(18);
let description = text(if "battery-full-charged-symbolic" == self.icon_name {
"Charging".to_string()
} else {
format!(
"{} until empty ({:.0}%)",
format_duration(self.time_remaining),
self.battery_percent
)
})
.size(12);
popup_container(
column![
row![
icon(&self.icon_name, 24)
.style(Svg::Custom(|theme| {
svg::Appearance {
fill: Some(theme.palette().text),
}
}))
.width(Length::Units(24))
.height(Length::Units(24)),
column![name, description]
]
.align_items(Alignment::Center),
separator!(1),
// text{"Limit Battery Charging"},
widget::Toggler::new(self.charging_limit, String::from("Increase the lifespan of your battery by settings a maximum charger valur of 80%"), |_| Message::SetChargingLimit(!self.charging_limit)),
separator!(1),
row![icon("display-brightness-symbolic", 24)
.style(
Svg::Custom(|theme| {
svg::Appearance {
fill: Some(theme.palette().text),
}
}))
.width(Length::Units(24))
.height(Length::Units(24)),
widget::slider(0..=100, (self.screen_brightness * 100.0) as i32, Message::SetScreenBrightness),
text(format!("{:.0}%", self.screen_brightness * 100.0)).width(Length::Units(40)).horizontal_alignment(Horizontal::Right)
].spacing(12),
row![
icon("keyboard-brightness-symbolic", 24)
.style(Svg::Custom(|theme| {
svg::Appearance {
fill: Some(theme.palette().text),
}
}))
.width(Length::Units(24))
.height(Length::Units(24)),
widget::slider(0..=100, (self.kbd_brightness * 100.0) as i32, Message::SetKbdBrightness),
text(format!("{:.0}%", self.kbd_brightness * 100.0)).width(Length::Units(40)).horizontal_alignment(Horizontal::Right)
].spacing(12),
button(text("Power Settings...").horizontal_alignment(Horizontal::Center).width(Length::Fill).style(theme::Text::Custom(|theme| {
let cosmic = theme.cosmic();
iced_style::text::Appearance {
color: Some(cosmic.accent.on.into())
}
}))).width(Length::Fill)
]
.spacing(4)
.padding(8),
)
.into()
}
}
}
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 {
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),
}),
screen_backlight_subscription(0).map(|(_, event)| match event {
ScreenBacklightUpdate::Update(b) => Message::UpdateScreenBrightness(b),
ScreenBacklightUpdate::Init(tx, b) => Message::InitScreenBacklight(tx, b),
})
])
}
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, _id: iced_sctk::application::SurfaceIdWrapper) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}
}

View file

@ -2,13 +2,19 @@
// How should key bindings be handled? Need something like gnome-settings-daemon?
use std::{
fs::{self, File},
fs::File,
io::{self, Read},
os::unix::ffi::OsStrExt,
path::Path,
str::{self, FromStr},
hash::Hash,
fmt::Debug
};
use cosmic::iced;
use iced_sctk::subscription;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
const BACKLIGHT_SYSDIR: &str = "/sys/class/backlight";
#[zbus::dbus_proxy(
@ -24,13 +30,13 @@ trait LogindSession {
pub struct Backlight(String);
impl Backlight {
pub fn brightness(&self) -> Option<u32> {
self.prop("brightness")
pub async fn brightness(&self) -> Option<u32> {
self.prop("brightness").await
}
// XXX cache value. Async?
pub fn max_brightness(&self) -> Option<u32> {
self.prop("max_brightness")
pub async fn max_brightness(&self) -> Option<u32> {
self.prop("max_brightness").await
}
pub async fn set_brightness(
@ -41,7 +47,7 @@ impl Backlight {
session.set_brightness("backlight", &self.0, value).await
}
fn prop<T: FromStr>(&self, name: &str) -> Option<T> {
async fn prop<T: FromStr>(&self, name: &str) -> Option<T> {
let path = Path::new(BACKLIGHT_SYSDIR).join(&self.0).join(name);
let mut file = File::open(path).ok()?;
let mut s = String::new();
@ -51,13 +57,14 @@ impl Backlight {
}
// Choose backlight with most "precision". This is what `light` does.
pub fn backlight() -> io::Result<Option<Backlight>> {
pub async fn backlight() -> io::Result<Option<Backlight>> {
let mut best_backlight = None;
let mut best_max_brightness = 0;
for i in fs::read_dir(BACKLIGHT_SYSDIR)? {
if let Ok(filename) = str::from_utf8(i?.file_name().as_bytes()) {
let mut dir_stream = tokio::fs::read_dir(BACKLIGHT_SYSDIR).await?;
while let Ok(Some(entry)) = dir_stream.next_entry().await {
if let Ok(filename) = str::from_utf8(entry.file_name().as_bytes()) {
let backlight = Backlight(filename.to_string());
if let Some(max_brightness) = backlight.max_brightness() {
if let Some(max_brightness) = backlight.max_brightness().await {
if max_brightness > best_max_brightness {
best_backlight = Some(backlight);
best_max_brightness = max_brightness;
@ -68,6 +75,87 @@ pub fn backlight() -> io::Result<Option<Backlight>> {
Ok(best_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))
}
pub enum State {
Ready,
Waiting(Backlight, LogindSessionProxy<'static>, UnboundedReceiver<ScreenBacklightRequest>),
Finished,
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, ScreenBacklightUpdate)>, State) {
match state {
State::Ready => {
let conn = match zbus::Connection::system().await {
Ok(conn) => conn,
Err(_) => return (None, State::Finished),
};
let screen_proxy = match LogindSessionProxy::builder(&conn).build().await {
Ok(p) => p,
Err(_) => return (None, State::Finished),
};
let backlight = match backlight().await {
Ok(Some(b)) => b,
_ => return (None, State::Finished),
};
let (tx, rx) = unbounded_channel();
return (
Some((
id,
ScreenBacklightUpdate::Init(tx, backlight.brightness().await.unwrap_or_default() as f64)
)),
State::Waiting(backlight, screen_proxy, rx),
);
}
State::Waiting(backlight, proxy, mut rx) => {
match rx.recv().await {
Some(req) => match req {
ScreenBacklightRequest::Get => (
Some((
id,
ScreenBacklightUpdate::Update(backlight.brightness().await.unwrap_or_default() as f64)
)),
State::Waiting(backlight, proxy, rx),
),
ScreenBacklightRequest::Set(value) => {
if let Some(max_brightness) = backlight.max_brightness().await {
let value = value.clamp(0., 1.) * (max_brightness as f64);
let value = value.round() as u32;
let _ = backlight.set_brightness(&proxy, value).await;
}
(
None,
State::Waiting(backlight, proxy, rx),
)
},
},
None => (None, State::Finished),
}
}
State::Finished => iced::futures::future::pending().await,
}
}
#[derive(Debug, Clone)]
pub enum ScreenBacklightUpdate {
Update(f64),
Init(UnboundedSender<ScreenBacklightRequest>, f64)
}
#[derive(Debug, Clone)]
pub enum ScreenBacklightRequest {
Get,
Set(f64),
}
/*
// TODO: Cache device, max_brightness, etc.
async fn set_display_brightness(brightness: f64) -> io::Result<()> {

View file

@ -0,0 +1,3 @@
pub const APP_ID: &str = "com.system76.CosmicAppletButton";
pub const PROFILE: &str = "";
pub const VERSION: &str = "0.1.0";

View file

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MPL-2.0-only
use i18n_embed::{
fluent::{fluent_language_loader, FluentLanguageLoader},
DefaultLocalizer, LanguageLoader, Localizer,
};
use once_cell::sync::Lazy;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_fallback_language(&Localizations)
.expect("Error while loading fallback language");
loader
});
#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
}};
($message_id:literal, $($args:expr),*) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}
// Get the `Localizer` to be used for localizing this library.
pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}
pub fn localize() {
let localizer = localizer();
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for App List {}", error);
}
}

View file

@ -1,366 +1,28 @@
// TODO: don't allow brightness 0?
// TODO: handle dbus service start/stop?
use futures::prelude::*;
use gtk4::{gio::ApplicationFlags, glib, prelude::*, Application};
use relm4::{
component::ComponentSenderInner, ComponentParts, ComponentSender, RelmApp, SimpleComponent,
};
use std::{process::Command, sync::Arc, time::Duration};
#[rustfmt::skip]
mod backlight;
use backlight::{backlight, Backlight, LogindSessionProxy};
mod config;
mod app;
mod localize;
mod power_daemon;
mod upower;
use upower::UPowerProxy;
mod upower_device;
use upower_device::DeviceProxy;
mod upower_kbdbacklight;
use upower_kbdbacklight::KbdBacklightProxy;
use config::APP_ID;
use log::info;
async fn display_device() -> zbus::Result<DeviceProxy<'static>> {
let connection = zbus::Connection::system().await?;
let upower = UPowerProxy::new(&connection).await?;
let device_path = upower.get_display_device().await?;
DeviceProxy::builder(&connection)
.path(device_path)?
.cache_properties(zbus::CacheProperties::Yes)
.build()
.await
}
// XXX improve
// TODO: time to empty varies? needs averaging?
fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs();
if secs > 60 {
let min = secs / 60;
if min > 60 {
format!("{}:{:02}", min / 60, min % 60)
} else {
format!("{}m", min)
}
} else {
format!("{}s", secs)
}
}
#[derive(Default)]
struct AppModel {
icon_name: String,
battery_percent: f64,
time_remaining: Duration,
display_brightness: f64,
keyboard_brightness: f64,
device: Option<DeviceProxy<'static>>,
session: Option<LogindSessionProxy<'static>>,
backlight: Option<Backlight>,
kbd_backlight: Option<KbdBacklightProxy<'static>>,
}
#[derive(Debug)]
enum AppMsg {
SetDisplayBrightness(f64),
SetKeyboardBrightness(f64),
SetDevice(DeviceProxy<'static>),
SetSession(LogindSessionProxy<'static>),
SetKbdBacklight(KbdBacklightProxy<'static>),
UpdateProperties,
UpdateKbdBrightness(f64),
}
#[relm4::component]
impl SimpleComponent for AppModel {
type Widgets = AppWidgets;
type InitParams = ();
type Input = AppMsg;
type Output = ();
view! {
libcosmic_applet::AppletWindow {
#[wrap(Some)]
set_child = &libcosmic_applet::AppletButton {
#[watch]
set_button_icon_name: &model.icon_name,
#[wrap(Some)]
set_popover_child = &gtk4::Box {
set_orientation: gtk4::Orientation::Vertical,
// Battery
gtk4::Box {
set_orientation: gtk4::Orientation::Horizontal,
gtk4::Image {
#[watch]
set_icon_name: Some(&model.icon_name),
},
gtk4::Box {
set_orientation: gtk4::Orientation::Vertical,
gtk4::Label {
set_halign: gtk4::Align::Start,
set_label: "Battery",
},
gtk4::Label {
set_halign: gtk4::Align::Start,
// XXX time to full, fully changed, etc.
#[watch]
set_label: &format!("{} until empty ({:.0}%)", format_duration(model.time_remaining), model.battery_percent),
},
},
},
gtk4::Separator {
},
// Limit charging
gtk4::Box {
set_orientation: gtk4::Orientation::Horizontal,
gtk4::Box {
set_orientation: gtk4::Orientation::Vertical,
gtk4::Label {
set_halign: gtk4::Align::Start,
set_label: "Limit Battery Charging",
},
gtk4::Label {
set_halign: gtk4::Align::Start,
set_label: "Increase the lifespan of your battery by setting a maximum charge value of 80%."
},
},
gtk4::Switch {
set_valign: gtk4::Align::Center,
},
},
gtk4::Separator {
},
// Brightness
gtk4::Box {
#[watch]
set_visible: model.backlight.is_some(),
set_orientation: gtk4::Orientation::Horizontal,
gtk4::Image {
set_icon_name: Some("display-brightness-symbolic"),
},
gtk4::Scale {
set_hexpand: true,
set_adjustment: &gtk4::Adjustment::new(0., 0., 1., 1., 1., 0.),
#[watch]
set_value: model.display_brightness,
connect_change_value[sender] => move |_, _, value| {
sender.input(AppMsg::SetDisplayBrightness(value));
gtk4::Inhibit(false)
},
},
gtk4::Label {
#[watch]
set_label: &format!("{:.0}%", model.display_brightness * 100.),
},
},
gtk4::Box {
#[watch]
set_visible: model.kbd_backlight.is_some(),
set_orientation: gtk4::Orientation::Horizontal,
gtk4::Image {
set_icon_name: Some("keyboard-brightness-symbolic"),
},
gtk4::Scale {
set_hexpand: true,
set_adjustment: &gtk4::Adjustment::new(0., 0., 1., 1., 1., 0.),
#[watch]
set_value: model.keyboard_brightness,
connect_change_value[sender] => move |_, _, value| {
sender.input(AppMsg::SetKeyboardBrightness(value));
gtk4::Inhibit(false)
},
},
gtk4::Label {
#[watch]
set_label: &format!("{:.0}%", model.keyboard_brightness * 100.),
},
},
gtk4::Separator {
},
gtk4::Button {
set_label: "Power Settings...",
connect_clicked => move |_| {
// XXX open subpanel
let _ = Command::new("cosmic-settings").spawn();
// TODO hide
}
}
}
}
}
}
fn init(
_params: Self::InitParams,
root: &Self::Root,
sender: Arc<ComponentSenderInner<AppMsg, (), ()>>,
) -> ComponentParts<Self> {
let mut model = AppModel {
icon_name: "battery-symbolic".to_string(),
..Default::default()
};
let widgets = view_output!();
match backlight() {
Ok(Some(backlight)) => {
if let (Some(brightness), Some(max_brightness)) =
(backlight.brightness(), backlight.max_brightness())
{
model.display_brightness = brightness as f64 / max_brightness as f64;
}
model.backlight = Some(backlight);
}
Ok(None) => {}
Err(err) => eprintln!("Error finding backlight: {}", err),
};
glib::MainContext::default().spawn(glib::clone!(@strong sender => async move {
match display_device().await {
Ok(device) => sender.input(AppMsg::SetDevice(device)),
Err(err) => eprintln!("Failed to open UPower display device: {}", err),
}
}));
glib::MainContext::default().spawn(glib::clone!(@strong sender => async move {
// XXX avoid multiple connections?
let proxy = async {
let connection = zbus::Connection::system().await?;
LogindSessionProxy::builder(&connection).build().await
}.await;
match proxy {
Ok(session) => sender.input(AppMsg::SetSession(session)),
Err(err) => eprintln!("Failed to open logind session: {}", err),
}
}));
glib::MainContext::default().spawn(glib::clone!(@strong sender => async move {
let proxy = async {
let connection = zbus::Connection::system().await?;
KbdBacklightProxy::builder(&connection).build().await
}.await;
match proxy {
Ok(kbd_backlight) => sender.input(AppMsg::SetKbdBacklight(kbd_backlight)),
Err(err) => eprintln!("Failed to open kbd_backlight: {}", err),
}
}));
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: Arc<ComponentSenderInner<AppMsg, (), ()>>) {
match msg {
AppMsg::SetDisplayBrightness(value) => {
self.display_brightness = value;
// XXX clone
if let Some(backlight) = self.backlight.clone() {
if let Some(session) = self.session.clone() {
// XXX cache max brightness
if let Some(max_brightness) = backlight.max_brightness() {
let value = value.clamp(0., 1.) * (max_brightness as f64);
let value = value.round() as u32;
// XXX limit queueing?
glib::MainContext::default().spawn(async move {
if let Err(err) = backlight.set_brightness(&session, value).await {
eprintln!("Failed to set backlight: {}", err);
}
});
}
}
}
}
AppMsg::SetKeyboardBrightness(value) => {
self.keyboard_brightness = value;
if let Some(kbd_backlight) = self.kbd_backlight.clone() {
glib::MainContext::default().spawn(async move {
let res = async {
// XXX cache
let max_brightness = kbd_backlight.get_max_brightness().await?;
let value = value.clamp(0., 1.) * (max_brightness as f64);
let value = value.round() as i32;
kbd_backlight.set_brightness(value).await
}
.await;
if let Err(err) = res {
eprintln!("Failed to set keyboard backlight: {}", err);
}
});
}
}
AppMsg::SetDevice(device) => {
self.device = Some(device.clone());
let sender = sender.clone();
glib::MainContext::default().spawn(async move {
let mut stream = futures::stream_select!(
device.receive_icon_name_changed().await.map(|_| ()),
device.receive_percentage_changed().await.map(|_| ()),
device.receive_time_to_empty_changed().await.map(|_| ()),
);
sender.input(AppMsg::UpdateProperties);
while let Some(()) = stream.next().await {
sender.input(AppMsg::UpdateProperties);
}
});
}
AppMsg::SetSession(session) => {
self.session = Some(session);
}
AppMsg::SetKbdBacklight(kbd_backlight) => {
self.kbd_backlight = Some(kbd_backlight.clone());
glib::MainContext::default().spawn(glib::clone!(@strong sender => async move {
let res = async {
let stream = kbd_backlight.receive_brightness_changed().await?;
let brightness = kbd_backlight.get_brightness().await?;
let max_brightness = kbd_backlight.get_max_brightness().await?;
zbus::Result::Ok((brightness, max_brightness, stream))
}.await;
match res {
Ok((brightness, max_brightness, mut stream)) => {
let value = (brightness as f64) / (max_brightness as f64);
sender.input(AppMsg::UpdateKbdBrightness(value));
while let Some(evt) = stream.next().await {
// TODO
}
}
Err(err) => {
}
}
}));
}
AppMsg::UpdateProperties => {
if let Some(device) = self.device.as_ref() {
if let Ok(Some(percentage)) = device.cached_percentage() {
self.battery_percent = percentage;
}
if let Ok(Some(icon_name)) = device.cached_icon_name() {
self.icon_name = icon_name;
}
if let Ok(Some(secs)) = device.cached_time_to_empty() {
self.time_remaining = Duration::from_secs(secs as u64);
}
}
}
AppMsg::UpdateKbdBrightness(value) => {
self.keyboard_brightness = value;
}
}
}
}
fn main() {
let _monitors = libcosmic::init();
let app = RelmApp::with_app(Application::new(None, ApplicationFlags::default()));
app.run::<AppModel>(());
use localize::localize;
use crate::config::{PROFILE, VERSION};
fn main() -> cosmic::iced::Result {
// Initialize logger
pretty_env_logger::init();
info!("Iced Workspaces Applet ({})", APP_ID);
info!("Version: {} ({})", VERSION, PROFILE);
// Prepare i18n
localize();
app::run()
}

View file

@ -63,3 +63,5 @@ trait PowerDaemon {
#[dbus_proxy(signal)]
fn power_profile_switch(&self, profile: &str) -> zbus::Result<()>;
}
// TODO power subscription

View file

@ -3,8 +3,13 @@
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
//! Source: `Interface '/org/freedesktop/UPower/devices/DisplayDevice' from service 'org.freedesktop.UPower' on system bus`.
use cosmic::iced::{self, subscription};
use futures::StreamExt;
use std::{fmt::Debug, hash::Hash};
use zbus::dbus_proxy;
use crate::upower::UPowerProxy;
#[dbus_proxy(
default_service = "org.freedesktop.UPower",
interface = "org.freedesktop.UPower.Device"
@ -144,3 +149,97 @@ trait Device {
#[dbus_proxy(property)]
fn warning_level(&self) -> zbus::Result<u32>;
}
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))
}
#[derive(Debug)]
pub enum State {
Ready,
Waiting(DeviceProxy<'static>),
Finished,
}
async fn display_device() -> zbus::Result<DeviceProxy<'static>> {
let connection = zbus::Connection::system().await?;
let upower = UPowerProxy::new(&connection).await?;
let device_path = upower.get_display_device().await?;
DeviceProxy::builder(&connection)
.path(device_path)?
.cache_properties(zbus::CacheProperties::Yes)
.build()
.await
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, DeviceDbusEvent)>, State) {
match state {
State::Ready => {
if let Ok(device) = display_device().await {
return (
Some((
id,
DeviceDbusEvent::Update {
icon_name: device
.cached_icon_name()
.unwrap_or_default()
.unwrap_or_default(),
percent: device
.cached_percentage()
.unwrap_or_default()
.unwrap_or_default(),
time_to_empty: device
.cached_time_to_empty()
.unwrap_or_default()
.unwrap_or_default(),
},
)),
State::Waiting(device),
);
}
return (None, State::Finished);
}
State::Waiting(device) => {
let mut stream = futures::stream_select!(
device.receive_icon_name_changed().await.map(|_| ()),
device.receive_percentage_changed().await.map(|_| ()),
device.receive_time_to_empty_changed().await.map(|_| ()),
);
match stream.next().await {
Some(_) => (
Some((
id,
DeviceDbusEvent::Update {
icon_name: device
.cached_icon_name()
.unwrap_or_default()
.unwrap_or_default(),
percent: device
.cached_percentage()
.unwrap_or_default()
.unwrap_or_default(),
time_to_empty: device
.cached_time_to_empty()
.unwrap_or_default()
.unwrap_or_default(),
},
)),
State::Waiting(device),
),
None => (None, State::Finished),
}
}
State::Finished => iced::futures::future::pending().await,
}
}
#[derive(Debug, Clone)]
pub enum DeviceDbusEvent {
Update {
icon_name: String,
percent: f64,
time_to_empty: i64,
},
}

View file

@ -3,8 +3,11 @@
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
//! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`.
use cosmic::iced;
use iced::subscription;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
use zbus::dbus_proxy;
use std::{fmt::Debug, hash::Hash};
#[dbus_proxy(
default_service = "org.freedesktop.UPower",
interface = "org.freedesktop.UPower.KbdBacklight",
@ -28,3 +31,80 @@ trait KbdBacklight {
#[dbus_proxy(signal)]
fn brightness_changed_with_source(&self, value: i32, source: &str) -> zbus::Result<()>;
}
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))
}
#[derive(Debug)]
pub enum State {
Ready,
Waiting(KbdBacklightProxy<'static>, UnboundedReceiver<KeyboardBacklightRequest>),
Finished,
}
async fn start_listening<I: Copy>(id: I, state: State) -> (Option<(I, KeyboardBacklightUpdate)>, State) {
match state {
State::Ready => {
let conn = match zbus::Connection::system().await {
Ok(conn) => conn,
Err(_) => return (None, State::Finished),
};
let kbd_proxy = match KbdBacklightProxy::builder(&conn).build().await {
Ok(p) => p,
Err(_) => return (None, State::Finished),
};
let (tx, rx) = unbounded_channel();
return (
Some((
id,
KeyboardBacklightUpdate::Init(tx, kbd_proxy.get_brightness().await.unwrap_or_default() as f64)
)),
State::Waiting(kbd_proxy, rx),
);
}
State::Waiting(proxy, mut rx) => {
match rx.recv().await {
Some(req) => match req {
KeyboardBacklightRequest::Get => (
Some((
id,
KeyboardBacklightUpdate::Update(proxy.get_brightness().await.unwrap_or_default() as f64)
)),
State::Waiting(proxy, rx),
),
KeyboardBacklightRequest::Set(value) => {
if let Ok(max_brightness) = proxy.get_max_brightness().await {
let value = value.clamp(0., 1.) * (max_brightness as f64);
let value = value.round() as i32;
let _ = proxy.set_brightness(value).await;
}
(
None,
State::Waiting(proxy, rx),
)
},
},
None => (None, State::Finished),
}
}
State::Finished => iced::futures::future::pending().await,
}
}
#[derive(Debug, Clone)]
pub enum KeyboardBacklightUpdate {
Update(f64),
Init(UnboundedSender<KeyboardBacklightRequest>, f64)
}
#[derive(Debug, Clone)]
pub enum KeyboardBacklightRequest {
Get,
Set(f64),
}

View file

@ -1351,7 +1351,7 @@ dependencies = [
[[package]]
name = "iced_sctk"
version = "0.1.0"
source = "git+https://github.com/pop-os/iced-sctk#a2c24d95ebc795245495677b99b631a4322ef876"
source = "git+https://github.com/pop-os/iced-sctk#d126b62ef1001b22dc946db91928395890ff8d9f"
dependencies = [
"enum-repr",
"futures",
@ -1531,7 +1531,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libcosmic"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic/?branch=sctk-cosmic-design-system#e87fe7056d09530c35e2c39d470205c6a7a920c6"
source = "git+https://github.com/pop-os/libcosmic/?branch=sctk-cosmic-design-system#478869302720fa5674ac471235715bbd308695e8"
dependencies = [
"apply",
"cosmic-panel-config",
@ -1707,9 +1707,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.25.0"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags",
@ -1815,9 +1815,9 @@ dependencies = [
[[package]]
name = "ordered-stream"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef"
checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea"
dependencies = [
"futures-core",
"pin-project-lite",
@ -2535,9 +2535,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.104"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
dependencies = [
"proc-macro2",
"quote",
@ -3010,9 +3010,9 @@ dependencies = [
[[package]]
name = "wgpu"
version = "0.14.0"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2272b17bffc8a0c7d53897435da7c1db587c87d3a14e8dae9cdb8d1d210fc0f"
checksum = "81f643110d228fd62a60c5ed2ab56c4d5b3704520bd50561174ec4ec74932937"
dependencies = [
"arrayvec 0.7.2",
"js-sys",
@ -3032,9 +3032,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "0.14.0"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73d14cad393054caf992ee02b7da6a372245d39a484f7461c1f44f6f6359bd28"
checksum = "6000d1284ef8eec6076fd5544a73125fd7eb9b635f18dceeb829d826f41724ca"
dependencies = [
"arrayvec 0.7.2",
"bit-vec",

7
debian/rules vendored
View file

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

View file

@ -31,6 +31,9 @@ build: _extract_vendor
pushd applets/cosmic-applet-graphics/
cargo build {{cargo_args}}
popd
pushd applets/cosmic-applet-battery/
cargo build {{cargo_args}}
popd
pushd applets/cosmic-applet-workspaces/
cargo build {{cargo_args}}
popd
@ -50,11 +53,6 @@ install:
install -Dm0644 applets/cosmic-applet-audio/data/{{audio_id}}.desktop {{sharedir}}/applications/{{audio_id}}.desktop
install -Dm0755 target/release/cosmic-applet-audio {{bindir}}/cosmic-applet-audio
# battery
install -Dm0644 applets/cosmic-applet-battery/data/icons/{{battery_id}}.svg {{iconsdir}}/{{battery_id}}.svg
install -Dm0644 applets/cosmic-applet-battery/data/{{battery_id}}.desktop {{sharedir}}/applications/{{battery_id}}.desktop
install -Dm0755 target/release/cosmic-applet-battery {{bindir}}/cosmic-applet-battery
# network
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg
install -Dm0644 applets/cosmic-applet-network/data/{{network_id}}.desktop {{sharedir}}/applications/{{network_id}}.desktop
@ -101,6 +99,11 @@ install:
install -Dm0644 applets/cosmic-applet-workspaces/data/{{workspaces_id}}.desktop {{sharedir}}/applications/{{workspaces_id}}.desktop
install -Dm0755 applets/cosmic-applet-workspaces/target/release/cosmic-applet-workspaces {{bindir}}/cosmic-applet-workspaces
# battery
install -Dm0644 applets/cosmic-applet-battery/data/icons/{{battery_id}}.svg {{iconsdir}}/{{battery_id}}.svg
install -Dm0644 applets/cosmic-applet-battery/data/{{battery_id}}.desktop {{sharedir}}/applications/{{battery_id}}.desktop
install -Dm0755 applets/cosmic-applet-battery/target/release/cosmic-applet-battery {{bindir}}/cosmic-applet-battery
# Extracts vendored dependencies if vendor=1
_extract_vendor:
#!/usr/bin/env sh
@ -108,4 +111,5 @@ _extract_vendor:
rm -rf vendor; tar pxf vendor.tar
rm -rf applets/cosmic-applet-graphics/vendor; tar xf applets/cosmic-applet-graphics/vendor.tar --directory applets/cosmic-applet-graphics
rm -rf applets/cosmic-applet-workspaces/vendor; tar xf applets/cosmic-applet-workspaces/vendor.tar --directory applets/cosmic-applet-workspaces
rm -rf applets/cosmic-applet-battery/vendor; tar xf applets/cosmic-applet-battery/vendor.tar --directory applets/cosmic-applet-battery
fi