lios 0.1.14

A GTK4/VTE Linux terminal emulator with configurable themes, backgrounds, and desktop launcher install.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

const DESKTOP_FILE_NAME: &str = "dev.lios.Terminal.desktop";

pub fn install_desktop_entry() -> Result<String, String> {
    let executable = env::current_exe()
        .map_err(|error| format!("failed to determine current executable: {error}"))?
        .canonicalize()
        .map_err(|error| format!("failed to resolve current executable path: {error}"))?;
    let applications_dir = applications_dir()?;
    let desktop_file = applications_dir.join(DESKTOP_FILE_NAME);

    fs::create_dir_all(&applications_dir).map_err(|error| {
        format!(
            "failed to create applications directory '{}': {error}",
            applications_dir.display()
        )
    })?;
    fs::write(&desktop_file, desktop_entry(&executable)).map_err(|error| {
        format!(
            "failed to write desktop launcher '{}': {error}",
            desktop_file.display()
        )
    })?;

    Ok(format!(
        "installed desktop launcher\n  file: {}\n  exec: {}\n",
        desktop_file.display(),
        executable.display()
    ))
}

pub fn uninstall_desktop_entry() -> Result<String, String> {
    let desktop_file = applications_dir()?.join(DESKTOP_FILE_NAME);
    if !desktop_file.exists() {
        return Ok(format!(
            "desktop launcher is not installed at {}\n",
            desktop_file.display()
        ));
    }

    fs::remove_file(&desktop_file).map_err(|error| {
        format!(
            "failed to remove desktop launcher '{}': {error}",
            desktop_file.display()
        )
    })?;

    Ok(format!(
        "removed desktop launcher\n  file: {}\n",
        desktop_file.display()
    ))
}

fn desktop_entry(executable: &Path) -> String {
    format!(
        "[Desktop Entry]\nType=Application\nName=Lios\nGenericName=Terminal\nComment=GTK/VTE terminal emulator\nExec={}\nTryExec={}\nIcon=utilities-terminal\nTerminal=false\nCategories=System;TerminalEmulator;Utility;\nKeywords=shell;prompt;command;terminal;\nStartupNotify=true\nStartupWMClass=dev.lios.Terminal\n",
        desktop_exec_path(executable),
        desktop_exec_path(executable),
    )
}

fn applications_dir() -> Result<PathBuf, String> {
    if let Ok(data_home) = env::var("XDG_DATA_HOME") {
        if !data_home.is_empty() {
            return Ok(PathBuf::from(data_home).join("applications"));
        }
    }

    let home = env::var("HOME").map_err(|_| {
        "unable to determine applications directory; set HOME or XDG_DATA_HOME".to_string()
    })?;
    if home.is_empty() {
        return Err("unable to determine applications directory; HOME is empty".to_string());
    }

    Ok(PathBuf::from(home).join(".local/share/applications"))
}

fn desktop_exec_path(path: &Path) -> String {
    let text = path.to_string_lossy();
    if text
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '+'))
    {
        escape_percent(&text)
    } else {
        let escaped = text.chars().fold(String::new(), |mut output, ch| {
            match ch {
                '%' => output.push_str("%%"),
                '\\' | '"' | '$' | '`' => {
                    output.push('\\');
                    output.push(ch);
                }
                _ => output.push(ch),
            }
            output
        });
        format!("\"{escaped}\"")
    }
}

fn escape_percent(text: &str) -> String {
    text.replace('%', "%%")
}

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

    #[test]
    fn desktop_exec_path_leaves_simple_paths_unquoted() {
        assert_eq!(
            desktop_exec_path(Path::new("/usr/bin/lios")),
            "/usr/bin/lios"
        );
    }

    #[test]
    fn desktop_exec_path_quotes_paths_with_spaces() {
        assert_eq!(
            desktop_exec_path(Path::new("/tmp/Lios Build/lios")),
            "\"/tmp/Lios Build/lios\""
        );
    }

    #[test]
    fn desktop_exec_path_escapes_field_codes() {
        assert_eq!(
            desktop_exec_path(Path::new("/tmp/%/lios")),
            "\"/tmp/%%/lios\""
        );
    }
}