find_browsers 0.2.1

A library for retrieving installed browsers (supports Windows and Linux).
Documentation
use crate::Browser;
use dirs::home_dir;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

const HTTP_HANDLER: &str = "x-scheme-handler/http";

const SYSTEM_DESKTOP_PATH: &str = "/usr/share/applications/";
const SYSTEM_MIMEINFO_PATH: &str = "/usr/share/applications/mimeinfo.cache";
const LOCAL_DESKTOP_PATH: &str = ".local/share/applications/";
const LOCAL_MIMEINFO_PATH: &str = ".local/share/applications/mimeinfo.cache";
const FLATPAK_DESKTOP_PATH: &str = "/var/lib/flatpak/exports/share/applications";
const FLATPAK_MIMEINFO_PATH: &str = "/var/lib/flatpak/exports/share/applications/mimeinfo.cache";

pub fn get_browsers() -> Result<Vec<Browser>, Box<dyn std::error::Error>> {
    let browser_names = find_installed_browsers()?;
    Ok(resolve_browser_exec_paths(&browser_names))
}

fn sanitize_exec_path(exec_path: String) -> String {
    exec_path.replace("%u", "").replace("%U", "")
}

fn find_installed_browsers() -> Result<Vec<String>, std::io::Error> {
    let mut installed_browsers = Vec::new();
    for mimeinfo_path in get_mimeinfo_paths() {
        if mimeinfo_path.exists() {
            let file_content = fs::read_to_string(&mimeinfo_path)?;
            extract_browsers_from_mimeinfo(&file_content, &mut installed_browsers);
        }
    }
    Ok(installed_browsers)
}

fn get_mimeinfo_paths() -> Vec<PathBuf> {
    let mut paths = vec![
        PathBuf::from(SYSTEM_MIMEINFO_PATH),
        PathBuf::from(FLATPAK_MIMEINFO_PATH),
    ];
    if let Some(home) = home_dir() {
        let local_mimeinfo = home.join(LOCAL_MIMEINFO_PATH);
        paths.push(local_mimeinfo);
    }
    paths
}

fn extract_browsers_from_mimeinfo(content: &str, installed_browsers: &mut Vec<String>) {
    content
        .lines()
        .find(|line| line.starts_with(HTTP_HANDLER))
        .and_then(|line| {
            line.split('=').nth(1).map(|browsers| {
                installed_browsers.extend(browsers.split(';').map(str::to_string));
            })
        });
}

fn resolve_browser_exec_paths(browser_names: &[String]) -> Vec<Browser> {
    let mut browsers_map = HashMap::new();
    for desktop_path in get_desktop_paths() {
        if desktop_path.exists() {
            let entries = fs::read_dir(desktop_path).unwrap_or_else(|_| fs::read_dir("/").unwrap());
            for entry in entries.flatten() {
                let entry_path = entry.path();
                if let Some(file_name) = entry_path.file_name().and_then(|s| s.to_str()) {
                    if browser_names.contains(&file_name.to_string()) {
                        if let Some(browser) = parse_desktop_entry(&entry_path) {
                            browsers_map.insert(browser.name.clone(), browser);
                        }
                    }
                }
            }
        }
    }
    browsers_map.into_values().collect()
}

fn get_desktop_paths() -> Vec<PathBuf> {
    let mut paths = vec![
        PathBuf::from(SYSTEM_DESKTOP_PATH),
        PathBuf::from(FLATPAK_DESKTOP_PATH),
    ];
    if let Some(home) = home_dir() {
        paths.push(home.join(LOCAL_DESKTOP_PATH));
    }
    paths
}

fn resolve_executable_path(exec: String) -> PathBuf {
    let exec = sanitize_exec_path(exec.clone());
    if exec.contains("flatpak") {
        let parts: Vec<&str> = exec.split_whitespace().collect();
        if parts.len() > 2 && parts[0] == "/usr/bin/flatpak" && parts[1] == "run" {
            let sanitized_exec = sanitize_flatpak_exec(exec);
            return PathBuf::from(sanitized_exec);
        }
    }

    let path = PathBuf::from(exec.trim());
    if path.is_absolute() {
        path
    } else {
        if let Ok(output) = Command::new("which")
            .arg(path.to_string_lossy().as_ref())
            .output()
        {
            if output.status.success() {
                String::from_utf8_lossy(&output.stdout)
                    .trim()
                    .into()
            } else {
                path
            }
        } else {
            path
        }
    }
}

fn sanitize_flatpak_exec(exec: String) -> String {
    exec.split("@@").next().unwrap_or(&exec).trim().to_string()
}

fn parse_desktop_entry(path: &Path) -> Option<Browser> {
    let content = fs::read_to_string(path).ok()?;
    let name = extract_field_from_desktop_file("Name=", &content)?;
    let exec = extract_field_from_desktop_file("Exec=", &content)?;
    Some(Browser {
        name,
        exec: resolve_executable_path(exec),
    })
}

fn extract_field_from_desktop_file(prefix: &str, content: &str) -> Option<String> {
    content
        .lines()
        .find(|line| line.starts_with(prefix))
        .map(|line| line.trim_start_matches(prefix).to_string())
}