cosmic-greeter/src/locker.rs

1075 lines
42 KiB
Rust
Raw Normal View History

2023-10-05 17:47:23 -06:00
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
2025-03-12 16:07:57 -04:00
use cosmic::app::{Core, Settings, Task};
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
use cosmic::iced::{Point, Rectangle, Size};
use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
2025-03-05 23:08:56 -05:00
use cosmic::surface;
2023-10-05 17:47:23 -06:00
use cosmic::{
Element, executor,
2023-10-05 17:47:23 -06:00
iced::{
self, Length, Subscription, alignment,
2024-01-17 09:37:15 -07:00
event::{
self,
wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent},
},
2023-10-05 17:47:23 -06:00
futures::{self, SinkExt},
platform_specific::shell::wayland::commands::session_lock::{
destroy_lock_surface, get_lock_surface, lock, unlock,
},
2023-10-05 17:47:23 -06:00
},
2023-10-06 10:44:05 -06:00
iced_runtime::core::window::Id as SurfaceId,
2025-04-10 16:39:32 -04:00
widget,
2023-10-05 17:47:23 -06:00
};
2025-05-09 14:16:12 -06:00
use cosmic_config::CosmicConfigEntry;
use cosmic_greeter_daemon::{BgSource, TimeAppletConfig, UserData};
use std::time::Duration;
2023-10-06 10:44:05 -06:00
use std::{
any::TypeId,
2023-10-06 10:44:05 -06:00
collections::HashMap,
2024-11-07 09:37:16 -07:00
env,
2023-10-06 10:44:05 -06:00
ffi::{CStr, CString},
fs,
2024-04-05 19:02:20 -06:00
os::fd::OwnedFd,
path::PathBuf,
2024-01-17 12:37:22 -07:00
process,
2024-04-05 19:02:20 -06:00
sync::Arc,
2023-10-06 10:44:05 -06:00
};
use tokio::{sync::mpsc, task};
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
2023-10-05 17:47:23 -06:00
2024-11-07 09:37:16 -07:00
fn lockfile_opt() -> Option<PathBuf> {
let runtime_dir = dirs::runtime_dir()?;
let session_id = env::var("XDG_SESSION_ID").ok()?;
2024-11-07 09:37:16 -07:00
Some(runtime_dir.join(format!("cosmic-greeter-{}.lock", session_id)))
}
pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
2024-05-07 10:07:48 -06:00
crate::localize::localize();
let mut user_data = UserData::from(user);
// We are already the user at this point
user_data.load_config_as_user();
2023-10-06 10:44:05 -06:00
2025-03-12 16:07:57 -04:00
let fallback_background =
widget::image::Handle::from_bytes(include_bytes!("../res/background.jpg").as_slice());
2023-10-06 10:44:05 -06:00
let flags = Flags {
user_data,
2024-11-07 09:37:16 -07:00
lockfile_opt: lockfile_opt(),
2025-03-12 16:07:57 -04:00
fallback_background,
2023-10-06 10:44:05 -06:00
};
2023-10-05 17:47:23 -06:00
let settings = Settings::default().no_main_window(true);
2023-10-05 17:47:23 -06:00
cosmic::app::run::<App>(settings, flags)?;
Ok(())
}
pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pam_client::Error> {
//TODO: send errors to GUI, restart process
// Create PAM context
let mut context = pam_client::Context::new("cosmic-greeter", Some(&username), conversation)?;
2023-10-05 17:47:23 -06:00
// Authenticate the user (ask for password, 2nd-factor token, fingerprint, etc.)
log::info!("authenticate");
context.authenticate(pam_client::Flag::NONE)?;
// Validate the account (is not locked, expired, etc.)
log::info!("acct_mgmt");
context.acct_mgmt(pam_client::Flag::NONE)?;
Ok(())
}
pub struct Conversation {
msg_tx: futures::channel::mpsc::Sender<cosmic::Action<Message>>,
2023-10-05 17:47:23 -06:00
value_rx: mpsc::Receiver<String>,
}
impl Conversation {
fn prompt_value(
&mut self,
prompt_c: &CStr,
secret: bool,
) -> Result<CString, pam_client::ErrorCode> {
let prompt = prompt_c.to_str().map_err(|err| {
log::error!("failed to convert prompt to UTF-8: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})?;
futures::executor::block_on(async {
self.msg_tx
.send(cosmic::Action::App(Message::Prompt(
prompt.to_string(),
secret,
Some(String::new()),
)))
.await
})
.map_err(|err| {
log::error!("failed to send prompt: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})?;
2023-10-05 17:47:23 -06:00
let value = self.value_rx.blocking_recv().ok_or_else(|| {
log::error!("failed to receive value: channel closed");
pam_client::ErrorCode::CONV_ERR
})?;
CString::new(value).map_err(|err| {
log::error!("failed to convert value to C string: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})
}
fn message(&mut self, prompt_c: &CStr) -> Result<(), pam_client::ErrorCode> {
let prompt = prompt_c.to_str().map_err(|err| {
log::error!("failed to convert prompt to UTF-8: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})?;
futures::executor::block_on(async {
self.msg_tx
.send(cosmic::Action::App(Message::Prompt(
prompt.to_string(),
false,
None,
)))
.await
})
.map_err(|err| {
log::error!("failed to send prompt: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})
}
2023-10-05 17:47:23 -06:00
}
impl pam_client::ConversationHandler for Conversation {
fn prompt_echo_on(&mut self, prompt_c: &CStr) -> Result<CString, pam_client::ErrorCode> {
log::info!("prompt_echo_on {:?}", prompt_c);
self.prompt_value(prompt_c, false)
}
fn prompt_echo_off(&mut self, prompt_c: &CStr) -> Result<CString, pam_client::ErrorCode> {
log::info!("prompt_echo_off {:?}", prompt_c);
self.prompt_value(prompt_c, true)
}
fn text_info(&mut self, prompt_c: &CStr) {
log::info!("text_info {:?}", prompt_c);
match self.message(prompt_c) {
Ok(()) => (),
Err(err) => {
log::warn!("failed to send text_info: {:?}", err);
}
}
2023-10-05 17:47:23 -06:00
}
fn error_msg(&mut self, prompt_c: &CStr) {
//TODO: treat error type differently?
log::info!("error_msg {:?}", prompt_c);
match self.message(prompt_c) {
Ok(()) => (),
Err(err) => {
log::warn!("failed to send error_msg: {:?}", err);
}
}
2023-10-05 17:47:23 -06:00
}
}
#[derive(Clone)]
pub struct Flags {
user_data: UserData,
2024-11-07 09:37:16 -07:00
lockfile_opt: Option<PathBuf>,
2025-03-12 16:07:57 -04:00
fallback_background: widget::image::Handle,
2023-10-05 17:47:23 -06:00
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
2023-10-06 13:07:53 -06:00
None,
2023-10-06 10:44:05 -06:00
OutputEvent(OutputEvent, WlOutput),
2023-10-26 15:10:09 -07:00
SessionLockEvent(SessionLockEvent),
2023-10-05 17:47:23 -06:00
Channel(mpsc::Sender<String>),
BackgroundState(cosmic_bg_config::state::State),
2025-05-09 14:16:12 -06:00
TimeAppletConfig(TimeAppletConfig),
2025-03-12 16:07:57 -04:00
Focus(SurfaceId),
2024-04-05 19:02:20 -06:00
Inhibit(Arc<OwnedFd>),
2024-01-17 11:18:58 -07:00
NetworkIcon(Option<&'static str>),
2024-01-17 12:37:22 -07:00
PowerInfo(Option<(String, f64)>),
Prompt(String, bool, Option<String>),
2025-03-12 16:07:57 -04:00
Submit(String),
2025-03-05 23:08:56 -05:00
Surface(surface::Action),
2023-11-29 08:02:14 -07:00
Suspend,
2023-10-05 17:47:23 -06:00
Error(String),
2024-04-05 14:08:40 -06:00
Lock,
2025-04-10 16:39:32 -04:00
Tick,
Tz(chrono_tz::Tz),
2024-04-05 11:31:15 -06:00
Unlock,
2023-10-05 17:47:23 -06:00
}
2024-04-05 14:08:40 -06:00
#[derive(Clone, Debug)]
enum State {
Locking,
Locked {
task_handle: cosmic::iced::task::Handle,
},
2024-04-05 14:08:40 -06:00
Unlocking,
Unlocked,
}
impl Drop for State {
fn drop(&mut self) {
// Abort the locked task when the state is changed.
if let Self::Locked { task_handle } = self {
log::info!("dropping lockscreen tasks");
task_handle.abort();
}
}
}
2023-10-05 17:47:23 -06:00
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
flags: Flags,
2024-04-05 14:08:40 -06:00
state: State,
2025-03-12 16:07:57 -04:00
output_names: HashMap<WlOutput, String>,
2023-10-06 10:44:05 -06:00
surface_ids: HashMap<WlOutput, SurfaceId>,
2025-03-12 16:07:57 -04:00
subsurface_rects: HashMap<WlOutput, Rectangle>,
active_surface_id_opt: Option<SurfaceId>,
2023-10-09 12:31:13 -06:00
surface_images: HashMap<SurfaceId, widget::image::Handle>,
surface_names: HashMap<SurfaceId, String>,
2025-03-12 16:07:57 -04:00
text_input_ids: HashMap<String, widget::Id>,
2024-04-05 19:02:20 -06:00
inhibit_opt: Option<Arc<OwnedFd>>,
2024-01-17 11:18:58 -07:00
network_icon_opt: Option<&'static str>,
2024-01-17 12:37:22 -07:00
power_info_opt: Option<(String, f64)>,
2023-10-05 17:47:23 -06:00
value_tx_opt: Option<mpsc::Sender<String>>,
prompt_opt: Option<(String, bool, Option<String>)>,
2023-10-05 17:47:23 -06:00
error_opt: Option<String>,
2025-04-10 16:39:32 -04:00
time: crate::time::Time,
window_size: HashMap<SurfaceId, Size>,
2023-10-05 17:47:23 -06:00
}
impl App {
fn menu(&self, surface_id: SurfaceId) -> Element<Message> {
let window_width = self
.window_size
.get(&surface_id)
.map(|s| s.width)
.unwrap_or(800.);
let menu_width = if window_width > 800. {
800.
} else {
window_width
};
2025-03-12 16:07:57 -04:00
let left_element = {
2025-05-09 14:16:12 -06:00
let military_time = self.flags.user_data.time_applet_config.military_time;
2025-04-10 16:39:32 -04:00
let date_time_column = self.time.date_time_widget(military_time);
2025-03-12 16:07:57 -04:00
let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0);
if let Some(network_icon) = self.network_icon_opt {
status_row = status_row.push(widget::icon::from_name(network_icon));
}
if let Some((power_icon, power_percent)) = &self.power_info_opt {
status_row = status_row.push(iced::widget::row![
widget::icon::from_name(power_icon.clone()),
widget::text(format!("{:.0}%", power_percent)),
]);
}
//TODO: implement these buttons
let button_row = iced::widget::row![
widget::button::custom(widget::icon::from_name(
"applications-accessibility-symbolic"
))
.padding(12.0)
.on_press(Message::None),
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button::custom(widget::icon::from_name("system-users-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button::custom(widget::icon::from_name("system-suspend-symbolic"))
.padding(12.0)
.on_press(Message::Suspend),
]
.padding([16.0, 0.0, 0.0, 0.0])
.spacing(8.0);
widget::container(iced::widget::column![
date_time_column,
widget::divider::horizontal::default().width(Length::Fixed(menu_width / 2. - 16.)),
2025-03-12 16:07:57 -04:00
status_row,
widget::divider::horizontal::default().width(Length::Fixed(menu_width / 2. - 16.)),
2025-03-12 16:07:57 -04:00
button_row,
])
.align_x(alignment::Horizontal::Left)
};
let right_element = {
let mut column = widget::column::with_capacity(2)
.spacing(12.0)
.max_width(280.0);
match &self.flags.user_data.icon_opt {
2025-03-12 16:07:57 -04:00
Some(icon) => {
column = column.push(
widget::container(
//TODO: cache image handle?
widget::Image::new(widget::image::Handle::from_bytes(icon.clone()))
2025-03-12 16:07:57 -04:00
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
}
None => {}
}
column = column.push(
2025-05-09 14:16:12 -06:00
widget::container(widget::text::title4(&self.flags.user_data.full_name))
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
);
2025-03-12 16:07:57 -04:00
match &self.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => {
let text_input_id = self
.surface_names
.get(&surface_id)
.and_then(|id| self.text_input_ids.get(id))
.cloned()
.unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
let mut text_input = widget::secure_input(
prompt.clone(),
"",
Some(Message::Prompt(
prompt.clone(),
!*secret,
Some(value.clone()),
)),
*secret,
)
.id(text_input_id)
.manage_value(true)
.on_submit(Message::Submit);
2025-03-12 16:07:57 -04:00
if *secret {
text_input = text_input.password()
}
column = column.push(text_input);
}
None => {
column = column.push(widget::text(prompt));
}
},
None => {}
}
if let Some(error) = &self.error_opt {
column = column.push(widget::text(error));
}
widget::container(column)
.align_x(alignment::Horizontal::Center)
.width(Length::Fill)
};
widget::container(
widget::layer_container(
iced::widget::row![left_element, right_element]
.align_y(alignment::Alignment::Center),
)
.layer(cosmic::cosmic_theme::Layer::Background)
.padding(16)
.class(cosmic::theme::Container::Custom(Box::new(
|theme: &cosmic::Theme| {
// Use background appearance as the base
let mut appearance = widget::container::Catalog::style(
theme,
&cosmic::theme::Container::Background,
);
appearance.border = iced::Border::default().rounded(16.0);
appearance
},
)))
.width(Length::Fill)
.height(Length::Shrink),
)
.padding([32.0, 0.0, 0.0, 0.0])
.width(Length::Fill)
.height(Length::Fill)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Top)
.class(cosmic::theme::Container::Transparent)
.into()
}
//TODO: cache wallpapers by source?
fn update_wallpapers(&mut self) {
2025-05-09 14:16:12 -06:00
let user_data = &self.flags.user_data;
for (_output, surface_id) in self.surface_ids.iter() {
if self.surface_images.contains_key(surface_id) {
continue;
}
let Some(output_name) = self.surface_names.get(surface_id) else {
continue;
};
log::info!("updating wallpaper for {:?}", output_name);
2025-05-09 14:16:12 -06:00
for (wallpaper_output_name, wallpaper_source) in user_data.bg_state.wallpapers.iter() {
if wallpaper_output_name == output_name {
2025-05-09 14:16:12 -06:00
match wallpaper_source {
BgSource::Path(path) => {
match user_data.bg_path_data.get(path) {
Some(bytes) => {
let image = widget::image::Handle::from_bytes(bytes.clone());
self.surface_images.insert(*surface_id, image);
//TODO: what to do about duplicates?
}
None => {
log::warn!(
"output {}: failed to find wallpaper data for source {:?}",
output_name,
path
);
}
}
break;
}
2025-05-09 14:16:12 -06:00
BgSource::Color(color) => {
//TODO: support color sources
2025-05-09 14:16:12 -06:00
log::warn!("output {}: unsupported source {:?}", output_name, color);
}
}
}
}
}
}
}
2023-10-05 17:47:23 -06:00
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl cosmic::Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = Flags;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "com.system76.CosmicGreeter";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits command on initialize.
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
2023-10-05 17:47:23 -06:00
core.window.show_window_menu = false;
core.window.show_headerbar = false;
2025-03-12 16:07:57 -04:00
// XXX must be false or define custom style to have transparent bg
core.window.sharp_corners = false;
2023-10-05 17:47:23 -06:00
core.window.show_maximize = false;
core.window.show_minimize = false;
core.window.use_template = false;
2024-11-07 09:37:16 -07:00
let already_locked = match flags.lockfile_opt {
Some(ref lockfile) => lockfile.exists(),
None => false,
};
let mut app = App {
core,
flags,
state: State::Unlocked,
surface_ids: HashMap::new(),
active_surface_id_opt: None,
2025-03-12 16:07:57 -04:00
output_names: HashMap::new(),
2024-11-07 09:37:16 -07:00
surface_images: HashMap::new(),
surface_names: HashMap::new(),
text_input_ids: HashMap::new(),
2025-03-12 16:07:57 -04:00
subsurface_rects: HashMap::new(),
2024-11-07 09:37:16 -07:00
inhibit_opt: None,
network_icon_opt: None,
power_info_opt: None,
value_tx_opt: None,
prompt_opt: None,
error_opt: None,
2025-04-10 16:39:32 -04:00
time: crate::time::Time::new(),
window_size: HashMap::new(),
2024-11-07 09:37:16 -07:00
};
2025-04-10 16:39:32 -04:00
let task = if cfg!(feature = "logind") {
2024-11-07 09:37:16 -07:00
if already_locked {
// Recover previously locked state
log::info!("recovering previous locked state");
app.state = State::Locking;
lock()
} else {
2024-04-05 14:08:40 -06:00
// When logind feature is used, wait for lock signal
Task::none()
2024-11-07 09:37:16 -07:00
}
} else {
// When logind feature not used, lock immediately
log::info!("locking immediately");
app.state = State::Locking;
lock()
};
2025-04-10 16:39:32 -04:00
(
app,
Task::batch(vec![
task,
crate::time::tick().map(|_| cosmic::Action::App(Message::Tick)),
crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz))),
]),
)
2023-10-05 17:47:23 -06:00
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
2023-10-05 17:47:23 -06:00
match message {
2023-10-06 13:07:53 -06:00
Message::None => {}
2023-10-09 12:31:13 -06:00
Message::OutputEvent(output_event, output) => {
match output_event {
OutputEvent::Created(output_info_opt) => {
log::info!("output {}: created", output.id());
2024-01-17 09:37:15 -07:00
let surface_id = SurfaceId::unique();
2025-03-12 16:07:57 -04:00
let subsurface_id = SurfaceId::unique();
if let Some(old_surface_id) =
self.surface_ids.insert(output.clone(), surface_id)
{
//TODO: remove old surface?
log::warn!(
"output {}: already had surface ID {:?}",
output.id(),
old_surface_id
);
return Task::none();
2023-10-06 10:44:05 -06:00
}
2025-03-12 16:07:57 -04:00
let size = if let Some((w, h)) =
output_info_opt.as_ref().and_then(|info| info.logical_size)
{
Some((Some(w as u32), Some(h as u32)))
} else {
Some((None, None))
};
2023-10-09 12:31:13 -06:00
match output_info_opt {
Some(output_info) => match output_info.name {
Some(output_name) => {
2025-03-12 16:07:57 -04:00
self.output_names
.insert(output.clone(), output_name.clone());
self.surface_names.insert(surface_id, output_name.clone());
2025-03-12 16:07:57 -04:00
self.surface_names
.insert(subsurface_id, output_name.clone());
self.surface_images.remove(&surface_id);
self.update_wallpapers();
2025-03-12 16:07:57 -04:00
let text_input_id =
widget::Id::new(format!("input-{output_name}",));
self.text_input_ids
.insert(output_name.clone(), text_input_id.clone());
2023-10-09 12:31:13 -06:00
}
None => {
log::warn!("output {}: no output name", output.id());
}
},
2023-10-09 12:31:13 -06:00
None => {
log::warn!("output {}: no output info", output.id());
}
2023-10-06 10:44:05 -06:00
}
2023-10-09 12:31:13 -06:00
2025-03-12 16:07:57 -04:00
let unwrapped_size = size
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
.unwrap_or((1920, 1080));
let (loc, sub_size) = if unwrapped_size.0 > 800 {
(
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.),
)
} else {
(
Point::new(0., 32.),
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.),
)
};
self.window_size.insert(
surface_id,
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32),
);
2025-03-12 16:07:57 -04:00
self.subsurface_rects
.insert(output.clone(), Rectangle::new(loc, sub_size));
let msg = cosmic::surface::action::subsurface(
move |_: &mut App| SctkSubsurfaceSettings {
parent: surface_id,
id: subsurface_id,
loc,
size: Some(sub_size),
z: 10,
steal_keyboard_focus: true,
gravity: Gravity::BottomRight,
offset: (0, 0),
input_zone: None,
},
Some(Box::new(move |app: &App| {
app.menu(subsurface_id).map(cosmic::Action::App)
})),
);
2024-01-17 09:37:15 -07:00
if matches!(self.state, State::Locked { .. }) {
return Task::batch([
2024-04-05 14:08:40 -06:00
get_lock_surface(surface_id, output),
2025-03-12 16:07:57 -04:00
cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
)),
2024-04-05 14:08:40 -06:00
]);
}
2023-10-09 12:31:13 -06:00
}
OutputEvent::Removed => {
log::info!("output {}: removed", output.id());
match self.surface_ids.remove(&output) {
Some(surface_id) => {
self.surface_images.remove(&surface_id);
self.surface_names.remove(&surface_id);
self.window_size.remove(&surface_id);
2025-03-12 16:07:57 -04:00
if let Some(n) = self.surface_names.remove(&surface_id) {
self.text_input_ids.remove(&n);
}
if matches!(self.state, State::Locked { .. }) {
2024-04-05 14:08:40 -06:00
return destroy_lock_surface(surface_id);
}
2023-10-09 12:31:13 -06:00
}
None => {
log::warn!("output {}: no surface found", output.id());
}
2023-10-06 10:44:05 -06:00
}
}
2025-03-12 16:07:57 -04:00
OutputEvent::InfoUpdate(info) => {
let size = if let Some((w, h)) = info.logical_size {
Some((Some(w as u32), Some(h as u32)))
} else {
Some((None, None))
};
let unwrapped_size = size
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
.unwrap_or((1920, 1080));
let (loc, sub_size) = if unwrapped_size.0 > 800 {
(
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.),
)
} else {
(Point::ORIGIN, Size::new(1920., 1080.))
};
self.subsurface_rects
.insert(output.clone(), Rectangle::new(loc, sub_size));
2024-04-05 14:08:40 -06:00
log::info!("output {}: info update", output.id());
2023-10-09 12:31:13 -06:00
}
2023-10-06 10:44:05 -06:00
}
2023-10-09 12:31:13 -06:00
}
2023-10-26 15:10:09 -07:00
Message::SessionLockEvent(session_lock_event) => match session_lock_event {
2025-03-12 16:07:57 -04:00
SessionLockEvent::Focused(..) => {}
2024-04-05 14:08:40 -06:00
SessionLockEvent::Locked => {
log::info!("session locked");
if matches!(self.state, State::Locked { .. }) {
2025-03-12 16:07:57 -04:00
return Task::none();
}
let username = self.flags.user_data.name.clone();
let (locked_task, locked_handle) = cosmic::task::stream(
cosmic::iced_futures::stream::channel(16, |mut msg_tx| async move {
// Send heartbeat once a second to update time.
let heartbeat_future = {
let mut output = msg_tx.clone();
async move {
let mut interval =
tokio::time::interval(Duration::from_secs(1));
loop {
output
.send(cosmic::Action::App(Message::None))
.await
.unwrap();
interval.tick().await;
}
}
};
let pam_future = async {
loop {
let (value_tx, value_rx) = mpsc::channel(16);
msg_tx
.send(cosmic::Action::App(Message::Channel(value_tx)))
.await
.unwrap();
let pam_res = {
let username = username.clone();
let msg_tx = msg_tx.clone();
task::spawn_blocking(move || {
pam_thread(username, Conversation { msg_tx, value_rx })
})
.await
.unwrap()
};
match pam_res {
Ok(()) => {
log::info!("successfully authenticated");
msg_tx
.send(cosmic::Action::App(Message::Unlock))
.await
.unwrap();
break;
}
Err(err) => {
log::warn!("authentication error: {}", err);
msg_tx
.send(cosmic::Action::App(Message::Error(
err.to_string(),
)))
.await
.unwrap();
}
}
}
};
futures::pin_mut!(heartbeat_future);
futures::pin_mut!(pam_future);
futures::future::select(heartbeat_future, pam_future).await;
}),
)
.abortable();
let mut commands = Vec::with_capacity(self.surface_ids.len() + 1);
commands.push(locked_task);
self.state = State::Locked {
task_handle: locked_handle,
};
2024-04-05 19:02:20 -06:00
// Allow suspend
self.inhibit_opt = None;
2024-11-07 09:37:16 -07:00
// Create lock surfaces
2025-03-12 16:07:57 -04:00
2024-04-05 14:08:40 -06:00
for (output, surface_id) in self.surface_ids.iter() {
commands.push(get_lock_surface(*surface_id, output.clone()));
2025-03-12 16:07:57 -04:00
if let Some((rect, name)) = self
.subsurface_rects
.get(output)
.copied()
.zip(self.output_names.get(output))
{
let subsurface_id = SurfaceId::unique();
let surface_id = *surface_id;
self.surface_names.insert(surface_id, name.clone());
self.surface_names.insert(subsurface_id, name.clone());
let msg = cosmic::surface::action::subsurface(
move |_: &mut App| SctkSubsurfaceSettings {
parent: surface_id,
id: subsurface_id,
loc: Point::new(rect.x, rect.y),
size: Some(Size::new(rect.width, rect.height)),
z: 10,
steal_keyboard_focus: true,
gravity: Gravity::BottomRight,
offset: (0, 0),
input_zone: None,
},
Some(Box::new(move |app: &App| {
app.menu(subsurface_id).map(cosmic::Action::App)
})),
);
commands.push(cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
)));
} else {
log::error!("no rectangle for subsurface...");
}
2024-04-05 14:08:40 -06:00
}
return Task::batch(commands);
2024-04-05 14:08:40 -06:00
}
2023-10-26 15:10:09 -07:00
SessionLockEvent::Unlocked => {
2024-04-05 14:08:40 -06:00
log::info!("session unlocked");
self.state = State::Unlocked;
2025-03-12 16:07:57 -04:00
let mut commands = Vec::new();
for (_output, surface_id) in self.surface_ids.iter() {
self.surface_names.remove(surface_id);
self.window_size.remove(surface_id);
2025-03-12 16:07:57 -04:00
commands.push(destroy_lock_surface(*surface_id));
}
2024-04-05 14:08:40 -06:00
if cfg!(feature = "logind") {
2025-03-12 16:07:57 -04:00
return Task::batch(commands);
2024-04-05 14:08:40 -06:00
// When using logind feature, stick around for more lock signals
} else {
// When not using logind feature, exit immediately after unlocking
//TODO: cleaner method to exit?
process::exit(0);
}
}
2024-04-05 14:08:40 -06:00
//TODO: handle finished signal
2023-10-26 15:10:09 -07:00
_ => {}
},
2023-10-05 17:47:23 -06:00
Message::Channel(value_tx) => {
self.value_tx_opt = Some(value_tx);
}
2025-05-09 14:16:12 -06:00
Message::BackgroundState(bg_state) => {
eprintln!("{:#?}", bg_state);
self.flags.user_data.bg_state = bg_state;
self.flags.user_data.load_wallpapers_as_user();
self.surface_images.clear();
self.update_wallpapers();
}
2025-05-09 14:16:12 -06:00
Message::TimeAppletConfig(config) => {
self.flags.user_data.time_applet_config = config;
}
2024-04-05 19:02:20 -06:00
Message::Inhibit(inhibit) => {
self.inhibit_opt = Some(inhibit);
}
2024-01-17 11:18:58 -07:00
Message::NetworkIcon(network_icon_opt) => {
self.network_icon_opt = network_icon_opt;
}
2024-01-17 12:37:22 -07:00
Message::PowerInfo(power_info_opt) => {
self.power_info_opt = power_info_opt;
}
2025-03-12 16:07:57 -04:00
Message::Focus(surface_id) => {
self.active_surface_id_opt = Some(surface_id);
self.active_surface_id_opt = Some(surface_id);
if let Some(text_input_id) = self
.surface_names
.get(&surface_id)
.and_then(|id| self.text_input_ids.get(id))
{
return widget::text_input::focus(text_input_id.clone());
}
}
Message::Prompt(prompt, secret, value_opt) => {
let prompt_was_none = self.prompt_opt.is_none();
self.prompt_opt = Some((prompt, secret, value_opt));
if prompt_was_none {
if let Some(surface_id) = self.active_surface_id_opt {
2025-03-12 16:07:57 -04:00
if let Some(text_input_id) = self
.surface_names
.get(&surface_id)
.and_then(|id| self.text_input_ids.get(id))
{
log::error!("focus surface found id {:?}", text_input_id);
2024-01-17 09:37:15 -07:00
return widget::text_input::focus(text_input_id.clone());
}
}
}
2023-10-05 17:47:23 -06:00
}
2025-03-12 16:07:57 -04:00
Message::Submit(value) => match self.value_tx_opt.take() {
Some(value_tx) => {
// Clear errors
self.error_opt = None;
return cosmic::task::future(async move {
value_tx.send(value).await.unwrap();
Message::Channel(value_tx)
});
}
None => log::warn!("tried to submit when value_tx_opt not set"),
2023-10-05 17:47:23 -06:00
},
2023-11-29 08:02:14 -07:00
Message::Suspend => {
#[cfg(feature = "logind")]
return cosmic::Task::future(async move { crate::logind::suspend().await.err() })
.and_then(|err| {
log::error!("failed to suspend: {:?}", err);
cosmic::task::message(cosmic::Action::App(Message::Error(err.to_string())))
});
2023-11-29 08:02:14 -07:00
}
2023-10-05 17:47:23 -06:00
Message::Error(error) => {
self.error_opt = Some(error);
}
2024-04-05 14:08:40 -06:00
Message::Lock => match self.state {
State::Unlocked => {
log::info!("session locking");
self.state = State::Locking;
// Clear errors
self.error_opt = None;
// Clear value_tx
self.value_tx_opt = None;
2024-11-07 09:37:16 -07:00
// Try to create lockfile when locking
if let Some(ref lockfile) = self.flags.lockfile_opt {
if let Err(err) = fs::File::create(lockfile) {
log::warn!("failed to create lockfile {:?}: {}", lockfile, err);
}
}
// Tell compositor to lock
2024-04-05 14:08:40 -06:00
return lock();
}
State::Unlocking => {
log::info!("session still unlocking");
}
State::Locking | State::Locked { .. } => {
2024-04-05 14:08:40 -06:00
log::info!("session already locking or locked");
}
},
2024-04-05 11:31:15 -06:00
Message::Unlock => {
2024-04-05 14:08:40 -06:00
match self.state {
State::Locked { .. } => {
2024-04-05 14:08:40 -06:00
log::info!("sessing unlocking");
self.state = State::Unlocking;
// Clear errors
self.error_opt = None;
// Clear value_tx
self.value_tx_opt = None;
2024-11-07 09:37:16 -07:00
// Try to delete lockfile when unlocking
if let Some(ref lockfile) = self.flags.lockfile_opt {
if let Err(err) = fs::remove_file(lockfile) {
log::warn!("failed to remove lockfile {:?}: {}", lockfile, err);
}
}
2025-03-12 16:07:57 -04:00
2024-11-07 09:37:16 -07:00
// Destroy lock surfaces
2024-04-05 14:08:40 -06:00
let mut commands = Vec::with_capacity(self.surface_ids.len() + 1);
for (_output, surface_id) in self.surface_ids.iter() {
self.surface_names.remove(surface_id);
self.window_size.remove(&surface_id);
commands.push(destroy_lock_surface(*surface_id));
}
2024-11-07 09:37:16 -07:00
// Tell compositor to unlock
2024-04-05 14:08:40 -06:00
commands.push(unlock());
2025-03-12 16:07:57 -04:00
2024-04-05 14:08:40 -06:00
// Wait to exit until `Unlocked` event, when server has processed unlock
return Task::batch(commands);
2024-04-05 14:08:40 -06:00
}
State::Locking => {
log::info!("session still locking");
}
State::Unlocking | State::Unlocked => {
log::info!("session already unlocking or unlocked");
}
2023-10-06 13:07:53 -06:00
}
2023-10-05 17:47:23 -06:00
}
2025-03-05 23:08:56 -05:00
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
2025-04-10 16:39:32 -04:00
Message::Tick => {
self.time.tick();
}
Message::Tz(tz) => {
self.time.set_tz(tz);
}
2023-10-05 17:47:23 -06:00
}
Task::none()
2023-10-05 17:47:23 -06:00
}
2023-10-06 10:44:05 -06:00
// Not used for layer surface window
2023-10-05 17:47:23 -06:00
fn view(&self) -> Element<Self::Message> {
2023-10-06 10:44:05 -06:00
unimplemented!()
}
/// Creates a view after each update.
fn view_window(&self, surface_id: SurfaceId) -> Element<Self::Message> {
2025-03-12 16:07:57 -04:00
let img = self
.surface_images
.get(&surface_id)
.unwrap_or(&self.flags.fallback_background);
widget::image(img)
.content_fit(iced::ContentFit::Cover)
.width(Length::Fill)
.height(Length::Fill)
2025-03-12 16:07:57 -04:00
.into()
2023-10-05 17:47:23 -06:00
}
fn subscription(&self) -> Subscription<Self::Message> {
2024-04-05 14:08:40 -06:00
let mut subscriptions = Vec::with_capacity(7);
2025-03-12 16:07:57 -04:00
subscriptions.push(event::listen_with(|event, _, id| match event {
2024-04-05 14:08:40 -06:00
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
wayland_event,
)) => match wayland_event {
WaylandEvent::Output(output_event, output) => {
Some(Message::OutputEvent(output_event, output))
}
2024-04-05 14:08:40 -06:00
WaylandEvent::SessionLock(evt) => Some(Message::SessionLockEvent(evt)),
2023-10-06 10:44:05 -06:00
_ => None,
2024-04-05 14:08:40 -06:00
},
2025-03-12 16:07:57 -04:00
iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)),
2024-04-05 14:08:40 -06:00
_ => None,
}));
struct BackgroundSubscription;
subscriptions.push(
cosmic_config::config_state_subscription(
TypeId::of::<BackgroundSubscription>(),
cosmic_bg_config::NAME.into(),
cosmic_bg_config::state::State::version(),
)
2024-02-23 19:25:35 -05:00
.map(|res| {
if !res.errors.is_empty() {
log::info!("errors loading background state: {:?}", res.errors);
}
2024-02-23 19:25:35 -05:00
Message::BackgroundState(res.config)
}),
2024-04-05 14:08:40 -06:00
);
2025-05-09 14:16:12 -06:00
struct TimeAppletSubscription;
subscriptions.push(
cosmic_config::config_subscription(
TypeId::of::<TimeAppletSubscription>(),
"com.system76.CosmicAppletTime".into(),
TimeAppletConfig::VERSION,
)
.map(|res| {
if !res.errors.is_empty() {
log::info!("errors loading background state: {:?}", res.errors);
}
Message::TimeAppletConfig(res.config)
}),
);
2024-04-05 14:08:40 -06:00
#[cfg(feature = "logind")]
{
2024-04-05 19:02:20 -06:00
subscriptions.push(crate::logind::subscription());
2024-04-05 14:08:40 -06:00
}
#[cfg(feature = "networkmanager")]
{
subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon));
2024-04-05 14:08:40 -06:00
}
#[cfg(feature = "upower")]
{
subscriptions.push(crate::upower::subscription().map(Message::PowerInfo));
2024-04-05 14:08:40 -06:00
}
Subscription::batch(subscriptions)
2023-10-05 17:47:23 -06:00
}
}