phrog 0.53.0-rc.2

Mobile-friendly greeter for greetd
use crate::session_object::SessionObject;
use glib::{warn, KeyFile, KeyFileFlags};
use glob::glob;
use lazy_static::lazy_static;
use std::{collections::HashMap, env, path::Path, path::PathBuf};

static G_LOG_DOMAIN: &str = "phrog-sessions";

lazy_static! {
    // Snippet copied from https://github.com/apognu/tuigreet

    static ref XDG_DATA_DIRS: Vec<PathBuf> = {
        let envvar = env::var("XDG_DATA_DIRS");

        let value = match envvar {
            Ok(var) if !var.is_empty() => var,
            _ => "/usr/local/share:/usr/share".to_string(),
        };

        env::split_paths(&value).filter(|p| p.is_absolute()).collect()
    };
}

pub fn sessions() -> Vec<SessionObject> {
    let mut sessions = HashMap::new();

    for dir in XDG_DATA_DIRS.iter() {
        let wl_dir = dir.join("wayland-sessions/*.desktop");
        let x11_dir = dir.join("xsessions/*.desktop");

        session_list(&wl_dir, "wayland", &mut sessions);

        session_list(&x11_dir, "x11", &mut sessions);
    }

    sessions.values().cloned().collect()
}

fn session_list(path: &Path, session_type: &str, sessions: &mut HashMap<String, SessionObject>) {
    let keyfile_locale_string = |key_file: &KeyFile, key: &str| {
        key_file
            .locale_string("Desktop Entry", key, None)
            .or_else(|_| key_file.string("Desktop Entry", key))
            .ok()
    };

    for f in match glob(path.to_str().unwrap()) {
        Err(e) => {
            warn!("couldn't check sessions in {}: {}", path.display(), e);
            return;
        }
        Ok(iter) => iter,
    }
    .flatten()
    {
        let id = f.file_stem().unwrap().to_string_lossy().to_string();
        if sessions.contains_key(&id) {
            continue;
        }

        let key_file = KeyFile::new();
        if let Err(err) = key_file.load_from_file(&f, KeyFileFlags::NONE) {
            warn!("Unable to parse session file {:?}: {}", f, err);
            continue;
        }

        let name = match keyfile_locale_string(&key_file, "Name") {
            Some(value) => value,
            None => {
                warn!("Missing Name in session file {:?}", f);
                continue;
            }
        };
        let command = key_file
            .string("Desktop Entry", "Exec")
            .ok()
            .unwrap_or_default();
        let desktop_names = key_file
            .string("Desktop Entry", "DesktopNames")
            .ok()
            .map(|value| value.trim_end_matches(';').replace(';', ":"))
            .unwrap_or_default();
        sessions.insert(
            id.clone(),
            SessionObject::new(&id, &name, session_type, &command, &desktop_names),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::session_list;
    use crate::session_object::SessionObject;
    use gtk::prelude::ObjectExt;
    use std::collections::HashMap;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn session_list_reads_desktop_keyfile() {
        let temp = tempdir().expect("create tempdir");
        let session_dir = temp.path().join("wayland-sessions");
        fs::create_dir_all(&session_dir).expect("create session dir");

        let desktop_entry = "[Desktop Entry]\n\
Name=Phosh\n\
Comment=Phone Shell\n\
Comment=This session logs you into Phosh\n\
Exec=phosh-session\n\
Type=Application\n\
DesktopNames=Phosh;GNOME;\n";

        let desktop_path = session_dir.join("phosh.desktop");
        fs::write(&desktop_path, desktop_entry).expect("write desktop file");

        let mut sessions: HashMap<String, SessionObject> = HashMap::new();
        let pattern = session_dir.join("*.desktop");
        session_list(&pattern, "wayland", &mut sessions);

        let session = sessions.get("phosh").expect("session added");
        let name: String = session.property("name");
        let session_type: String = session.property("session-type");
        let command: String = session.property("command");
        let desktop_names: String = session.property("desktop-names");

        assert_eq!(name, "Phosh");
        assert_eq!(session_type, "wayland");
        assert_eq!(command, "phosh-session");
        assert_eq!(desktop_names, "Phosh:GNOME");
    }

    #[test]
    fn session_list_accepts_keyfile_without_type() {
        let temp = tempdir().expect("create tempdir");
        let session_dir = temp.path().join("wayland-sessions");
        fs::create_dir_all(&session_dir).expect("create session dir");

        let desktop_entry = "[Desktop Entry]\n\
Name=Phosh\n\
Comment=Phone Shell\n\
Exec=phosh-session\n\
DesktopNames=Phosh;GNOME;\n";

        let desktop_path = session_dir.join("phosh.desktop");
        fs::write(&desktop_path, desktop_entry).expect("write desktop file");

        let mut sessions: HashMap<String, SessionObject> = HashMap::new();
        let pattern = session_dir.join("*.desktop");
        session_list(&pattern, "wayland", &mut sessions);

        assert!(sessions.contains_key("phosh"));
    }
}