infoterm 0.1.1

ncurses-compatible terminfo parsing library
Documentation
//! Search for terminfo entries.

use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use thiserror::Error;

use crate::entry::{Entry, ParseError};

/// Error returned when loading an entry with [`Search::load`] fails.
#[derive(Debug, Error)]
pub enum LoadError {
    #[error("entry could not be found")]
    EntryNotFound,

    #[error("reading entry failed: {0}")]
    Io(#[from] io::Error),

    #[error(transparent)]
    Parse(#[from] ParseError),
}

/// A builder used to configure search paths.
///
/// The following directories are searched:
/// - Any paths passed to [`path()`][SearchBuilder::path], in reverse order.
/// - A single directory pointed to by the `TERMINFO` environment variable.
/// - The `.terminfo` directory in the user's home directory.
/// - A list of directories pointed to by the `TERMINFO_DIRS` environment variable.
/// - A set of platform-specific fixed directories:
///     - `/etc/terminfo` and `/lib/terminfo` on Linux.
///     - `/usr/share/terminfo` on Unix.
pub struct SearchBuilder {
    search_env: bool,
    search_home: bool,
    search_system: bool,
    extra_paths: Vec<PathBuf>,
}

impl SearchBuilder {
    /// Whether to search directories specified in the environment variables `TERMINFO` and `TERMINFO_DIRS`.
    /// Defaults to true.
    pub fn search_env(&mut self, search: bool) -> &mut Self {
        self.search_env = search;
        self
    }

    /// Whether to search the `.terminfo` directory in the user's home directory.
    /// Defaults to true.
    pub fn search_home(&mut self, search: bool) -> &mut Self {
        self.search_home = search;
        self
    }

    /// Whether to search the default system paths.
    /// Defaults to true.
    pub fn search_system(&mut self, search: bool) -> &mut Self {
        self.search_system = search;
        self
    }

    /// Add an additional path to the search. Takes precedence over all other paths.
    pub fn path(&mut self, path: impl AsRef<Path>) -> &mut Self {
        self.extra_paths.push(path.as_ref().to_path_buf());
        self
    }

    /// Compiles all of the configured paths into a [`Search`]. Any environment variables needed are
    /// looked up at this point and not cached.
    pub fn build(self) -> Search {
        let mut paths = Vec::from_iter(self.extra_paths.into_iter().rev());

        if self.search_env {
            if let Some(terminfo) = env::var_os("TERMINFO") {
                paths.push(PathBuf::from(terminfo));
            }
        }

        if self.search_home {
            if let Some(mut home) = dirs::home_dir() {
                home.push(".terminfo");
                paths.push(home);
            }
        }

        if self.search_env {
            if let Some(terminfo_dirs) = env::var_os("TERMINFO_DIRS") {
                paths.extend(env::split_paths(&terminfo_dirs))
            }
        }

        if self.search_system {
            // Debian and Ubuntu use these
            if cfg!(target_os = "linux") {
                paths.push(PathBuf::from("/etc/terminfo"));
                paths.push(PathBuf::from("/lib/terminfo"));
            }

            if cfg!(unix) {
                paths.push(PathBuf::from("/usr/share/terminfo"));
            }
        }

        Search { paths: paths.into() }
    }
}

/// Searches the filesystem for terminfo entries.
///
/// See [`SearchBuilder`] for which paths are searched.
///
/// # Example
///
/// ```rust
/// use infoterm::Search;
///
/// let xterm = Search::standard().find("xterm-256color");
///
/// println!("Entry for xterm-256color: {xterm:?}");
/// ```
#[derive(Clone, Debug)]
pub struct Search {
    paths: Arc<[PathBuf]>
}

impl Search {
    /// Creates a new builder with a default configuration.
    pub fn new() -> SearchBuilder {
        SearchBuilder {
            search_env: true,
            search_home: true,
            search_system: true,
            extra_paths: Vec::new(),
        }
    }

    /// Creates a new search that only uses the standard paths.
    ///
    /// Equivalent to `Search::new().build()`.
    pub fn standard() -> Search {
        Search::new().build()
    }

    /// Creates a new builder that only searches the user-specified paths.
    ///
    /// Equivalent to `Search::new().search_env(false).search_home(false).search_system(false)`.
    pub fn custom() -> SearchBuilder {
        SearchBuilder {
            search_env: false,
            search_home: false,
            search_system: false,
            extra_paths: Vec::new(),
        }
    }

    /// Attempts to locate the entry for `term`.
    pub fn find(&self, term: impl AsRef<OsStr>) -> Option<PathBuf> {
        let term = super::valid_terminal(term.as_ref())?;

        self._find(term)
    }

    /// Attempts to locate the entry for `term` and read the file.
    pub fn read(&self, term: impl AsRef<OsStr>) -> io::Result<Vec<u8>> {
        self.find(term)
            .ok_or(io::Error::from(io::ErrorKind::NotFound))
            .and_then(fs::read)
    }

    /// Attempts to locate the entry for `term`, read the file, and parse it.
    pub fn load(&self, term: impl AsRef<OsStr>) -> Result<Entry, LoadError> {
        let p = self.find(term).ok_or(LoadError::EntryNotFound)?;
        let b = fs::read(p)?;
        let e = Entry::parse(&b)?;
        Ok(e)
    }

    fn _find(&self, term: &str) -> Option<PathBuf> {
        // xterm -> x/xterm
        let entry_name = format!("{}/{term}", &term[..1]);
        // xterm -> 78/xterm, for case-insensitive filesystems
        let entry_name2 = format!("{:02x}/{term}", term.as_bytes()[0]);

        for path in self.paths.iter() {
            let p1 = path.join(&entry_name);

            if p1.is_file() {
                return Some(p1);
            }

            let p2 = path.join(&entry_name2);

            if p2.is_file() {
                return Some(p2);
            }
        }

        None
    }
}