use std::{
env,
ffi::OsStr,
path::{Path, PathBuf},
};
const TERMINFO_DIRS: &[&str] = &[
"/etc/terminfo",
"/lib/terminfo",
"/usr/share/terminfo",
"/usr/lib/terminfo",
"/boot/system/data/terminfo", ];
#[derive(thiserror::Error, Debug, PartialEq)]
#[non_exhaustive]
pub enum Error {
#[error("InvalidTerminalName")]
InvalidTerminalName,
#[error("File not found")]
FileNotFound,
}
fn find_in_directory(term_name: &OsStr, dir: &Path) -> Result<PathBuf, Error> {
let Some(first_byte) = term_name.as_encoded_bytes().first() else {
return Err(Error::InvalidTerminalName);
};
let first_char = *first_byte as char;
let filename = dir.join(first_char.to_string()).join(term_name);
if filename.exists() {
return Ok(filename);
}
let first_byte_hex = format!("{:02x}", *first_byte);
let filename = dir.join(first_byte_hex).join(term_name);
if filename.exists() {
return Ok(filename);
}
Err(Error::FileNotFound)
}
pub fn search_directories() -> Vec<PathBuf> {
let mut search_dirs = vec![];
let mut default_dirs = TERMINFO_DIRS.iter().map(PathBuf::from);
if let Ok(dir) = env::var("TERMINFO") {
search_dirs.push(PathBuf::from(&dir));
}
if let Some(home_dir) = env::home_dir() {
let dir = home_dir.join(".terminfo");
search_dirs.push(dir);
}
if let Ok(dirs) = env::var("TERMINFO_DIRS") {
for dir in dirs.split(':') {
if dir.is_empty() {
search_dirs.extend(&mut default_dirs);
} else {
search_dirs.push(PathBuf::from(dir));
}
}
}
search_dirs.extend(&mut default_dirs);
search_dirs
}
pub fn locate(term_name: impl AsRef<OsStr>) -> Result<PathBuf, Error> {
for dir in search_directories() {
match find_in_directory(term_name.as_ref(), &dir) {
Ok(file) => return Ok(file),
Err(Error::FileNotFound) => {}
Err(err) => return Err(err),
}
}
Err(Error::FileNotFound)
}
#[cfg(test)]
mod test {
use std::fs::{File, create_dir, exists};
use tempfile::tempdir;
use super::*;
const TERM_NAME: &str = "no-such-terminal-123";
#[test]
fn empty_name() {
assert_eq!(locate(""), Err(Error::InvalidTerminalName));
}
#[test]
fn missing_file() {
assert_eq!(locate("no-such-terminal-1"), Err(Error::FileNotFound));
}
#[test]
fn found_xterm() {
let found_file = locate("xterm");
assert!(found_file.is_ok());
assert!(exists(found_file.unwrap()).unwrap());
}
#[test]
fn found_standard_layout_terminfo_dirs() {
let temp_dir = tempdir().unwrap();
let temp_dir = temp_dir.path();
let leaf_dir = temp_dir.join("n");
let terminfo_file = leaf_dir.join(TERM_NAME);
create_dir(leaf_dir).unwrap();
File::create(&terminfo_file).unwrap();
let terminfo_dirs = format!("foo:{}:bar", temp_dir.display());
temp_env::with_vars(
[("TERMINFO_DIRS", Some(terminfo_dirs)), ("TERMINFO", None)],
|| {
assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
},
);
}
#[test]
fn found_hex_layout_terminfo_dirs() {
let temp_dir = tempdir().unwrap();
let temp_dir = temp_dir.path();
let leaf_dir = temp_dir.join("6e");
let terminfo_file = leaf_dir.join(TERM_NAME);
create_dir(leaf_dir).unwrap();
File::create(&terminfo_file).unwrap();
let terminfo_dirs = format!("foo:{}:bar", temp_dir.display());
temp_env::with_vars(
[("TERMINFO_DIRS", Some(terminfo_dirs)), ("TERMINFO", None)],
|| {
assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
},
);
}
#[test]
fn found_standard_layout_terminfo_variable() {
let temp_dir = tempdir().unwrap();
let temp_dir = temp_dir.path();
let leaf_dir = temp_dir.join("n");
let terminfo_file = leaf_dir.join(TERM_NAME);
create_dir(leaf_dir).unwrap();
File::create(&terminfo_file).unwrap();
temp_env::with_vars(
[("TERMINFO_DIRS", None), ("TERMINFO", Some(temp_dir))],
|| {
assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
},
);
}
#[test]
fn dot_terminfo_standard_layout() {
let temp_dir = tempdir().unwrap();
let temp_dir = temp_dir.path();
let dot_terminfo = temp_dir.join(".terminfo");
let leaf_dir = dot_terminfo.join("n");
let terminfo_file = leaf_dir.join(TERM_NAME);
create_dir(dot_terminfo).unwrap();
create_dir(leaf_dir).unwrap();
File::create(&terminfo_file).unwrap();
temp_env::with_vars(
[
("TERMINFO_DIRS", None),
("TERMINFO", None),
("HOME", Some(temp_dir)),
],
|| {
assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
},
);
}
#[test]
fn search_order() {
let expected_dirs: Vec<PathBuf> = [
"/my/terminfo",
"/home/user/.terminfo",
"/my/terminfo1",
"/my/terminfo2",
"/etc/terminfo",
"/lib/terminfo",
"/usr/share/terminfo",
"/usr/lib/terminfo",
"/boot/system/data/terminfo",
]
.iter()
.map(PathBuf::from)
.collect();
temp_env::with_vars(
[
("TERMINFO_DIRS", Some("/my/terminfo1:/my/terminfo2")),
("TERMINFO", Some("/my/terminfo")),
("HOME", Some("/home/user")),
],
|| {
assert_eq!(search_directories(), expected_dirs);
},
);
}
#[test]
fn search_order_with_empty_element() {
let expected_dirs: Vec<PathBuf> = [
"/my/terminfo",
"/home/user/.terminfo",
"/my/terminfo1",
"/etc/terminfo",
"/lib/terminfo",
"/usr/share/terminfo",
"/usr/lib/terminfo",
"/boot/system/data/terminfo",
"/my/terminfo2",
]
.iter()
.map(PathBuf::from)
.collect();
temp_env::with_vars(
[
("TERMINFO_DIRS", Some("/my/terminfo1::/my/terminfo2")),
("TERMINFO", Some("/my/terminfo")),
("HOME", Some("/home/user")),
],
|| {
assert_eq!(search_directories(), expected_dirs);
},
);
}
}