Unconditionally default emoji width to 2

"Emoji width" refers to the width of emoji codepoints. Since Unicode
9, they're classified as "wide" according to
TR11 (https://www.unicode.org/reports/tr11/).

Unicode 9 was released in 2016, and this slowly percolated into C
libraries and terminals. Glibc updated its default in 2.26, released
in August 2017.

Until now, we'd guess support for unicode 9 by checking the system
wcwidth function for an emoji - if it returned 2, we'd set our emoji
width to 2 as well.

However, that's a problem in the common case of using ssh to connect
to an old server - modern desktop OS, old server LTS OS, boom.

So now we instead just figure you've got a system that's *displaying*
the emoji that has been updated in the last 9 years.

In effect we're putting the burden on those who run old RHEL et al as
their client OS. They need to set $fish_emoji_width to 1.

Fixes #12500

Part of #12562
This commit is contained in:
Fabian Boehm
2026-03-11 18:31:39 +01:00
committed by Johannes Altmanninger
parent 88d01f7eb8
commit 8561008513
4 changed files with 6 additions and 40 deletions

View File

@@ -25,7 +25,7 @@
/// Valid values are 1, and 2. 1 is the typical emoji width used in Unicode 8 while some newer
/// terminals use a width of 2 since Unicode 9.
// For some reason, this is declared here and exposed here, but is set in `env_dispatch`.
pub static FISH_EMOJI_WIDTH: AtomicIsize = AtomicIsize::new(1);
pub static FISH_EMOJI_WIDTH: AtomicIsize = AtomicIsize::new(2);
static WC_LOOKUP_TABLE: LazyLock<WcLookupTable> = LazyLock::new(WcLookupTable::new);

View File

@@ -14,7 +14,6 @@
};
use crate::screen::{IS_DUMB, ONLY_GRAYSCALE, screen_set_midnight_commander_hack};
use crate::terminal::ColorSupport;
use crate::tty_handoff::xtversion;
use crate::wutil::fish_wcstoi;
use fish_wcstringutil::{bool_from_string, string_prefixes_string};
use std::collections::HashMap;
@@ -61,7 +60,7 @@ macro_rules! vars {
L!("fish_sequence_key_delay_ms"),
vars!(update_wait_on_sequence_key_ms),
);
table.add_anon(L!("fish_emoji_width"), vars!(guess_emoji_width));
table.add_anon(L!("fish_emoji_width"), vars!(handle_emoji_width));
table.add_anon(
L!("fish_ambiguous_width"),
vars!(handle_change_ambiguous_width),
@@ -159,7 +158,7 @@ fn handle_timezone(var_name: &wstr, vars: &EnvStack) {
}
/// Update the value of [`FISH_EMOJI_WIDTH`](fish_fallback::FISH_EMOJI_WIDTH).
pub fn guess_emoji_width(vars: &EnvStack) {
pub fn handle_emoji_width(vars: &EnvStack) {
use fish_fallback::FISH_EMOJI_WIDTH;
if let Some(width_str) = vars.get(L!("fish_emoji_width")) {
@@ -171,38 +170,7 @@ pub fn guess_emoji_width(vars: &EnvStack) {
"Overriding default fish_emoji_width w/",
new_width
);
return;
}
let term_program = vars
.get(L!("TERM_PROGRAM"))
.map_or_else(WString::new, |v| v.as_string());
// 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 = fish_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);
@@ -325,7 +293,6 @@ fn handle_locale_change(vars: &EnvStack) {
}
fn handle_term_change(vars: &EnvStack, suppress_repaint: bool) {
guess_emoji_width(vars);
init_terminal(vars);
if !suppress_repaint {
reader_schedule_prompt_repaint();
@@ -393,7 +360,7 @@ fn run_inits(vars: &EnvStack) {
init_locale(vars);
init_special_chars_once();
init_terminal(vars);
guess_emoji_width(vars);
handle_emoji_width(vars);
update_wait_on_escape_ms(vars);
update_wait_on_sequence_key_ms(vars);
handle_read_limit_change(vars);

View File

@@ -39,7 +39,7 @@
use crate::env::EnvStack;
use crate::env::{EnvMode, Environment, Statuses};
use crate::env_dispatch::MIDNIGHT_COMMANDER_SID;
use crate::env_dispatch::guess_emoji_width;
use crate::env_dispatch::handle_emoji_width;
use crate::exec::exec_subshell;
use crate::expand::expand_one;
use crate::expand::{ExpandFlags, ExpandResultCode, expand_string, expand_tilde};
@@ -386,7 +386,7 @@ pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConf
background_color,
} = terminal_init(parser.vars(), inputfd);
let input_data = input_queue.get_input_data_mut();
guess_emoji_width(parser.vars());
handle_emoji_width(parser.vars());
// Provide value for `status current-command`
parser.libdata_mut().status_vars.command = L!("fish").to_owned();

View File

@@ -67,7 +67,6 @@ def makeenv(script_path: Path, home: Path) -> Dict[str, str]:
"STY",
"TERM", # Erase this since we still respect TERM=dumb etc.
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
]:
if var in env:
del env[var]