fish-shell/fish-rust/src/env_dispatch.rs
Johannes Altmanninger 55fd43d86c Port reader
2024-01-07 00:54:22 +01:00

834 lines
30 KiB
Rust

use crate::common::ToCString;
use crate::complete::complete_invalidate_path;
use crate::curses::{self, Term};
use crate::env::{setenv_lock, unsetenv_lock, EnvMode, EnvStack, Environment};
use crate::env::{CURSES_INITIALIZED, READ_BYTE_LIMIT, TERM_HAS_XN};
use crate::flog::FLOG;
use crate::function;
use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms};
use crate::output::ColorSupport;
use crate::proc::is_interactive_session;
use crate::reader::{
reader_change_cursor_selection_mode, reader_change_history, reader_schedule_prompt_repaint,
reader_set_autosuggestion_enabled,
};
use crate::screen::screen_set_midnight_commander_hack;
use crate::screen::LAYOUT_CACHE_SHARED;
use crate::wchar::prelude::*;
use crate::wutil::fish_wcstoi;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
#[cxx::bridge]
mod env_dispatch_ffi {
extern "Rust" {
fn env_dispatch_init_ffi();
fn term_supports_setting_title() -> bool;
fn use_posix_spawn() -> bool;
}
}
/// List of all locale environment variable names that might trigger (re)initializing of the locale
/// subsystem. These are only the variables we're possibly interested in.
#[rustfmt::skip]
const LOCALE_VARIABLES: [&wstr; 10] = [
L!("LANG"), L!("LANGUAGE"), L!("LC_ALL"),
L!("LC_COLLATE"), L!("LC_CTYPE"), L!("LC_MESSAGES"),
L!("LC_NUMERIC"), L!("LC_TIME"), L!("LOCPATH"),
L!("fish_allow_singlebyte_locale"),
];
#[rustfmt::skip]
const CURSES_VARIABLES: [&wstr; 3] = [
L!("TERM"), L!("TERMINFO"), L!("TERMINFO_DIRS")
];
/// Whether to use `posix_spawn()` when possible.
static USE_POSIX_SPAWN: AtomicBool = AtomicBool::new(false);
/// Whether we think we can set the terminal title or not.
static CAN_SET_TERM_TITLE: AtomicBool = AtomicBool::new(false);
/// The variable dispatch table. This is set at startup and cannot be modified after.
static VAR_DISPATCH_TABLE: once_cell::sync::Lazy<VarDispatchTable> =
once_cell::sync::Lazy::new(|| {
let mut table = VarDispatchTable::default();
for name in LOCALE_VARIABLES {
table.add_anon(name, handle_locale_change);
}
for name in CURSES_VARIABLES {
table.add_anon(name, handle_curses_change);
}
table.add(L!("TZ"), handle_tz_change);
table.add_anon(L!("fish_term256"), handle_fish_term_change);
table.add_anon(L!("fish_term24bit"), handle_fish_term_change);
table.add_anon(L!("fish_escape_delay_ms"), update_wait_on_escape_ms);
table.add_anon(
L!("fish_sequence_key_delay_ms"),
update_wait_on_sequence_key_ms,
);
table.add_anon(L!("fish_emoji_width"), guess_emoji_width);
table.add_anon(L!("fish_ambiguous_width"), handle_change_ambiguous_width);
table.add_anon(L!("LINES"), handle_term_size_change);
table.add_anon(L!("COLUMNS"), handle_term_size_change);
table.add_anon(L!("fish_complete_path"), handle_complete_path_change);
table.add_anon(L!("fish_function_path"), handle_function_path_change);
table.add_anon(L!("fish_read_limit"), handle_read_limit_change);
table.add_anon(L!("fish_history"), handle_fish_history_change);
table.add_anon(
L!("fish_autosuggestion_enabled"),
handle_autosuggestion_change,
);
table.add_anon(
L!("fish_use_posix_spawn"),
handle_fish_use_posix_spawn_change,
);
table.add_anon(L!("fish_trace"), handle_fish_trace);
table.add_anon(
L!("fish_cursor_selection_mode"),
handle_fish_cursor_selection_mode_change,
);
table
});
type NamedEnvCallback = fn(name: &wstr, env: &EnvStack);
type AnonEnvCallback = fn(env: &EnvStack);
enum EnvCallback {
Named(NamedEnvCallback),
Anon(AnonEnvCallback),
}
#[derive(Default)]
struct VarDispatchTable {
table: HashMap<&'static wstr, EnvCallback>,
}
impl VarDispatchTable {
/// Add a callback for the variable `name`. We must not already be observing this variable.
pub fn add(&mut self, name: &'static wstr, callback: NamedEnvCallback) {
let prev = self.table.insert(name, EnvCallback::Named(callback));
assert!(prev.is_none(), "Already observing {}", name);
}
/// Add an callback for the variable `name`. We must not already be observing this variable.
pub fn add_anon(&mut self, name: &'static wstr, callback: AnonEnvCallback) {
let prev = self.table.insert(name, EnvCallback::Anon(callback));
assert!(prev.is_none(), "Already observing {}", name);
}
pub fn dispatch(&self, key: &wstr, vars: &EnvStack) {
match self.table.get(key) {
Some(EnvCallback::Named(named)) => (named)(key, vars),
Some(EnvCallback::Anon(anon)) => (anon)(vars),
None => (),
}
}
}
fn handle_timezone(var_name: &wstr, vars: &EnvStack) {
let var = vars.get_unless_empty(var_name).map(|v| v.as_string());
FLOG!(
env_dispatch,
"handle_timezone() current timezone var:",
var_name,
"=>",
var.as_ref()
.map(|v| v.as_utfstr())
.unwrap_or(L!("MISSING/EMPTY")),
);
if let Some(value) = var {
setenv_lock(var_name, &value, true);
} else {
unsetenv_lock(var_name);
}
extern "C" {
fn tzset();
}
unsafe {
tzset();
}
}
/// Update the value of [`FISH_EMOJI_WIDTH`].
fn guess_emoji_width(vars: &EnvStack) {
use crate::fallback::FISH_EMOJI_WIDTH;
if let Some(width_str) = vars.get(L!("fish_emoji_width")) {
// The only valid values are 1 or 2; we default to 1 if it was an invalid int.
let new_width = fish_wcstoi(&width_str.as_string()).unwrap_or(1).clamp(1, 2);
FISH_EMOJI_WIDTH.store(new_width, Ordering::Relaxed);
FLOG!(
term_support,
"Overriding default fish_emoji_width w/",
new_width
);
return;
}
let term = vars
.get(L!("TERM_PROGRAM"))
.map(|v| v.as_string())
.unwrap_or_else(WString::new);
// The format and contents of $TERM_PROGRAM_VERSION depend on $TERM_PROGRAM. Under
// Apple_Terminal, this is an integral value in the hundreds corresponding to the
// CFBundleVersion of Terminal.app; under iTerm, this is the version number which can contain
// multiple periods (e.g 3.4.19). Currently we only care about Apple_Terminal but the C++ code
// used wcstod() to parse at least the major.minor value of cases like the latter.
//
// TODO: Move this inside the Apple_Terminal branch and use i32::FromStr (i.e. str::parse())
// instead.
let version = vars
.get(L!("TERM_PROGRAM_VERSION"))
.map(|v| v.as_string())
.and_then(|v| {
let mut consumed = 0;
crate::wutil::wcstod::wcstod(&v, '.', &mut consumed).ok()
})
.unwrap_or(0.0);
if term == "Apple_Terminal" && version as i32 >= 400 {
// Apple Terminal on High Sierra
FISH_EMOJI_WIDTH.store(2, Ordering::Relaxed);
FLOG!(term_support, "default emoji width: 2 for", term);
} else if term == "iTerm.app" {
// iTerm2 now defaults to Unicode 9 sizes for anything after macOS 10.12
FISH_EMOJI_WIDTH.store(2, Ordering::Relaxed);
FLOG!(term_support, "default emoji width 2 for iTerm2");
} else {
// Default to whatever the system's wcwidth gives for U+1F603, but only if it's at least
// 1 and at most 2.
let width = crate::fallback::wcwidth('😃').clamp(1, 2);
FISH_EMOJI_WIDTH.store(width, Ordering::Relaxed);
FLOG!(term_support, "default emoji width:", width);
}
}
/// React to modifying the given variable.
pub fn env_dispatch_var_change(key: &wstr, vars: &EnvStack) {
use once_cell::sync::Lazy;
// We want to ignore variable changes until the dispatch table is explicitly initialized.
if let Some(dispatch_table) = Lazy::get(&VAR_DISPATCH_TABLE) {
dispatch_table.dispatch(key, vars);
}
}
fn handle_fish_term_change(vars: &EnvStack) {
update_fish_color_support(vars);
reader_schedule_prompt_repaint();
}
fn handle_change_ambiguous_width(vars: &EnvStack) {
let new_width = vars
.get(L!("fish_ambiguous_width"))
.map(|v| v.as_string())
// We use the default value of 1 if it was an invalid int.
.and_then(|fish_ambiguous_width| fish_wcstoi(&fish_ambiguous_width).ok())
.unwrap_or(1)
// Clamp in case of negative values.
.max(0);
crate::fallback::FISH_AMBIGUOUS_WIDTH.store(new_width, Ordering::Relaxed);
}
fn handle_term_size_change(vars: &EnvStack) {
crate::termsize::handle_columns_lines_var_change(vars);
}
fn handle_fish_history_change(vars: &EnvStack) {
let session_id = crate::history::history_session_id(vars);
reader_change_history(&session_id);
}
fn handle_fish_cursor_selection_mode_change(vars: &EnvStack) {
use crate::reader::CursorSelectionMode;
let inclusive = vars
.get(L!("fish_cursor_selection_mode"))
.as_ref()
.map(|v| v.as_string())
.map(|v| v == "inclusive")
.unwrap_or(false);
let mode = if inclusive {
CursorSelectionMode::Inclusive
} else {
CursorSelectionMode::Exclusive
};
reader_change_cursor_selection_mode(mode);
}
fn handle_autosuggestion_change(vars: &EnvStack) {
reader_set_autosuggestion_enabled(vars);
}
fn handle_function_path_change(_: &EnvStack) {
function::invalidate_path();
}
fn handle_complete_path_change(_: &EnvStack) {
complete_invalidate_path()
}
fn handle_tz_change(var_name: &wstr, vars: &EnvStack) {
handle_timezone(var_name, vars);
}
fn handle_locale_change(vars: &EnvStack) {
init_locale(vars);
// We need to re-guess emoji width because the locale might have changed to a multibyte one.
guess_emoji_width(vars);
}
fn handle_curses_change(vars: &EnvStack) {
guess_emoji_width(vars);
init_curses(vars);
}
fn handle_fish_use_posix_spawn_change(vars: &EnvStack) {
// Note that if the variable is missing or empty we default to true (if allowed).
if !allow_use_posix_spawn() {
USE_POSIX_SPAWN.store(false, Ordering::Relaxed);
} else if let Some(var) = vars.get(L!("fish_use_posix_spawn")) {
let use_posix_spawn =
var.is_empty() || crate::wcstringutil::bool_from_string(&var.as_string());
USE_POSIX_SPAWN.store(use_posix_spawn, Ordering::Relaxed);
} else {
USE_POSIX_SPAWN.store(true, Ordering::Relaxed);
}
}
/// Allow the user to override the limits on how much data the `read` command will process. This is
/// primarily intended for testing, but could also be used directly by users in special situations.
fn handle_read_limit_change(vars: &EnvStack) {
let read_byte_limit = vars
.get_unless_empty(L!("fish_read_limit"))
.map(|v| v.as_string())
.and_then(|v| {
// We use fish_wcstoul() to support leading/trailing whitespace
match (crate::wutil::fish_wcstoul(&v).ok())
// wcstoul() returns a u64 but want a usize. Handle overflow on 32-bit platforms.
.and_then(|_u64| usize::try_from(_u64).ok())
{
Some(v) => Some(v),
None => {
// We intentionally warn here even in non-interactive mode.
FLOG!(warning, "Ignoring invalid $fish_read_limit");
None
}
}
});
// Clippy should recognize comments in an empty match branch as a valid pattern!
#[allow(clippy::single_match)]
match read_byte_limit {
Some(new_limit) => READ_BYTE_LIMIT.store(new_limit, Ordering::Relaxed),
None => {
// TODO: reset READ_BYTE_LIMIT to the default value on receiving an invalid value
// instead of persisting the previous value, which may or may not have been the
// default.
}
}
}
fn handle_fish_trace(vars: &EnvStack) {
let enabled = vars.get_unless_empty(L!("fish_trace")).is_some();
crate::trace::trace_set_enabled(enabled);
}
pub fn env_dispatch_init(vars: &EnvStack) {
use once_cell::sync::Lazy;
run_inits(vars);
// env_dispatch_var_change() purposely supresses change notifications until the dispatch table
// was initialized elsewhere (either explicitly as below or via deref of VAR_DISPATCH_TABLE).
Lazy::force(&VAR_DISPATCH_TABLE);
}
pub fn env_dispatch_init_ffi() {
let vars = EnvStack::principal();
env_dispatch_init(vars);
}
/// Runs the subset of dispatch functions that need to be called at startup.
fn run_inits(vars: &EnvStack) {
init_locale(vars);
init_curses(vars);
guess_emoji_width(vars);
update_wait_on_escape_ms(vars);
update_wait_on_sequence_key_ms(vars);
handle_read_limit_change(vars);
handle_fish_use_posix_spawn_change(vars);
handle_fish_trace(vars);
}
/// Updates our idea of whether we support term256 and term24bit (see issue #10222).
fn update_fish_color_support(vars: &EnvStack) {
// Detect or infer term256 support. If fish_term256 is set, we respect it. Otherwise, infer it
// from $TERM or use terminfo.
let term = vars
.get(L!("TERM"))
.map(|v| v.as_string())
.unwrap_or_else(WString::new);
let max_colors = curses::term().and_then(|term| term.max_colors);
let mut supports_256color = false;
let mut supports_24bit = false;
if let Some(fish_term256) = vars.get(L!("fish_term256")).map(|v| v.as_string()) {
// $fish_term256
supports_256color = crate::wcstringutil::bool_from_string(&fish_term256);
FLOG!(
term_support,
"256-color support determined by $fish_term256:",
supports_256color
);
} else if term.find(L!("256color")).is_some() {
// TERM contains "256color": 256 colors explicitly supported.
supports_256color = true;
FLOG!(term_support, "256-color support enabled for TERM", term);
} else if term.find(L!("xterm")).is_some() {
// Assume that all "xterm" terminals can handle 256
supports_256color = true;
FLOG!(term_support, "256-color support enabled for TERM", term);
}
// See if terminfo happens to identify 256 colors
else if let Some(max_colors) = max_colors {
supports_256color = max_colors >= 256;
FLOG!(
term_support,
"256-color support:",
max_colors,
"per termcap/terminfo entry for",
term
);
}
if let Some(fish_term24bit) = vars.get(L!("fish_term24bit")).map(|v| v.as_string()) {
// $fish_term24bit
supports_24bit = crate::wcstringutil::bool_from_string(&fish_term24bit);
FLOG!(
term_support,
"$fish_term24bit preference: 24-bit color",
if supports_24bit {
"enabled"
} else {
"disabled"
}
);
} else if vars.get(L!("STY")).is_some() || term.starts_with(L!("eterm")) {
// Screen and emacs' ansi-term swallow true-color sequences, so we ignore them unless
// force-enabled.
supports_24bit = false;
FLOG!(
term_support,
"True-color support: disabled for eterm/screen"
);
} else if max_colors.unwrap_or(0) > 32767 {
// $TERM wins, xterm-direct reports 32767 colors and we assume that's the minimum as xterm
// is weird when it comes to color.
supports_24bit = true;
FLOG!(
term_support,
"True-color support: enabled per termcap/terminfo for",
term,
"with",
max_colors.unwrap(),
"colors"
);
} else if let Some(ct) = vars.get(L!("COLORTERM")).map(|v| v.as_string()) {
// If someone sets $COLORTERM, that's the sort of color they want.
if ct == "truecolor" || ct == "24bit" {
supports_24bit = true;
}
FLOG!(
term_support,
"True-color support",
if supports_24bit {
"enabled"
} else {
"disabled"
},
"per $COLORTERM",
ct
);
} else if vars.get(L!("KONSOLE_VERSION")).is_some()
|| vars.get(L!("KONSOLE_PROFILE_NAME")).is_some()
{
// All Konsole versions that use $KONSOLE_VERSION are new enough to support this, so no
// check is needed.
supports_24bit = true;
FLOG!(term_support, "True-color support: enabled for Konsole");
} else if let Some(it) = vars.get(L!("ITERM_SESSION_ID")).map(|v| v.as_string()) {
// Supporting versions of iTerm include a colon here.
// We assume that if this is iTerm it can't also be st, so having this check inside is okay.
if !it.contains(':') {
supports_24bit = true;
FLOG!(term_support, "True-color support: enabled for iTerm");
}
} else if term.starts_with("st-") {
supports_24bit = true;
FLOG!(term_support, "True-color support: enabling for st");
} else if let Some(vte) = vars.get(L!("VTE_VERSION")).map(|v| v.as_string()) {
if fish_wcstoi(&vte).unwrap_or(0) > 3600 {
supports_24bit = true;
FLOG!(
term_support,
"True-color support: enabled for VTE version",
vte
);
}
}
let mut color_support = ColorSupport::default();
color_support.set(ColorSupport::TERM_256COLOR, supports_256color);
color_support.set(ColorSupport::TERM_24BIT, supports_24bit);
crate::output::set_color_support(color_support);
}
/// Try to initialize the terminfo/curses subsystem using our fallback terminal name. Do not set
/// `$TERM` to our fallback. We're only doing this in the hope of getting a functional shell.
/// If we launch an external command that uses `$TERM`, it should get the same value we were given,
/// if any.
fn initialize_curses_using_fallbacks(vars: &EnvStack) {
// xterm-256color is the most used terminal type by a massive margin, especially counting
// terminals that are mostly compatible.
const FALLBACKS: [&str; 4] = ["xterm-256color", "xterm", "ansi", "dumb"];
let current_term = vars
.get_unless_empty(L!("TERM"))
.map(|v| v.as_string())
.unwrap_or(Default::default());
for term in FALLBACKS {
// If $TERM is already set to the fallback name we're about to use, there's no point in
// seeing if the fallback name can be used.
if current_term == term {
continue;
}
// `term` here is one of our hard-coded strings above; we can unwrap because we can
// guarantee it doesn't contain any interior NULs.
let term_cstr = CString::new(term).unwrap();
let success = curses::setup(Some(&term_cstr), libc::STDOUT_FILENO, |term| {
apply_term_hacks(vars, term)
})
.is_some();
if is_interactive_session() {
if success {
FLOG!(warning, wgettext!("Using fallback terminal type"), term);
} else {
FLOG!(
warning,
wgettext!("Could not set up terminal using the fallback terminal type"),
term,
);
}
}
if success {
break;
}
}
}
/// Apply any platform- or environment-specific hacks to our curses [`Term`] instance.
fn apply_term_hacks(vars: &EnvStack, term: &mut Term) {
if cfg!(target_os = "macos") {
// Hack in missing italics and dim capabilities omitted from macOS xterm-256color terminfo.
// Improves the user experience under Terminal.app and iTerm.
let term_prog = vars
.get(L!("TERM_PROGRAM"))
.map(|v| v.as_string())
.unwrap_or(WString::new());
if term_prog == "Apple_Terminal" || term_prog == "iTerm.app" {
if let Some(term_val) = vars.get(L!("TERM")).map(|v| v.as_string()) {
if term_val == "xterm-256color" {
const SITM_ESC: &[u8] = b"\x1B[3m";
const RITM_ESC: &[u8] = b"\x1B[23m";
const DIM_ESC: &[u8] = b"\x1B[2m";
if term.enter_italics_mode.is_none() {
term.enter_italics_mode = Some(SITM_ESC.to_cstring());
}
if term.exit_italics_mode.is_none() {
term.exit_italics_mode = Some(RITM_ESC.to_cstring());
}
if term.enter_dim_mode.is_none() {
term.enter_dim_mode = Some(DIM_ESC.to_cstring());
}
}
}
}
}
}
/// Apply any platform- or environment-specific hacks that don't involve a `Term` instance.
fn apply_non_term_hacks(vars: &EnvStack) {
// Midnight Commander tries to extract the last line of the prompt, and does so in a way that is
// broken if you do '\r' after it like we normally do.
// See https://midnight-commander.org/ticket/4258.
if vars.get(L!("MC_SID")).is_some() {
screen_set_midnight_commander_hack();
}
}
/// This is a pretty lame heuristic for detecting terminals that do not support setting the title.
/// If we recognise the terminal name as that of a virtual terminal, we assume it supports setting
/// the title. If we recognise it as that of a console, we assume it does not support setting the
/// title. Otherwise we check the ttyname and see if we believe it is a virtual terminal.
///
/// One situation in which this breaks down is with screen, since screen supports setting the
/// terminal title if the underlying terminal does so, but will print garbage on terminals that
/// don't. Since we can't see the underlying terminal below screen there is no way to fix this.
fn does_term_support_setting_title(vars: &EnvStack) -> bool {
#[rustfmt::skip]
const TITLE_TERMS: &[&wstr] = &[
L!("xterm"), L!("screen"), L!("tmux"), L!("nxterm"),
L!("rxvt"), L!("alacritty"), L!("wezterm"), L!("rio"),
L!("foot"),
];
let Some(term) = vars.get_unless_empty(L!("TERM")).map(|v| v.as_string()) else {
return false;
};
let term: &wstr = term.as_ref();
if curses::term().map(|term| term.set_title.is_some()) == Some(true) {
return true;
}
let recognized = TITLE_TERMS.contains(&term)
|| term.starts_with(L!("xterm-"))
|| term.starts_with(L!("screen-"))
|| term.starts_with(L!("tmux-"));
if !recognized {
if [
L!("linux"),
L!("dumb"),
L!("vt100"), // NetBSD
L!("wsvt25"),
]
.contains(&term)
{
return false;
}
let mut buf = [b'\0'; libc::PATH_MAX as usize];
let retval =
unsafe { libc::ttyname_r(libc::STDIN_FILENO, buf.as_mut_ptr().cast(), buf.len()) };
let buf = &buf[..buf.iter().position(|c| *c == b'\0').unwrap()];
if retval != 0
|| buf.windows(b"tty".len()).any(|w| w == b"tty")
|| buf.windows(b"/vc/".len()).any(|w| w == b"/vc/")
{
return false;
}
}
true
}
// Initialize the curses subsystem
fn init_curses(vars: &EnvStack) {
for var_name in CURSES_VARIABLES {
if let Some(value) = vars
.getf_unless_empty(var_name, EnvMode::EXPORT)
.map(|v| v.as_string())
{
FLOG!(term_support, "curses var", var_name, "=", value);
setenv_lock(var_name, &value, true);
} else {
FLOG!(term_support, "curses var", var_name, "is missing or empty");
unsetenv_lock(var_name);
}
}
if curses::setup(None, libc::STDOUT_FILENO, |term| {
apply_term_hacks(vars, term)
})
.is_none()
{
if is_interactive_session() {
let term = vars.get_unless_empty(L!("TERM")).map(|v| v.as_string());
FLOG!(warning, wgettext!("Could not set up terminal."));
if let Some(term) = term {
FLOG!(warning, wgettext!("TERM environment variable set to"), term);
FLOG!(
warning,
wgettext!("Check that this terminal type is supported on this system.")
);
} else {
FLOG!(warning, wgettext!("TERM environment variable not set."));
}
}
initialize_curses_using_fallbacks(vars);
}
// Configure hacks that apply regardless of whether we successfully init curses or not.
apply_non_term_hacks(vars);
// Store some global variables that reflect the term's capabilities
CAN_SET_TERM_TITLE.store(does_term_support_setting_title(vars), Ordering::Relaxed);
if let Some(term) = curses::term() {
TERM_HAS_XN.store(term.eat_newline_glitch, Ordering::Relaxed);
}
update_fish_color_support(vars);
// Invalidate the cached escape sequences since they may no longer be valid.
unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() }.clear();
CURSES_INITIALIZED.store(true, Ordering::Relaxed);
}
/// Initialize the locale subsystem
fn init_locale(vars: &EnvStack) {
#[rustfmt::skip]
const UTF8_LOCALES: &[&str] = &[
"C.UTF-8", "en_US.UTF-8", "en_GB.UTF-8", "de_DE.UTF-8", "C.utf8", "UTF-8",
];
let old_msg_locale: CString = {
let old = unsafe { libc::setlocale(libc::LC_MESSAGES, ptr::null()) };
assert_ne!(old, ptr::null_mut());
// We have to make a copy because the subsequent setlocale() call to change the locale will
// invalidate the pointer from this setlocale() call.
// safety: `old` is not a null-pointer, and should be a reference to the currently set locale
unsafe { CStr::from_ptr(old.cast()) }.to_owned()
};
for var_name in LOCALE_VARIABLES {
let var = vars
.getf_unless_empty(var_name, EnvMode::EXPORT)
.map(|v| v.as_string());
if let Some(value) = var {
FLOG!(env_locale, "locale var", var_name, "=", value);
setenv_lock(var_name, &value, true);
} else {
FLOG!(env_locale, "locale var", var_name, "is missing or empty");
unsetenv_lock(var_name);
}
}
let user_locale = {
let loc_ptr = unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr().cast()) };
if loc_ptr.is_null() {
FLOG!(env_locale, "user has an invalid locale configured");
None
} else {
// safety: setlocale did not return a null-pointer, so it is a valid pointer
Some(unsafe { CStr::from_ptr(loc_ptr) })
}
};
// Try to get a multibyte-capable encoding.
// A "C" locale is broken for our purposes: any wchar function will break on it. So we try
// *really, really, really hard* to not have one.
let fix_locale = vars
.get_unless_empty(L!("fish_allow_singlebyte_locale"))
.map(|v| v.as_string())
.map(|allow_c| !crate::wcstringutil::bool_from_string(&allow_c))
.unwrap_or(true);
if fix_locale && crate::compat::MB_CUR_MAX() == 1 {
FLOG!(env_locale, "Have singlebyte locale, trying to fix.");
for locale in UTF8_LOCALES {
{
let locale = CString::new(locale.to_owned()).unwrap();
// this can fail, that is fine
unsafe { libc::setlocale(libc::LC_CTYPE, locale.as_ptr()) };
}
if crate::compat::MB_CUR_MAX() > 1 {
FLOG!(env_locale, "Fixed locale:", locale);
break;
}
}
if crate::compat::MB_CUR_MAX() == 1 {
FLOG!(env_locale, "Failed to fix locale.");
}
}
// We *always* use a C-locale for numbers because we want '.' (except for in printf).
let loc_ptr = unsafe { libc::setlocale(libc::LC_NUMERIC, b"C\0".as_ptr().cast()) };
// should never fail, the C locale should always be defined
assert_ne!(loc_ptr, ptr::null_mut());
// See that we regenerate our special locale for numbers
crate::locale::invalidate_numeric_locale();
crate::common::fish_setlocale();
FLOG!(
env_locale,
"init_locale() setlocale():",
user_locale
.map(CStr::to_string_lossy)
.unwrap_or(Cow::Borrowed("(null)"))
);
let new_msg_loc_ptr = unsafe { libc::setlocale(libc::LC_MESSAGES, std::ptr::null()) };
// should never fail
assert_ne!(new_msg_loc_ptr, ptr::null_mut());
// safety: we just asserted it is not a null-pointer.
let new_msg_locale = unsafe { CStr::from_ptr(new_msg_loc_ptr) };
FLOG!(
env_locale,
"Old LC_MESSAGES locale:",
old_msg_locale.to_string_lossy()
);
FLOG!(
env_locale,
"New LC_MESSAGES locale:",
new_msg_locale.to_string_lossy()
);
#[cfg(feature = "gettext")]
{
if old_msg_locale.as_c_str() != new_msg_locale {
// Make change known to GNU gettext.
extern "C" {
static mut _nl_msg_cat_cntr: libc::c_int;
}
unsafe {
_nl_msg_cat_cntr += 1;
}
}
}
}
pub fn use_posix_spawn() -> bool {
USE_POSIX_SPAWN.load(Ordering::Relaxed)
}
/// Whether or not we are running on an OS where we allow ourselves to use `posix_spawn()`.
const fn allow_use_posix_spawn() -> bool {
#![allow(clippy::if_same_then_else)]
#![allow(clippy::needless_bool)]
// OpenBSD's posix_spawn returns status 127 instead of erroring with ENOEXEC when faced with a
// shebang-less script. Disable posix_spawn on OpenBSD.
if cfg!(target_os = "openbsd") {
false
} else if cfg!(not(target_os = "linux")) {
true
} else {
// The C++ code used __GLIBC_PREREQ(2, 24) && !defined(__UCLIBC__) to determine if we'll use
// posix_spawn() by default on Linux. Surprise! We don't have to worry about porting that
// logic here because the libc crate only supports 2.26+ atm.
// See https://github.com/rust-lang/libc/issues/1412
true
}
}
/// Returns true if we think the terminal support setting its title.
pub fn term_supports_setting_title() -> bool {
CAN_SET_TERM_TITLE.load(Ordering::Relaxed)
}