Use XTVERSION for terminal-specific workarounds

As mentioned in earlier commit
("Query terminal before reading config").

Closes #11812
This commit is contained in:
Johannes Altmanninger
2025-09-21 23:55:07 +02:00
parent 99854c107a
commit c31e769f7d
10 changed files with 74 additions and 58 deletions

View File

@@ -1693,6 +1693,10 @@ Fish also provides additional information through the values of certain environm
the process ID (PID) of the shell. the process ID (PID) of the shell.
.. envvar:: fish_terminal
the name and version of the terminal fish is running inside (for example as reported via :ref:`XTVERSION <term-compat-xtversion>`).
.. envvar:: history .. envvar:: history
a list containing the last commands that were entered. a list containing the last commands that were entered.

View File

@@ -224,9 +224,12 @@ Optional Commands
- Ss - Ss
- Set cursor style (DECSCUSR); Ps is 2, 4 or 6 for block, underscore or line shape. - Set cursor style (DECSCUSR); Ps is 2, 4 or 6 for block, underscore or line shape.
- VT520 - VT520
* - ``\e[ Ps q`` * - .. _term-compat-xtversion:
``\e[ Ps q``
- n/a - n/a
- Request terminal name and version (XTVERSION). - Request terminal name and version (XTVERSION).
This is only used for temporary workarounds for incompatible terminals.
- XTerm - XTerm
* - ``\e[?25h`` * - ``\e[?25h``
- cvvis - cvvis

View File

@@ -145,16 +145,14 @@ end" >$__fish_config_dir/config.fish
if not set -q fish_handle_reflow if not set -q fish_handle_reflow
# VTE reflows the text itself, so us doing it inevitably races against it. # VTE reflows the text itself, so us doing it inevitably races against it.
# Guidance from the VTE developers is to let them repaint. # Guidance from the VTE developers is to let them repaint.
if set -q VTE_VERSION # Konsole reflows since version 21.04. Konsole added XTVERSION
# Same for these terminals # in v22.03.80~7.
if string match -rq -- "$fish_terminal" '^(?:VTE\b|Konsole |WezTerm )'
or begin
set -q KONSOLE_VERSION
and test "$KONSOLE_VERSION" -ge 210400 2>/dev/null
end
or string match -q -- 'alacritty*' $TERM or string match -q -- 'alacritty*' $TERM
or test "$TERM_PROGRAM" = WezTerm
set -g fish_handle_reflow 0
else if set -q KONSOLE_VERSION
and test "$KONSOLE_VERSION" -ge 210400 2>/dev/null
# Konsole since version 21.04(.00)
# Note that this is optional, but since we have no way of detecting it
# we go with the default, which is true.
set -g fish_handle_reflow 0 set -g fish_handle_reflow 0
else else
set -g fish_handle_reflow 1 set -g fish_handle_reflow 1
@@ -171,6 +169,10 @@ end" >$__fish_config_dir/config.fish
if not functions --query __fish_update_cwd_osc if not functions --query __fish_update_cwd_osc
function __fish_update_cwd_osc --on-variable PWD --description 'Notify terminals when $PWD changes' function __fish_update_cwd_osc --on-variable PWD --description 'Notify terminals when $PWD changes'
set -l host $hostname set -l host $hostname
# if set -l konsole_version (string match -r -- '^Konsole (\d+)\..*' "$fish_terminal")[2]
# # To-do: use a Konsole version where KF6_DEP_VERSION is >= 6.12
# and $konsole_version -lt ???
# end
if set -q KONSOLE_VERSION if set -q KONSOLE_VERSION
set host '' set host ''
end end

View File

@@ -39,6 +39,7 @@
environment::{env_init, EnvStack, Environment}, environment::{env_init, EnvStack, Environment},
EnvMode, Statuses, EnvMode, Statuses,
}, },
env_dispatch::guess_emoji_width,
eprintf, eprintf,
event::{self, Event}, event::{self, Event},
flog::{self, activate_flog_categories_by_pattern, set_flog_file_fd, FLOG, FLOGF}, flog::{self, activate_flog_categories_by_pattern, set_flog_file_fd, FLOG, FLOGF},
@@ -62,6 +63,7 @@
signal::{signal_clear_cancel, signal_unblock_all}, signal::{signal_clear_cancel, signal_unblock_all},
threads::{self}, threads::{self},
topic_monitor, topic_monitor,
tty_handoff::xtversion,
wchar::prelude::*, wchar::prelude::*,
wutil::waccess, wutil::waccess,
}; };
@@ -553,6 +555,12 @@ enum CommandSource {
.pending_input .pending_input
.borrow_mut() .borrow_mut()
.extend(std::mem::take(&mut input_data.queue)); .extend(std::mem::take(&mut input_data.queue));
parser.vars().set_one(
L!("fish_terminal"),
EnvMode::GLOBAL,
xtversion().unwrap().to_owned(),
);
guess_emoji_width(parser.vars());
} }
if !opts.no_exec && !opts.no_config { if !opts.no_exec && !opts.no_config {

1
src/env/var.rs vendored
View File

@@ -249,6 +249,7 @@ pub struct ElectricVar {
ElectricVar{name: L!("fish_kill_signal"), flags:electric::READONLY | electric::COMPUTED}, ElectricVar{name: L!("fish_kill_signal"), flags:electric::READONLY | electric::COMPUTED},
ElectricVar{name: L!("fish_killring"), flags:electric::READONLY | electric::COMPUTED}, ElectricVar{name: L!("fish_killring"), flags:electric::READONLY | electric::COMPUTED},
ElectricVar{name: L!("fish_pid"), flags:electric::READONLY}, ElectricVar{name: L!("fish_pid"), flags:electric::READONLY},
ElectricVar{name: L!("fish_terminal"), flags:electric::READONLY},
ElectricVar{name: L!("history"), flags:electric::READONLY | electric::COMPUTED}, ElectricVar{name: L!("history"), flags:electric::READONLY | electric::COMPUTED},
ElectricVar{name: L!("hostname"), flags:electric::READONLY}, ElectricVar{name: L!("hostname"), flags:electric::READONLY},
ElectricVar{name: L!("pipestatus"), flags:electric::READONLY | electric::COMPUTED}, ElectricVar{name: L!("pipestatus"), flags:electric::READONLY | electric::COMPUTED},

View File

@@ -12,6 +12,7 @@
}; };
use crate::terminal::use_terminfo; use crate::terminal::use_terminfo;
use crate::terminal::ColorSupport; use crate::terminal::ColorSupport;
use crate::tty_handoff::xtversion;
use crate::wchar::prelude::*; use crate::wchar::prelude::*;
use crate::wutil::fish_wcstoi; use crate::wutil::fish_wcstoi;
use crate::{function, terminal}; use crate::{function, terminal};
@@ -153,7 +154,7 @@ fn handle_timezone(var_name: &wstr, vars: &EnvStack) {
} }
/// Update the value of [`FISH_EMOJI_WIDTH`](crate::fallback::FISH_EMOJI_WIDTH). /// Update the value of [`FISH_EMOJI_WIDTH`](crate::fallback::FISH_EMOJI_WIDTH).
fn guess_emoji_width(vars: &EnvStack) { pub fn guess_emoji_width(vars: &EnvStack) {
use crate::fallback::FISH_EMOJI_WIDTH; use crate::fallback::FISH_EMOJI_WIDTH;
if let Some(width_str) = vars.get(L!("fish_emoji_width")) { if let Some(width_str) = vars.get(L!("fish_emoji_width")) {
@@ -168,7 +169,7 @@ fn guess_emoji_width(vars: &EnvStack) {
return; return;
} }
let term = vars let term_program = vars
.get(L!("TERM_PROGRAM")) .get(L!("TERM_PROGRAM"))
.map(|v| v.as_string()) .map(|v| v.as_string())
.unwrap_or_else(WString::new); .unwrap_or_else(WString::new);
@@ -189,14 +190,14 @@ fn guess_emoji_width(vars: &EnvStack) {
}) })
.unwrap_or(0.0); .unwrap_or(0.0);
if term == "Apple_Terminal" && version as i32 >= 400 { if xtversion().unwrap().starts_with(L!("iTerm2 ")) {
// 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 // iTerm2 now defaults to Unicode 9 sizes for anything after macOS 10.12
FISH_EMOJI_WIDTH.store(2, Ordering::Relaxed); FISH_EMOJI_WIDTH.store(2, Ordering::Relaxed);
FLOG!(term_support, "default emoji width 2 for iTerm2"); FLOG!(term_support, "default emoji width 2 for iTerm2");
} else if term_program == "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_program);
} else { } else {
// Default to whatever the system's wcwidth gives for U+1F603, but only if it's at least // Default to whatever the system's wcwidth gives for U+1F603, but only if it's at least
// 1 and at most 2. // 1 and at most 2.
@@ -372,7 +373,6 @@ pub fn env_dispatch_init(vars: &EnvStack) {
fn run_inits(vars: &EnvStack) { fn run_inits(vars: &EnvStack) {
init_locale(vars); init_locale(vars);
init_terminal(vars); init_terminal(vars);
guess_emoji_width(vars);
update_wait_on_escape_ms(vars); update_wait_on_escape_ms(vars);
update_wait_on_sequence_key_ms(vars); update_wait_on_sequence_key_ms(vars);
handle_read_limit_change(vars); handle_read_limit_change(vars);

View File

@@ -13,7 +13,9 @@
use crate::reader::reader_test_and_clear_interrupted; use crate::reader::reader_test_and_clear_interrupted;
use crate::terminal::{SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE}; use crate::terminal::{SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::threads::iothread_port; use crate::threads::iothread_port;
use crate::tty_handoff::{get_kitty_keyboard_capability, maybe_set_kitty_keyboard_capability}; use crate::tty_handoff::{
get_kitty_keyboard_capability, maybe_set_kitty_keyboard_capability, XTVERSION,
};
use crate::universal_notifier::default_notifier; use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*}; use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate}; use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
@@ -1439,13 +1441,14 @@ fn parse_xtversion(&mut self, buffer: &mut Vec<u8>) -> Option<()> {
if buffer.get(3)? != &b'|' { if buffer.get(3)? != &b'|' {
return None; return None;
} }
FLOG!( XTVERSION.get_or_init(|| {
reader, let xtversion = str2wcstring(&buffer[4..buffer.len()]);
format!( FLOG!(
"Received XTVERSION response: {}", reader,
str2wcstring(&buffer[4..buffer.len()]) format!("Received XTVERSION response: {}", xtversion)
) );
); xtversion
});
None None
} }

View File

@@ -146,6 +146,7 @@
tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED, tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED,
TOK_SHOW_COMMENTS, TOK_SHOW_COMMENTS,
}; };
use crate::tty_handoff::XTVERSION;
use crate::tty_handoff::{ use crate::tty_handoff::{
get_tty_protocols_active, initialize_tty_metadata, maybe_set_kitty_keyboard_capability, get_tty_protocols_active, initialize_tty_metadata, maybe_set_kitty_keyboard_capability,
safe_deactivate_tty_protocols, TtyHandoff, safe_deactivate_tty_protocols, TtyHandoff,
@@ -272,7 +273,7 @@ pub fn terminal_init() -> InputEventQueue {
let mut input_queue = InputEventQueue::new(STDIN_FILENO); let mut input_queue = InputEventQueue::new(STDIN_FILENO);
let _init_tty_metadata = ScopeGuard::new((), |()| { let _init_tty_metadata = ScopeGuard::new((), |()| {
initialize_tty_metadata(); initialize_tty_metadata(XTVERSION.get_or_init(WString::new));
}); });
if !querying_allowed(STDIN_FILENO) { if !querying_allowed(STDIN_FILENO) {

View File

@@ -14,13 +14,20 @@
}; };
use crate::terminal::{Output, Outputter}; use crate::terminal::{Output, Outputter};
use crate::threads::assert_is_main_thread; use crate::threads::assert_is_main_thread;
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString; use crate::wchar_ext::ToWString;
use crate::wutil::perror; use crate::wutil::{perror, wcstoi};
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG}; use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
pub static XTVERSION: OnceCell<WString> = OnceCell::new();
pub fn xtversion() -> Option<&'static wstr> {
XTVERSION.get().as_ref().map(|s| s.as_utfstr())
}
// Facts about our environment, which inform how we handle the tty. // Facts about our environment, which inform how we handle the tty.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct TtyMetadata { pub struct TtyMetadata {
@@ -33,13 +40,11 @@ pub struct TtyMetadata {
impl TtyMetadata { impl TtyMetadata {
// Create a new TtyMetadata instance with the current environment. // Create a new TtyMetadata instance with the current environment.
fn detect() -> Self { fn detect(xtversion: &wstr) -> Self {
use std::env::var_os; let in_tmux = xtversion.starts_with(L!("tmux "));
let in_tmux = var_os("TMUX").is_some();
// Detect iTerm2 before 3.5.12. // Detect iTerm2 before 3.5.12.
let pre_kitty_iterm2 = get_iterm2_version().is_some_and(|v| v < (3, 5, 12)); let pre_kitty_iterm2 = get_iterm2_version(xtversion).is_some_and(|v| v < (3, 5, 12));
Self { Self {
in_tmux, in_tmux,
pre_kitty_iterm2, pre_kitty_iterm2,
@@ -185,30 +190,21 @@ fn tty_protocols() -> Option<&'static TtyProtocolsSet> {
unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() } unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() }
} }
// Get the TTY protocols, initializing it if necessary. // Initialize TTY metadata.
// This also initializes the terminal enable and disable serialized commands. // This also initializes the terminal enable and disable serialized commands.
// Note in practice this is only used from the main thread - races are very unlikely. pub fn initialize_tty_metadata(xtversion: &wstr) {
fn get_or_init_tty_protocols() -> &'static TtyProtocolsSet {
use std::sync::atomic::Ordering::{Acquire, Release}; use std::sync::atomic::Ordering::{Acquire, Release};
// Standard lazy-init pattern from rust-atomics-and-locks. // Standard lazy-init pattern from rust-atomics-and-locks.
let mut p = TTY_PROTOCOLS.load(Acquire); let mut p = TTY_PROTOCOLS.load(Acquire);
if p.is_null() { if p.is_null() {
// Try to swap in a new TTY protocols set. // Try to swap in a new TTY protocols set.
p = Box::into_raw(Box::new(TtyMetadata::detect().get_protocols())); p = Box::into_raw(Box::new(TtyMetadata::detect(xtversion).get_protocols()));
if let Err(e) = TTY_PROTOCOLS.compare_exchange(std::ptr::null_mut(), p, Release, Acquire) { if let Err(_e) = TTY_PROTOCOLS.compare_exchange(std::ptr::null_mut(), p, Release, Acquire) {
// Safety: p comes from Box::into_raw right above, // Safety: p comes from Box::into_raw right above,
// and wasn't shared with any other thread. // and wasn't shared with any other thread.
drop(unsafe { Box::from_raw(p) }); drop(unsafe { Box::from_raw(p) });
p = e;
} }
} }
// Safety: p is not null and points to a properly initialized value.
unsafe { &*p }
}
// Initialize TTY metadata.
pub fn initialize_tty_metadata() {
get_or_init_tty_protocols();
} }
// A marker of the current state of the tty protocols. // A marker of the current state of the tty protocols.
@@ -563,17 +559,18 @@ fn drop(&mut self) {
} }
// If we are running under iTerm2, get the version as a tuple of (major, minor, patch). // If we are running under iTerm2, get the version as a tuple of (major, minor, patch).
fn get_iterm2_version() -> Option<(u32, u32, u32)> { fn get_iterm2_version(xtversion: &wstr) -> Option<(u32, u32, u32)> {
use std::env::var; // TODO split_once
let term = var("LC_TERMINAL").ok()?; let mut xtversion = xtversion.split(' ');
if term != "iTerm2" { let name = xtversion.next().unwrap();
let version = xtversion.next()?;
if name != "iTerm2" {
return None; return None;
} }
let version = var("LC_TERMINAL_VERSION").ok()?;
let mut parts = version.split('.'); let mut parts = version.split('.');
Some(( Some((
parts.next()?.parse().ok()?, wcstoi(parts.next()?).ok()?,
parts.next()?.parse().ok()?, wcstoi(parts.next()?).ok()?,
parts.next()?.parse().ok()?, wcstoi(parts.next()?).ok()?,
)) ))
} }

View File

@@ -62,15 +62,12 @@ def makeenv(script_path: Path, home: Path) -> Dict[str, str]:
"LANGUAGE", "LANGUAGE",
"MC_SID", "MC_SID",
"MC_TMPDIR", "MC_TMPDIR",
"LC_TERMINAL",
"LC_TERMINAL_VERSION",
"COLORTERM", "COLORTERM",
"KONSOLE_VERSION", "KONSOLE_VERSION",
"STY", "STY",
"TERM", # Erase this since we still respect TERM=dumb etc. "TERM", # Erase this since we still respect TERM=dumb etc.
"TERM_PROGRAM", "TERM_PROGRAM",
"TERM_PROGRAM_VERSION", "TERM_PROGRAM_VERSION",
"VTE_VERSION",
]: ]:
if var in env: if var in env:
del env[var] del env[var]