use std::sync::OnceLock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Utf8Locale {
Export(String),
Inherit,
Unavailable,
}
pub(super) fn fixture_environment() -> Option<Vec<String>> {
match host_locale() {
Utf8Locale::Export(name) => Some(vec![format!("LC_ALL={name}")]),
Utf8Locale::Inherit => None,
Utf8Locale::Unavailable => panic!(
"no UTF-8 locale is established on this host: `locale -a` listed none and the \
inherited LC_ALL/LC_CTYPE/LANG do not select one, so a pane shell would discard \
multi-byte input before rmux observed it"
),
}
}
fn host_locale() -> &'static Utf8Locale {
static HOST_LOCALE: OnceLock<Utf8Locale> = OnceLock::new();
HOST_LOCALE.get_or_init(|| {
let installed = std::process::Command::new("locale")
.arg("-a")
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).into_owned());
choose(
installed.as_deref(),
environment_value("LC_ALL").as_deref(),
environment_value("LC_CTYPE").as_deref(),
environment_value("LANG").as_deref(),
)
})
}
fn choose(
installed: Option<&str>,
lc_all: Option<&str>,
lc_ctype: Option<&str>,
lang: Option<&str>,
) -> Utf8Locale {
if let Some(name) = installed.and_then(installed_utf8_locale) {
return Utf8Locale::Export(name.to_owned());
}
if effective_ctype(lc_all, lc_ctype, lang).is_some_and(is_utf8_locale) {
return Utf8Locale::Inherit;
}
Utf8Locale::Unavailable
}
fn installed_utf8_locale(listing: &str) -> Option<&str> {
let names = || {
listing
.lines()
.map(str::trim)
.filter(|name| !name.is_empty())
};
names()
.find(|name| name.eq_ignore_ascii_case("C.UTF-8"))
.or_else(|| names().find(|name| name.eq_ignore_ascii_case("en_US.UTF-8")))
.or_else(|| names().find(|name| is_utf8_locale(name)))
}
fn effective_ctype<'a>(
lc_all: Option<&'a str>,
lc_ctype: Option<&'a str>,
lang: Option<&'a str>,
) -> Option<&'a str> {
lc_all.or(lc_ctype).or(lang)
}
fn is_utf8_locale(name: &str) -> bool {
let name = name.to_ascii_lowercase();
name.ends_with(".utf-8") || name.ends_with(".utf8") || name == "utf-8"
}
fn environment_value(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|value| !value.is_empty())
}
#[cfg(test)]
mod tests {
use super::{choose, effective_ctype, installed_utf8_locale, Utf8Locale};
const LISTING: &str = "C\nC.UTF-8\nen_US.ISO8859-1\nen_US.UTF-8\nPOSIX\n";
#[test]
fn the_parser_prefers_c_utf8_then_en_us_then_any_utf8_entry() {
assert_eq!(installed_utf8_locale(LISTING), Some("C.UTF-8"));
assert_eq!(
installed_utf8_locale("C\nen_US.ISO8859-1\nen_US.UTF-8\n"),
Some("en_US.UTF-8")
);
assert_eq!(
installed_utf8_locale("C\n fr_FR.utf8 \nPOSIX\n"),
Some("fr_FR.utf8")
);
}
#[test]
fn the_parser_reports_no_match_for_a_single_byte_only_listing() {
assert_eq!(installed_utf8_locale("C\nPOSIX\nen_US.ISO8859-1\n"), None);
assert_eq!(installed_utf8_locale(""), None);
}
#[test]
fn an_installed_utf8_locale_is_exported_by_name() {
assert_eq!(
choose(Some(LISTING), None, None, None),
Utf8Locale::Export("C.UTF-8".to_owned())
);
}
#[test]
fn a_missing_or_failed_locale_command_keeps_an_inherited_utf8_locale() {
assert_eq!(
choose(None, None, None, Some("en_US.UTF-8")),
Utf8Locale::Inherit
);
assert_eq!(
choose(None, None, Some("fr_FR.utf8"), None),
Utf8Locale::Inherit
);
}
#[test]
fn a_listing_without_any_utf8_entry_keeps_an_inherited_utf8_locale() {
assert_eq!(
choose(Some("C\nPOSIX\n"), None, None, Some("en_US.UTF-8")),
Utf8Locale::Inherit
);
}
#[test]
fn nothing_is_forced_when_no_probe_establishes_a_utf8_locale() {
assert_eq!(choose(None, None, None, None), Utf8Locale::Unavailable);
assert_eq!(
choose(Some("C\nPOSIX\n"), None, None, Some("en_US.ISO8859-1")),
Utf8Locale::Unavailable
);
}
#[test]
fn lc_all_overrides_lc_ctype_and_lang() {
assert_eq!(
effective_ctype(Some("C"), Some("en_US.UTF-8"), Some("en_US.UTF-8")),
Some("C")
);
assert_eq!(
effective_ctype(None, Some("C"), Some("en_US.UTF-8")),
Some("C")
);
assert_eq!(
effective_ctype(None, None, Some("en_US.UTF-8")),
Some("en_US.UTF-8")
);
assert_eq!(
choose(None, Some("C"), None, Some("en_US.UTF-8")),
Utf8Locale::Unavailable
);
}
}