Skip to main content

spell_framework/wayland_adapter/
lock.rs

1use crate::{
2    SpellAssociatedNew,
3    configure::{LayerConf, set_up_tracing},
4    slint_adapter::{SpellLockShell, SpellMultiWinHandler, SpellSkiaWinAdapter},
5    wayland_adapter::{
6        common::PointerState,
7        lock::{self, wayland::SpellSlintLock},
8    },
9};
10use i_slint_core::items::MouseCursor;
11use nonstick::{
12    AuthnFlags, ConversationAdapter, Result as PamResult, Transaction, TransactionBuilder,
13};
14use slint::PhysicalSize;
15use smithay_client_toolkit::{
16    compositor::CompositorState,
17    delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry,
18    delegate_seat, delegate_session_lock, delegate_shm, delegate_touch,
19    output::{self, OutputState},
20    reexports::{
21        calloop::{
22            self, EventLoop, LoopHandle, RegistrationToken,
23            channel::{self, Sender},
24        },
25        calloop_wayland_source::WaylandSource,
26        client::{
27            Connection, QueueHandle,
28            globals::registry_queue_init,
29            protocol::{wl_keyboard::WlKeyboard, wl_shm, wl_touch::WlTouch},
30        },
31    },
32    registry::RegistryState,
33    seat::{SeatState, pointer::cursor_shape::CursorShapeManager},
34    session_lock::{SessionLock, SessionLockState, SessionLockSurface},
35    shm::{
36        Shm,
37        slot::{Buffer, Slot, SlotPool},
38    },
39};
40use std::{cell::RefCell, process::Command, rc::Rc};
41use tracing::{Level, info, span, warn};
42
43mod input;
44mod internal;
45mod nonstick_impl;
46mod wayland;
47
48/// SpellLock is a struct which represents a window lock. It can be run and initialised
49/// on a custom lockscreen implementation with slint.
50/// Know limitations include the abscence to verify from fingerprints and unideal issues on
51/// multi-monitor setup. You can add the path of binary of your lock in your compositor config and idle
52/// manager config to use the program. It will be linked to spell-cli directly in coming releases.
53///
54/// ## Example
55/// Here is a minimal example of rust side, for complete code of slint, check
56/// the codebase of young-shell.
57///
58/// ```rust
59/// use spell_framework::cast_spell;
60/// use std::{error::Error, sync::{Arc, RwLock}};
61/// use slint::ComponentHandle;
62/// use spell_framework::{layer_properties::ForeignController, wayland_adapter::SpellLock};
63/// slint::include_modules!();
64///
65/// fn main() -> Result<(), Box<dyn Error>> {
66///     let lock = SpellLock::invoke_lock_spell();
67///     let lock_ui = LockScreen::new().unwrap();
68///     let looop_handle = lock.get_handler();
69///     lock_ui.on_check_pass({
70///         let lock_handle = lock_ui.as_weak();
71///         move |string_val| {
72///             let lock_handle_a = lock_handle.clone().unwrap();
73///             let lock_handle_b = lock_handle.clone().unwrap();
74///             looop_handle.unlock(
75///                 None,
76///                 string_val.to_string(),
77///                 Box::new(move || {
78///                     lock_handle_a.set_lock_error(true);
79///                 }),
80///                 Box::new(move || {
81///                     lock_handle_b.set_is_lock_activated(false);
82///                 }),
83///             );
84///         }
85///     });
86///     lock_ui.set_is_lock_activated(true);
87///     cast_spell(
88///         lock,
89///         None,
90///         None::<fn(Arc<RwLock<Box<dyn ForeignController>>>)>,
91///     )
92/// }
93/// ```
94pub struct SpellLock {
95    loop_handle: LoopHandle<'static, SpellLock>,
96    conn: Connection,
97    compositor_state: CompositorState,
98    registry_state: RegistryState,
99    output_state: OutputState,
100    keyboard_state: Option<WlKeyboard>,
101    pointer_state: PointerState,
102    touch_state: Option<WlTouch>,
103    seat_state: SeatState,
104    shm: Shm,
105    session_lock: Option<SessionLock>,
106    lock_surfaces: Vec<SessionLockSurface>,
107    slint_part: Option<SpellSlintLock>,
108    is_locked: bool,
109    /// span used for logging and tracing lockscreen eveents.
110    pub span: span::Span,
111    unlock_screen: Sender<bool>,
112    // TODO, check if it need internal mutability?
113    event_loop: Rc<RefCell<EventLoop<'static, SpellLock>>>,
114    backspace: Option<RegistrationToken>,
115}
116
117impl std::fmt::Debug for SpellLock {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("SpellLock")
120            .field("is_locked", &self.is_locked)
121            .finish()
122    }
123}
124
125impl SpellLock {
126    /// This function creates an instance of SpellLock which can be combined with
127    /// slint windows to create a lockscreen.
128    pub fn invoke_lock_spell() -> Self {
129        let conn = Connection::connect_to_env().unwrap();
130        let _ = set_up_tracing("SpellLock");
131        let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
132        let qh: QueueHandle<SpellLock> = event_queue.handle();
133        let registry_state = RegistryState::new(&globals);
134        let shm = Shm::bind(&globals, &qh).unwrap();
135        let event_loop: EventLoop<'static, SpellLock> =
136            EventLoop::try_new().expect("Failed to initialize the event loop!");
137        let output_state = OutputState::new(&globals, &qh);
138        let session_lock_state = SessionLockState::new(&globals, &qh);
139        let compositor_state =
140            CompositorState::bind(&globals, &qh).expect("Faild to create compositor state");
141        let cursor_manager =
142            CursorShapeManager::bind(&globals, &qh).expect("cursor shape is not available");
143        let mut win_handler_vec: Vec<(String, (u32, u32))> = Vec::new();
144        let lock_surfaces = Vec::new();
145
146        let pointer_state = PointerState {
147            pointer: None,
148            pointer_data: None,
149            cursor_shape: cursor_manager,
150            last_cursor_enter_serial: None,
151            current_wayland_cursor: MouseCursor::Default,
152        };
153        let (sender, rx) = channel::channel::<bool>();
154        let mut spell_lock = SpellLock {
155            loop_handle: event_loop.handle().clone(),
156            conn: conn.clone(),
157            compositor_state,
158            output_state,
159            keyboard_state: None,
160            touch_state: None,
161            pointer_state,
162            registry_state,
163            seat_state: SeatState::new(&globals, &qh),
164            slint_part: None,
165            shm,
166            session_lock: None,
167            lock_surfaces,
168            unlock_screen: sender,
169            span: span!(Level::INFO, "lock", name = "lock-screen",),
170            is_locked: true,
171            event_loop: Rc::new(RefCell::new(event_loop)),
172            backspace: None,
173        };
174
175        let _ = event_queue.roundtrip(&mut spell_lock);
176
177        let session_lock = Some(
178            session_lock_state
179                .lock(&qh)
180                .expect("ext-session-lock not supported"),
181        );
182
183        spell_lock.session_lock = session_lock;
184        for output in spell_lock.output_state.outputs() {
185            let output_info: output::OutputInfo = spell_lock.output_state.info(&output).unwrap();
186            let output_name: String = output_info.name.unwrap_or_else(|| "SomeOutput".to_string());
187            let dimensions = (
188                output_info.logical_size.unwrap().0 as u32,
189                output_info.logical_size.unwrap().1 as u32,
190            );
191            win_handler_vec.push((output_name, dimensions));
192
193            let session_lock = spell_lock.session_lock.as_ref().unwrap();
194            let surface = spell_lock.compositor_state.create_surface(&qh);
195
196            // It's important to keep the `SessionLockSurface` returned here around, as the
197            // surface will be destroyed when the `SessionLockSurface` is dropped.
198            let lock_surface = session_lock.create_lock_surface(surface, &output, &qh);
199            spell_lock.lock_surfaces.push(lock_surface);
200        }
201        let multi_handler = SpellMultiWinHandler::new_lock(win_handler_vec);
202        let sizes: Vec<PhysicalSize> = multi_handler
203            .borrow()
204            .windows
205            .iter()
206            .map(|(_, conf)| {
207                if let LayerConf::Lock(width, height) = conf {
208                    PhysicalSize {
209                        width: *width,
210                        height: *height,
211                    }
212                } else {
213                    panic!("Shouldn't enter here");
214                }
215            })
216            .collect();
217
218        let mut pool = SlotPool::new(
219            (sizes[0].width * sizes[0].height * 4) as usize,
220            &spell_lock.shm,
221        )
222        .expect("Couldn't create pool");
223        let mut buffer_slots: Vec<RefCell<Slot>> = Vec::new();
224        let buffers: Vec<Buffer> = sizes
225            .iter()
226            .map(|physical_size| {
227                let stride = physical_size.width as i32 * 4;
228                let (wayland_buffer, _) = pool
229                    .create_buffer(
230                        physical_size.width as i32,
231                        physical_size.height as i32,
232                        stride,
233                        wl_shm::Format::Argb8888,
234                    )
235                    .expect("Creating Buffer");
236                buffer_slots.push(RefCell::new(wayland_buffer.slot()));
237                wayland_buffer
238            })
239            .collect();
240        let (slint_event_sender, slint_event_receiver) =
241            calloop::channel::channel::<Box<dyn FnOnce() + Send>>();
242        let pool: Rc<RefCell<SlotPool>> = Rc::new(RefCell::new(pool));
243        let mut adapters: Vec<Rc<SpellSkiaWinAdapter>> = Vec::new();
244        buffer_slots
245            .into_iter()
246            .enumerate()
247            .for_each(|(index, slot)| {
248                let adapter = SpellSkiaWinAdapter::new(
249                    pool.clone(),
250                    slot,
251                    sizes[index].width,
252                    sizes[index].height,
253                );
254                adapters.push(adapter);
255            });
256
257        multi_handler.borrow_mut().adapter = adapters.clone();
258        spell_lock.slint_part = Some(SpellSlintLock {
259            adapters,
260            size: sizes,
261            wayland_buffer: buffers,
262        });
263
264        spell_lock.set_event_sources(slint_event_receiver, rx);
265        let _ = slint::platform::set_platform(Box::new(SpellLockShell::new(
266            multi_handler,
267            slint_event_sender,
268        )));
269
270        WaylandSource::new(spell_lock.conn.clone(), event_queue)
271            .insert(spell_lock.loop_handle.clone())
272            .unwrap();
273        spell_lock
274    }
275
276    fn unlock_finger(&mut self, error_callback: Box<dyn FnOnce() + Send>) {
277        let sender = self.unlock_screen.clone();
278        let span = self.span.clone();
279        std::thread::spawn(move || {
280            let _guard = span.enter();
281            fn unlock_internal(sender: Sender<bool>) -> PamResult<()> {
282                let finger = lock::nonstick_impl::FingerprintInfo;
283                let output = Command::new("sh")
284                    .arg("-c")
285                    .arg("last | awk '{print $1}' | sort | uniq -c | sort -nr")
286                    .output()
287                    .expect("Couldn't retrive username");
288
289                let val = String::from_utf8_lossy(&output.stdout);
290                let val_2 = val.split('\n').collect::<Vec<_>>()[0].trim();
291                let user_name = val_2.split(" ").collect::<Vec<_>>()[1].to_string();
292
293                let mut txn = TransactionBuilder::new_with_service("login")
294                    .username(user_name)
295                    .build(finger.into_conversation())?;
296                // If authentication fails, this will return an error.
297                // We immediately give up rather than re-prompting the user.
298                txn.authenticate(AuthnFlags::empty())?;
299                txn.account_management(AuthnFlags::empty())?;
300                if let Err(err) = sender.send(true) {
301                    warn!("Error sending unlock via sender: {err}");
302                }
303                Ok(())
304            }
305            if let Err(err) = unlock_internal(sender) {
306                warn!("{:?}", err);
307                error_callback();
308            } else {
309                info!("Password passed");
310            }
311        });
312    }
313
314    fn unlock(
315        &mut self,
316        username: Option<&str>,
317        password: &str,
318        on_unlock_callback: Box<dyn FnOnce()>,
319    ) -> PamResult<()> {
320        let user_name;
321        if let Some(username) = username {
322            user_name = username.to_string();
323        } else {
324            let output = Command::new("sh")
325                .arg("-c")
326                .arg("last | awk '{print $1}' | sort | uniq -c | sort -nr")
327                .output()
328                .expect("Couldn't retrive username");
329
330            let val = String::from_utf8_lossy(&output.stdout);
331            let val_2 = val.split('\n').collect::<Vec<_>>()[0].trim();
332            user_name = val_2.split(" ").collect::<Vec<_>>()[1].to_string();
333        }
334
335        let user_pass = lock::nonstick_impl::UsernamePassConvo {
336            username: user_name.clone(),
337            password: password.into(),
338        };
339
340        let mut txn = TransactionBuilder::new_with_service("login")
341            .username(user_name)
342            .build(user_pass.into_conversation())?;
343        // If authentication fails, this will return an error.
344        // We immediately give up rather than re-prompting the user.
345        txn.authenticate(AuthnFlags::empty())?;
346        txn.account_management(AuthnFlags::empty())?;
347
348        on_unlock_callback();
349        if let Some(locked_val) = self.session_lock.take() {
350            locked_val.unlock();
351        } else {
352            warn!("Authentication verified but couldn't unlock");
353        }
354        self.is_locked = false;
355        self.conn.roundtrip().unwrap();
356        Ok(())
357    }
358
359    /// Provides a lockscreen handler used to invoke the unlock
360    /// callback with the user entered password.For more details
361    /// view [`LockHandle`].
362    pub fn get_handler(&self) -> LockHandle {
363        LockHandle(self.loop_handle.clone())
364    }
365}
366
367impl SpellAssociatedNew for SpellLock {
368    fn on_call(&mut self) -> Result<(), Box<dyn std::error::Error>> {
369        let event_loop = self.event_loop.clone();
370        event_loop
371            .borrow_mut()
372            .dispatch(std::time::Duration::from_millis(1), self)?;
373        Ok(())
374    }
375
376    fn is_locked(&self) -> bool {
377        self.is_locked
378    }
379
380    fn get_span(&self) -> span::Span {
381        self.span.clone()
382    }
383}
384
385delegate_keyboard!(SpellLock);
386delegate_compositor!(SpellLock);
387delegate_output!(SpellLock);
388delegate_shm!(SpellLock);
389delegate_registry!(SpellLock);
390delegate_pointer!(SpellLock);
391delegate_touch!(SpellLock);
392delegate_session_lock!(SpellLock);
393delegate_seat!(SpellLock);
394
395/// Struct to handle unlocking of a SpellLock instance. It can be captured from
396/// [`SpellLock::get_handler`].
397#[derive(Debug, Clone)]
398pub struct LockHandle(LoopHandle<'static, SpellLock>);
399
400impl LockHandle {
401    /// Call this method to unlock Spelllock. It also takes two callbacks which
402    /// are invoked when the password parsed is wrong or right (i.e. resulting
403    /// in an screen unlock) respectively. Callbacks can be used to invoke UI
404    /// specific changes for your slint frontend.
405    pub fn unlock(
406        &self,
407        username: Option<String>,
408        password: String,
409        on_err_callback: Box<dyn FnOnce()>,
410        on_unlock_callback: Box<dyn FnOnce()>,
411    ) {
412        self.0.insert_idle(move |app_data| {
413            if app_data
414                .unlock(username.as_deref(), &password, on_unlock_callback)
415                .is_err()
416            {
417                on_err_callback();
418            }
419        });
420    }
421
422    /// Function which opens fingerprint device for authentication.
423    /// error_callback is executed when fingerprint is not registered and fails
424    /// to unlock the lockscreen.
425    pub fn verify_fingerprint(&self, error_callback: Box<dyn FnOnce() + Send>) {
426        self.0.insert_idle(move |app_data| {
427            app_data.unlock_finger(error_callback);
428        });
429    }
430}