limes-login 0.2.0

Login manager/session-launch library for limes frontends.
Documentation
use std::fs;
use std::path::Path;

use limes_proto::SessionChoice;

use crate::session_catalog::command::{
    clean_desktop_exec_word, command_available, split_desktop_words,
};

pub(super) fn parse_desktop_session(path: &Path) -> Option<SessionChoice> {
    let content = fs::read_to_string(path).ok()?;
    parse_desktop_session_content(&content, path)
}

fn parse_desktop_session_content(content: &str, path: &Path) -> Option<SessionChoice> {
    let mut in_desktop_entry = false;
    let mut name = None;
    let mut exec = None;
    let mut try_exec = None;
    let mut hidden = false;
    let mut no_display = false;

    for raw_line in content.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        if line.starts_with('[') && line.ends_with(']') {
            in_desktop_entry = line == "[Desktop Entry]";
            continue;
        }

        if !in_desktop_entry {
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let value = value.trim();

        match key.trim() {
            "Name" => name = Some(value.to_owned()),
            "Exec" => exec = Some(value.to_owned()),
            "TryExec" => try_exec = Some(value.to_owned()),
            "Hidden" => hidden = value.eq_ignore_ascii_case("true"),
            "NoDisplay" => no_display = value.eq_ignore_ascii_case("true"),
            _ => {}
        }
    }

    if hidden || no_display {
        return None;
    }

    if let Some(try_exec) = try_exec.as_deref() {
        if !command_available(try_exec) {
            return None;
        }
    }

    let command = parse_desktop_exec(exec.as_deref()?)?;
    if !command
        .first()
        .is_some_and(|program| command_available(program))
    {
        return None;
    }

    let name = name.unwrap_or_else(|| {
        path.file_stem()
            .and_then(|stem| stem.to_str())
            .unwrap_or("Session")
            .to_owned()
    });

    Some(SessionChoice {
        name,
        command: Some(command),
    })
}

fn parse_desktop_exec(exec: &str) -> Option<Vec<String>> {
    let command = split_desktop_words(exec)
        .into_iter()
        .filter_map(|word| clean_desktop_exec_word(&word))
        .collect::<Vec<_>>();

    (!command.is_empty()).then_some(command)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_desktop_exec_keeps_quoted_words_together() {
        let command = parse_desktop_exec(r#"env FOO="bar baz" start-session"#).unwrap();

        assert_eq!(
            command,
            vec![
                "env".to_owned(),
                "FOO=bar baz".to_owned(),
                "start-session".to_owned()
            ]
        );
    }

    #[test]
    fn parse_desktop_exec_removes_field_codes() {
        let command = parse_desktop_exec("start-session %f --literal %%").unwrap();

        assert_eq!(
            command,
            vec![
                "start-session".to_owned(),
                "--literal".to_owned(),
                "%".to_owned()
            ]
        );
    }
}