implement a more consistent login mask with a stable layout and improved error messages

This commit is contained in:
Frederic Laing 2025-11-12 20:35:10 +01:00 committed by Ashley Wulber
parent f7e470ca58
commit 97d69f37e9
5 changed files with 352 additions and 78 deletions

View file

@ -54,10 +54,14 @@ fn main() {
Request::PostAuthMessageResponse { response } => { Request::PostAuthMessageResponse { response } => {
match response.as_deref() { match response.as_deref() {
Some("password") => Response::Success, Some("password") => Response::Success,
_ => Response::Error { _ => {
error_type: ErrorType::AuthError, // Add 1 second delay to simulate real PAM authentication failure behavior
description: "AUTH_ERR".to_string(), tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}, Response::Error {
error_type: ErrorType::AuthError,
description: "AUTH_ERR".to_string(),
}
}
} }
} }
Request::StartSession { .. } => Response::Success, Request::StartSession { .. } => Response::Success,

View file

@ -3,6 +3,7 @@ accessibility = Accessibility
.magnifier = Magnifier .magnifier = Magnifier
.high-contrast = High contrast .high-contrast = High contrast
.invert-colors = Invert Colors .invert-colors = Invert Colors
authenticating = Authenticating...
cancel = Cancel cancel = Cancel
caps-lock = Caps Lock is active. caps-lock = Caps Lock is active.
enter-user = Enter name manually... enter-user = Enter name manually...
@ -25,3 +26,10 @@ shutdown-timeout = The system will shut down automatically in
} }
suspend = Suspend suspend = Suspend
user = User user = User
# Authentication errors
auth-error-default = Authentication failed. Please try again.
auth-error-credentials = Incorrect password. Please check your keyboard layout and try again.
auth-error-denied = Access denied.
auth-error-maxtries = Too many failed authentication attempts.
auth-error-account = Account is unavailable or disabled.

View file

@ -410,6 +410,7 @@ pub enum Message {
HighContrast(bool), HighContrast(bool),
InvertColors(bool), InvertColors(bool),
WaylandUpdate(WaylandUpdate), WaylandUpdate(WaylandUpdate),
SpinnerTick,
} }
impl From<common::Message> for Message { impl From<common::Message> for Message {
@ -438,6 +439,9 @@ pub struct App {
randr_list: Option<cosmic_randr_shell::List>, randr_list: Option<cosmic_randr_shell::List>,
accessibility: Accessibility, accessibility: Accessibility,
authenticating: bool,
spinner_rotation: f32,
spinner_handle: Option<cosmic::iced::task::Handle>,
} }
#[derive(Default)] #[derive(Default)]
@ -617,7 +621,7 @@ impl App {
} }
let item_cnt = items.len(); let item_cnt = items.len();
let menu_button = widget::menu::menu_button(vec![ let menu_button = widget::menu::menu_button(vec![
Element::from(widget::Space::with_width(Length::Fixed(25.0))), Element::from(widget::Space::with_width(Length::Fixed(10.0))),
widget::text(fl!("enter-user")) widget::text(fl!("enter-user"))
.align_x(iced::alignment::Horizontal::Left) .align_x(iced::alignment::Horizontal::Left)
.into(), .into(),
@ -756,6 +760,21 @@ impl App {
.spacing(12.0) .spacing(12.0)
.max_width(280.0); .max_width(280.0);
let military_time = self
.selected_username
.data_idx
.and_then(|i| self.flags.user_datas.get(i))
.map(|user_data| user_data.time_applet_config.military_time)
.unwrap_or_default();
let space_height = match military_time {
true => 63.0,
false => 10.0,
};
// Add top spacing for better visual appearance
// Bottom of the password text input field should align with bottom of time widget
column = column.push(widget::Space::with_height(Length::Fixed(space_height)));
match &self.socket_state { match &self.socket_state {
SocketState::Pending => { SocketState::Pending => {
column = column.push(widget::text("Opening GREETD_SOCK")); column = column.push(widget::text("Opening GREETD_SOCK"));
@ -769,16 +788,32 @@ impl App {
{ {
if !self.entering_name && user_data.name == self.selected_username.username if !self.entering_name && user_data.name == self.selected_username.username
{ {
if let Some(icon) = user_icon { // Display user icon or empty transparent box
if let Some(icon_handle) = user_icon {
column = column.push( column = column.push(
widget::container( widget::container(
widget::image(icon) widget::image(icon_handle)
.width(Length::Fixed(78.0)) .width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)), .height(Length::Fixed(78.0))
.content_fit(iced::ContentFit::Fill),
) )
.padding(0.0)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fixed(78.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
) );
} else {
// Empty transparent box for users without icons
column = column.push(
widget::container(widget::Space::new(
Length::Fixed(78.0),
Length::Fixed(78.0),
))
.padding(0.0)
.width(Length::Fill)
.height(Length::Fixed(78.0))
.align_x(Alignment::Center),
);
} }
column = column.push( column = column.push(
widget::container(widget::text::title4(&user_data.full_name)) widget::container(widget::text::title4(&user_data.full_name))
@ -801,52 +836,59 @@ impl App {
if let Some((prompt, secret, value_opt)) = &self.common.prompt_opt { if let Some((prompt, secret, value_opt)) = &self.common.prompt_opt {
match value_opt { match value_opt {
Some(value) => { Some(value) => {
let text_input_id = self // Only show password input when not authenticating
.common if !self.authenticating {
.surface_names let text_input_id = self
.get(&id) .common
.and_then(|id| self.common.text_input_ids.get(id)) .surface_names
.cloned() .get(&id)
.unwrap_or_else(|| cosmic::widget::Id::new("text_input")); .and_then(|id| self.common.text_input_ids.get(id))
let mut text_input = widget::secure_input( .cloned()
prompt.clone(), .unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
value.as_str(), let mut text_input = widget::secure_input(
Some( 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( common::Message::Prompt(
prompt.clone(), prompt.clone(),
!*secret, *secret,
Some(value.clone()), Some(input),
) )
.into(),
),
*secret,
)
.id(text_input_id)
.on_input(|input| {
common::Message::Prompt(prompt.clone(), *secret, Some(input))
.into() .into()
}) })
.on_submit(|v| Message::Auth(Some(v))); .on_submit(|v| Message::Auth(Some(v)));
if let Some(text_input_id) = self if let Some(text_input_id) = self
.common .common
.surface_names .surface_names
.get(&id) .get(&id)
.and_then(|id| self.common.text_input_ids.get(id)) .and_then(|id| self.common.text_input_ids.get(id))
{ {
text_input = text_input.id(text_input_id.clone()); text_input = text_input.id(text_input_id.clone());
} }
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()
} }
column = column.push(text_input); column = column.push(text_input);
if self.common.caps_lock { if self.common.caps_lock {
column = column.push(widget::text(fl!("caps-lock"))); column = column.push(widget::text(fl!("caps-lock")));
} else if self.common.error_opt.is_none() { } else if self.common.error_opt.is_none() {
column = column.push(widget::text("")); column = column.push(widget::text(""));
}
} }
} }
None => { None => {
@ -868,7 +910,27 @@ impl App {
} }
} }
if let Some(error) = &self.common.error_opt { // Show either authenticating message or error message in the same location
if self.authenticating {
column = column.push(
widget::container(
widget::row::with_capacity(2)
.spacing(8.0)
.align_y(Alignment::Center)
.push(
widget::icon::from_name("process-working-symbolic")
.size(16)
.icon()
.rotation(iced::Rotation::Floating(iced::Radians(
self.spinner_rotation.to_radians(),
))),
)
.push(widget::text(fl!("authenticating"))),
)
.width(Length::Fill)
.align_x(Alignment::Center),
);
} else if let Some(error) = &self.common.error_opt {
column = column.push( column = column.push(
widget::text(error) widget::text(error)
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))), .class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
@ -885,9 +947,8 @@ impl App {
.width(Length::Fill) .width(Length::Fill)
}; };
let menu = widget::container(widget::column::with_children(vec![ let menu = widget::container(widget::column::with_children(vec![
widget::Space::with_height(Length::FillPortion(1)).into(),
widget::layer_container( widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Center), iced::widget::row![left_element, right_element].align_y(Alignment::Start),
) )
.layer(cosmic::cosmic_theme::Layer::Background) .layer(cosmic::cosmic_theme::Layer::Background)
.padding(16) .padding(16)
@ -905,7 +966,7 @@ impl App {
.class(cosmic::theme::Container::Background) .class(cosmic::theme::Container::Background)
.width(Length::Fixed(800.0)) .width(Length::Fixed(800.0))
.into(), .into(),
widget::Space::with_height(Length::FillPortion(4)).into(), widget::Space::with_height(Length::Fill).into(),
])) ]))
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@ -1114,6 +1175,9 @@ impl cosmic::Application for App {
theme_builder: Default::default(), theme_builder: Default::default(),
randr_list: None, randr_list: None,
surface_id_pairs: Vec::new(), surface_id_pairs: Vec::new(),
authenticating: false,
spinner_rotation: 0.0,
spinner_handle: None,
}; };
(app, Task::batch(tasks)) (app, Task::batch(tasks))
} }
@ -1297,6 +1361,7 @@ impl cosmic::Application for App {
} }
if self.entering_name || username != self.selected_username.username { if self.entering_name || username != self.selected_username.username {
self.entering_name = false; self.entering_name = false;
self.authenticating = false;
let data_idx = self let data_idx = self
.flags .flags
.user_datas .user_datas
@ -1408,13 +1473,40 @@ impl cosmic::Application for App {
} }
} }
Message::Auth(response) => { Message::Auth(response) => {
self.common.prompt_opt = None;
self.common.error_opt = None; self.common.error_opt = None;
self.authenticating = true;
self.send_request(Request::PostAuthMessageResponse { response }); self.send_request(Request::PostAuthMessageResponse { response });
// Start spinner animation if not already running
if self.spinner_handle.is_none() {
let (spinner_task, handle) = cosmic::task::stream(
cosmic::iced_futures::stream::channel(1, |mut msg_tx| async move {
let mut interval = time::interval(Duration::from_millis(16)); // ~60fps
loop {
msg_tx
.send(cosmic::Action::App(Message::SpinnerTick))
.await
.unwrap();
interval.tick().await;
}
}),
)
.abortable();
self.spinner_handle = Some(handle);
return spinner_task;
}
} }
Message::Login => { Message::Login => {
self.common.prompt_opt = None; self.common.prompt_opt = None;
self.common.error_opt = None; self.common.error_opt = None;
self.authenticating = false;
// Stop spinner animation
if let Some(handle) = self.spinner_handle.take() {
handle.abort();
}
self.spinner_rotation = 0.0;
match self.flags.sessions.get(&self.selected_session).cloned() { match self.flags.sessions.get(&self.selected_session).cloned() {
Some((cmd, env)) => { Some((cmd, env)) => {
self.send_request(Request::StartSession { cmd, env }); self.send_request(Request::StartSession { cmd, env });
@ -1425,6 +1517,14 @@ impl cosmic::Application for App {
} }
Message::Error(error) => { Message::Error(error) => {
self.common.error_opt = Some(error); self.common.error_opt = Some(error);
self.authenticating = false;
// Stop spinner animation
if let Some(handle) = self.spinner_handle.take() {
handle.abort();
}
self.spinner_rotation = 0.0;
self.send_request(Request::CancelSession); self.send_request(Request::CancelSession);
} }
Message::Reconnect => { Message::Reconnect => {
@ -1754,6 +1854,10 @@ impl cosmic::Application for App {
}; };
return reposition_subsurface(*subsurface_id, loc.x as i32, loc.y as i32); return reposition_subsurface(*subsurface_id, loc.x as i32, loc.y as i32);
} }
Message::SpinnerTick => {
// Update spinner rotation angle (360 degrees per second = 6 degrees per frame at 60fps)
self.spinner_rotation = (self.spinner_rotation + 6.0) % 360.0;
}
} }
Task::none() Task::none()
} }

View file

@ -10,7 +10,31 @@ use std::time::Duration;
use tokio::net::UnixStream; use tokio::net::UnixStream;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::common; use crate::{common, fl};
/// Convert greetd error descriptions to user-friendly localized messages
fn greetd_error_to_message(error_type: greetd_ipc::ErrorType, description: &str) -> String {
use greetd_ipc::ErrorType;
match error_type {
ErrorType::AuthError => {
// For authentication errors, check description for specific error types
if description.contains("PERM_DENIED") {
fl!("auth-error-denied")
} else if description.contains("MAXTRIES") {
fl!("auth-error-maxtries")
} else if description.contains("ACCT_EXPIRED") || description.contains("USER_UNKNOWN") {
fl!("auth-error-account")
} else {
fl!("auth-error-credentials")
}
}
ErrorType::Error => {
// For generic errors, show a generic message
fl!("auth-error-default")
}
}
}
pub fn subscription() -> Subscription<Message> { pub fn subscription() -> Subscription<Message> {
struct GreetdSubscription; struct GreetdSubscription;
@ -89,10 +113,9 @@ pub fn subscription() -> Subscription<Message> {
} }
}, },
greetd_ipc::Response::Error { greetd_ipc::Response::Error {
error_type: _, error_type,
description, description,
} => { } => {
//TODO: use error_type?
match request { match request {
greetd_ipc::Request::CancelSession => { greetd_ipc::Request::CancelSession => {
// Do not send errors for cancel session to gui // Do not send errors for cancel session to gui
@ -105,7 +128,12 @@ pub fn subscription() -> Subscription<Message> {
break; break;
} }
_ => { _ => {
_ = sender.send(Message::Error(description)).await; _ = sender
.send(Message::Error(greetd_error_to_message(
error_type,
&description,
)))
.await;
} }
} }
} }

View file

@ -103,6 +103,31 @@ pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
/// Convert PAM errors to user-friendly localized messages
fn pam_error_to_message(error: &pam_client::Error) -> String {
use pam_client::ErrorCode;
// Use the structured error code instead of string matching for reliability
match error.code() {
ErrorCode::AUTH_ERR | ErrorCode::CRED_INSUFFICIENT => {
fl!("auth-error-credentials")
}
ErrorCode::PERM_DENIED => {
fl!("auth-error-denied")
}
ErrorCode::MAXTRIES => {
fl!("auth-error-maxtries")
}
ErrorCode::ACCT_EXPIRED | ErrorCode::USER_UNKNOWN => {
fl!("auth-error-account")
}
_ => {
// For any other error, show a generic message
fl!("auth-error-default")
}
}
}
pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pam_client::Error> { pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pam_client::Error> {
//TODO: send errors to GUI, restart process //TODO: send errors to GUI, restart process
@ -241,6 +266,7 @@ pub enum Message {
Error(String), Error(String),
Lock, Lock,
Unlock, Unlock,
SpinnerTick,
} }
impl From<common::Message> for Message { impl From<common::Message> for Message {
@ -277,6 +303,9 @@ pub struct App {
dropdown_opt: Option<Dropdown>, dropdown_opt: Option<Dropdown>,
inhibit_opt: Option<Arc<OwnedFd>>, inhibit_opt: Option<Arc<OwnedFd>>,
value_tx_opt: Option<mpsc::Sender<String>>, value_tx_opt: Option<mpsc::Sender<String>>,
authenticating: bool,
spinner_rotation: f32,
spinner_handle: Option<cosmic::iced::task::Handle>,
} }
impl App { impl App {
@ -422,16 +451,39 @@ impl App {
.spacing(12.0) .spacing(12.0)
.max_width(280.0); .max_width(280.0);
if let Some(icon) = &self.flags.user_icon { let military_time = self.flags.user_data.time_applet_config.military_time;
let space_height = match military_time {
true => 63.0,
false => 10.0,
};
// Add top spacing for better visual appearance
// Bottom of the password text input field should align with bottom of time widget
column = column.push(widget::Space::with_height(Length::Fixed(space_height)));
// Display user icon or empty transparent box
if let Some(icon_handle) = &self.flags.user_icon {
column = column.push( column = column.push(
widget::container( widget::container(
widget::image(icon) widget::image(icon_handle)
.width(Length::Fixed(78.0)) .width(Length::Fixed(78.0))
.height(Length::Fixed(78.0)), .height(Length::Fixed(78.0))
.content_fit(iced::ContentFit::Fill),
) )
.padding(0.0)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fixed(78.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
) );
} else {
// Empty transparent box for users without icons
column = column.push(
widget::container(widget::Space::new(Length::Fixed(78.0), Length::Fixed(78.0)))
.padding(0.0)
.width(Length::Fill)
.height(Length::Fixed(78.0))
.align_x(Alignment::Center),
);
} }
column = column.push( column = column.push(
@ -464,11 +516,17 @@ impl App {
), ),
*secret, *secret,
) )
.id(text_input_id) .id(text_input_id);
.on_input(|input| {
common::Message::Prompt(prompt.clone(), *secret, Some(input)).into() // Don't allow input when authenticating
}) if !self.authenticating {
.on_submit(Message::Submit); text_input = text_input
.on_input(|input| {
common::Message::Prompt(prompt.clone(), *secret, Some(input))
.into()
})
.on_submit(Message::Submit);
}
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()
@ -476,7 +534,7 @@ impl App {
column = column.push(text_input); column = column.push(text_input);
if self.common.caps_lock { if self.common.caps_lock && !self.authenticating {
column = column.push(widget::text(fl!("caps-lock"))); column = column.push(widget::text(fl!("caps-lock")));
} else if self.common.error_opt.is_none() { } else if self.common.error_opt.is_none() {
column = column.push(widget::text("")); column = column.push(widget::text(""));
@ -488,7 +546,27 @@ impl App {
} }
} }
if let Some(error) = &self.common.error_opt { // Show either authenticating message or error message in the same location
if self.authenticating {
column = column.push(
widget::container(
widget::row::with_capacity(2)
.spacing(8.0)
.align_y(Alignment::Center)
.push(
widget::icon::from_name("process-working-symbolic")
.size(16)
.icon()
.rotation(iced::Rotation::Floating(iced::Radians(
self.spinner_rotation.to_radians(),
))),
)
.push(widget::text(fl!("authenticating"))),
)
.width(Length::Fill)
.align_x(Alignment::Center),
);
} else if let Some(error) = &self.common.error_opt {
column = column.push( column = column.push(
widget::text(error) widget::text(error)
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))), .class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
@ -506,9 +584,8 @@ impl App {
}; };
widget::container(widget::column::with_children(vec![ widget::container(widget::column::with_children(vec![
widget::Space::with_height(Length::FillPortion(1)).into(),
widget::layer_container( widget::layer_container(
iced::widget::row![left_element, right_element].align_y(Alignment::Center), iced::widget::row![left_element, right_element].align_y(Alignment::Start),
) )
.layer(cosmic::cosmic_theme::Layer::Background) .layer(cosmic::cosmic_theme::Layer::Background)
.padding(16) .padding(16)
@ -526,7 +603,7 @@ impl App {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Shrink) .height(Length::Shrink)
.into(), .into(),
widget::Space::with_height(Length::FillPortion(4)).into(), widget::Space::with_height(Length::Fill).into(),
])) ]))
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@ -579,6 +656,9 @@ impl cosmic::Application for App {
dropdown_opt: None, dropdown_opt: None,
inhibit_opt: None, inhibit_opt: None,
value_tx_opt: None, value_tx_opt: None,
authenticating: false,
spinner_rotation: 0.0,
spinner_handle: None,
}; };
let task = if cfg!(feature = "logind") { let task = if cfg!(feature = "logind") {
@ -817,7 +897,7 @@ impl cosmic::Application for App {
tracing::warn!("authentication error: {}", err); tracing::warn!("authentication error: {}", err);
msg_tx msg_tx
.send(cosmic::Action::App(Message::Error( .send(cosmic::Action::App(Message::Error(
err.to_string(), pam_error_to_message(&err),
))) )))
.await .await
.unwrap(); .unwrap();
@ -941,16 +1021,41 @@ impl cosmic::Application for App {
} }
} }
Message::Submit(value) => { Message::Submit(value) => {
self.common.prompt_opt = None;
self.common.error_opt = None; self.common.error_opt = None;
self.authenticating = true;
match self.value_tx_opt.take() { match self.value_tx_opt.take() {
Some(value_tx) => { Some(value_tx) => {
// Clear errors // Start spinner animation if not already running
self.common.error_opt = None; if self.spinner_handle.is_none() {
return cosmic::task::future(async move { let (spinner_task, handle) = cosmic::task::stream(
value_tx.send(value).await.unwrap(); cosmic::iced_futures::stream::channel(1, |mut msg_tx| async move {
Message::Channel(value_tx) let mut interval =
}); tokio::time::interval(Duration::from_millis(16)); // ~60fps
loop {
msg_tx
.send(cosmic::Action::App(Message::SpinnerTick))
.await
.unwrap();
interval.tick().await;
}
}),
)
.abortable();
self.spinner_handle = Some(handle);
return Task::batch([
spinner_task,
cosmic::task::future(async move {
value_tx.send(value).await.unwrap();
Message::Channel(value_tx)
}),
]);
} else {
return cosmic::task::future(async move {
value_tx.send(value).await.unwrap();
Message::Channel(value_tx)
});
}
} }
None => tracing::warn!("tried to submit when value_tx_opt not set"), None => tracing::warn!("tried to submit when value_tx_opt not set"),
} }
@ -968,6 +1073,17 @@ impl cosmic::Application for App {
} }
Message::Error(error) => { Message::Error(error) => {
self.common.error_opt = Some(error); self.common.error_opt = Some(error);
self.authenticating = false;
// Stop spinner animation
if let Some(handle) = self.spinner_handle.take() {
handle.abort();
}
self.spinner_rotation = 0.0;
}
Message::SpinnerTick => {
// Update spinner rotation angle (360 degrees per second = 6 degrees per frame at 60fps)
self.spinner_rotation = (self.spinner_rotation + 6.0) % 360.0;
} }
Message::Lock => match self.state { Message::Lock => match self.state {
State::Unlocked => { State::Unlocked => {
@ -977,6 +1093,12 @@ impl cosmic::Application for App {
self.common.error_opt = None; self.common.error_opt = None;
// Clear value_tx // Clear value_tx
self.value_tx_opt = None; self.value_tx_opt = None;
// Reset authenticating state
self.authenticating = false;
if let Some(handle) = self.spinner_handle.take() {
handle.abort();
}
self.spinner_rotation = 0.0;
// Try to create lockfile when locking // Try to create lockfile when locking
if let Some(ref lockfile) = self.flags.lockfile_opt { if let Some(ref lockfile) = self.flags.lockfile_opt {
if let Err(err) = fs::File::create(lockfile) { if let Err(err) = fs::File::create(lockfile) {
@ -1002,6 +1124,14 @@ impl cosmic::Application for App {
self.common.error_opt = None; self.common.error_opt = None;
// Clear value_tx // Clear value_tx
self.value_tx_opt = None; self.value_tx_opt = None;
// Stop authenticating
self.authenticating = false;
// Stop spinner animation
if let Some(handle) = self.spinner_handle.take() {
handle.abort();
}
self.spinner_rotation = 0.0;
// Try to delete lockfile when unlocking // Try to delete lockfile when unlocking
if let Some(ref lockfile) = self.flags.lockfile_opt { if let Some(ref lockfile) = self.flags.lockfile_opt {
if let Err(err) = fs::remove_file(lockfile) { if let Err(err) = fs::remove_file(lockfile) {