Merge pull request #6 from pop-os/status_jammy

Implement status icons
This commit is contained in:
Jeremy Soller 2024-01-17 13:04:19 -07:00 committed by GitHub
commit 8a5722aa1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 920 additions and 595 deletions

1164
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,17 @@ chrono = "0.4.31"
env_logger = "0.10.0" env_logger = "0.10.0"
freedesktop_entry_parser = "1.3.0" freedesktop_entry_parser = "1.3.0"
log = "0.4.20" log = "0.4.20"
logind-zbus = { version = "3.1.2", optional = true }
pam-client = "0.5.0" pam-client = "0.5.0"
pwd = "1.4.0" pwd = "1.4.0"
shlex = "1.2.0" shlex = "1.2.0"
wayland-client = "0.31.1" wayland-client = "0.31.1"
# For network status using networkmanager feature
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
# For logind integration using logind feature
logind-zbus = { version = "3.1.2", optional = true }
# For power status with upower feature
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
# Required for some features
zbus = { version = "3.14.1", optional = true } zbus = { version = "3.14.1", optional = true }
[dependencies.cosmic-bg-config] [dependencies.cosmic-bg-config]
@ -35,5 +41,7 @@ version = "1.33.0"
features = ["full"] features = ["full"]
[features] [features]
default = ["logind"] default = ["logind", "networkmanager", "upower"]
logind = ["logind-zbus", "zbus"] logind = ["logind-zbus", "zbus"]
networkmanager = ["cosmic-dbus-networkmanager", "zbus"]
upower = ["upower_dbus", "zbus"]

View file

@ -584,20 +584,24 @@ impl cosmic::Application for App {
match &self.prompt_opt { match &self.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt { Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => { Some(value) => {
let mut text_input = widget::text_input(&prompt, &value) let mut text_input =
.id(self.text_input_id.clone()) widget::text_input(prompt.clone(), value.clone())
.leading_icon( .id(self.text_input_id.clone())
widget::icon::from_name("system-lock-screen-symbolic") .leading_icon(
.into(), widget::icon::from_name("system-lock-screen-symbolic")
) .into(),
.trailing_icon( )
widget::icon::from_name("document-properties-symbolic") .trailing_icon(
.into(), widget::icon::from_name("document-properties-symbolic")
) .into(),
.on_input(|value| { )
Message::Prompt(prompt.clone(), *secret, Some(value)) .on_input(|value| {
}) Message::Prompt(prompt.clone(), *secret, Some(value))
.on_submit(Message::Auth(socket.clone(), Some(value.clone()))); })
.on_submit(Message::Auth(
socket.clone(),
Some(value.clone()),
));
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()

View file

@ -1,5 +1,5 @@
use cosmic::iced::widget::{ use cosmic::iced::widget::{
image::{draw, Handle}, image::{draw, FilterMethod, Handle},
Container, Container,
}; };
use cosmic::iced::ContentFit; use cosmic::iced::ContentFit;
@ -69,8 +69,13 @@ where
Widget::height(&self.container) Widget::height(&self.container)
} }
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { fn layout(
self.container.layout(renderer, limits) &self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.container.layout(tree, renderer, limits)
} }
fn operate( fn operate(
@ -127,6 +132,7 @@ where
layout, layout,
image, image,
self.content_fit, self.content_fit,
FilterMethod::Linear,
[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0],
), ),
None => {} None => {}

View file

@ -6,7 +6,10 @@ use cosmic::{
executor, executor,
iced::{ iced::{
self, alignment, self, alignment,
event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, event::{
self,
wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent},
},
futures::{self, SinkExt}, futures::{self, SinkExt},
subscription, subscription,
wayland::session_lock::{destroy_lock_surface, get_lock_surface, lock, unlock}, wayland::session_lock::{destroy_lock_surface, get_lock_surface, lock, unlock},
@ -22,6 +25,7 @@ use std::{
ffi::{CStr, CString}, ffi::{CStr, CString},
fs, fs,
path::Path, path::Path,
process,
}; };
use tokio::{sync::mpsc, task, time}; use tokio::{sync::mpsc, task, time};
use wayland_client::{protocol::wl_output::WlOutput, Proxy}; use wayland_client::{protocol::wl_output::WlOutput, Proxy};
@ -87,11 +91,6 @@ pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pa
Ok(()) Ok(())
} }
fn text_input_id(surface_id: SurfaceId) -> widget::Id {
//TODO: store this in a map?
widget::Id(iced::id::Internal::Unique(surface_id.0 as u64))
}
pub struct Conversation { pub struct Conversation {
msg_tx: futures::channel::mpsc::Sender<Message>, msg_tx: futures::channel::mpsc::Sender<Message>,
value_rx: mpsc::Receiver<String>, value_rx: mpsc::Receiver<String>,
@ -196,6 +195,8 @@ pub enum Message {
SessionLockEvent(SessionLockEvent), SessionLockEvent(SessionLockEvent),
Channel(mpsc::Sender<String>), Channel(mpsc::Sender<String>),
BackgroundState(cosmic_bg_config::state::State), BackgroundState(cosmic_bg_config::state::State),
NetworkIcon(Option<&'static str>),
PowerInfo(Option<(String, f64)>),
Prompt(String, bool, Option<String>), Prompt(String, bool, Option<String>),
Submit, Submit,
Suspend, Suspend,
@ -207,15 +208,16 @@ pub enum Message {
pub struct App { pub struct App {
core: Core, core: Core,
flags: Flags, flags: Flags,
next_surface_id: SurfaceId,
surface_ids: HashMap<WlOutput, SurfaceId>, surface_ids: HashMap<WlOutput, SurfaceId>,
active_surface_id_opt: Option<SurfaceId>, active_surface_id_opt: Option<SurfaceId>,
surface_images: HashMap<SurfaceId, widget::image::Handle>, surface_images: HashMap<SurfaceId, widget::image::Handle>,
surface_names: HashMap<SurfaceId, String>, surface_names: HashMap<SurfaceId, String>,
text_input_ids: HashMap<SurfaceId, widget::Id>,
network_icon_opt: Option<&'static str>,
power_info_opt: Option<(String, f64)>,
value_tx_opt: Option<mpsc::Sender<String>>, value_tx_opt: Option<mpsc::Sender<String>>,
prompt_opt: Option<(String, bool, Option<String>)>, prompt_opt: Option<(String, bool, Option<String>)>,
error_opt: Option<String>, error_opt: Option<String>,
exited: bool,
} }
impl App { impl App {
@ -300,15 +302,16 @@ impl cosmic::Application for App {
App { App {
core, core,
flags, flags,
next_surface_id: SurfaceId(1),
surface_ids: HashMap::new(), surface_ids: HashMap::new(),
active_surface_id_opt: None, active_surface_id_opt: None,
surface_images: HashMap::new(), surface_images: HashMap::new(),
surface_names: HashMap::new(), surface_names: HashMap::new(),
text_input_ids: HashMap::new(),
network_icon_opt: None,
power_info_opt: None,
value_tx_opt: None, value_tx_opt: None,
prompt_opt: None, prompt_opt: None,
error_opt: None, error_opt: None,
exited: false,
}, },
lock(), lock(),
) )
@ -323,16 +326,14 @@ impl cosmic::Application for App {
OutputEvent::Created(output_info_opt) => { OutputEvent::Created(output_info_opt) => {
log::info!("output {}: created", output.id()); log::info!("output {}: created", output.id());
let surface_id = self.next_surface_id; let surface_id = SurfaceId::unique();
self.next_surface_id.0 += 1;
match self.surface_ids.insert(output.clone(), surface_id) { match self.surface_ids.insert(output.clone(), surface_id) {
Some(old_surface_id) => { Some(old_surface_id) => {
//TODO: remove old surface? //TODO: remove old surface?
log::warn!( log::warn!(
"output {}: already had surface ID {}", "output {}: already had surface ID {:?}",
output.id(), output.id(),
old_surface_id.0 old_surface_id
); );
} }
None => {} None => {}
@ -354,9 +355,13 @@ impl cosmic::Application for App {
} }
} }
let text_input_id = widget::Id::unique();
self.text_input_ids
.insert(surface_id, text_input_id.clone());
return Command::batch([ return Command::batch([
get_lock_surface(surface_id, output), get_lock_surface(surface_id, output),
widget::text_input::focus(text_input_id(surface_id)), widget::text_input::focus(text_input_id),
]); ]);
} }
OutputEvent::Removed => { OutputEvent::Removed => {
@ -365,6 +370,7 @@ impl cosmic::Application for App {
Some(surface_id) => { Some(surface_id) => {
self.surface_images.remove(&surface_id); self.surface_images.remove(&surface_id);
self.surface_names.remove(&surface_id); self.surface_names.remove(&surface_id);
self.text_input_ids.remove(&surface_id);
return destroy_lock_surface(surface_id); return destroy_lock_surface(surface_id);
} }
None => { None => {
@ -379,12 +385,15 @@ impl cosmic::Application for App {
} }
Message::SessionLockEvent(session_lock_event) => match session_lock_event { Message::SessionLockEvent(session_lock_event) => match session_lock_event {
SessionLockEvent::Focused(_, surface_id) => { SessionLockEvent::Focused(_, surface_id) => {
log::info!("focus surface {}", surface_id.0); log::info!("focus surface {:?}", surface_id);
self.active_surface_id_opt = Some(surface_id); self.active_surface_id_opt = Some(surface_id);
return widget::text_input::focus(text_input_id(surface_id)); if let Some(text_input_id) = self.text_input_ids.get(&surface_id) {
return widget::text_input::focus(text_input_id.clone());
}
} }
SessionLockEvent::Unlocked => { SessionLockEvent::Unlocked => {
self.exited = true; //TODO: cleaner method to exit?
process::exit(0);
} }
_ => {} _ => {}
}, },
@ -396,12 +405,20 @@ impl cosmic::Application for App {
self.surface_images.clear(); self.surface_images.clear();
self.update_wallpapers(); self.update_wallpapers();
} }
Message::NetworkIcon(network_icon_opt) => {
self.network_icon_opt = network_icon_opt;
}
Message::PowerInfo(power_info_opt) => {
self.power_info_opt = power_info_opt;
}
Message::Prompt(prompt, secret, value_opt) => { Message::Prompt(prompt, secret, value_opt) => {
let prompt_was_none = self.prompt_opt.is_none(); let prompt_was_none = self.prompt_opt.is_none();
self.prompt_opt = Some((prompt, secret, value_opt)); self.prompt_opt = Some((prompt, secret, value_opt));
if prompt_was_none { if prompt_was_none {
if let Some(surface_id) = self.active_surface_id_opt { if let Some(surface_id) = self.active_surface_id_opt {
return widget::text_input::focus(text_input_id(surface_id)); if let Some(text_input_id) = self.text_input_ids.get(&surface_id) {
return widget::text_input::focus(text_input_id.clone());
}
} }
} }
} }
@ -446,6 +463,7 @@ impl cosmic::Application for App {
for (_output, surface_id) in self.surface_ids.drain() { for (_output, surface_id) in self.surface_ids.drain() {
self.surface_images.remove(&surface_id); self.surface_images.remove(&surface_id);
self.surface_names.remove(&surface_id); self.surface_names.remove(&surface_id);
self.text_input_ids.remove(&surface_id);
commands.push(destroy_lock_surface(surface_id)); commands.push(destroy_lock_surface(surface_id));
} }
commands.push(unlock()); commands.push(unlock());
@ -456,10 +474,6 @@ impl cosmic::Application for App {
Command::none() Command::none()
} }
fn should_exit(&self) -> bool {
self.exited
}
// Not used for layer surface window // Not used for layer surface window
fn view(&self) -> Element<Self::Message> { fn view(&self) -> Element<Self::Message> {
unimplemented!() unimplemented!()
@ -489,16 +503,18 @@ impl cosmic::Application for App {
column column
}; };
//TODO: get actual status let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0);
let status_row = iced::widget::row![
widget::icon::from_name("network-wireless-signal-ok-symbolic",), if let Some(network_icon) = self.network_icon_opt {
iced::widget::row![ status_row = status_row.push(widget::icon::from_name(network_icon));
widget::icon::from_name("battery-level-50-symbolic"), }
widget::text("50%"),
] if let Some((power_icon, power_percent)) = &self.power_info_opt {
] status_row = status_row.push(iced::widget::row![
.padding(16.0) widget::icon::from_name(power_icon.clone()),
.spacing(12.0); widget::text(format!("{:.0}%", power_percent)),
]);
}
//TODO: implement these buttons //TODO: implement these buttons
let button_row = iced::widget::row![ let button_row = iced::widget::row![
@ -565,8 +581,7 @@ impl cosmic::Application for App {
match &self.prompt_opt { match &self.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt { Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => { Some(value) => {
let mut text_input = widget::text_input(&prompt, &value) let mut text_input = widget::text_input(prompt.clone(), value.clone())
.id(text_input_id(surface_id))
.leading_icon( .leading_icon(
widget::icon::from_name("system-lock-screen-symbolic").into(), widget::icon::from_name("system-lock-screen-symbolic").into(),
) )
@ -576,6 +591,10 @@ impl cosmic::Application for App {
.on_input(|value| Message::Prompt(prompt.clone(), *secret, Some(value))) .on_input(|value| Message::Prompt(prompt.clone(), *secret, Some(value)))
.on_submit(Message::Submit); .on_submit(Message::Submit);
if let Some(text_input_id) = self.text_input_ids.get(&surface_id) {
text_input = text_input.id(text_input_id.clone());
}
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()
} }
@ -636,18 +655,31 @@ impl cosmic::Application for App {
} }
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {
if self.exited {
return Subscription::none();
}
struct BackgroundSubscription; struct BackgroundSubscription;
struct HeartbeatSubscription; struct HeartbeatSubscription;
struct PamSubscription; struct PamSubscription;
//TODO: just use one vec for all subscriptions
let mut extra_suscriptions = Vec::with_capacity(1);
#[cfg(feature = "networkmanager")]
{
extra_suscriptions.push(
crate::networkmanager::subscription()
.map(|icon_opt| Message::NetworkIcon(icon_opt)),
);
}
#[cfg(feature = "upower")]
{
extra_suscriptions
.push(crate::upower::subscription().map(|info_opt| Message::PowerInfo(info_opt)));
}
//TODO: how to avoid cloning this on every time subscription is called? //TODO: how to avoid cloning this on every time subscription is called?
let username = self.flags.current_user.name.clone(); let username = self.flags.current_user.name.clone();
Subscription::batch([ Subscription::batch([
subscription::events_with(|event, _| match event { event::listen_with(|event, _| match event {
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
wayland_event, wayland_event,
)) => match wayland_event { )) => match wayland_event {
@ -708,18 +740,18 @@ impl cosmic::Application for App {
break; break;
} }
Err(err) => { Err(err) => {
log::info!("authentication error: {:?}", err); log::warn!("authentication error: {}", err);
msg_tx.send(Message::Error(err.to_string())).await.unwrap(); msg_tx.send(Message::Error(err.to_string())).await.unwrap();
} }
} }
} }
//TODO: how to properly kill this task?
loop { loop {
time::sleep(time::Duration::new(1, 0)).await; time::sleep(time::Duration::new(60, 0)).await;
} }
}, },
), ),
Subscription::batch(extra_suscriptions),
]) ])
} }
} }

View file

@ -8,6 +8,12 @@ mod locker;
#[cfg(feature = "logind")] #[cfg(feature = "logind")]
mod logind; mod logind;
#[cfg(feature = "networkmanager")]
mod networkmanager;
#[cfg(feature = "upower")]
mod upower;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();

109
src/networkmanager.rs Normal file
View file

@ -0,0 +1,109 @@
use cosmic::iced::{
futures::{channel::mpsc, SinkExt, StreamExt},
subscription, Subscription,
};
use cosmic_dbus_networkmanager::{device::SpecificDevice, nm::NetworkManager};
use std::{any::TypeId, cmp};
use tokio::time;
use zbus::{Connection, Result};
#[derive(Clone, Copy, Debug)]
pub enum NetworkIcon {
None,
Wired,
Wireless(u8),
}
impl NetworkIcon {
pub fn name(&self) -> &'static str {
match self {
NetworkIcon::None => "network-wired-disconnected-symbolic",
NetworkIcon::Wired => "network-wired-symbolic",
NetworkIcon::Wireless(strength) => {
if *strength < 25 {
"network-wireless-signal-weak-symbolic"
} else if *strength < 50 {
"network-wireless-signal-ok-symbolic"
} else if *strength < 75 {
"network-wireless-signal-good-symbolic"
} else {
"network-wireless-signal-excellent-symbolic"
}
}
}
}
}
pub fn subscription() -> Subscription<Option<&'static str>> {
struct NetworkSubscription;
subscription::channel(
TypeId::of::<NetworkSubscription>(),
16,
|mut msg_tx| async move {
match handler(&mut msg_tx).await {
Ok(()) => {}
Err(err) => {
log::warn!("networkmanager error: {}", err);
//TODO: send error
}
}
// If reading network status failed, clear network icon
msg_tx.send(None).await.unwrap();
//TODO: should we retry on error?
loop {
time::sleep(time::Duration::new(60, 0)).await;
}
},
)
}
//TODO: use never type?
pub async fn handler(msg_tx: &mut mpsc::Sender<Option<&'static str>>) -> Result<()> {
let zbus = Connection::system().await?;
let nm = NetworkManager::new(&zbus).await?;
let mut active_conns_changed = nm.receive_active_connections_changed().await;
loop {
let mut icon = NetworkIcon::None;
for conn in nm.active_connections().await.unwrap_or_default() {
for dev in conn.devices().await.unwrap_or_default() {
match dev.downcast_to_device().await.unwrap_or_default() {
//TODO: more specific devices
Some(SpecificDevice::Wired(_)) => {
// Wired only overrides None
icon = match icon {
NetworkIcon::None => NetworkIcon::Wired,
other => other,
};
}
Some(SpecificDevice::Wireless(wireless)) => {
if let Ok(ap) = wireless.active_access_point().await {
if let Ok(strength) = ap.strength().await {
// Wireless always overrides with the highest strength
icon = match icon {
NetworkIcon::Wireless(other_strength) => {
NetworkIcon::Wireless(cmp::max(strength, other_strength))
}
_ => NetworkIcon::Wireless(strength),
};
}
}
}
_ => {}
}
}
}
msg_tx.send(Some(icon.name())).await.unwrap();
// Waits until active connections have changed and at least one second has passed
tokio::join!(
active_conns_changed.next(),
time::sleep(time::Duration::from_secs(1))
);
}
}

60
src/upower.rs Normal file
View file

@ -0,0 +1,60 @@
use cosmic::iced::{
futures::{channel::mpsc, SinkExt, StreamExt},
subscription, Subscription,
};
use std::any::TypeId;
use tokio::time;
use upower_dbus::UPowerProxy;
use zbus::{Connection, Result};
pub fn subscription() -> Subscription<Option<(String, f64)>> {
struct PowerSubscription;
subscription::channel(
TypeId::of::<PowerSubscription>(),
16,
|mut msg_tx| async move {
match handler(&mut msg_tx).await {
Ok(()) => {}
Err(err) => {
log::warn!("upower error: {}", err);
//TODO: send error
}
}
// If reading power status failed, clear power icon
msg_tx.send(None).await.unwrap();
//TODO: should we retry on error?
loop {
time::sleep(time::Duration::new(60, 0)).await;
}
},
)
}
//TODO: use never type?
pub async fn handler(msg_tx: &mut mpsc::Sender<Option<(String, f64)>>) -> Result<()> {
let zbus = Connection::system().await?;
let upower = UPowerProxy::new(&zbus).await?;
let dev = upower.get_display_device().await?;
let mut icon_name_changed = dev.receive_icon_name_changed().await;
let mut percentage_changed = dev.receive_percentage_changed().await;
loop {
let mut info_opt = None;
if let Ok(percent) = dev.percentage().await {
if let Ok(icon_name) = dev.icon_name().await {
if !icon_name.is_empty() {
info_opt = Some((icon_name, percent));
}
}
}
msg_tx.send(info_opt).await.unwrap();
// Waits until icon or percentage have changed
tokio::select!(_ = icon_name_changed.next() => (), _ = percentage_changed.next() => ());
}
}