Reduce differences between greeter and locker

This commit is contained in:
Jeremy Soller 2023-10-06 15:02:25 -06:00
parent b8f8202b0b
commit e028c2eac5
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
2 changed files with 347 additions and 160 deletions

View file

@ -2,9 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::app::{message, Command, Core, Settings}; use cosmic::app::{message, Command, Core, Settings};
use cosmic::{executor, iced, widget, Element}; use cosmic::{
executor,
iced::{self, alignment, Length},
style, widget, Element,
};
use greetd_ipc::{codec::SyncCodec, AuthMessageType, Request, Response}; use greetd_ipc::{codec::SyncCodec, AuthMessageType, Request, Response};
use std::{collections::HashMap, env, fs, io, path::Path, sync::Arc}; use std::{collections::HashMap, env, fs, io, path::Path, process, sync::Arc};
use tokio::net::UnixStream; use tokio::net::UnixStream;
pub fn main() -> Result<(), Box<dyn std::error::Error>> { pub fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -135,7 +139,14 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
sessions sessions
}; };
let flags = Flags { users, sessions }; //TODO: use background config
let background = widget::image::Handle::from_memory(include_bytes!("../res/background.png"));
let flags = Flags {
users,
sessions,
background,
};
let settings = Settings::default() let settings = Settings::default()
.antialiasing(true) .antialiasing(true)
@ -177,26 +188,14 @@ async fn request_message(socket: Arc<UnixStream>, request: Request) -> Message {
auth_message, auth_message,
} => match auth_message_type { } => match auth_message_type {
AuthMessageType::Secret => { AuthMessageType::Secret => {
return Message::Input(InputState::Auth { return Message::Prompt(auth_message, true, Some(String::new()));
prompt: auth_message,
value_opt: Some(String::new()),
secret: true,
})
} }
AuthMessageType::Visible => { AuthMessageType::Visible => {
return Message::Input(InputState::Auth { return Message::Prompt(auth_message, false, Some(String::new()));
prompt: auth_message,
value_opt: Some(String::new()),
secret: false,
})
} }
//TODO: treat error type differently? //TODO: treat error type differently?
AuthMessageType::Info | AuthMessageType::Error => { AuthMessageType::Info | AuthMessageType::Error => {
return Message::Input(InputState::Auth { return Message::Prompt(auth_message, false, None);
prompt: auth_message,
value_opt: None,
secret: false,
})
} }
}, },
Response::Error { Response::Error {
@ -250,6 +249,7 @@ fn request_command(socket: Arc<UnixStream>, request: Request) -> Command<Message
pub struct Flags { pub struct Flags {
users: Vec<(pwd::Passwd, Option<widget::image::Handle>)>, users: Vec<(pwd::Passwd, Option<widget::image::Handle>)>,
sessions: HashMap<String, Vec<String>>, sessions: HashMap<String, Vec<String>>,
background: widget::image::Handle,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -264,22 +264,12 @@ pub enum SocketState {
Error(Arc<io::Error>), Error(Arc<io::Error>),
} }
#[derive(Clone, Debug)]
pub enum InputState {
Username,
Auth {
prompt: String,
value_opt: Option<String>,
secret: bool,
},
}
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
None, None,
Socket(SocketState), Socket(SocketState),
Input(InputState), Prompt(String, bool, Option<String>),
Session(String), Session(String),
Error(String), Error(String),
Username(Arc<UnixStream>, String), Username(Arc<UnixStream>, String),
@ -293,7 +283,8 @@ pub struct App {
core: Core, core: Core,
flags: Flags, flags: Flags,
socket_state: SocketState, socket_state: SocketState,
input_state: InputState, username_opt: Option<String>,
prompt_opt: Option<(String, bool, Option<String>)>,
session_names: Vec<String>, session_names: Vec<String>,
selected_session: String, selected_session: String,
error_opt: Option<String>, error_opt: Option<String>,
@ -342,8 +333,9 @@ impl cosmic::Application for App {
core, core,
flags, flags,
socket_state: SocketState::Pending, socket_state: SocketState::Pending,
//TODO: set to pending until socket is open? //TODO: choose last used user
input_state: InputState::Username, username_opt: None,
prompt_opt: None,
session_names, session_names,
selected_session, selected_session,
error_opt: None, error_opt: None,
@ -371,8 +363,8 @@ impl cosmic::Application for App {
Message::Socket(socket_state) => { Message::Socket(socket_state) => {
self.socket_state = socket_state; self.socket_state = socket_state;
} }
Message::Input(input_state) => { Message::Prompt(prompt, secret, value) => {
self.input_state = input_state; self.prompt_opt = Some((prompt, secret, value));
//TODO: only focus text input on changes to the page //TODO: only focus text input on changes to the page
return widget::text_input::focus(self.text_input_id.clone()); return widget::text_input::focus(self.text_input_id.clone());
} }
@ -383,6 +375,7 @@ impl cosmic::Application for App {
self.error_opt = Some(error); self.error_opt = Some(error);
} }
Message::Username(socket, username) => { Message::Username(socket, username) => {
self.username_opt = Some(username.clone());
return request_command(socket, Request::CreateSession { username }); return request_command(socket, Request::CreateSession { username });
} }
Message::Auth(socket, response) => { Message::Auth(socket, response) => {
@ -403,7 +396,8 @@ impl cosmic::Application for App {
} }
} }
Message::Exit => { Message::Exit => {
return iced::window::close(); //TODO: cleaner method to exit?
process::exit(0);
} }
} }
Command::none() Command::none()
@ -411,107 +405,256 @@ impl cosmic::Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<Self::Message> { fn view(&self) -> Element<Self::Message> {
let content: Element<_> = match &self.socket_state { let left_element = {
SocketState::Pending => widget::text("Opening GREETD_SOCK").into(), let date_time_column = {
SocketState::Open(socket) => match &self.input_state { let mut column = widget::column::with_capacity(2).padding(16.0).spacing(12.0);
InputState::Username => {
let mut row = widget::row::with_capacity(self.flags.users.len()).spacing(12.0);
for (user, icon_opt) in &self.flags.users {
let mut column = widget::column::with_capacity(2).spacing(12.0);
match icon_opt {
Some(icon) => {
column = column.push(
widget::Image::new(icon.clone())
.width(iced::Length::Fixed(256.0))
.height(iced::Length::Fixed(256.0)),
)
}
None => {}
}
match &user.gecos {
Some(gecos) => {
column = column.push(widget::text(gecos));
}
None => {}
}
row = row.push(
widget::MouseArea::new(
widget::cosmic_container::container(column)
.layer(cosmic::cosmic_theme::Layer::Primary)
.padding(16)
.style(cosmic::theme::Container::Card),
)
.on_press(Message::Username(socket.clone(), user.name.clone())),
);
}
row.into()
}
InputState::Auth {
prompt,
value_opt,
secret,
} => {
let mut column = widget::column::with_capacity(2)
.spacing(12.0)
.width(iced::Length::Fixed(400.0));
column = column.push(widget::text(prompt));
match value_opt { let dt = chrono::Local::now();
Some(value) => {
let text_input = widget::text_input("", &value) //TODO: localized format
.id(self.text_input_id.clone()) let date = dt.format("%A, %B %-d");
.on_input(|value| { column = column
Message::Input(InputState::Auth { .push(widget::text::title2(format!("{}", date)).style(style::Text::Accent));
prompt: prompt.clone(),
value_opt: Some(value), //TODO: localized format
secret: *secret, let time = dt.format("%R");
}) column = column.push(
}) widget::text(format!("{}", time))
.on_submit(Message::Auth(socket.clone(), Some(value.clone()))); .size(112.0)
if *secret { .style(style::Text::Accent),
column = column.push(text_input.password()); );
} else {
column = column.push(text_input); column
};
//TODO: get actual status
let status_row = iced::widget::row![
widget::icon::from_name("network-wireless-signal-ok-symbolic",),
iced::widget::row![
widget::icon::from_name("battery-level-50-symbolic"),
widget::text("50%"),
],
]
.padding(16.0)
.spacing(12.0);
//TODO: implement these buttons
let button_row = iced::widget::row![
widget::button(widget::icon::from_name(
"applications-accessibility-symbolic"
))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("system-users-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("application-menu-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("system-suspend-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("system-reboot-symbolic"))
.padding(12.0)
.on_press(Message::None),
widget::button(widget::icon::from_name("system-shutdown-symbolic"))
.padding(12.0)
.on_press(Message::None),
]
.padding([16.0, 0.0, 0.0, 0.0])
.spacing(8.0);
widget::container(iced::widget::column![
date_time_column,
widget::divider::horizontal::default(),
status_row,
widget::divider::horizontal::default(),
button_row,
])
.width(Length::Fill)
.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.socket_state {
SocketState::Pending => {
column = column.push(widget::text("Opening GREETD_SOCK"));
}
SocketState::Open(socket) => {
match &self.username_opt {
Some(username) => {
for (user, icon_opt) in &self.flags.users {
if &user.name == username {
match icon_opt {
Some(icon) => {
column = column.push(
widget::container(
widget::Image::new(icon.clone())
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
}
None => {}
}
match &user.gecos {
Some(gecos) => {
column = column.push(
widget::container(widget::text::title4(gecos))
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
);
}
None => {}
}
}
} }
} }
None => { None => {
column = column.push( let mut row =
widget::button("Confirm") widget::row::with_capacity(self.flags.users.len()).spacing(12.0);
.on_press(Message::Auth(socket.clone(), None)), for (user, icon_opt) in &self.flags.users {
); let mut column = widget::column::with_capacity(2).spacing(12.0);
match icon_opt {
Some(icon) => {
column = column.push(
widget::container(
widget::Image::new(icon.clone())
.width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
}
None => {}
}
match &user.gecos {
Some(gecos) => {
column = column.push(
widget::container(widget::text::title4(gecos))
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
);
}
None => {}
}
row = row.push(
widget::MouseArea::new(
widget::cosmic_container::container(column)
.layer(cosmic::cosmic_theme::Layer::Primary)
.padding(16)
.style(cosmic::theme::Container::Card),
)
.on_press(Message::Username(socket.clone(), user.name.clone())),
);
}
column = column.push(row);
} }
} }
match &self.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => {
let mut text_input = widget::text_input(&prompt, &value)
.id(self.text_input_id.clone())
.leading_icon(
widget::icon::from_name("system-lock-screen-symbolic")
.into(),
)
.trailing_icon(
widget::icon::from_name("document-properties-symbolic")
.into(),
)
.on_input(|value| {
Message::Prompt(prompt.clone(), *secret, Some(value))
})
.on_submit(Message::Auth(socket.clone(), Some(value.clone())));
column.into() if *secret {
text_input = text_input.password()
}
column = column.push(text_input);
}
None => {
column = column.push(
widget::button("Confirm")
.on_press(Message::Auth(socket.clone(), None)),
);
}
},
None => {}
}
}
SocketState::NotSet => {
column = column.push(widget::text("GREETD_SOCK variable not set"));
}
SocketState::Error(err) => {
column = column.push(widget::text(format!(
"Failed to open GREETD_SOCK: {:?}",
err
)))
} }
},
SocketState::NotSet => widget::text("GREETD_SOCK variable not set").into(),
SocketState::Error(err) => {
widget::text(format!("Failed to open GREETD_SOCK: {:?}", err)).into()
} }
if let Some(error) = &self.error_opt {
column = column.push(widget::text(error));
}
column = column.push(
//TODO: use button
widget::pick_list(
&self.session_names,
Some(self.selected_session.clone()),
Message::Session,
),
);
widget::container(column)
.align_x(alignment::Horizontal::Center)
.width(Length::Fill)
}; };
let session_picker = widget::pick_list( crate::image_container::ImageContainer::new(
&self.session_names, widget::container(
Some(self.selected_session.clone()), widget::cosmic_container::container(
Message::Session, iced::widget::row![left_element, right_element]
); .align_items(alignment::Alignment::Center),
)
let mut column = widget::column::with_capacity(3) .layer(cosmic::cosmic_theme::Layer::Background)
.push(content) .padding(16)
.push(session_picker) .style(cosmic::theme::Container::Custom(Box::new(
.spacing(12.0); |theme: &cosmic::Theme| {
// Use background appearance as the base
if let Some(error) = &self.error_opt { let mut appearance = widget::container::StyleSheet::appearance(
column = column.push(widget::text(error.clone())); theme,
} &cosmic::theme::Container::Background,
);
let centered = widget::container(column) appearance.border_radius = 16.0.into();
.width(iced::Length::Fill) appearance
.height(iced::Length::Fill) },
.align_x(iced::alignment::Horizontal::Center) )))
.align_y(iced::alignment::Vertical::Center); .width(Length::Fixed(800.0)),
)
Element::from(centered) .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)
.style(cosmic::theme::Container::Transparent),
)
.image(self.flags.background.clone())
.into()
} }
} }

View file

@ -104,7 +104,11 @@ impl Conversation {
futures::executor::block_on(async { futures::executor::block_on(async {
self.msg_tx self.msg_tx
.send(Message::Prompt(prompt.to_string(), secret, String::new())) .send(Message::Prompt(
prompt.to_string(),
secret,
Some(String::new()),
))
.await .await
}) })
.map_err(|err| { .map_err(|err| {
@ -122,6 +126,23 @@ impl Conversation {
pam_client::ErrorCode::CONV_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(Message::Prompt(prompt.to_string(), false, None))
.await
})
.map_err(|err| {
log::error!("failed to send prompt: {:?}", err);
pam_client::ErrorCode::CONV_ERR
})
}
} }
impl pam_client::ConversationHandler for Conversation { impl pam_client::ConversationHandler for Conversation {
@ -133,11 +154,24 @@ impl pam_client::ConversationHandler for Conversation {
log::info!("prompt_echo_off {:?}", prompt_c); log::info!("prompt_echo_off {:?}", prompt_c);
self.prompt_value(prompt_c, true) self.prompt_value(prompt_c, true)
} }
fn text_info(&mut self, msg: &CStr) { fn text_info(&mut self, prompt_c: &CStr) {
log::warn!("TODO text_info: {:?}", msg); 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, msg: &CStr) { fn error_msg(&mut self, prompt_c: &CStr) {
log::info!("TODO error_msg: {:?}", msg); //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);
}
}
} }
} }
@ -154,7 +188,7 @@ pub enum Message {
None, None,
OutputEvent(OutputEvent, WlOutput), OutputEvent(OutputEvent, WlOutput),
Channel(mpsc::Sender<String>), Channel(mpsc::Sender<String>),
Prompt(String, bool, String), Prompt(String, bool, Option<String>),
Submit, Submit,
Error(String), Error(String),
Exit, Exit,
@ -167,7 +201,7 @@ pub struct App {
next_surface_id: SurfaceId, next_surface_id: SurfaceId,
surface_ids: HashMap<WlOutput, SurfaceId>, surface_ids: HashMap<WlOutput, SurfaceId>,
value_tx_opt: Option<mpsc::Sender<String>>, value_tx_opt: Option<mpsc::Sender<String>>,
prompt_opt: Option<(String, bool, String)>, prompt_opt: Option<(String, bool, Option<String>)>,
error_opt: Option<String>, error_opt: Option<String>,
text_input_id: widget::Id, text_input_id: widget::Id,
exited: bool, exited: bool,
@ -283,23 +317,26 @@ impl cosmic::Application for App {
Message::Channel(value_tx) => { Message::Channel(value_tx) => {
self.value_tx_opt = Some(value_tx); self.value_tx_opt = Some(value_tx);
} }
Message::Prompt(prompt, secret, value) => { Message::Prompt(prompt, secret, value_opt) => {
self.prompt_opt = Some((prompt, secret, value)); self.prompt_opt = Some((prompt, secret, value_opt));
//TODO: only focus text input on changes to the page //TODO: only focus text input on changes to the page
return widget::text_input::focus(self.text_input_id.clone()); return widget::text_input::focus(self.text_input_id.clone());
} }
Message::Submit => match self.prompt_opt.take() { Message::Submit => match self.prompt_opt.take() {
Some((_prompt, _secret, value)) => match self.value_tx_opt.take() { Some((_prompt, _secret, value_opt)) => match value_opt {
Some(value_tx) => { Some(value) => match self.value_tx_opt.take() {
return Command::perform( Some(value_tx) => {
async move { return Command::perform(
value_tx.send(value).await.unwrap(); async move {
message::app(Message::Channel(value_tx)) value_tx.send(value).await.unwrap();
}, message::app(Message::Channel(value_tx))
|x| x, },
); |x| x,
} );
None => log::warn!("tried to submit when value_tx_opt not set"), }
None => log::warn!("tried to submit when value_tx_opt not set"),
},
None => log::warn!("tried to submit without value"),
}, },
None => log::warn!("tried to submit without prompt"), None => log::warn!("tried to submit without prompt"),
}, },
@ -423,22 +460,29 @@ impl cosmic::Application for App {
} }
match &self.prompt_opt { match &self.prompt_opt {
Some((prompt, secret, value)) => { Some((prompt, secret, value_opt)) => match value_opt {
let mut text_input = widget::text_input(&prompt, &value) Some(value) => {
.id(self.text_input_id.clone()) let mut text_input = widget::text_input(&prompt, &value)
.leading_icon(widget::icon::from_name("system-lock-screen-symbolic").into()) .id(self.text_input_id.clone())
.trailing_icon( .leading_icon(
widget::icon::from_name("document-properties-symbolic").into(), widget::icon::from_name("system-lock-screen-symbolic").into(),
) )
.on_input(|value| Message::Prompt(prompt.clone(), *secret, value)) .trailing_icon(
.on_submit(Message::Submit); widget::icon::from_name("document-properties-symbolic").into(),
)
.on_input(|value| Message::Prompt(prompt.clone(), *secret, Some(value)))
.on_submit(Message::Submit);
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()
}
column = column.push(text_input);
} }
None => {
column = column.push(text_input); column = column.push(widget::text(prompt));
} }
},
None => {} None => {}
} }