diff --git a/Cargo.lock b/Cargo.lock index 6f563be..4904171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -418,6 +468,65 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clap_mangen" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -468,6 +577,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -1256,9 +1371,9 @@ dependencies = [ [[package]] name = "iced_exdevtools" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd37abad853d711ca1197a01a1fac657dd225826faf68d23595ab0380aba25ed" +checksum = "79aa630d54ce3fd340bd2cf64faee63aa614f5fb19cf8158f98ecb289d5a7608" dependencies = [ "iced_debug", "iced_devtools", @@ -1303,9 +1418,9 @@ dependencies = [ [[package]] name = "iced_layershell" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b93788b3b04289379ab3ada875dd93139d9c871a1fbb99286355d2b517b02c7" +checksum = "22a8e513ce86c82210538861618b3e948bad9b82abc0d7e127f061371c989269" dependencies = [ "enumflags2", "futures", @@ -1328,9 +1443,9 @@ dependencies = [ [[package]] name = "iced_layershell_macros" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de12a0d2d2faabd9d6b49cd1621ae1b6d7e0f1365332c18673d810116e89675e" +checksum = "8625bb576fdb0438e85d8e57ac7229868dde8e410cf32c004fffd588a2777bee" dependencies = [ "darling", "manyhow", @@ -1468,6 +1583,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" @@ -1545,9 +1666,9 @@ dependencies = [ [[package]] name = "layershellev" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90cbea1375f775abb76c9f2fc7b475dd17d740936dfbeeb0026bd2f09ad5624" +checksum = "66e9699736661513c75de0cbc887aa4656823b8c22c4ac4a955cd2ddea63e1b1" dependencies = [ "bitflags 2.11.0", "calloop 0.14.4", @@ -2104,6 +2225,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "orbclient" version = "0.3.51" @@ -2261,6 +2388,20 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ppd" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9336eeb6b2ea4e61900f44b539ca0d095593a19ba8e831825c30e906aa475c" +dependencies = [ + "clap", + "clap_complete", + "clap_mangen", + "serde", + "thiserror 2.0.18", + "zbus", +] + [[package]] name = "presser" version = "0.3.1" @@ -2404,6 +2545,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "roff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" + [[package]] name = "roxmltree" version = "0.20.0" @@ -3000,6 +3147,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.22.0" @@ -3154,9 +3307,9 @@ dependencies = [ [[package]] name = "waycrate_xkbkeycode" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4865c30460c0d213dac0ea8a5eadb0d3b620e6154bd95ca058377e4c1873515" +checksum = "caa5bc93c1dfce7c8d9b4a864028c2d5f64625f249b08900805acdbe8619561d" dependencies = [ "bitflags 2.11.0", "calloop 0.14.4", @@ -3310,6 +3463,7 @@ dependencies = [ "chrono", "iced", "iced_layershell", + "ppd", "tokio", "winit", "zbus", diff --git a/Cargo.toml b/Cargo.toml index 539476c..971025c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,8 @@ edition = "2024" [dependencies] chrono = "0.4.44" iced = { version = "0.14.0", default-features = false, features = ["wgpu", "wayland", "tokio"] } -iced_layershell = { version = "0.15.0", default-features = false } +iced_layershell = { version = "0.16.0", default-features = false } +ppd = "0.1.7" tokio = { version = "1.50.0", features = ["time"] } winit = { version = "0.30.12", default-features = false, features = ["wayland"] } zbus = "5.14.0" diff --git a/src/app.rs b/src/app.rs index 12a8041..f9d8e65 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,13 +1,14 @@ use crate::widget::{Message, PanelWidget}; use crate::widgets::battery::BatteryWidget; use crate::widgets::clock::ClockWidget; +use crate::widgets::power_management::PowerManagementWidget; use crate::widgets::powerbutton::ShutdownWidget; use crate::widgets::spacer::Spacer; -use iced::Color; -use iced::Element; use iced::Subscription; use iced::Task; +use iced::{Color, Theme}; +use iced::{Element, color}; pub struct App { widgets: Vec>, @@ -19,11 +20,11 @@ impl App { pub fn new() -> Self { Self { widgets: vec![ + Box::new(ShutdownWidget::new()), Box::new(ClockWidget::new()), Box::new(Spacer::new(iced::Length::Fill)), - Box::new(ShutdownWidget::new()), - Box::new(Spacer::new(iced::Length::Fill)), Box::new(BatteryWidget::new()), + Box::new(PowerManagementWidget::new()), ], } } @@ -36,16 +37,16 @@ impl App { if let Some(elem) = self .widgets .iter() - .find(|widget| widget.has_window(id)) - .map(|widget| widget.view(id)) + .find_map(|widget| widget.render_window(id)) { return elem; } - iced::widget::Row::with_children(self.widgets.iter().map(|widget| widget.view(id))) + iced::widget::Row::with_children(self.widgets.iter().filter_map(|widget| widget.view())) .padding(iced::Padding::from([0, 5])) .height(iced::Length::Fill) .width(iced::Length::Fill) + .spacing(5) .align_y(iced::Alignment::Center) .into() } @@ -67,7 +68,16 @@ impl App { } pub fn theme(&self, _id: iced::window::Id) -> iced::Theme { - iced::Theme::GruvboxDark + let palette = iced::theme::Palette { + background: color!(0x282828), // dark BG_0 + text: color!(0xfbf1c7), // dark FG0_29 + primary: color!(0x8A493B), // dark BLUE_4 + success: color!(0x98971a), // dark GREEN_2 + warning: color!(0xd79921), // dark YELLOW_3 + danger: color!(0xcc241d), // dark RED_1 + }; + + Theme::custom("Better gruvbox", palette) } pub fn subscription(&self) -> iced::Subscription { diff --git a/src/widget.rs b/src/widget.rs index 17beceb..491de69 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -3,14 +3,15 @@ use iced::Subscription; use iced::Task; use iced_layershell::to_layer_message; +use crate::widgets::power_management::PowerManagement; use crate::widgets::powerbutton::ShutdownEvents; pub trait PanelWidget { fn update(&mut self, message: &Message) -> Task; fn subscribe(&self) -> Subscription; - fn view(&self, id: iced::window::Id) -> Element<'_, Message>; - fn has_window(&self, _id: iced::window::Id) -> bool { - false + fn view(&self) -> Option>; + fn render_window(&self, _id: iced::window::Id) -> Option> { + None } } @@ -20,4 +21,5 @@ pub enum Message { Battery(Option), Time, ShutdownEvent(ShutdownEvents), + PowerManagement(PowerManagement), } diff --git a/src/widgets/battery.rs b/src/widgets/battery.rs index bfe8ded..0d9bfa5 100644 --- a/src/widgets/battery.rs +++ b/src/widgets/battery.rs @@ -65,12 +65,11 @@ impl PanelWidget for BatteryWidget { }) } - fn view(&self, _id: iced::window::Id) -> Element<'_, Message> { + fn view(&self) -> Option> { match self.capacity { - Some(cap) => text!("{} {}", cap, Self::icon(cap)), - None => text!("󰂃"), + Some(cap) => Some(text!("{} {}", cap, Self::icon(cap)).into()), + None => None, } - .into() } } diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index d29f7d7..638410c 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -28,8 +28,8 @@ impl PanelWidget for ClockWidget { iced::time::every(Duration::from_secs(1)).map(|_| Message::Time) } - fn view(&self, _id: iced::window::Id) -> iced::Element<'_, Message> { + fn view(&self) -> Option> { let formatted_time = self.current_time.format("%H:%M"); - iced::widget::text!("{}", formatted_time).into() + Some(iced::widget::text!("{}", formatted_time).into()) } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 22184f7..15c857e 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,4 +1,5 @@ pub mod battery; pub mod clock; +pub mod power_management; pub mod powerbutton; pub mod spacer; diff --git a/src/widgets/power_management.rs b/src/widgets/power_management.rs new file mode 100644 index 0000000..889c3a2 --- /dev/null +++ b/src/widgets/power_management.rs @@ -0,0 +1,138 @@ +use iced::{ + Subscription, Task, + widget::{Column, button, text}, +}; +use iced_layershell::reexport::{Anchor, NewLayerShellSettings}; +use ppd::{PpdProxyBlocking, Result}; +use zbus::blocking::Connection; + +use crate::widget::{Message, PanelWidget}; + +pub struct PowerManagementWidget<'a> { + connection: Option>, +} + +struct Connected<'a> { + window: Option, + current_mode: Box, + modes: Box<[Box]>, + proxy: PpdProxyBlocking<'a>, +} + +#[derive(Debug, Clone)] +pub enum PowerManagement { + ToggleWindow, + SetMode(String), +} + +impl PowerManagementWidget<'_> { + pub fn new() -> Self { + Self { + connection: Self::establish_connection().ok(), + } + } + + fn establish_connection<'a>() -> Result> { + let conn = Connection::system().unwrap(); + let proxy = PpdProxyBlocking::new(&conn)?; + + let modes: Box<[Box]> = proxy + .profiles()? + .iter() + .map(|f| f.profile.clone().into_boxed_str()) + .collect(); + + let current_mode = proxy.active_profile()?.into_boxed_str(); + + Ok(Connected { + current_mode, + modes, + proxy, + window: None, + }) + } +} + +impl PanelWidget for PowerManagementWidget<'_> { + fn update(&mut self, message: &crate::widget::Message) -> iced::Task { + let Some(conn) = &mut self.connection else { + return Task::none(); + }; + + let Message::PowerManagement(msg) = message else { + return Task::none(); + }; + match msg { + PowerManagement::ToggleWindow => match conn.window { + Some(child) => { + conn.window = None; + Task::done(Message::RemoveWindow(child)) + } + None => { + let id = iced::window::Id::unique(); + conn.window = Some(id); + + Task::done(Message::NewLayerShell { + settings: NewLayerShellSettings { + size: Some((220, 400)), + anchor: Anchor::Top, + layer: iced_layershell::reexport::Layer::Overlay, + keyboard_interactivity: + iced_layershell::reexport::KeyboardInteractivity::OnDemand, + exclusive_zone: None, + ..Default::default() + }, + id, + }) + } + }, + + PowerManagement::SetMode(mode) => { + let _ = conn.proxy.set_active_profile(mode.into()); + + conn.current_mode = conn.proxy.active_profile().unwrap().into_boxed_str(); + Task::none() + } + } + } + + fn subscribe(&self) -> iced::Subscription { + Subscription::none() + } + + fn view(&self) -> Option> { + let Some(conn) = &self.connection else { + return None; + }; + + let output = button(text!("{}", conn.current_mode)) + .on_press(Message::PowerManagement(PowerManagement::ToggleWindow)) + .into(); + + Some(output) + } + + fn render_window( + &self, + id: iced::window::Id, + ) -> Option> { + let Some(conn) = &self.connection else { + return None; + }; + + if conn.window != Some(id) { + return None; + } + + let output = Column::with_children(conn.modes.iter().map(|f| { + button(text!("{}", f)) + .on_press(Message::PowerManagement(PowerManagement::SetMode( + f.clone().into_string(), + ))) + .into() + })) + .into(); + + Some(output) + } +} diff --git a/src/widgets/powerbutton.rs b/src/widgets/powerbutton.rs index 841d5c3..ce29027 100644 --- a/src/widgets/powerbutton.rs +++ b/src/widgets/powerbutton.rs @@ -1,6 +1,6 @@ use crate::widget::{Message, PanelWidget}; use iced::{ - Subscription, Task, + Element, Subscription, Task, widget::{button, row, text}, }; use iced_layershell::reexport::{Anchor, NewLayerShellSettings}; @@ -67,24 +67,28 @@ impl PanelWidget for ShutdownWidget { Subscription::none() } - fn has_window(&self, id: iced::window::Id) -> bool { - self.window == Some(id) + fn render_window(&self, id: iced::window::Id) -> Option> { + if self.window != Some(id) { + return None; + } + + let output = row![ + text("Shut down?"), + button("✓").on_press(Message::ShutdownEvent(ShutdownEvents::ShutdownConfirmed)), + button("✗").on_press(Message::ShutdownEvent(ShutdownEvents::ShutdownCanceled)), + ] + .spacing(8) + .align_y(iced::Alignment::Center) + .into(); + + Some(output) } - fn view(&self, id: iced::window::Id) -> iced::Element<'_, crate::widget::Message> { - match self.window { - Some(child_id) if id == child_id => row![ - text("Shut down?"), - button("✓").on_press(Message::ShutdownEvent(ShutdownEvents::ShutdownConfirmed)), - button("✗").on_press(Message::ShutdownEvent(ShutdownEvents::ShutdownCanceled)), - ] - .spacing(8) - .align_y(iced::Alignment::Center) - .into(), + fn view(&self) -> Option> { + let output = button("⏻") + .on_press(Message::ShutdownEvent(ShutdownEvents::PowerButtonPressed)) + .into(); - _ => button("⏻") - .on_press(Message::ShutdownEvent(ShutdownEvents::PowerButtonPressed)) - .into(), - } + Some(output) } } diff --git a/src/widgets/spacer.rs b/src/widgets/spacer.rs index 68e720f..b0b9399 100644 --- a/src/widgets/spacer.rs +++ b/src/widgets/spacer.rs @@ -19,7 +19,7 @@ impl PanelWidget for Spacer { iced::Subscription::none() } - fn view(&self, _id: iced::window::Id) -> iced::Element<'_, crate::widget::Message> { - iced::widget::space().width(self.space).into() + fn view(&self) -> Option> { + Some(iced::widget::space().width(self.space).into()) } }