1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use lazy_static::lazy_static;
use std::path::{Path, PathBuf};

/// terminfo directories for Debian based distributions.
///
/// Search for `--with-terminfo-dirs` at
/// https://salsa.debian.org/debian/ncurses/blob/master/debian/rules to find
/// the source of truth for this.
const TERMINFO_DIRS_DEBIAN: &str = "/etc/terminfo:/lib/terminfo:/usr/share/terminfo";

/// terminfo directories for RedHat based distributions.
///
/// CentOS compiled with
/// `--with-terminfo-dirs=%{_sysconfdir}/terminfo:%{_datadir}/terminfo`.
const TERMINFO_DIRS_REDHAT: &str = "/etc/terminfo:/usr/share/terminfo";

/// terminfo directories for macOS.
const TERMINFO_DIRS_MACOS: &str = "/usr/share/terminfo";

lazy_static! {
    static ref TERMINFO_DIRS_COMMON: Vec<PathBuf> = {
        vec![
            PathBuf::from("/usr/local/etc/terminfo"),
            PathBuf::from("/usr/local/lib/terminfo"),
            PathBuf::from("/usr/local/share/terminfo"),
            PathBuf::from("/etc/terminfo"),
            PathBuf::from("/usr/lib/terminfo"),
            PathBuf::from("/lib/terminfo"),
            PathBuf::from("/usr/share/terminfo"),
        ]
    };
}

#[derive(Clone)]
enum OsVariant {
    Linux,
    MacOs,
    Windows,
    Other,
}

enum LinuxDistroVariant {
    Debian,
    RedHat,
    Unknown,
}

lazy_static! {
    static ref TARGET_OS: OsVariant = {
        if cfg!(target_os = "linux") {
            OsVariant::Linux
        } else if cfg!(target_os = "macos") {
            OsVariant::MacOs
        } else if cfg!(target_os = "windows") {
            OsVariant::Windows
        } else {
            OsVariant::Other
        }
    };
}

struct OsInfo {
    os: OsVariant,
    linux_distro: Option<LinuxDistroVariant>,
}

fn resolve_linux_distro() -> LinuxDistroVariant {
    // Attempt to resolve the Linux distro by parsing /etc files.
    let os_release = Path::new("/etc/os-release");

    if let Ok(data) = std::fs::read_to_string(os_release) {
        for line in data.split('\n') {
            if line.starts_with("ID_LIKE=") {
                if line.contains("debian") {
                    return LinuxDistroVariant::Debian;
                } else if line.contains("rhel") || line.contains("fedora") {
                    return LinuxDistroVariant::RedHat;
                }
            } else if line.starts_with("ID=") && line.contains("fedora") {
                return LinuxDistroVariant::RedHat;
            }
        }
    }

    LinuxDistroVariant::Unknown
}

fn resolve_os_info() -> OsInfo {
    let os = TARGET_OS.clone();
    let linux_distro = match os {
        OsVariant::Linux => Some(resolve_linux_distro()),
        _ => None,
    };

    OsInfo { os, linux_distro }
}

/// Attempt to resolve the value for the `TERMINFO_DIRS` environment variable.
///
/// Returns Some() value that `TERMINFO_DIRS` should be set to or None if
/// no environment variable should be set.
pub fn resolve_terminfo_dirs() -> Option<String> {
    // Always respect an environment variable, if present.
    if std::env::var("TERMINFO_DIRS").is_ok() {
        return None;
    }

    let os_info = resolve_os_info();

    match os_info.os {
        OsVariant::Linux => match os_info.linux_distro.unwrap() {
            // TODO we could stat() the well-known paths ourselves and omit
            // paths that don't exist. This /might/ save some syscalls, since
            // ncurses doesn't appear to be the most frugal w.r.t. filesystem
            // requests.
            LinuxDistroVariant::Debian => Some(TERMINFO_DIRS_DEBIAN.to_string()),
            LinuxDistroVariant::RedHat => Some(TERMINFO_DIRS_REDHAT.to_string()),
            LinuxDistroVariant::Unknown => {
                // We don't know this Linux variant. Look for common terminfo
                // database directories and use paths that are found.
                let paths = TERMINFO_DIRS_COMMON
                    .iter()
                    .filter_map(|p| {
                        if p.exists() {
                            Some(p.display().to_string())
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<String>>()
                    .join(":");

                Some(paths)
            }
        },
        OsVariant::MacOs => Some(TERMINFO_DIRS_MACOS.to_string()),
        // Windows doesn't use the terminfo database.
        OsVariant::Windows => None,
        OsVariant::Other => None,
    }
}