feat: iced battery applet
This commit is contained in:
parent
a7b099b4b3
commit
6a98d7f7c8
17 changed files with 3520 additions and 964 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
3345
applets/cosmic-applet-battery/Cargo.lock
generated
3345
applets/cosmic-applet-battery/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"]
|
||||
|
|
|
|||
4
applets/cosmic-applet-battery/i18n.toml
Normal file
4
applets/cosmic-applet-battery/i18n.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
|
|
@ -0,0 +1 @@
|
|||
cosmic-applet-button = Cosmic Button
|
||||
326
applets/cosmic-applet-battery/src/app.rs
Normal file
326
applets/cosmic-applet-battery/src/app.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<()> {
|
||||
|
|
|
|||
3
applets/cosmic-applet-battery/src/config.rs
Normal file
3
applets/cosmic-applet-battery/src/config.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const APP_ID: &str = "com.system76.CosmicAppletButton";
|
||||
pub const PROFILE: &str = "";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
47
applets/cosmic-applet-battery/src/localize.rs
Normal file
47
applets/cosmic-applet-battery/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,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
|
||||
}
|
||||
use localize::localize;
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
use crate::config::{PROFILE, VERSION};
|
||||
|
||||
#[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>>,
|
||||
}
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
pretty_env_logger::init();
|
||||
info!("Iced Workspaces Applet ({})", APP_ID);
|
||||
info!("Version: {} ({})", VERSION, PROFILE);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AppMsg {
|
||||
SetDisplayBrightness(f64),
|
||||
SetKeyboardBrightness(f64),
|
||||
SetDevice(DeviceProxy<'static>),
|
||||
SetSession(LogindSessionProxy<'static>),
|
||||
SetKbdBacklight(KbdBacklightProxy<'static>),
|
||||
UpdateProperties,
|
||||
UpdateKbdBrightness(f64),
|
||||
}
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
#[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 = >k4::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: >k4::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: >k4::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>(());
|
||||
app::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,3 +63,5 @@ trait PowerDaemon {
|
|||
#[dbus_proxy(signal)]
|
||||
fn power_profile_switch(&self, profile: &str) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
// TODO power subscription
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
24
applets/cosmic-applet-graphics/Cargo.lock
generated
24
applets/cosmic-applet-graphics/Cargo.lock
generated
|
|
@ -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
7
debian/rules
vendored
|
|
@ -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:
|
||||
|
|
|
|||
14
justfile
14
justfile
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue