Fix lock-screen input and wallpaper handling

This commit is contained in:
Samuel Onoja committed 2026-10-07 07:19:06 +01:00
1 parent bbc5b27c25
commit cfdd2a148a
7 files changed
+348 -55

No files matched your search

+28 -12
View File
@@ -120,18 +120,7 @@ impl Ferese {
} }
if self.session_lock.active() { if self.session_lock.active() {
// An already-bound IME or drag client may install a grab after the self.prepare_lock_input();
// lock request. Never let that grab receive subsequent lock input.
if let Some(keyboard) = seat.get_keyboard() {
keyboard.unset_grab(self);
}
if let Some(pointer) = seat.get_pointer() {
pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), 0);
}
if let Some(touch) = seat.get_touch() {
touch.unset_grab(self);
}
self.focus_lock_surface();
} }
if self.input_capture.active() if self.input_capture.active()
@@ -514,6 +503,33 @@ impl Ferese {
} }
let pointer = self.seat.get_pointer().expect("seat has a pointer"); let pointer = self.seat.get_pointer().expect("seat has a pointer");
let serial = SERIAL_COUNTER.next_serial(); let serial = SERIAL_COUNTER.next_serial();
if self.session_lock.active() {
if event.state() == ButtonState::Pressed {
self.focus_window_at(pointer.current_location(), serial, true);
if !pointer.is_grabbed() {
pointer.motion(
self,
self.lock_surface_under(pointer.current_location()),
&MotionEvent {
location: pointer.current_location(),
serial,
time: event.time_msec(),
},
);
}
}
pointer.button(
self,
&ButtonEvent {
button: event.button_code(),
state: event.state(),
serial,
time: event.time_msec(),
},
);
pointer.frame(self);
return;
}
// Native popup_done destroys Iced's window immediately. For // Native popup_done destroys Iced's window immediately. For
// effects-capable shell popups, release input now but defer // effects-capable shell popups, release input now but defer
+2
View File
@@ -2,6 +2,8 @@
//! logical pixels. Content, clips and decorations share those exact edges. //! logical pixels. Content, clips and decorations share those exact edges.
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use ferese_animation::AnimatedRect;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates}; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
+56 -4
View File
@@ -273,6 +273,58 @@ impl Ferese {
}) })
} }
pub(crate) fn prepare_lock_input(&mut self) {
// Reject desktop grabs installed after acquisition, but keep the lock
// owner's implicit click grab intact until its button release.
if let Some(keyboard) = self.seat.get_keyboard()
&& keyboard.is_grabbed()
{
keyboard.unset_grab(self);
}
if let Some(pointer) = self.seat.get_pointer() {
let lock_click = pointer
.with_grab(|_, grab| {
grab.is::<smithay::input::pointer::ClickGrab<Ferese>>()
&& grab.start_data().focus.as_ref().is_some_and(|(surface, _)| {
self.session_lock
.surfaces
.values()
.any(|lock| lock.alive() && lock.wl_surface() == surface)
})
})
.unwrap_or(false);
if pointer.is_grabbed() && !lock_click {
pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), self.last_pointer_time);
}
let stale_focus = pointer.current_focus().is_some_and(|surface| {
!self
.session_lock
.surfaces
.values()
.any(|lock| lock.alive() && lock.wl_surface() == &surface)
});
if stale_focus {
let location = pointer.current_location();
pointer.motion(
self,
self.lock_surface_under(location),
&smithay::input::pointer::MotionEvent {
location,
serial: SERIAL_COUNTER.next_serial(),
time: self.last_pointer_time,
},
);
pointer.frame(self);
}
}
if let Some(touch) = self.seat.get_touch()
&& touch.is_grabbed()
{
touch.unset_grab(self);
}
self.focus_lock_surface();
}
pub(crate) fn focus_lock_surface(&mut self) { pub(crate) fn focus_lock_surface(&mut self) {
let surface = self let surface = self
.focused_output() .focused_output()
@@ -289,10 +341,10 @@ impl Ferese {
.map(|(_, surface)| surface) .map(|(_, surface)| surface)
}) })
.map(|surface| surface.wl_surface().clone()); .map(|surface| surface.wl_surface().clone());
self.seat let keyboard = self.seat.get_keyboard().unwrap();
.get_keyboard() if keyboard.current_focus() != surface {
.unwrap() keyboard.set_focus(self, surface, SERIAL_COUNTER.next_serial());
.set_focus(self, surface, SERIAL_COUNTER.next_serial()); }
} }
pub(crate) fn lock_frame_presented(&mut self, output: &Output) { pub(crate) fn lock_frame_presented(&mut self, output: &Output) {
+63
View File
@@ -626,3 +626,66 @@ fn acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame() {
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
replacement.expect_locked(); replacement.expect_locked();
} }
#[test]
fn lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces")
else {
return;
};
let output = fixture.output("lock-pointer");
let mut owner = fixture.acquire();
let surface = owner.surface(&mut fixture, &output);
let pointer = fixture.state.seat.get_pointer().unwrap();
let serial = SERIAL_COUNTER.next_serial();
pointer.motion(
&mut fixture.state,
Some((surface.wl_surface().clone(), (0.0, 0.0).into())),
&smithay::input::pointer::MotionEvent {
location: (10.0, 10.0).into(),
serial,
time: 1,
},
);
pointer.button(
&mut fixture.state,
&smithay::input::pointer::ButtonEvent {
button: 0x110,
state: smithay::backend::input::ButtonState::Pressed,
serial,
time: 2,
},
);
assert!(pointer.is_grabbed());
fixture.state.prepare_lock_input();
assert!(
pointer.is_grabbed(),
"the lock owner's click must survive until release"
);
pointer.button(
&mut fixture.state,
&smithay::input::pointer::ButtonEvent {
button: 0x110,
state: smithay::backend::input::ButtonState::Released,
serial: SERIAL_COUNTER.next_serial(),
time: 3,
},
);
assert!(!pointer.is_grabbed());
pointer.button(
&mut fixture.state,
&smithay::input::pointer::ButtonEvent {
button: 0x110,
state: smithay::backend::input::ButtonState::Pressed,
serial: SERIAL_COUNTER.next_serial(),
time: 4,
},
);
assert!(pointer.is_grabbed());
fixture.state.session_lock.surfaces.clear();
fixture.state.prepare_lock_input();
assert!(!pointer.is_grabbed(), "a retired surface cannot keep a lock grab");
assert_eq!(pointer.current_focus(), None);
assert_eq!(fixture.state.seat.get_keyboard().unwrap().current_focus(), None);
}
+18 -5
View File
@@ -1,5 +1,6 @@
//! One decoded image and one texture per GPU context, not double-buffered //! One wallpaper texture per GPU context. Decoded pixels are staging data,
//! full-screen UI surfaces per monitor. Resize changes sampling only. //! released after upload and decoded again when a new context needs them.
//! Resize changes sampling only.
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@@ -279,7 +280,7 @@ impl WallpaperState {
width = pixels.width(), width = pixels.width(),
height = pixels.height(), height = pixels.height(),
bytes = pixels.as_raw().len(), bytes = pixels.as_raw().len(),
"decoded compositor wallpaper once" "decoded compositor wallpaper for texture upload"
); );
self.pixels = Some(pixels); self.pixels = Some(pixels);
@@ -540,6 +541,10 @@ mod tests {
fn decoder_wakes_an_idle_event_loop() { fn decoder_wakes_an_idle_event_loop() {
use smithay::reexports::calloop::EventLoop; use smithay::reexports::calloop::EventLoop;
use smithay::reexports::calloop::timer::Timer; use smithay::reexports::calloop::timer::Timer;
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("wallpaper.png");
let expected = image::RgbaImage::from_pixel(2, 2, image::Rgba([32, 80, 160, 255]));
expected.save(&path).unwrap();
let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap(); let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap();
event_loop event_loop
.handle() .handle()
@@ -548,11 +553,19 @@ mod tests {
}) })
.unwrap(); .unwrap();
let signal = event_loop.get_signal(); let signal = event_loop.get_signal();
let mut state = WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone())); let mut state = WallpaperState::with_wakeup(
WallpaperConfig {
path: Some(path),
mode: WallpaperMode::Fill,
},
Some(signal.clone()),
);
event_loop event_loop
.run(None, &mut state, |state| { .run(None, &mut state, |state| {
if state.poll() { if state.poll() {
assert!(state.pixels.is_some()); let decoded = state.pixels.as_ref().unwrap();
assert_eq!(decoded.dimensions(), expected.dimensions());
assert_eq!(&decoded.as_raw()[..], expected.as_raw());
signal.stop(); signal.stop();
} }
}) })
+145 -10
View File
@@ -20,6 +20,8 @@ pub struct Appearance {
pub wallpaper_path: std::path::PathBuf, pub wallpaper_path: std::path::PathBuf,
pub wallpaper_blur: f32, pub wallpaper_blur: f32,
pub wallpaper_revision: u64, pub wallpaper_revision: u64,
wallpaper_loaded: bool,
wallpaper_pending: Option<(std::path::PathBuf, bool)>,
pub reduce_transparency: bool, pub reduce_transparency: bool,
pub avatar: Option<image::Handle>, pub avatar: Option<image::Handle>,
} }
@@ -75,8 +77,9 @@ impl Appearance {
.unwrap_or(18.) .unwrap_or(18.)
.clamp(0., 40.) as f32; .clamp(0., 40.) as f32;
let reduce_transparency = theme.accessibility.reduce_transparency; let reduce_transparency = theme.accessibility.reduce_transparency;
let wallpaper = let loaded = load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur });
load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur }).unwrap_or_else(|_| { let wallpaper_loaded = loaded.is_ok();
let wallpaper = loaded.unwrap_or_else(|_| {
image::Handle::from_bytes(include_bytes!("../../../assets/wallpapers/ferese.png").as_slice()) image::Handle::from_bytes(include_bytes!("../../../assets/wallpapers/ferese.png").as_slice())
}); });
let boolean = |key, fallback| { let boolean = |key, fallback| {
@@ -107,11 +110,60 @@ impl Appearance {
wallpaper_path, wallpaper_path,
wallpaper_blur: blur, wallpaper_blur: blur,
wallpaper_revision: 0, wallpaper_revision: 0,
wallpaper_loaded,
wallpaper_pending: None,
reduce_transparency, reduce_transparency,
avatar: account_picture(user), avatar: account_picture(user),
} }
} }
pub fn request_wallpaper(
&mut self,
theme: &ferese_config::theme::ResolvedTheme,
) -> Option<(u64, std::path::PathBuf, f32)> {
let path = theme
.tokens
.background
.lock_path
.as_ref()
.or(theme.tokens.background.path.as_ref())
.cloned()
.unwrap_or_else(|| ferese_config::default_wallpaper_for(theme.appearance).into());
let reduce = theme.accessibility.reduce_transparency;
let desired = (path.clone(), reduce);
if self.wallpaper_pending.as_ref() == Some(&desired) {
return None;
}
if self.wallpaper_loaded && path == self.wallpaper_path && reduce == self.reduce_transparency {
if self.wallpaper_pending.take().is_some() {
self.wallpaper_revision = self.wallpaper_revision.wrapping_add(1);
}
return None;
}
self.wallpaper_pending = Some(desired);
self.wallpaper_revision = self.wallpaper_revision.wrapping_add(1);
Some((
self.wallpaper_revision,
path,
if reduce { 0.0 } else { self.wallpaper_blur },
))
}
pub fn finish_wallpaper(&mut self, revision: u64, result: Result<image::Handle, String>) -> Result<(), String> {
if revision != self.wallpaper_revision {
return Ok(());
}
let Some((path, reduce)) = self.wallpaper_pending.take() else {
return Ok(());
};
let wallpaper = result?;
self.wallpaper = wallpaper;
self.wallpaper_path = path;
self.reduce_transparency = reduce;
self.wallpaper_loaded = true;
Ok(())
}
pub fn clock_format(&self) -> &'static str { pub fn clock_format(&self) -> &'static str {
if self.twelve_hour { "%I:%M %p" } else { "%H:%M" } if self.twelve_hour { "%I:%M %p" } else { "%H:%M" }
} }
@@ -215,6 +267,17 @@ fn decode_avatar(path: &std::path::Path) -> Result<::image::RgbaImage, String> {
} }
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> { pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
let result = prepare_wallpaper(path, blur);
// Decode and resize scratch buffers have been dropped. Return their freed
// pages even when glibc would otherwise retain them in an arena.
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe {
libc::malloc_trim(0);
}
result
}
fn prepare_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
use ::image::ImageDecoder; use ::image::ImageDecoder;
let mut reader = ::image::ImageReader::open(path) let mut reader = ::image::ImageReader::open(path)
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
@@ -225,16 +288,18 @@ pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle
reader.limits(limits); reader.limits(limits);
let decoder = reader.into_decoder().map_err(|e| e.to_string())?; let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
let (width, height) = decoder.dimensions(); let (width, height) = decoder.dimensions();
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 { if width == 0 || height == 0 || u64::from(width) * u64::from(height) > (256 * 1024 * 1024) / 4 {
return Err("Lock wallpaper exceeds the decode limit".into()); return Err("Lock wallpaper exceeds the decode limit".into());
} }
let pixels = ::image::DynamicImage::from_decoder(decoder) if blur <= 0. {
.map_err(|e| e.to_string())? // Iced decodes the path and releases its host buffer after GPU upload.
.into_rgba8(); // An RGBA handle would retain those pixels for the handle's lifetime.
let pixels = if blur > 0. { return Ok(image::Handle::from_path(path));
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur) }
} else { let pixels = {
pixels let decoded = ::image::DynamicImage::from_decoder(decoder).map_err(|e| e.to_string())?;
let thumbnail = decoded.thumbnail(640, 640).into_rgba8();
::image::imageops::blur(&thumbnail, blur)
}; };
Ok(image::Handle::from_rgba( Ok(image::Handle::from_rgba(
pixels.width(), pixels.width(),
@@ -246,6 +311,76 @@ pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn wallpaper_handles_keep_only_paths_or_blurred_thumbnails() {
let file = tempfile::NamedTempFile::new().unwrap();
::image::RgbaImage::from_pixel(1280, 720, ::image::Rgba([32, 80, 160, 255]))
.save_with_format(file.path(), ::image::ImageFormat::Png)
.unwrap();
match load_wallpaper(file.path(), 0.).unwrap() {
image::Handle::Path(_, path) => assert_eq!(path, file.path()),
_ => panic!("unblurred wallpaper must not retain RGBA pixels"),
}
match load_wallpaper(file.path(), 2.).unwrap() {
image::Handle::Rgba {
width, height, pixels, ..
} => {
assert_eq!((width, height), (640, 360));
assert_eq!(pixels.len(), 640 * 360 * 4);
}
_ => panic!("blurred wallpaper must retain only the processed thumbnail"),
}
}
#[test]
fn wallpaper_path_handles_require_a_valid_image_header() {
let file = tempfile::NamedTempFile::new().unwrap();
assert!(load_wallpaper(file.path(), 0.).is_err());
std::fs::write(file.path(), b"not an image").unwrap();
assert!(load_wallpaper(file.path(), 0.).is_err());
assert!(load_wallpaper(&file.path().with_extension("missing"), 0.).is_err());
}
#[test]
fn wallpaper_requests_retry_failures_and_ignore_superseded_results() {
let directory = tempfile::tempdir().unwrap();
let initial = directory.path().join("initial.png");
::image::RgbaImage::new(2, 2).save(&initial).unwrap();
let mut theme = ferese_config::theme::default_theme();
theme.tokens.background.path = Some(initial.clone());
theme.tokens.background.lock_path = None;
let document = Document::parse("").unwrap();
let mut appearance = Appearance::from_document("no-such-test-user", Some(document), &theme);
assert!(appearance.request_wallpaper(&theme).is_none());
let next = directory.path().join("next.png");
theme.tokens.background.path = Some(next.clone());
let (failed, _, _) = appearance.request_wallpaper(&theme).unwrap();
assert!(appearance.request_wallpaper(&theme).is_none());
assert!(
appearance
.finish_wallpaper(failed, Err("decode failed".into()))
.is_err()
);
assert_eq!(appearance.wallpaper_path, initial);
let (retry, path, _) = appearance.request_wallpaper(&theme).unwrap();
assert_eq!(path, next);
let last = directory.path().join("last.png");
theme.tokens.background.path = Some(last.clone());
let (latest, _, _) = appearance.request_wallpaper(&theme).unwrap();
appearance
.finish_wallpaper(retry, Ok(image::Handle::from_rgba(1, 1, vec![255; 4])))
.unwrap();
assert_eq!(appearance.wallpaper_path, initial);
appearance
.finish_wallpaper(latest, Ok(image::Handle::from_rgba(1, 1, vec![255; 4])))
.unwrap();
assert_eq!(appearance.wallpaper_path, last);
assert!(appearance.request_wallpaper(&theme).is_none());
theme.tokens.background.lock_path = Some(initial.clone());
let (_, path, _) = appearance.request_wallpaper(&theme).unwrap();
assert_eq!(path, initial, "an explicit lock image overrides the desktop wallpaper");
}
#[test] #[test]
fn avatar_decoding_bounds_dimensions_and_encoded_size() { fn avatar_decoding_bounds_dimensions_and_encoded_size() {
let file = tempfile::NamedTempFile::new().unwrap(); let file = tempfile::NamedTempFile::new().unwrap();
+36 -24
View File
@@ -88,6 +88,7 @@ enum Message {
Authenticated(bool), Authenticated(bool),
Tick, Tick,
RetryReady, RetryReady,
FocusPassword,
} }
// Never derive Debug for password-bearing messages. // Never derive Debug for password-bearing messages.
@@ -111,6 +112,15 @@ impl AuthState {
matches!(self, Self::Ready) matches!(self, Self::Ready)
} }
fn retry_ready(&mut self, now: Instant) -> bool {
if matches!(self, Self::Rejected(at) if now.saturating_duration_since(*at) >= Duration::from_secs(2)) {
*self = Self::Ready;
true
} else {
false
}
}
fn complete(&mut self, success: bool) -> bool { fn complete(&mut self, success: bool) -> bool {
if *self != Self::Checking { if *self != Self::Checking {
return false; return false;
@@ -228,22 +238,7 @@ impl cosmic::Application for Locker {
self.appearance.radius = palette.radius; self.appearance.radius = palette.radius;
self.appearance.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); self.appearance.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
let theme = cosmic::command::set_theme(self.appearance.theme()); let theme = cosmic::command::set_theme(self.appearance.theme());
let target = &snapshot.theme; if let Some((revision, path, blur)) = self.appearance.request_wallpaper(&snapshot.theme) {
let path = target
.tokens
.background
.lock_path
.as_ref()
.or(target.tokens.background.path.as_ref())
.cloned()
.unwrap_or_else(|| ferese_config::default_wallpaper().into());
let reduce = target.accessibility.reduce_transparency;
if path != self.appearance.wallpaper_path || reduce != self.appearance.reduce_transparency {
self.appearance.wallpaper_path = path.clone();
self.appearance.reduce_transparency = reduce;
self.appearance.wallpaper_revision = self.appearance.wallpaper_revision.wrapping_add(1);
let revision = self.appearance.wallpaper_revision;
let blur = if reduce { 0. } else { self.appearance.wallpaper_blur };
let wallpaper = cosmic::task::future(async move { let wallpaper = cosmic::task::future(async move {
let result = tokio::task::spawn_blocking(move || appearance::load_wallpaper(&path, blur)) let result = tokio::task::spawn_blocking(move || appearance::load_wallpaper(&path, blur))
.await .await
@@ -255,11 +250,8 @@ impl cosmic::Application for Locker {
return theme; return theme;
} }
Message::WallpaperLoaded(revision, result) => { Message::WallpaperLoaded(revision, result) => {
if revision == self.appearance.wallpaper_revision { if let Err(error) = self.appearance.finish_wallpaper(revision, result) {
match result { eprintln!("ferese-lock: retained wallpaper: {error}");
Ok(image) => self.appearance.wallpaper = image,
Err(error) => eprintln!("ferese-lock: retained wallpaper: {error}"),
}
} }
} }
Message::Event(event) => return self.handle_event(*event), Message::Event(event) => return self.handle_event(*event),
@@ -269,8 +261,13 @@ impl cosmic::Application for Locker {
self.date = now.strftime("%A, %B %-d").to_string(); self.date = now.strftime("%A, %B %-d").to_string();
} }
Message::RetryReady => { Message::RetryReady => {
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) { if self.state.retry_ready(Instant::now()) {
self.state = AuthState::Ready; return focus();
}
}
Message::FocusPassword => {
if self.state.may_submit() {
return focus();
} }
} }
Message::Input(value) => { Message::Input(value) => {
@@ -546,7 +543,9 @@ impl Locker {
.height(Length::Fill) .height(Length::Fill)
.content_fit(iced::ContentFit::Cover) .content_fit(iced::ContentFit::Cover)
.into(); .into();
iced::widget::stack![background, foreground].into() widget::mouse_area(iced::widget::stack![background, foreground])
.on_press(Message::FocusPassword)
.into()
} }
} }
@@ -567,6 +566,19 @@ fn ferese_symbol() -> widget::icon::Icon {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn retry_reenables_input_only_after_the_deadline() {
let now = Instant::now();
let mut state = AuthState::Rejected(now);
assert!(!state.retry_ready(now + Duration::from_millis(1999)));
assert!(!state.may_submit());
assert!(state.retry_ready(now + Duration::from_secs(2)));
assert!(state.may_submit());
assert!(!state.retry_ready(now + Duration::from_secs(3)));
state = AuthState::Checking;
assert!(!state.retry_ready(now + Duration::from_secs(3)));
}
#[test] #[test]
fn only_an_active_successful_attempt_unlocks() { fn only_an_active_successful_attempt_unlocks() {
let mut state = AuthState::Ready; let mut state = AuthState::Ready;