refactor + broken battery

This commit is contained in:
Jiří Maxmilián Stříbrný 2026-03-20 20:29:34 +01:00
parent 58166358ba
commit 93857e2f18
15 changed files with 598 additions and 2440 deletions

View file

@ -1,28 +1,29 @@
use iced::Color;
use iced::Element;
use iced::Subscription;
use iced::Task;
use iced::widget::button;
use iced::widget::container;
use iced::widget::row;
use iced::widget::text;
use iced_layershell::daemon;
use iced_layershell::reexport::Anchor;
use iced_layershell::reexport::NewLayerShellSettings;
use iced_layershell::settings::LayerShellSettings;
use iced_layershell::settings::Settings;
use iced_layershell::to_layer_message;
use std::collections::HashMap;
use std::time::Duration;
mod widget;
mod widgets;
use crate::widget::Message;
use crate::widgets::battery::BatteryWidget;
use crate::widgets::clock::ClockWidget;
use crate::widgets::spacer::Spacer;
pub fn main() -> Result<(), iced_layershell::Error> {
daemon(App::default, App::name, App::update, App::view)
daemon(App::new, App::name, App::update, App::view)
.style(App::style)
.theme(App::theme)
.subscription(App::subscription)
.settings(Settings {
layer_settings: LayerShellSettings {
size: Some((0, 32)),
exclusive_zone: 32,
size: Some((0, App::WINDOW_HEIGHT)),
exclusive_zone: App::WINDOW_HEIGHT as i32,
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
start_mode: iced_layershell::settings::StartMode::AllScreens,
..Default::default()
@ -33,112 +34,56 @@ pub fn main() -> Result<(), iced_layershell::Error> {
}
struct App {
popups: HashMap<iced::window::Id, PopupKind>,
power_off_widget: PowerOffWidget,
time_widget: TimeWidget,
battery_widget: BatteryWidget,
}
enum PopupKind {
PowerOff,
}
impl Default for App {
fn default() -> Self {
let mut output = Self {
popups: HashMap::new(),
power_off_widget: PowerOffWidget(None),
time_widget: Default::default(),
battery_widget: Default::default(),
};
let _ = output.update(Message::Clock);
output
}
widgets: Vec<Box<dyn widget::PanelWidget>>,
}
impl App {
const WINDOW_HEIGHT: u32 = 32;
fn new() -> Self {
Self {
widgets: vec![
Box::new(ClockWidget::new()),
Box::new(Spacer::new(iced::Length::Fill)),
Box::new(BatteryWidget::new()),
],
}
}
fn name() -> String {
"wayland_panel".into()
}
fn close_popup(&mut self, id: iced::window::Id) -> Task<Message> {
self.popups.remove(&id);
return Task::done(Message::RemoveWindow(id));
}
fn view(&self, id: iced::window::Id) -> Element<'_, Message> {
if let Some(kind) = self.popups.get(&id) {
return match kind {
PopupKind::PowerOff => container(
row![
text("Shut down?"),
button("").on_press(Message::PowerOffConfirm),
button("").on_press(Message::PowerOffCancel),
]
.spacing(8)
.align_y(iced::Alignment::Center),
)
.center(iced::Length::Fill)
.into(),
};
if let Some(elem) = self
.widgets
.iter()
.find(|widget| widget.own_window(id))
.map(|widget| widget.view(id))
{
return elem;
}
// default: the bar
let content = row![
self.time_widget.view(),
iced::widget::space().width(iced::Length::Fill),
self.power_off_widget.view(),
iced::widget::space().width(iced::Length::Fill),
self.battery_widget.view(),
]
.padding(iced::Padding::from([0, 5]))
.height(iced::Length::Fill)
.width(iced::Length::Fill)
.align_y(iced::Alignment::Center);
container(content).into()
iced::widget::Row::with_children(self.widgets.iter().map(|widget| widget.view(id)))
.padding(iced::Padding::from([0, 5]))
.height(iced::Length::Fill)
.width(iced::Length::Fill)
.align_y(iced::Alignment::Center)
.into()
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Clock => {
self.time_widget.update();
self.battery_widget.update();
Task::none()
}
Message::PowerOff => {
let id = iced::window::Id::unique();
self.popups.insert(id, PopupKind::PowerOff);
self.power_off_widget.0 = Some(id);
Task::done(Message::NewLayerShell {
settings: NewLayerShellSettings {
size: Some((220, 40)),
anchor: Anchor::Top,
layer: iced_layershell::reexport::Layer::Overlay,
keyboard_interactivity:
iced_layershell::reexport::KeyboardInteractivity::OnDemand,
exclusive_zone: None,
..Default::default()
},
id,
})
}
Message::PowerOffConfirm => {
std::process::Command::new("poweroff").spawn().ok();
Task::none()
}
Message::PowerOffCancel => self.close_popup(self.power_off_widget.0.unwrap()),
_ => Task::none(),
let mut output = Vec::new();
for widget in &mut self.widgets {
output.push(widget.update(&message));
}
Task::batch(output)
}
fn style(&self, theme: &iced::Theme) -> iced::theme::Style {
iced::theme::Style {
background_color: Color::TRANSPARENT,
background_color: Color::from_rgba(0., 0., 0., 1.),
text_color: theme.palette().text,
}
}
@ -148,100 +93,6 @@ impl App {
}
fn subscription(&self) -> iced::Subscription<Message> {
iced::time::every(Duration::from_secs(1)).map(|_| Message::Clock)
Subscription::batch(self.widgets.iter().map(|w| w.subscribe()))
}
}
trait PanelWidget {
fn update(&mut self);
fn view(&self) -> Element<'_, Message>;
}
#[derive(Default)]
struct BatteryWidget {
battery: Option<usize>,
}
impl PanelWidget for BatteryWidget {
fn update(&mut self) {
let file_content = std::fs::read_to_string(std::path::Path::new(
"/sys/class/power_supply/BAT0/capacity",
));
let Ok(file_string) = file_content else {
self.battery = None;
return;
};
let file_string = file_string.trim();
let Ok(value) = file_string.parse::<usize>() else {
self.battery = None;
return;
};
self.battery = Some(value);
}
fn view(&self) -> Element<'_, Message> {
match self.battery {
Some(battery) => text!("{} {}", battery.to_string(), Self::get_icon(battery)),
None => text("󰂃"),
}
.into()
}
}
impl BatteryWidget {
fn get_icon(battery: usize) -> &'static str {
match battery {
0..6 => "󰂎",
6..11 => "󰁺",
11..21 => "󰁻",
21..31 => "󰁼",
31..41 => "󰁽",
41..51 => "󰁾",
51..61 => "󰁿",
61..71 => "󰂀",
71..81 => "󰂁",
81..91 => "󰂂",
91..101 => "󰁹",
_ => "󰂃",
}
}
}
#[derive(Default)]
struct TimeWidget {
current_time: chrono::DateTime<chrono::Local>,
}
impl PanelWidget for TimeWidget {
fn update(&mut self) {
self.current_time = chrono::Local::now();
}
fn view(&self) -> Element<'_, Message> {
text!("{}", self.current_time.format("%H:%m")).into()
}
}
#[derive(Default)]
struct PowerOffWidget(Option<iced::window::Id>);
impl PanelWidget for PowerOffWidget {
fn update(&mut self) {}
fn view(&self) -> Element<'_, Message> {
button("").on_press(Message::PowerOff).into()
}
}
#[to_layer_message(multi)]
#[derive(Debug, Clone)]
pub enum Message {
Clock,
PowerOff,
PowerOffConfirm,
PowerOffCancel,
}

20
src/widget.rs Normal file
View file

@ -0,0 +1,20 @@
use iced::Element;
use iced::Subscription;
use iced::Task;
use iced_layershell::to_layer_message;
pub trait PanelWidget {
fn update(&mut self, message: &Message) -> Task<Message>;
fn subscribe(&self) -> Subscription<Message>;
fn view(&self, id: iced::window::Id) -> Element<'_, Message>;
fn own_window(&self, _id: iced::window::Id) -> bool {
false
}
}
#[to_layer_message(multi)]
#[derive(Debug, Clone)]
pub enum Message {
Battery(Option<f64>),
Time,
}

94
src/widgets/battery.rs Normal file
View file

@ -0,0 +1,94 @@
use crate::widget::{Message, PanelWidget};
use iced::Element;
use iced::Subscription;
use iced::Task;
use iced::futures::SinkExt;
use iced::futures::StreamExt;
use iced::widget::text;
use std::time::Duration;
#[zbus::proxy(
interface = "org.freedesktop.UPower.Device",
default_service = "org.freedesktop.UPower",
default_path = "/org/freedesktop/UPower/devices/DisplayDevice"
)]
trait UPowerDevice {
#[zbus(property)]
fn percentage(&self) -> zbus::Result<f64>;
}
pub struct BatteryWidget {
capacity: Option<f64>,
}
impl BatteryWidget {
pub fn new() -> Self {
Self { capacity: None }
}
}
impl PanelWidget for BatteryWidget {
fn update(&mut self, message: &Message) -> Task<Message> {
if let Message::Battery(capacity) = message {
self.capacity = *capacity;
}
Task::none()
}
fn subscribe(&self) -> Subscription<Message> {
Subscription::run(|| {
iced::stream::channel(16, async |mut tx| {
loop {
let Ok(conn) = zbus::Connection::system().await else {
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
let Ok(proxy) = UPowerDeviceProxy::new(&conn).await else {
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
if let Ok(pct) = proxy.percentage().await {
tx.send(Message::Battery(Some(pct))).await.ok();
}
let mut changes = proxy.receive_percentage_changed().await;
while let Some(change) = changes.next().await {
if let Ok(pct) = change.get().await {
tx.send(Message::Battery(Some(pct))).await.ok();
}
}
tx.send(Message::Battery(None)).await.ok();
}
})
})
}
fn view(&self, _id: iced::window::Id) -> Element<'_, Message> {
match self.capacity {
Some(cap) => text!("{} {}", cap, Self::icon(cap)),
None => text!("󰂃"),
}
.into()
}
}
impl BatteryWidget {
fn icon(capacity: f64) -> &'static str {
match capacity {
0f64..6f64 => "󰂎",
6f64..11f64 => "󰁺",
11f64..21f64 => "󰁻",
21f64..31f64 => "󰁼",
31f64..41f64 => "󰁽",
41f64..51f64 => "󰁾",
51f64..61f64 => "󰁿",
61f64..71f64 => "󰂀",
71f64..81f64 => "󰂁",
81f64..91f64 => "󰂂",
91f64..=100f64 => "󰁹",
_ => "󰂃",
}
}
}

35
src/widgets/clock.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::widget::{Message, PanelWidget};
use iced::Task;
use std::time::Duration;
pub struct ClockWidget {
current_time: chrono::DateTime<chrono::Local>,
}
impl ClockWidget {
pub fn new() -> Self {
Self {
current_time: chrono::Local::now(),
}
}
}
impl PanelWidget for ClockWidget {
fn update(&mut self, message: &Message) -> Task<Message> {
let Message::Time = message else {
return Task::none();
};
self.current_time = chrono::Local::now();
Task::none()
}
fn subscribe(&self) -> iced::Subscription<Message> {
iced::time::every(Duration::from_secs(1)).map(|_| Message::Time)
}
fn view(&self, _id: iced::window::Id) -> iced::Element<'_, Message> {
let formatted_time = self.current_time.format("%H:%M");
iced::widget::text!("{}", formatted_time).into()
}
}

3
src/widgets/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod battery;
pub mod clock;
pub mod spacer;

25
src/widgets/spacer.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::widget::PanelWidget;
pub struct Spacer {
space: iced::Length,
}
impl Spacer {
pub fn new(space: iced::Length) -> Self {
Self { space }
}
}
impl PanelWidget for Spacer {
fn update(&mut self, _message: &crate::widget::Message) -> iced::Task<crate::widget::Message> {
iced::Task::none()
}
fn subscribe(&self) -> iced::Subscription<crate::widget::Message> {
iced::Subscription::none()
}
fn view(&self, _id: iced::window::Id) -> iced::Element<'_, crate::widget::Message> {
iced::widget::space().width(self.space).into()
}
}