term-detect 0.1.8

Terminal emulator detector
Documentation
//! DE-specific terminal detection code  
//!
//! TODO: Merge similar code

use crate::{DetectionError, Terminal};
use configparser::ini::Ini;
use std::{env::var, path::Path, process::Command};

/// Spawn a process and return a trimmed string from stdout
trait SpawnCleanStringify {
    fn spawn_clean_stringify(&mut self) -> Result<String, DetectionError>;
}

impl SpawnCleanStringify for Command {
    fn spawn_clean_stringify(&mut self) -> Result<String, DetectionError> {
        let child = self.stdout(std::process::Stdio::piped()).spawn()?;
        let output = child.wait_with_output()?;
        Ok(String::from_utf8(output.stdout)
            .unwrap_or_default()
            .trim()
            .to_owned())
    }
}

fn get_home() -> String {
    var("HOME").unwrap_or_default()
}

/// Get the default terminal property with gsettings from a specified desktop
fn gsettings_desktop_get(desktop_name: &str) -> Result<Terminal, DetectionError> {
    let mut maybe_term = Command::new("gsettings")
        .arg("get")
        .arg(format!(
            "{}.desktop.default-applications.terminal",
            desktop_name
        ))
        .arg("exec")
        .spawn_clean_stringify()?;
    if maybe_term.starts_with('\'') && maybe_term.ends_with('\'') && maybe_term.len() > 1 {
        maybe_term = maybe_term[1..maybe_term.len() - 1].to_owned();
    }
    if maybe_term.is_empty() {
        Err(DetectionError::DEFindError)
    } else {
        Ok(maybe_term.into())
    }
}

/// Load an ini-like file and get a key from a section
fn ini_get<P: AsRef<Path>>(
    path: P,
    section: &str,
    key: &str,
) -> Result<Option<String>, DetectionError> {
    let mut config = Ini::new_cs();
    if let Err(e) = config.load(path) {
        return Err(DetectionError::IniParseError(e));
    };
    Ok(config.get(section, key))
}

/// Cinnamon detector
///
/// Same as GNOME except the id is "org.cinnamon" instead of "org.gnome"
pub fn detect_terminal_desktop_cinnamon() -> Result<Terminal, DetectionError> {
    gsettings_desktop_get("org.cinnamon")
}

/// Deepin detector
///
/// Same as GNOME except the id is "org.deepin" instead of "org.gnome"
pub fn detect_terminal_desktop_deepin() -> Result<Terminal, DetectionError> {
    gsettings_desktop_get("org.deepin")
}

/// GNOME and Budgie detector
///
/// GNOME stored its default terminal app in GConf  
///
/// This also works for Budgie, since it's a GNOME fork
///
/// This method is deprecated in GNOME and may not work in the future  
pub fn detect_terminal_desktop_gnome() -> Result<Terminal, DetectionError> {
    gsettings_desktop_get("org.gnome")
}

/// KDE Plasma detector
///
/// KDE Plasma 5 stores its default terminal config in ~/.config/kdeglobals  
///
/// If konsole is set as the default, then it's empty
pub fn detect_terminal_desktop_kde() -> Result<Terminal, DetectionError> {
    let maybe_term = Command::new("kreadconfig5")
        .arg("--group")
        .arg("General")
        .arg("--key")
        .arg("TerminalApplication")
        .spawn_clean_stringify()?;
    Ok(if maybe_term.is_empty() {
        "konsole".into()
    } else {
        maybe_term.into()
    })
}

/// LXDE detector
///
/// LXDE stores its config in ~/.config/lxsession/**NAME**/desktop.conf
///
/// where **NAME** is the desktop session name ex. "Lubuntu" or "LXDE"
pub fn detect_terminal_desktop_lxde() -> Result<Terminal, DetectionError> {
    let filename = format!(
        "{}/.config/lxsession/{}/desktop.conf",
        get_home(),
        var("XDG_SESSION_DESKTOP").unwrap_or("LXDE".to_owned()),
    );
    let Some(term) = ini_get(filename, "Session", "terminal_manager/command")? else {
        return Err(DetectionError::DEFindError);
    };
    Ok(term.into())
}

/// LXQt detector
///
/// It's kind of a mess. <https://github.com/lxqt/lxqt/issues/433>  
///
/// LXQt stores the terminal in the env variable LXQT_TERMINAL_EMULATOR
/// OR in ~/.config/lxqt/session.conf
pub fn detect_terminal_desktop_lxqt() -> Result<Terminal, DetectionError> {
    if let Ok(term) = var("LXQT_TERMINAL_EMULATOR") {
        return Ok(term.into());
    }
    let filename = format!("{}/.config/lxqt/session.conf", get_home());
    let Some(term) = ini_get(filename, "Environment", "TERM")? else {
        return Err(DetectionError::DEFindError);
    };
    Ok(term.into())
}

/// XFCE detector
///
/// In XFCE the default terminal is stored in ~/.config/xfce4/helpers.rc
pub fn detect_terminal_desktop_xfce() -> Result<Terminal, DetectionError> {
    let filename = format!("{}/.config/xfce4/helpers.rc", get_home());
    Ok(ini_get(filename, "default", "TerminalEmulator")?
        .unwrap_or("xfce4-terminal".to_owned())
        .into())
}