git.lucas.co / cce-display-manager
login greeter
git clone https://git.lucas.co/cce-display-manager.git

commit542cd87e694ed40696132ce3c530aea1ad6eb710
parent87b6dca7a7
authorLucas Galante <[email protected]>
date2026-05-29 17:36
Update system configuration and interface modules

 [email protected]      |  20 +++
 pam/clear-display-manager           |   1 +
 pam/clear-display-manager-autologin |  14 ++
 src/main.rs                         | 247 ++++++++++++++++++++++++++++++------
 4 files changed, 243 insertions(+), 39 deletions(-)

diff --git a/[email protected] b/[email protected]
new file mode 100644
index 0000000..91956c0
--- /dev/null
+++ b/[email protected]
@@ -0,0 +1,20 @@
+[Unit]
+Description=Clear Display Manager on %I
+After=systemd-user-sessions.service plymouth-quit-active.service
+Conflicts=getty@%i.service
+Before=graphical.target
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/clear-display-manager
+StandardInput=tty
+StandardOutput=tty
+TTYPath=/dev/%I
+TTYReset=yes
+TTYVHangup=yes
+TTYVTDisallocate=yes
+UtmpIdentifier=%I
+UtmpMode=user
+
+[Install]
+WantedBy=graphical.target
diff --git a/pam/clear-display-manager b/pam/clear-display-manager
index 9a1e579..972de3e 100644
--- a/pam/clear-display-manager
+++ b/pam/clear-display-manager
@@ -1,5 +1,6 @@
 #%PAM-1.0
 
+auth       sufficient   pam_fprintd.so
 auth       include      system-local-login
 -auth      optional     pam_gnome_keyring.so
 -auth      optional     pam_kwallet5.so
diff --git a/pam/clear-display-manager-autologin b/pam/clear-display-manager-autologin
new file mode 100644
index 0000000..b0eb0b6
--- /dev/null
+++ b/pam/clear-display-manager-autologin
@@ -0,0 +1,14 @@
+#%PAM-1.0
+
+auth       required     pam_permit.so
+-auth      optional     pam_gnome_keyring.so
+-auth      optional     pam_kwallet5.so
+
+account    include      system-local-login
+
+password   include      system-local-login
+-password  optional     pam_gnome_keyring.so use_authtok
+
+session    include      system-local-login
+-session   optional     pam_gnome_keyring.so auto_start
+-session   optional     pam_kwallet5.so      auto_start
diff --git a/src/main.rs b/src/main.rs
index d7c3ac4..6f9c671 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -577,11 +577,6 @@ impl State {
         };
 
         state.apply_layout();
-        if state.username_box.text.is_empty() {
-            state.username_box.focus();
-        } else {
-            state.password_box.focus();
-        }
         state.upload_vertices();
         state
     }
@@ -801,37 +796,36 @@ impl State {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 enum AuthEvent {
-    Success { username: String },
-    Failure(String),
+    Success { request_id: u64, username: String },
+    Failure { request_id: u64, err_msg: String },
+    Info { request_id: u64, msg: String },
 }
 
-fn authenticate_user(username: String, password: String, sender: channel::Sender<AuthEvent>) {
+fn authenticate_user(request_id: u64, username: String, password: String, sender: channel::Sender<AuthEvent>) {
     std::thread::spawn(move || {
         let service = "clear-display-manager";
         
-        let mut auth = match pam::Authenticator::with_password(service) {
+        let mut auth = match PamSession::new(service, &username, &password, request_id, Some(sender.clone())) {
             Ok(a) => a,
             Err(_) => {
-                match pam::Authenticator::with_password("login") {
+                match PamSession::new("login", &username, &password, request_id, Some(sender.clone())) {
                     Ok(a) => a,
                     Err(e) => {
-                        let _ = sender.send(AuthEvent::Failure(format!("PAM Init Error: {}", e)));
+                        let _ = sender.send(AuthEvent::Failure { request_id, err_msg: format!("PAM Init Error: {:?}", e) });
                         return;
                     }
                 }
             }
         };
 
-        auth.get_handler().set_credentials(username.clone(), password);
-
         match auth.authenticate() {
             Ok(_) => {
-                let _ = sender.send(AuthEvent::Success { username });
+                let _ = sender.send(AuthEvent::Success { request_id, username });
             }
             Err(e) => {
-                let _ = sender.send(AuthEvent::Failure(format!("Authentication failed: {}", e)));
+                let _ = sender.send(AuthEvent::Failure { request_id, err_msg: format!("Authentication failed: {:?}", e) });
             }
         }
     });
@@ -860,6 +854,7 @@ struct AppState {
     shift_pressed: bool,
     pressed_key: Option<PressedKey>,
     auth_sender: channel::Sender<AuthEvent>,
+    auth_request_id: u64,
 }
 
 #[allow(dead_code)]
@@ -1099,15 +1094,18 @@ impl PointerHandler for AppState {
                                 if username.is_empty() {
                                     st.status_lbl.text = "Username cannot be empty".to_string();
                                     st.status_lbl.is_error = true;
+                                    st.username_box.focus();
                                 } else if password.is_empty() {
                                     st.status_lbl.text = "Password cannot be empty".to_string();
                                     st.status_lbl.is_error = true;
+                                    st.password_box.focus();
                                 } else {
+                                    self.auth_request_id += 1;
                                     st.status_lbl.text = "Authenticating...".to_string();
                                     st.status_lbl.is_error = false;
                                     st.is_authenticating = true;
-                                    st.login_btn.label = Some("Authenticating...".to_string());
-                                    authenticate_user(username, password, self.auth_sender.clone());
+                                    st.login_btn.base_mut().unwrap().label = Some("Authenticating...".to_string());
+                                    authenticate_user(self.auth_request_id, username, password, self.auth_sender.clone());
                                 }
                                 changed = true;
                             }
@@ -1169,13 +1167,48 @@ impl AppState {
 
         if state == ElementState::Pressed {
             if let Some(st) = &mut self.state {
+                // If we are currently in fingerprint authentication and the user starts typing a password,
+                // cancel the fingerprint auth and let them type.
                 if st.is_authenticating {
-                    return;
+                    if st.password_box.text.is_empty() {
+                        let is_typing = match &logical_key {
+                            Key::Character(_) | Key::Named(NamedKey::Backspace) | Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Space) => true,
+                            _ => false,
+                        };
+                        if is_typing {
+                            self.auth_request_id += 1;
+                            st.is_authenticating = false;
+                            st.login_btn.base_mut().unwrap().label = Some("Log In".to_string());
+                            st.status_lbl.text = "Enter password to start".to_string();
+                            st.status_lbl.is_error = false;
+                        } else {
+                            let is_nav = match &logical_key {
+                                Key::Named(NamedKey::ArrowUp) | Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::Tab) => true,
+                                _ => false,
+                            };
+                            if !is_nav {
+                                return;
+                            }
+                        }
+                    } else {
+                        return;
+                    }
                 }
+
                 let mut changed = false;
 
-                // Handle Tab navigation between username and password boxes
-                if logical_key == Key::Named(NamedKey::Tab) {
+                // Handle Up/Down navigation to cycle sessions
+                if logical_key == Key::Named(NamedKey::ArrowUp) && !st.session_list.sessions.is_empty() {
+                    let len = st.session_list.sessions.len();
+                    st.session_list.selected_idx = (st.session_list.selected_idx + len - 1) % len;
+                    st.session_list.hovered_idx = None;
+                    changed = true;
+                } else if logical_key == Key::Named(NamedKey::ArrowDown) && !st.session_list.sessions.is_empty() {
+                    let len = st.session_list.sessions.len();
+                    st.session_list.selected_idx = (st.session_list.selected_idx + 1) % len;
+                    st.session_list.hovered_idx = None;
+                    changed = true;
+                } else if logical_key == Key::Named(NamedKey::Tab) {
                     let is_user_focused = clear_ui::widget::focus::is_focused(&st.username_box);
                     if is_user_focused {
                         st.username_box.unfocus();
@@ -1185,6 +1218,37 @@ impl AppState {
                         st.username_box.focus();
                     }
                     changed = true;
+                } else if logical_key == Key::Named(NamedKey::Enter) && clear_ui::widget::focus::is_focused(&st.password_box) {
+                    // Process Enter in the password box to commit the buffer
+                    st.password_box.keyboard_input(&custom_event);
+
+                    // Extract login username and password
+                    let username = st.username_box.text.trim().to_string();
+                    let password = st.password_box.text.trim().to_string();
+
+                    if username.is_empty() {
+                        st.status_lbl.text = "Username cannot be empty".to_string();
+                        st.status_lbl.is_error = true;
+                        st.username_box.focus();
+                    } else if password.is_empty() {
+                        st.status_lbl.text = "Password cannot be empty".to_string();
+                        st.status_lbl.is_error = true;
+                        st.password_box.focus();
+                    } else {
+                        self.auth_request_id += 1;
+                        st.status_lbl.text = "Authenticating...".to_string();
+                        st.status_lbl.is_error = false;
+                        st.is_authenticating = true;
+                        st.login_btn.base_mut().unwrap().label = Some("Authenticating...".to_string());
+                        authenticate_user(self.auth_request_id, username, password, self.auth_sender.clone());
+                    }
+                    changed = true;
+                } else if logical_key == Key::Named(NamedKey::Enter) && clear_ui::widget::focus::is_focused(&st.username_box) {
+                    // Pressing enter in the username box commits and shifts focus to the password box
+                    st.username_box.keyboard_input(&custom_event);
+                    st.username_box.unfocus();
+                    st.password_box.focus();
+                    changed = true;
                 } else {
                     for w in st.widgets_iter_mut() {
                         if w.keyboard_input(&custom_event) {
@@ -1358,6 +1422,7 @@ fn run_greeter() {
         shift_pressed: false,
         pressed_key: None,
         auth_sender,
+        auth_request_id: 0,
     };
 
     event_queue.roundtrip(&mut app).unwrap();
@@ -1392,6 +1457,22 @@ fn run_greeter() {
     app.surface = Some(surface);
     app.state = Some(state);
 
+    // Set initial focus now that State is in its final, stable memory location inside app.state
+    if let Some(ref mut st) = app.state {
+        st.password_box.focus();
+        st.upload_vertices();
+
+        // Start background fingerprint/empty-password authentication if username is prepopulated!
+        let username = st.username_box.text.trim().to_string();
+        if !username.is_empty() {
+            app.auth_request_id += 1;
+            st.is_authenticating = true;
+            st.login_btn.base_mut().unwrap().label = Some("Authenticating...".to_string());
+            st.status_lbl.text = "Scan finger to login or type password".to_string();
+            authenticate_user(app.auth_request_id, username, String::new(), app.auth_sender.clone());
+        }
+    }
+
     let mut event_loop = EventLoop::try_new().unwrap();
     let loop_handle = event_loop.handle();
     WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
@@ -1399,19 +1480,39 @@ fn run_greeter() {
     loop_handle.insert_source(auth_receiver, |event, _metadata, app_state| {
         match event {
             channel::Event::Msg(msg) => {
+                let ev_request_id = match &msg {
+                    AuthEvent::Success { request_id, .. } => *request_id,
+                    AuthEvent::Failure { request_id, .. } => *request_id,
+                    AuthEvent::Info { request_id, .. } => *request_id,
+                };
+
+                if ev_request_id != app_state.auth_request_id {
+                    // Ignore stale/cancelled auth events
+                    return;
+                }
+
                 if let Some(st) = &mut app_state.state {
-                    st.is_authenticating = false;
-                    st.login_btn.label = Some("Log In".to_string());
                     match msg {
-                        AuthEvent::Success { username } => {
+                        AuthEvent::Success { username, .. } => {
+                            st.is_authenticating = false;
+                            st.login_btn.base_mut().unwrap().label = Some("Log In".to_string());
                             st.status_lbl.text = format!("Welcome, {}!", username);
                             st.status_lbl.is_error = false;
                             st.login_success = true;
                             app_state.exit = true;
                         }
-                        AuthEvent::Failure(err_msg) => {
+                        AuthEvent::Failure { err_msg, .. } => {
+                            st.is_authenticating = false;
+                            st.login_btn.base_mut().unwrap().label = Some("Log In".to_string());
                             st.status_lbl.text = err_msg;
                             st.status_lbl.is_error = true;
+                            st.password_box.text.clear();
+                            st.password_box.edit_buffer.clear();
+                            st.password_box.focus();
+                        }
+                        AuthEvent::Info { msg, .. } => {
+                            st.status_lbl.text = msg;
+                            st.status_lbl.is_error = false;
                         }
                     }
                     st.upload_vertices();
@@ -1449,6 +1550,8 @@ fn run_greeter() {
 struct PamSessionData {
     username: String,
     password: String,
+    request_id: u64,
+    sender: Option<channel::Sender<AuthEvent>>,
 }
 
 extern "C" fn pam_conversation_fn(
@@ -1468,12 +1571,20 @@ extern "C" fn pam_conversation_fn(
         unsafe {
             let m = &**msg.offset(i);
             let r = &mut *resp.offset(i);
-            if m.msg_style == pam_sys::PamMessageStyle::PROMPT_ECHO_ON as libc::c_int {
+            let style = m.msg_style;
+            if style == pam_sys::PamMessageStyle::PROMPT_ECHO_ON as libc::c_int {
                 let user_c = std::ffi::CString::new(data.username.clone()).unwrap();
                 r.resp = libc::strdup(user_c.as_ptr());
-            } else if m.msg_style == pam_sys::PamMessageStyle::PROMPT_ECHO_OFF as libc::c_int {
+            } else if style == pam_sys::PamMessageStyle::PROMPT_ECHO_OFF as libc::c_int {
                 let pass_c = std::ffi::CString::new(data.password.clone()).unwrap();
                 r.resp = libc::strdup(pass_c.as_ptr());
+            } else if style == pam_sys::PamMessageStyle::ERROR_MSG as libc::c_int || style == pam_sys::PamMessageStyle::TEXT_INFO as libc::c_int {
+                if !m.msg.is_null() {
+                    let msg_str = std::ffi::CStr::from_ptr(m.msg).to_string_lossy().into_owned();
+                    if let Some(ref sender) = data.sender {
+                        let _ = sender.send(AuthEvent::Info { request_id: data.request_id, msg: msg_str });
+                    }
+                }
             }
         }
     }
@@ -1489,11 +1600,13 @@ struct PamSession {
 }
 
 impl PamSession {
-    fn new(service: &str, username: &str, password: &str) -> Result<Self, pam_sys::PamReturnCode> {
+    fn new(service: &str, username: &str, password: &str, request_id: u64, sender: Option<channel::Sender<AuthEvent>>) -> Result<Self, pam_sys::PamReturnCode> {
         let mut handle: *mut pam_sys::PamHandle = std::ptr::null_mut();
         let data = Box::new(PamSessionData {
             username: username.to_string(),
             password: password.to_string(),
+            request_id,
+            sender,
         });
         
         let conv = pam_sys::PamConversation {
@@ -1510,13 +1623,30 @@ impl PamSession {
             let pass_c = std::ffi::CString::new(password).unwrap();
             let _ = pam_sys::raw::pam_set_item(handle, pam_sys::PamItemType::AUTHTOK as libc::c_int, pass_c.as_ptr() as *const libc::c_void);
             
-            let tty_c = std::ffi::CString::new("tty1").unwrap();
+            let raw_tty = std::fs::read_link("/proc/self/fd/0")
+                .ok()
+                .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
+                .unwrap_or_else(|| "tty1".to_string());
+            let is_real_tty = raw_tty.starts_with("tty");
+            let tty_name = if is_real_tty { raw_tty } else { "tty1".to_string() };
+
+            let tty_c = std::ffi::CString::new(tty_name).unwrap();
             let _ = pam_sys::raw::pam_set_item(handle, pam_sys::PamItemType::TTY as libc::c_int, tty_c.as_ptr() as *const libc::c_void);
         }
 
         Ok(Self { handle, _data: data, has_open_session: false })
     }
 
+    fn putenv(&mut self, name_value: &str) -> Result<(), pam_sys::PamReturnCode> {
+        let c_str = std::ffi::CString::new(name_value).unwrap();
+        let rc = unsafe { pam_sys::raw::pam_putenv(self.handle, c_str.as_ptr()) };
+        if rc == 0 {
+            Ok(())
+        } else {
+            Err(unsafe { std::mem::transmute(rc as u8) })
+        }
+    }
+
     fn authenticate(&mut self) -> Result<(), pam_sys::PamReturnCode> {
         unsafe {
             let rc = pam_sys::authenticate(&mut *self.handle, pam_sys::PamFlag::NONE);
@@ -1601,12 +1731,20 @@ fn run_daemon() {
         std::process::exit(1);
     }
 
+    let raw_tty = std::fs::read_link("/proc/self/fd/0")
+        .ok()
+        .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
+        .unwrap_or_else(|| "tty1".to_string());
+    let is_real_tty = raw_tty.starts_with("tty");
+    let tty_name = if is_real_tty { raw_tty } else { "tty1".to_string() };
+
     // Redirect stdout and stderr of the daemon to a log file
+    let log_path = format!("/tmp/clear-display-manager-daemon-{}.log", tty_name);
     if let Ok(log_file) = std::fs::OpenOptions::new()
         .create(true)
         .write(true)
         .truncate(true)
-        .open("/tmp/clear-display-manager-daemon.log")
+        .open(&log_path)
     {
         use std::os::unix::io::AsRawFd;
         let fd = log_file.as_raw_fd();
@@ -1616,17 +1754,31 @@ fn run_daemon() {
         }
     }
 
-    println!("[clear-display-manager] Starting display manager daemon...");
+    println!("[clear-display-manager] Starting display manager daemon on {}...", tty_name);
 
-    let runtime_dir = "/run/clear-display-manager";
-    if !std::path::Path::new(runtime_dir).exists() {
-        std::fs::create_dir_all(runtime_dir).expect("failed to create runtime dir");
+    let runtime_dir = format!("/run/clear-display-manager-{}", tty_name);
+    if !std::path::Path::new(&runtime_dir).exists() {
+        std::fs::create_dir_all(&runtime_dir).expect("failed to create runtime dir");
         use std::os::unix::fs::PermissionsExt;
-        std::fs::set_permissions(runtime_dir, std::fs::Permissions::from_mode(0o700))
+        std::fs::set_permissions(&runtime_dir, std::fs::Permissions::from_mode(0o700))
             .expect("failed to set runtime dir permissions");
     }
 
     loop {
+        if is_real_tty {
+            println!("[clear-display-manager] Waiting for {} to become the active TTY...", tty_name);
+            loop {
+                if let Ok(active_tty) = std::fs::read_to_string("/sys/class/tty/tty0/active") {
+                    let active_tty = active_tty.trim();
+                    if active_tty == tty_name {
+                        println!("[clear-display-manager] {} is now active. Spawning greeter.", tty_name);
+                        break;
+                    }
+                }
+                std::thread::sleep(std::time::Duration::from_millis(250));
+            }
+        }
+
         println!("[clear-display-manager] Spawning greeter session via cage...");
 
         let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("/usr/bin/clear-display-manager"));
@@ -1636,7 +1788,7 @@ fn run_daemon() {
             .arg("--")
             .arg(exe_path)
             .arg("--greeter")
-            .env("XDG_RUNTIME_DIR", runtime_dir)
+            .env("XDG_RUNTIME_DIR", &runtime_dir)
             .env("LIBSEAT_BACKEND", "seatd")
             .env("WLR_DRM_NO_MODIFIERS", "1")
             .env("WLR_DRM_DEVICES", "/dev/dri/card1:/dev/dri/card0")
@@ -1691,11 +1843,20 @@ fn run_daemon() {
 
             println!("[clear-display-manager] Launching user session Exec: '{}' (Wayland: {}) for user: '{}'", exec, is_wayland, username);
             
-            let service = "clear-display-manager";
-            let mut auth = match PamSession::new(service, &username, &password) {
+            let service = if password.is_empty() {
+                "clear-display-manager-autologin"
+            } else {
+                "clear-display-manager"
+            };
+            let mut auth = match PamSession::new(service, &username, &password, 0, None) {
                 Ok(a) => a,
                 Err(_) => {
-                    match PamSession::new("login", &username, &password) {
+                    let fallback_service = if password.is_empty() {
+                        "ly-autologin"
+                    } else {
+                        "login"
+                    };
+                    match PamSession::new(fallback_service, &username, &password, 0, None) {
                         Ok(a) => a,
                         Err(e) => {
                             eprintln!("[clear-display-manager] PAM Init Error in daemon: {:?}", e);
@@ -1705,6 +1866,14 @@ fn run_daemon() {
                 }
             };
 
+            let session_type_env = if is_wayland {
+                "XDG_SESSION_TYPE=wayland"
+            } else {
+                "XDG_SESSION_TYPE=x11"
+            };
+            let _ = auth.putenv(session_type_env);
+            let _ = auth.putenv("XDG_SESSION_CLASS=user");
+
             if let Err(e) = auth.authenticate() {
                 eprintln!("[clear-display-manager] PAM Authentication failed in daemon: {:?}", e);
                 continue;