lnchr 0.2.2

A fuzzy terminal app launcher
use std::env;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};

use crate::path_ext::IsExecutable;

pub struct DesktopInfo {
    pub name: String,
    pub exec: String,
    pub terminal: bool,
}

pub fn parse_desktop_info(path: &PathBuf) -> Option<DesktopInfo> {
    let content = std::fs::read_to_string(path).ok()?;
    let (mut name, mut exec, mut terminal, mut no_display, mut hidden, mut app_type) =
        (None, None, false, false, false, None);
    let mut in_de = false;

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if line == "[Desktop Entry]" {
            in_de = true;
            continue;
        }
        if !in_de || line.starts_with('[') {
            if in_de {
                break;
            }
            continue;
        }
        if let Some(v) = line.strip_prefix("Type=") {
            app_type = Some(v);
        } else if let Some(v) = line.strip_prefix("Name=") {
            name = Some(v.to_owned());
        } else if let Some(v) = line.strip_prefix("Exec=") {
            exec = Some(v.to_owned());
        } else if line == "Terminal=true" {
            terminal = true;
        } else if line == "NoDisplay=true" {
            no_display = true;
        } else if line == "Hidden=true" {
            hidden = true;
        } else if let Some(v) = line.strip_prefix("TryExec=") {
            let exists = if v.starts_with('/') {
                PathBuf::from(v).is_executable()
            } else {
                env::var_os("PATH")
                    .map(|p| env::split_paths(&p).collect::<Vec<_>>())
                    .unwrap_or_default()
                    .iter()
                    .any(|dir| dir.join(v).is_executable())
            };
            if !exists {
                return None;
            }
        }
    }

    if app_type? != "Application" {
        return None;
    }
    if no_display || hidden {
        return None;
    }

    Some(DesktopInfo {
        name: name?,
        exec: exec?,
        terminal,
    })
}

fn tokenize(s: &str) -> Vec<String> {
    let mut args = Vec::new();
    let mut current = String::new();
    let mut chars = s.chars();

    while let Some(c) = chars.next() {
        match c {
            '\\' => {
                if let Some(next) = chars.next() {
                    current.push(next);
                }
            }
            '"' => {
                while let Some(c) = chars.next() {
                    match c {
                        '\\' => {
                            if let Some(next) = chars.next() {
                                current.push(next);
                            }
                        }
                        '"' => break,
                        _ => current.push(c),
                    }
                }
            }
            c if c.is_ascii_whitespace() => {
                if !current.is_empty() {
                    args.push(current.clone());
                    current.clear();
                }
            }
            _ => current.push(c),
        }
    }
    if !current.is_empty() {
        args.push(current);
    }
    args
}

fn is_field_code(t: &str) -> bool {
    t.len() == 2 && t.starts_with('%') && t.as_bytes()[1].is_ascii_alphabetic()
}

fn parse_exec(s: &str) -> Vec<String> {
    tokenize(s)
        .into_iter()
        .filter(|t| !is_field_code(t))
        .map(|t| t.replace("%%", "%"))
        .collect()
}

pub fn launch(info: &DesktopInfo, cli_terminal: Option<&str>) -> bool {
    let args = parse_exec(&info.exec);
    if args.is_empty() {
        return false;
    }

    if info.terminal {
        return crate::terminal::exec(&args, cli_terminal);
    }
    unsafe {
        Command::new(&args[0])
            .args(&args[1..])
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .pre_exec(|| {
                libc::setsid();
                Ok(())
            })
            .spawn()
            .is_ok()
    }
}