diff --git a/doc_src/index.hdr.in b/doc_src/index.hdr.in
index cf11ccba2..04d277f6e 100644
--- a/doc_src/index.hdr.in
+++ b/doc_src/index.hdr.in
@@ -847,6 +847,8 @@ The user can change the settings of `fish` by changing the values of certain var
- A large number of variable starting with the prefixes `fish_color` and `fish_pager_color.` See Variables for changing highlighting colors for more information.
+- `fish_emoji_width` controls the computed width of certain characters, in particular emoji, whose rendered width varies across terminal emulators. This should be set to 1 if your terminal emulator renders emoji single-width, or 2 if double-width. Set this only if you see graphical glitching when printing emoji.
+
- `fish_escape_delay_ms` overrides the default timeout of 300ms (default key bindings) or 10ms (vi key bindings) after seeing an escape character before giving up on matching a key binding. See the documentation for the bind builtin command. This delay facilitates using escape as a meta key.
- `fish_greeting`, the greeting message printed on startup.
diff --git a/src/env.cpp b/src/env.cpp
index 144a86493..27963610e 100644
--- a/src/env.cpp
+++ b/src/env.cpp
@@ -571,6 +571,30 @@ static void init_path_vars() {
}
}
+/// Update the value of g_guessed_fish_emoji_width
+static void guess_emoji_width() {
+ wcstring term;
+ if (auto term_var = env_get(L"TERM_PROGRAM")) {
+ term = term_var->as_string();
+ }
+
+ double version = 0;
+ if (auto version_var = env_get(L"TERM_PROGRAM_VERSION")) {
+ std::string narrow_version = wcs2string(version_var->as_string());
+ version = strtod(narrow_version.c_str(), NULL);
+ }
+
+ // iTerm2 defaults to Unicode 8 sizes.
+ // See https://gitlab.com/gnachman/iterm2/wikis/unicodeversionswitching
+
+ if (term == L"Apple_Terminal" && version >= 400) {
+ // Apple Terminal on High Sierra
+ g_guessed_fish_emoji_width = 2;
+ } else {
+ g_guessed_fish_emoji_width = 1;
+ }
+}
+
/// Initialize the curses subsystem.
static void init_curses() {
for (const auto &var_name : curses_variables) {
@@ -798,6 +822,14 @@ static void handle_escape_delay_change(const wcstring &op, const wcstring &var_n
update_wait_on_escape_ms();
}
+static void handle_change_emoji_width(const wcstring &op, const wcstring &var_name) {
+ int new_width = 0;
+ if (auto width_str = env_get(L"fish_emoji_width")) {
+ new_width = fish_wcstol(width_str->as_string().c_str());
+ }
+ g_fish_emoji_width = std::max(0, new_width);
+}
+
static void handle_term_size_change(const wcstring &op, const wcstring &var_name) {
UNUSED(op);
UNUSED(var_name);
@@ -847,6 +879,7 @@ static void handle_locale_change(const wcstring &op, const wcstring &var_name) {
static void handle_curses_change(const wcstring &op, const wcstring &var_name) {
UNUSED(op);
UNUSED(var_name);
+ guess_emoji_width();
init_curses();
}
@@ -868,6 +901,7 @@ static void setup_var_dispatch_table() {
var_dispatch_table.emplace(L"fish_term256", handle_fish_term_change);
var_dispatch_table.emplace(L"fish_term24bit", handle_fish_term_change);
var_dispatch_table.emplace(L"fish_escape_delay_ms", handle_escape_delay_change);
+ var_dispatch_table.emplace(L"fish_emoji_width", handle_change_emoji_width);
var_dispatch_table.emplace(L"LINES", handle_term_size_change);
var_dispatch_table.emplace(L"COLUMNS", handle_term_size_change);
var_dispatch_table.emplace(L"fish_complete_path", handle_complete_path_change);
@@ -925,6 +959,7 @@ void env_init(const struct config_paths_t *paths /* or NULL */) {
init_curses();
init_input();
init_path_vars();
+ guess_emoji_width();
// Set up the USER and PATH variables
setup_path();
diff --git a/src/fallback.cpp b/src/fallback.cpp
index 7e11cb8b3..2fc34c148 100644
--- a/src/fallback.cpp
+++ b/src/fallback.cpp
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
#if HAVE_GETTEXT
#include
#endif
@@ -263,6 +264,18 @@ int killpg(int pgr, int sig) {
}
#endif
+int g_fish_emoji_width = 0;
+
+// 1 is the typical emoji width in Unicode 8.
+int g_guessed_fish_emoji_width = 1;
+
+int fish_get_emoji_width(wchar_t c) {
+ // Respect an explicit value. If we don't have one, use the guessed value. Do not try to fall
+ // back to wcwidth(), it's hopeless.
+ if (g_fish_emoji_width > 0) return g_fish_emoji_width;
+ return g_guessed_fish_emoji_width;
+}
+
// Big hack to use our versions of wcswidth where we know them to be broken, which is
// EVERYWHERE (https://github.com/fish-shell/fish-shell/issues/2199)
#ifndef HAVE_BROKEN_WCWIDTH
@@ -276,10 +289,43 @@ int fish_wcswidth(const wchar_t *str, size_t n) { return wcswidth(str, n); }
#include "wcwidth9/wcwidth9.h"
+// This is the sort listed of inclusive ranges of characters whose width was 1 in Unicode 8, but was
+// changed to width 2 in Unicode 9. Note that no characters became narrower from Unicode 8 to 9.
+static bool is_width_2_in_Uni9_but_1_in_Uni8(wchar_t c) {
+ const struct pair_t {
+ int lo;
+ int hi;
+ } pairs[] = {{0x0231A, 0x0231B}, {0x023E9, 0x023EC}, {0x023F0, 0x023F0}, {0x023F3, 0x023F3},
+ {0x025FD, 0x025FE}, {0x02614, 0x02615}, {0x02648, 0x02653}, {0x0267F, 0x0267F},
+ {0x02693, 0x02693}, {0x026A1, 0x026A1}, {0x026AA, 0x026AB}, {0x026BD, 0x026BE},
+ {0x026C4, 0x026C5}, {0x026CE, 0x026CE}, {0x026D4, 0x026D4}, {0x026EA, 0x026EA},
+ {0x026F2, 0x026F3}, {0x026F5, 0x026F5}, {0x026FA, 0x026FA}, {0x026FD, 0x026FD},
+ {0x02705, 0x02705}, {0x0270A, 0x0270B}, {0x02728, 0x02728}, {0x0274C, 0x0274C},
+ {0x0274E, 0x0274E}, {0x02753, 0x02755}, {0x02757, 0x02757}, {0x02795, 0x02797},
+ {0x027B0, 0x027B0}, {0x027BF, 0x027BF}, {0x02B1B, 0x02B1C}, {0x02B50, 0x02B50},
+ {0x02B55, 0x02B55}, {0x16FE0, 0x16FE0}, {0x17000, 0x187EC}, {0x18800, 0x18AF2},
+ {0x1F004, 0x1F004}, {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A},
+ {0x1F23B, 0x1F23B}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C},
+ {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0},
+ {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC},
+ {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A},
+ {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5},
+ {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6F6},
+ {0x1F910, 0x1F91E}, {0x1F920, 0x1F927}, {0x1F930, 0x1F930}, {0x1F933, 0x1F93E},
+ {0x1F940, 0x1F94B}, {0x1F950, 0x1F95E}, {0x1F980, 0x1F991}, {0x1F9C0, 0x1F9C0}};
+ auto where = std::lower_bound(std::begin(pairs), std::end(pairs), c,
+ [](pair_t p, wchar_t c) { return p.hi < c; });
+ assert((where == std::end(pairs) || where->hi >= c) && "unexpected binary search result");
+ return where != std::end(pairs) && where->lo <= c;
+}
+
// Possible negative return values from wcwidth9()
enum { width_non_printable = -1, width_ambiguous = -2, width_private_use = -3 };
int fish_wcwidth(wchar_t wc) {
+ // Check for certain characters whose width is terminal emulator dependent.
+ if (is_width_2_in_Uni9_but_1_in_Uni8(wc)) return fish_get_emoji_width(wc);
+
int w9_width = wcwidth9(wc);
if (w9_width >= 0) return w9_width;
diff --git a/src/fallback.h b/src/fallback.h
index c3a152793..4647a6f67 100644
--- a/src/fallback.h
+++ b/src/fallback.h
@@ -16,6 +16,15 @@
// substitution if wchar.h is included after this header.
#include // IWYU pragma: keep
+/// The column width of emoji characters. This must be configurable because the value changed
+/// between Unicode 8 and Unicode 9, wcwidth() is emoji-ignorant, and terminal emulators do
+/// different things. See issues like #4539 and https://github.com/neovim/neovim/issues/4976 for how
+/// painful this is. A value of 0 means to use the guessed value.
+extern int g_fish_emoji_width;
+
+/// The guessed value of the emoji width based on TERM.
+extern int g_guessed_fish_emoji_width;
+
/// fish's internal versions of wcwidth and wcswidth, which can use an internal implementation if
/// the system one is busted.
int fish_wcwidth(wchar_t wc);