cosmic-greeter/src/locker.rs

1063 lines
42 KiB
Rust

// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
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;
use cosmic::surface;
use cosmic::{
Element, executor,
iced::{
self, Background, Border, Length, Subscription, alignment,
event::wayland::{OutputEvent, SessionLockEvent},
futures::{self, SinkExt},
platform_specific::shell::wayland::commands::session_lock::{
destroy_lock_surface, get_lock_surface, lock, unlock,
},
},
iced_runtime::core::window::Id as SurfaceId,
theme, widget,
};
use cosmic_config::CosmicConfigEntry;
use cosmic_greeter_daemon::{TimeAppletConfig, UserData};
use std::time::Duration;
use std::{
any::TypeId,
env,
ffi::{CStr, CString},
fs,
os::fd::OwnedFd,
path::PathBuf,
process,
sync::Arc,
};
use tokio::{sync::mpsc, task};
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
use crate::{
common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT},
fl,
};
fn lockfile_opt() -> Option<PathBuf> {
let runtime_dir = dirs::runtime_dir()?;
let session_id = env::var("XDG_SESSION_ID").ok()?;
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();
crate::localize::localize();
let mut user_data = UserData::from(user);
// We are already the user at this point
user_data.load_config_as_user();
let flags = Flags {
user_data,
lockfile_opt: lockfile_opt(),
};
let settings = Settings::default().no_main_window(true);
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)?;
// 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>>,
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(
common::Message::Prompt(prompt.to_string(), secret, Some(String::new())).into(),
))
.await
})
.map_err(|err| {
log::error!("failed to send prompt: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})?;
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(
common::Message::Prompt(prompt.to_string(), false, None).into(),
))
.await
})
.map_err(|err| {
log::error!("failed to send prompt: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})
}
}
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);
}
}
}
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);
}
}
}
}
#[derive(Clone)]
pub struct Flags {
user_data: UserData,
lockfile_opt: Option<PathBuf>,
}
///TODO: this is custom code that should be better handled by libcosmic
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Dropdown {
Keyboard,
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
None,
Common(common::Message),
OutputEvent(OutputEvent, WlOutput),
SessionLockEvent(SessionLockEvent),
Channel(mpsc::Sender<String>),
BackgroundState(cosmic_bg_config::state::State),
DropdownToggle(Dropdown),
KeyboardLayout(usize),
Inhibit(Arc<OwnedFd>),
Submit(String),
Surface(surface::Action),
Suspend,
TimeAppletConfig(TimeAppletConfig),
Error(String),
Lock,
Unlock,
}
impl From<common::Message> for Message {
fn from(message: common::Message) -> Self {
Self::Common(message)
}
}
#[derive(Clone, Debug)]
enum State {
Locking,
Locked {
task_handle: cosmic::iced::task::Handle,
},
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();
}
}
}
/// The [`App`] stores application-specific state.
pub struct App {
common: Common<Message>,
flags: Flags,
state: State,
dropdown_opt: Option<Dropdown>,
inhibit_opt: Option<Arc<OwnedFd>>,
value_tx_opt: Option<mpsc::Sender<String>>,
}
impl App {
fn menu(&self, surface_id: SurfaceId) -> Element<Message> {
let window_width = self
.common
.window_size
.get(&surface_id)
.map(|s| s.width)
.unwrap_or(800.);
let menu_width = if window_width > 800. {
800.
} else {
window_width
};
let left_element = {
let military_time = self.flags.user_data.time_applet_config.military_time;
let date_time_column = self.common.time.date_time_widget(military_time);
let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0);
if let Some(network_icon) = self.common.network_icon_opt {
status_row = status_row.push(widget::icon::from_name(network_icon));
}
if let Some((power_icon, power_percent)) = &self.common.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: move code for custom dropdowns to libcosmic
let menu_checklist = |label, value, message| {
Element::from(
widget::menu::menu_button(vec![
if value {
widget::icon::from_name("object-select-symbolic")
.size(16)
.icon()
.width(Length::Fixed(16.0))
.into()
} else {
widget::Space::with_width(Length::Fixed(17.0)).into()
},
widget::Space::with_width(Length::Fixed(8.0)).into(),
widget::text(label)
.align_x(iced::alignment::Horizontal::Left)
.into(),
])
.on_press(message),
)
};
let dropdown_menu = |items: Vec<_>| {
let item_cnt = items.len();
let items = widget::column::with_children(items);
let items = if item_cnt > 7 {
Element::from(
widget::scrollable(items)
.height(Length::Fixed(DEFAULT_MENU_ITEM_HEIGHT * 7.)),
)
} else {
Element::from(items)
};
widget::container(items)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background.component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: 8.0.into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
}))
.width(Length::Fixed(240.0))
};
let mut input_button = widget::popover(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Keyboard)),
)
.position(widget::popover::Position::Bottom);
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
let mut items = Vec::with_capacity(self.common.active_layouts.len());
for (i, layout) in self.common.active_layouts.iter().enumerate() {
items.push(menu_checklist(
&layout.description,
i == 0,
Message::KeyboardLayout(i),
));
}
input_button = input_button.popup(dropdown_menu(items));
}
//TODO: implement these buttons
let button_row = iced::widget::row![
/*TODO: greeter accessibility options
widget::button::custom(widget::icon::from_name(
"applications-accessibility-symbolic"
))
.padding(12.0)
.on_press(Message::None),
*/
widget::tooltip(
input_button,
widget::text(fl!("keyboard-layout")),
widget::tooltip::Position::Top
),
widget::tooltip(
widget::button::custom(widget::icon::from_name("system-suspend-symbolic"))
.padding(12.0)
.on_press(Message::Suspend),
widget::text(fl!("suspend")),
widget::tooltip::Position::Top
),
]
.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.)),
status_row,
widget::divider::horizontal::default().width(Length::Fixed(menu_width / 2. - 16.)),
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 {
Some(icon) => {
column = column.push(
widget::container(
//TODO: cache image handle?
widget::Image::new(widget::image::Handle::from_bytes(icon.clone()))
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
}
None => {}
}
column = column.push(
widget::container(widget::text::title4(&self.flags.user_data.full_name))
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
);
match &self.common.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => {
let text_input_id = self
.common
.surface_names
.get(&surface_id)
.and_then(|id| self.common.text_input_ids.get(id))
.cloned()
.unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
let mut text_input = widget::secure_input(
prompt.clone(),
value.as_str(),
Some(
common::Message::Prompt(
prompt.clone(),
!*secret,
Some(value.clone()),
)
.into(),
),
*secret,
)
.id(text_input_id)
.on_input(|input| {
common::Message::Prompt(prompt.clone(), *secret, Some(input)).into()
})
.on_submit(Message::Submit);
if *secret {
text_input = text_input.password()
}
column = column.push(text_input);
if self.common.caps_lock {
column = column.push(widget::text(fl!("caps-lock")));
}
}
None => {
column = column.push(widget::text(prompt));
}
},
None => {}
}
if let Some(error) = &self.common.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()
}
}
/// 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.common.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.common.core
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
let (mut common, common_task) = Common::init(core);
common.on_output_event = Some(Box::new(|output_event, output| {
Message::OutputEvent(output_event, output)
}));
common.on_session_lock_event = Some(Box::new(|evt| Message::SessionLockEvent(evt)));
common.update_user_data(&flags.user_data);
let already_locked = match flags.lockfile_opt {
Some(ref lockfile) => lockfile.exists(),
None => false,
};
let mut app = App {
common,
flags,
state: State::Unlocked,
dropdown_opt: None,
inhibit_opt: None,
value_tx_opt: None,
};
let task = if cfg!(feature = "logind") {
if already_locked {
// Recover previously locked state
log::info!("recovering previous locked state");
app.state = State::Locking;
lock()
} else {
// When logind feature is used, wait for lock signal
Task::none()
}
} else {
// When logind feature not used, lock immediately
log::info!("locking immediately");
app.state = State::Locking;
lock()
};
(app, Task::batch([task, common_task]))
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::None => {}
Message::Common(common_message) => {
return self.common.update(common_message);
}
Message::OutputEvent(output_event, output) => {
match output_event {
OutputEvent::Created(output_info_opt) => {
log::info!("output {}: created", output.id());
let surface_id = SurfaceId::unique();
let subsurface_id = SurfaceId::unique();
if let Some(old_surface_id) =
self.common.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();
}
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))
};
match output_info_opt {
Some(output_info) => match output_info.name {
Some(output_name) => {
self.common
.output_names
.insert(output.clone(), output_name.clone());
self.common
.surface_names
.insert(surface_id, output_name.clone());
self.common
.surface_names
.insert(subsurface_id, output_name.clone());
self.common.surface_images.remove(&surface_id);
self.common.update_wallpapers(&self.flags.user_data);
let text_input_id =
widget::Id::new(format!("input-{output_name}",));
self.common
.text_input_ids
.insert(output_name.clone(), text_input_id.clone());
}
None => {
log::warn!("output {}: no output name", output.id());
}
},
None => {
log::warn!("output {}: no output info", output.id());
}
}
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.common.window_size.insert(
surface_id,
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32),
);
self.common
.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)
})),
);
if matches!(self.state, State::Locked { .. }) {
return Task::batch([
get_lock_surface(surface_id, output),
cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(msg),
)),
]);
}
}
OutputEvent::Removed => {
log::info!("output {}: removed", output.id());
match self.common.surface_ids.remove(&output) {
Some(surface_id) => {
self.common.surface_images.remove(&surface_id);
self.common.surface_names.remove(&surface_id);
self.common.window_size.remove(&surface_id);
if let Some(n) = self.common.surface_names.remove(&surface_id) {
self.common.text_input_ids.remove(&n);
}
if matches!(self.state, State::Locked { .. }) {
return destroy_lock_surface(surface_id);
}
}
None => {
log::warn!("output {}: no surface found", output.id());
}
}
}
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.common
.subsurface_rects
.insert(output.clone(), Rectangle::new(loc, sub_size));
log::info!("output {}: info update", output.id());
}
}
}
Message::SessionLockEvent(session_lock_event) => match session_lock_event {
SessionLockEvent::Focused(..) => {}
SessionLockEvent::Locked => {
log::info!("session locked");
if matches!(self.state, State::Locked { .. }) {
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.common.surface_ids.len() + 1);
commands.push(locked_task);
self.state = State::Locked {
task_handle: locked_handle,
};
// Allow suspend
self.inhibit_opt = None;
// Create lock surfaces
for (output, surface_id) in self.common.surface_ids.iter() {
commands.push(get_lock_surface(*surface_id, output.clone()));
if let Some((rect, name)) = self
.common
.subsurface_rects
.get(output)
.copied()
.zip(self.common.output_names.get(output))
{
let subsurface_id = SurfaceId::unique();
let surface_id = *surface_id;
self.common.surface_names.insert(surface_id, name.clone());
self.common
.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...");
}
}
return Task::batch(commands);
}
SessionLockEvent::Unlocked => {
log::info!("session unlocked");
self.state = State::Unlocked;
let mut commands = Vec::new();
for (_output, surface_id) in self.common.surface_ids.iter() {
self.common.surface_names.remove(surface_id);
self.common.window_size.remove(surface_id);
commands.push(destroy_lock_surface(*surface_id));
}
if cfg!(feature = "logind") {
return Task::batch(commands);
// 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);
}
}
//TODO: handle finished signal
_ => {}
},
Message::Channel(value_tx) => {
self.value_tx_opt = Some(value_tx);
}
Message::BackgroundState(bg_state) => {
self.flags.user_data.bg_state = bg_state;
self.flags.user_data.load_wallpapers_as_user();
self.common.surface_images.clear();
self.common.update_wallpapers(&self.flags.user_data);
}
Message::DropdownToggle(dropdown) => {
if self.dropdown_opt == Some(dropdown) {
self.dropdown_opt = None;
} else {
self.dropdown_opt = Some(dropdown);
}
}
Message::Inhibit(inhibit) => match self.state {
State::Locked { .. } => {
log::info!("no need to inhibit sleep when already locked");
}
_ => {
self.inhibit_opt = Some(inhibit);
}
},
Message::KeyboardLayout(layout_i) => {
if layout_i < self.common.active_layouts.len() {
self.common.active_layouts.swap(0, layout_i);
self.common.set_xkb_config(&self.flags.user_data);
}
if self.dropdown_opt == Some(Dropdown::Keyboard) {
self.dropdown_opt = None
}
}
Message::Submit(value) => {
self.common.prompt_opt = None;
self.common.error_opt = None;
match self.value_tx_opt.take() {
Some(value_tx) => {
// Clear errors
self.common.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"),
}
}
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())))
});
}
Message::TimeAppletConfig(config) => {
self.flags.user_data.time_applet_config = config;
}
Message::Error(error) => {
self.common.error_opt = Some(error);
}
Message::Lock => match self.state {
State::Unlocked => {
log::info!("session locking");
self.state = State::Locking;
// Clear errors
self.common.error_opt = None;
// Clear value_tx
self.value_tx_opt = None;
// 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
return lock();
}
State::Unlocking => {
log::info!("session still unlocking");
}
State::Locking | State::Locked { .. } => {
log::info!("session already locking or locked");
}
},
Message::Unlock => {
match self.state {
State::Locked { .. } => {
log::info!("sessing unlocking");
self.state = State::Unlocking;
// Clear errors
self.common.error_opt = None;
// Clear value_tx
self.value_tx_opt = None;
// 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);
}
}
// Destroy lock surfaces
let mut commands = Vec::with_capacity(self.common.surface_ids.len() + 1);
for (_output, surface_id) in self.common.surface_ids.iter() {
self.common.surface_names.remove(surface_id);
self.common.window_size.remove(&surface_id);
commands.push(destroy_lock_surface(*surface_id));
}
// Tell compositor to unlock
commands.push(unlock());
// Wait to exit until `Unlocked` event, when server has processed unlock
return Task::batch(commands);
}
State::Locking => {
log::info!("session still locking");
}
State::Unlocking | State::Unlocked => {
log::info!("session already unlocking or unlocked");
}
}
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
}
Task::none()
}
// Not used for layer surface window
fn view(&self) -> Element<Self::Message> {
unimplemented!()
}
/// Creates a view after each update.
fn view_window(&self, surface_id: SurfaceId) -> Element<Self::Message> {
let img = self
.common
.surface_images
.get(&surface_id)
.unwrap_or(&self.common.fallback_background);
widget::image(img)
.content_fit(iced::ContentFit::Cover)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
let mut subscriptions = Vec::with_capacity(7);
subscriptions.push(self.common.subscription().map(Message::from));
struct BackgroundSubscription;
subscriptions.push(
cosmic_config::config_state_subscription(
TypeId::of::<BackgroundSubscription>(),
cosmic_bg_config::NAME.into(),
cosmic_bg_config::state::State::version(),
)
.map(|res| {
if !res.errors.is_empty() {
log::info!("errors loading background state: {:?}", res.errors);
}
Message::BackgroundState(res.config)
}),
);
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)
}),
);
#[cfg(feature = "logind")]
{
subscriptions.push(crate::logind::subscription());
}
Subscription::batch(subscriptions)
}
}