mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-25 10:11:17 -03:00
This is *simpler*, and would have made us detect the issue with broken includes before. Part of #12055
541 lines
18 KiB
Rust
541 lines
18 KiB
Rust
use crate::common::init_special_chars_once;
|
|
use crate::complete::complete_invalidate_path;
|
|
use crate::env::{DEFAULT_READ_BYTE_LIMIT, READ_BYTE_LIMIT};
|
|
use crate::env::{EnvMode, EnvStack, Environment, setenv_lock, unsetenv_lock};
|
|
use crate::flog::FLOG;
|
|
use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms};
|
|
use crate::locale::{invalidate_numeric_locale, set_libc_locales};
|
|
use crate::reader::{
|
|
reader_change_cursor_end_mode, reader_change_cursor_selection_mode, reader_change_history,
|
|
reader_schedule_prompt_repaint, reader_set_autosuggestion_enabled, reader_set_transient_prompt,
|
|
};
|
|
use crate::screen::{
|
|
IS_DUMB, LAYOUT_CACHE_SHARED, ONLY_GRAYSCALE, screen_set_midnight_commander_hack,
|
|
};
|
|
use crate::terminal::ColorSupport;
|
|
use crate::terminal::use_terminfo;
|
|
use crate::tty_handoff::xtversion;
|
|
use crate::wchar::prelude::*;
|
|
use crate::wutil::fish_wcstoi;
|
|
use crate::{function, terminal};
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
/// 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.
|
|
const LOCALE_VARIABLES: [&wstr; 7] = [
|
|
L!("LANG"),
|
|
L!("LANGUAGE"),
|
|
L!("LC_ALL"),
|
|
L!("LC_MESSAGES"),
|
|
L!("LC_NUMERIC"),
|
|
L!("LC_TIME"),
|
|
L!("LOCPATH"),
|
|
];
|
|
|
|
#[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);
|
|
|
|
/// 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_term_change);
|
|
}
|
|
|
|
table.add(L!("TZ"), handle_tz_change);
|
|
table.add_anon(L!("COLORTERM"), handle_fish_term_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_transient_prompt"), handle_transient_prompt_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.add_anon(
|
|
L!("fish_cursor_end_mode"),
|
|
handle_fish_cursor_end_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);
|
|
}
|
|
|
|
unsafe extern "C" {
|
|
unsafe fn tzset();
|
|
}
|
|
|
|
unsafe {
|
|
tzset();
|
|
}
|
|
}
|
|
|
|
/// Update the value of [`FISH_EMOJI_WIDTH`](crate::fallback::FISH_EMOJI_WIDTH).
|
|
pub 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) as isize;
|
|
FISH_EMOJI_WIDTH.store(new_width, Ordering::Relaxed);
|
|
FLOG!(
|
|
term_support,
|
|
"Overriding default fish_emoji_width w/",
|
|
new_width
|
|
);
|
|
return;
|
|
}
|
|
|
|
let term_program = vars
|
|
.get(L!("TERM_PROGRAM"))
|
|
.map(|v| v.as_string())
|
|
.unwrap_or_else(WString::new);
|
|
|
|
// TODO(term-workaround)
|
|
if xtversion().unwrap_or(L!("")).starts_with(L!("iTerm2 ")) {
|
|
// 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 if term_program == "Apple_Terminal" && {
|
|
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);
|
|
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_program);
|
|
} else {
|
|
// Default to whatever the system's wcwidth gives for U+1F603, but only if it's at least
|
|
// 1 and at most 2.
|
|
#[cfg(not(cygwin))]
|
|
let width = crate::fallback::wcwidth('😃').clamp(1, 2);
|
|
#[cfg(cygwin)]
|
|
let width = 2_isize;
|
|
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) as isize;
|
|
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_fish_cursor_end_mode_change(vars: &EnvStack) {
|
|
use crate::reader::CursorEndMode;
|
|
|
|
let inclusive = vars
|
|
.get(L!("fish_cursor_end_mode"))
|
|
.as_ref()
|
|
.map(|v| v.as_string())
|
|
.map(|v| v == "inclusive")
|
|
.unwrap_or(false);
|
|
let mode = if inclusive {
|
|
CursorEndMode::Inclusive
|
|
} else {
|
|
CursorEndMode::Exclusive
|
|
};
|
|
|
|
reader_change_cursor_end_mode(mode);
|
|
}
|
|
|
|
fn handle_autosuggestion_change(vars: &EnvStack) {
|
|
reader_set_autosuggestion_enabled(vars);
|
|
}
|
|
|
|
fn handle_transient_prompt_change(vars: &EnvStack) {
|
|
reader_set_transient_prompt(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);
|
|
}
|
|
|
|
fn handle_term_change(vars: &EnvStack) {
|
|
guess_emoji_width(vars);
|
|
init_terminal(vars);
|
|
read_terminfo_database(vars);
|
|
reader_schedule_prompt_repaint();
|
|
}
|
|
|
|
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 !cfg!(have_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
|
|
}
|
|
}
|
|
});
|
|
|
|
match read_byte_limit {
|
|
Some(new_limit) => READ_BYTE_LIMIT.store(new_limit, Ordering::Relaxed),
|
|
None => READ_BYTE_LIMIT.store(DEFAULT_READ_BYTE_LIMIT, Ordering::Relaxed),
|
|
}
|
|
}
|
|
|
|
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 suppresses 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);
|
|
}
|
|
|
|
/// Runs the subset of dispatch functions that need to be called at startup.
|
|
fn run_inits(vars: &EnvStack) {
|
|
init_locale(vars);
|
|
init_special_chars_once();
|
|
init_terminal(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_unless_empty(L!("TERM"));
|
|
let term = term.as_ref().map_or(L!(""), |term| &term.as_list()[0]);
|
|
let is_xterm_16color = term == "xterm-16color";
|
|
|
|
let supports_256color = if let Some(fish_term256) = vars.get(L!("fish_term256")) {
|
|
let ok = crate::wcstringutil::bool_from_string(&fish_term256.as_string());
|
|
FLOG!(
|
|
term_support,
|
|
"256-color support determined by $fish_term256:",
|
|
ok
|
|
);
|
|
ok
|
|
} else {
|
|
!is_xterm_16color
|
|
};
|
|
|
|
let supports_24bit;
|
|
#[allow(unused_parens)]
|
|
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 (
|
|
// TODO(term-workaround)
|
|
vars.get(L!("STY")).is_some()
|
|
) {
|
|
// Screen requires "truecolor on" to enable true-color sequences, so we ignore them
|
|
// unless force-enabled.
|
|
supports_24bit = false;
|
|
FLOG!(term_support, "True-color support: disabled for screen");
|
|
} 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.
|
|
supports_24bit = ct == "truecolor" || ct == "24bit";
|
|
FLOG!(
|
|
term_support,
|
|
"True-color support",
|
|
if supports_24bit {
|
|
"enabled"
|
|
} else {
|
|
"disabled"
|
|
},
|
|
"per $COLORTERM",
|
|
ct
|
|
);
|
|
} else {
|
|
supports_24bit = !is_xterm_16color && {
|
|
// TODO(term-workaround)
|
|
vars.get_unless_empty(L!("TERM_PROGRAM"))
|
|
.is_none_or(|term| term.as_list()[0] != "Apple_Terminal")
|
|
};
|
|
FLOG!(
|
|
term_support,
|
|
"True-color support",
|
|
if supports_24bit {
|
|
"enabled"
|
|
} else {
|
|
"disabled"
|
|
},
|
|
);
|
|
}
|
|
|
|
let mut color_support = ColorSupport::default();
|
|
color_support.set(ColorSupport::TERM_256COLOR, supports_256color);
|
|
color_support.set(ColorSupport::TERM_24BIT, supports_24bit);
|
|
crate::terminal::set_color_support(color_support);
|
|
}
|
|
|
|
pub const MIDNIGHT_COMMANDER_SID: &wstr = L!("MC_SID");
|
|
|
|
// Initialize the terminal subsystem
|
|
fn init_terminal(vars: &EnvStack) {
|
|
let term = vars.get(L!("TERM"));
|
|
let term = term
|
|
.as_ref()
|
|
.and_then(|v| v.as_list().first())
|
|
.map(|v| v.as_utfstr())
|
|
.unwrap_or(L!(""));
|
|
|
|
IS_DUMB.store(term == "dumb");
|
|
ONLY_GRAYSCALE.store(term == "ansi-m" || term == "linux-m" || term == "xterm-mono");
|
|
|
|
// TODO(term-workaround)
|
|
if vars.get(MIDNIGHT_COMMANDER_SID).is_some() {
|
|
screen_set_midnight_commander_hack();
|
|
}
|
|
|
|
update_fish_color_support(vars);
|
|
}
|
|
|
|
pub fn read_terminfo_database(vars: &EnvStack) {
|
|
if !use_terminfo() {
|
|
return;
|
|
}
|
|
|
|
// The current process' environment needs to be modified because the terminfo crate will
|
|
// read these variables
|
|
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);
|
|
}
|
|
}
|
|
|
|
terminal::setup();
|
|
|
|
// Invalidate the cached escape sequences since they may no longer be valid.
|
|
LAYOUT_CACHE_SHARED.lock().unwrap().clear();
|
|
}
|
|
|
|
/// Initialize the locale subsystem
|
|
fn init_locale(vars: &EnvStack) {
|
|
let _guard = crate::locale::LOCALE_LOCK.lock().unwrap();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Safety: we hold the locale lock.
|
|
if !unsafe {
|
|
set_libc_locales(/*log_ok=*/ true)
|
|
} {
|
|
FLOG!(env_locale, "user has an invalid locale configured");
|
|
}
|
|
|
|
// Invalidate the cached numeric locale.
|
|
invalidate_numeric_locale();
|
|
|
|
#[cfg(feature = "localize-messages")]
|
|
crate::wutil::gettext::update_locale_from_env(vars);
|
|
}
|
|
|
|
pub fn use_posix_spawn() -> bool {
|
|
USE_POSIX_SPAWN.load(Ordering::Relaxed)
|
|
}
|