Prefer terminal (client) OS for selecting native key bindings

When running fish inside SSH and local and remote OS differ, fish
uses key bindings for the remote OS, which is weird.  Fix that by
asking the terminal for the OS name.

This should be available on foot and kitty soon, see
https://codeberg.org/dnkl/foot/pulls/2217#issuecomment-8249741

Ref: #11107
This commit is contained in:
Johannes Altmanninger
2025-11-19 13:26:54 +01:00
parent b1e681030b
commit 790beedbb0
21 changed files with 106 additions and 53 deletions

View File

@@ -14,6 +14,7 @@ Interactive improvements
Improved terminal support
-------------------------
- OSC 133 prompt markers now also mark the prompt end, which improves shell integration with terminals like iTerm2 (:issue:`11837`).
- Operating-system-specific key bindings are now decided based on the :ref:`terminal's host OS <status-terminal-os>`.
- New :ref:`feature flag <featureflags>` ``omit-term-workarounds`` can be turned on to prevent fish from trying to work around incompatible terminals.
For distributors and developers

View File

@@ -138,6 +138,13 @@ The following operations (subcommands) are available:
This is not available during early startup but only starting from when the first interactive prompt is shown, possibly via builtin :doc:`read <read>`,
so before the first ``fish_prompt`` or ``fish_read`` :ref:`event <event>`.
.. _status-terminal-os:
**terminal-os**
Prints the name of the operating system (OS) the terminal is running on, as reported via :ref:`XTGETTCAP query-os-name <term-compat-xtgettcap>`.
Like :ref:`status terminal <status-terminal>`, this only works once the first interactive prompt is shown.
Returns 1 if the OS name is not available.
.. _status-test-terminal-features:
**test-terminal-feature** *FEATURE*

View File

@@ -261,12 +261,19 @@ Optional Commands
-
- Request terminfo capability (XTGETTCAP).
The parameter is the capability's hex-encoded terminfo code.
To advertise a capability, the response must be of the form
``\eP1+q Pt \e\\`` or ``\eP1+q Pt = Pt \e\\``.
In either variant the first parameter must be the hex-encoded terminfo code.
The second variant's second parameter is ignored.
Currently, fish only queries the :ref:`indn <term-compat-indn>` string capability.
The response must be of the form
``\eP1+q Pt \e\\`` ("boolean") or ``\eP1+q Pt = Pt \e\\`` ("string").
In either variant, the first parameter must be the same as the request parameter.
fish queries the following string capabilities:
* :ref:`indn <term-compat-indn>`
The response's second parameter is ignored.
* ``query-os-name`` (for :ref:`status terminal-os <status-terminal-os>`)
Terminals running on Unix should respond with the hex encoding of ``uname=$(uname)`` as second parameter.
.. _term-compat-dcs-gnu-screen:

View File

@@ -3147,6 +3147,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3145,6 +3145,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3276,6 +3276,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3141,6 +3141,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3146,6 +3146,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3142,6 +3142,9 @@ msgstr ""
msgid "Print the name of the currently running command or function"
msgstr ""
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr ""

View File

@@ -3174,6 +3174,9 @@ msgstr "打印当前函数的名称"
msgid "Print the name of the currently running command or function"
msgstr "打印当前运行的命令或函数的名称"
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr "打印当前运行的脚本的路径 (不含文件名)"

View File

@@ -3149,6 +3149,9 @@ msgstr "印出目前函式的名稱"
msgid "Print the name of the currently running command or function"
msgstr "印出目前執行的命令或函式名稱"
msgid "Print the operating system the terminal is running on"
msgstr ""
msgid "Print the path (without the file name) of the currently running script"
msgstr "印出目前執行的命令稿路徑(不包括檔名)"

View File

@@ -29,6 +29,7 @@ set -l __fish_status_all_commands \
print-stack-trace \
stack-trace \
terminal \
terminal-os \
test-feature \
test-terminal-feature
@@ -69,6 +70,7 @@ complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_com
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a help-sections -d "List section arguments for the 'help' command"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a fish-path -d "Print the path to the current instance of fish"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a terminal -d "Print name and version of the terminal fish is running in"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a terminal-os -d "Print the operating system the terminal is running on"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a test-terminal-feature -d "Test if the terminal suports the given feature"
complete -f -c status -n "__fish_seen_subcommand_from test-terminal-feature" -a 'scroll-content-up\t"Command for scrolling up terminal contents"'

View File

@@ -0,0 +1,12 @@
# localization: skip(private)
function __fish_per_os_bind
set -l macos $argv[-2]
set -l non_macos $argv[-1]
set -e argv[-2..-1]
for varname in macos non_macos
if contains -- $$varname (bind --function-names)
set $varname 'commandline -f '$$varname
end
end
bind $argv "if fish_in_macos_terminal; $macos; else $non_macos; end"
end

View File

@@ -19,13 +19,8 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
bind --preset $argv left backward-char
# Ctrl-left/right - these also work in vim.
if test (__fish_uname) = Darwin
bind --preset $argv ctrl-right forward-token
bind --preset $argv ctrl-left backward-token
else
bind --preset $argv ctrl-right forward-word
bind --preset $argv ctrl-left backward-word
end
__fish_per_os_bind --preset $argv ctrl-right forward-token forward-word
__fish_per_os_bind --preset $argv ctrl-left backward-token backward-word
bind --preset $argv pageup beginning-of-history
bind --preset $argv pagedown end-of-history
@@ -52,23 +47,13 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
bind --preset $argv alt-b prevd-or-backward-word
bind --preset $argv alt-f nextd-or-forward-word
# TODO(terminal-workaround)
set -l alt_right_aliases alt-right \e\[1\;9C # iTerm2 < 3.5.12
set -l alt_left_aliases alt-left \e\[1\;9D # iTerm2 < 3.5.12
if test (__fish_uname) = Darwin
for alt_right in $alt_right_aliases
bind --preset $argv $alt_right nextd-or-forward-word
end
for alt_left in $alt_left_aliases
bind --preset $argv $alt_left prevd-or-backward-word
end
else
for alt_right in $alt_right_aliases
bind --preset $argv $alt_right nextd-or-forward-token
end
for alt_left in $alt_left_aliases
bind --preset $argv $alt_left prevd-or-backward-token
end
for alt_right in alt-right \e\[1\;9C # TODO(terminal-workaround) iTerm2 < 3.5.12
__fish_per_os_bind --preset $argv $alt_right \
nextd-or-forward-word nextd-or-forward-token
end
for alt_left in alt-left \e\[1\;9D # TODO(terminal-workaround) iTerm2 < 3.5.12
__fish_per_os_bind --preset $argv $alt_left \
prevd-or-backward-word prevd-or-backward-token
end
bind --preset $argv alt-up history-token-search-backward

View File

@@ -50,19 +50,11 @@ function fish_default_key_bindings -d "emacs-like key binds"
bind --preset $argv alt-u upcase-word
bind --preset $argv alt-c capitalize-word
if test (__fish_uname) = Darwin
bind --preset $argv alt-backspace backward-kill-word
bind --preset $argv ctrl-alt-h backward-kill-word
bind --preset $argv ctrl-backspace backward-kill-token
bind --preset $argv alt-delete kill-word
bind --preset $argv ctrl-delete kill-token
else
bind --preset $argv alt-backspace backward-kill-token
bind --preset $argv ctrl-alt-h backward-kill-token
bind --preset $argv ctrl-backspace backward-kill-word
bind --preset $argv alt-delete kill-token
bind --preset $argv ctrl-delete kill-word
end
__fish_per_os_bind --preset $argv alt-backspace backward-kill-word backward-kill-token
__fish_per_os_bind --preset $argv ctrl-alt-h backward-kill-word backward-kill-token
__fish_per_os_bind --preset $argv ctrl-backspace backward-kill-token backward-kill-word
__fish_per_os_bind --preset $argv alt-delete kill-word kill-token
__fish_per_os_bind --preset $argv ctrl-delete kill-token kill-word
bind --preset $argv alt-\< beginning-of-buffer
bind --preset $argv alt-\> end-of-buffer

View File

@@ -0,0 +1,3 @@
function fish_in_macos_terminal
test "$(status terminal-os || echo uname="$(__fish_uname)")" = uname=Darwin
end

View File

@@ -8,7 +8,7 @@
JobControl, get_job_control_mode, get_login, is_interactive_session, set_job_control_mode,
};
use crate::reader::reader_in_interactive_read;
use crate::tty_handoff::{get_scroll_content_up_capability, xtversion};
use crate::tty_handoff::{TERMINAL_OS_NAME, get_scroll_content_up_capability, xtversion};
use crate::wutil::{Error, waccess, wbasename, wdirname, wrealpath};
use cfg_if::cfg_if;
use libc::F_OK;
@@ -66,6 +66,7 @@ enum StatusCmd {
STATUS_LIST_FILES,
STATUS_HELP_SECTIONS,
STATUS_TERMINAL,
STATUS_TERMINAL_OS,
STATUS_TEST_TERMINAL_FEATURE,
}
@@ -103,6 +104,7 @@ enum StatusCmd {
(STATUS_STACK_TRACE, "print-stack-trace"),
(STATUS_STACK_TRACE, "stack-trace"),
(STATUS_TERMINAL, "terminal"),
(STATUS_TERMINAL_OS, "terminal-os"),
(STATUS_TEST_FEATURE, "test-feature"),
(STATUS_TEST_TERMINAL_FEATURE, "test-terminal-feature"),
);
@@ -764,11 +766,13 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
}
STATUS_TERMINAL => {
let xtversion = xtversion().unwrap_or_default();
let first_line = &xtversion[..xtversion
.chars()
.position(|c| c == '\n')
.unwrap_or(xtversion.len())];
streams.out.appendln(first_line);
streams.out.appendln(xtversion);
}
STATUS_TERMINAL_OS => {
let Some(Some(terminal_os_name)) = TERMINAL_OS_NAME.get() else {
return Err(STATUS_CMD_ERROR);
};
streams.out.appendln(first_line(terminal_os_name));
}
STATUS_SET_JOB_CONTROL
| STATUS_FEATURES
@@ -784,3 +788,7 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
Ok(SUCCESS)
}
fn first_line(s: &wstr) -> &wstr {
&s[..s.chars().position(|c| c == '\n').unwrap_or(s.len())]
}

View File

@@ -12,8 +12,8 @@
};
use crate::reader::reader_test_and_clear_interrupted;
use crate::tty_handoff::{
SCROLL_CONTENT_UP_TERMINFO_CODE, XTVERSION, maybe_set_kitty_keyboard_capability,
maybe_set_scroll_content_up_capability,
SCROLL_CONTENT_UP_TERMINFO_CODE, TERMINAL_OS_NAME, XTGETTCAP_QUERY_OS_NAME, XTVERSION,
maybe_set_kitty_keyboard_capability, maybe_set_scroll_content_up_capability,
};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
@@ -1386,7 +1386,7 @@ fn parse_dcs(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
let mut buffer = buffer.splitn(2, |&c| c == b'=');
let key = buffer.next().unwrap();
let key = parse_hex(key)?;
if let Some(value) = buffer.next() {
let value = if let Some(value) = buffer.next() {
let value = parse_hex(value)?;
FLOG!(
reader,
@@ -1396,14 +1396,20 @@ fn parse_dcs(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
bytes2wcstring(&value)
)
);
Some(value)
} else {
FLOG!(
reader,
format!("Received XTGETTCAP response: {}", bytes2wcstring(&key))
);
}
None
};
if key == SCROLL_CONTENT_UP_TERMINFO_CODE.as_bytes() {
maybe_set_scroll_content_up_capability();
} else if key == XTGETTCAP_QUERY_OS_NAME.as_bytes() {
if let Some(value) = value {
TERMINAL_OS_NAME.get_or_init(|| Some(bytes2wcstring(&value)));
}
}
return None;
}

View File

@@ -143,6 +143,7 @@
Tokenizer, tok_command,
};
use crate::tty_handoff::SCROLL_CONTENT_UP_TERMINFO_CODE;
use crate::tty_handoff::XTGETTCAP_QUERY_OS_NAME;
use crate::tty_handoff::{
TtyHandoff, get_tty_protocols_active, initialize_tty_protocols, safe_deactivate_tty_protocols,
};
@@ -2724,6 +2725,7 @@ fn query_capabilities_via_dcs(out: &mut impl Output, vars: &dyn Environment) {
}
out.write_command(DecsetAlternateScreenBuffer); // enable alternative screen buffer
send_xtgettcap_query(out, SCROLL_CONTENT_UP_TERMINFO_CODE);
send_xtgettcap_query(out, XTGETTCAP_QUERY_OS_NAME);
out.write_command(DecrstAlternateScreenBuffer); // disable alternative screen buffer
}

View File

@@ -79,7 +79,7 @@ pub(crate) enum TerminalCommand<'a> {
CursorRight,
CursorMove(CardinalDirection, usize),
// Commands related to querying (used for backwards-incompatible features).
// Commands related to querying (used mainly for backwards-incompatible features).
QueryPrimaryDeviceAttribute,
QueryXtversion,
QueryXtgettcap(&'static str),

View File

@@ -47,6 +47,9 @@ pub fn maybe_set_scroll_content_up_capability() {
});
}
pub static TERMINAL_OS_NAME: OnceCell<Option<WString>> = OnceCell::new();
pub(crate) const XTGETTCAP_QUERY_OS_NAME: &str = "query-os-name";
pub static XTVERSION: OnceCell<WString> = OnceCell::new();
pub fn xtversion() -> Option<&'static wstr> {
@@ -242,6 +245,7 @@ pub fn initialize_tty_protocols(vars: &dyn Environment) {
// Default missing query responses.
KITTY_KEYBOARD_SUPPORTED.get_or_init(|| false);
SCROLL_CONTENT_UP_SUPPORTED.get_or_init(|| false);
TERMINAL_OS_NAME.get_or_init(|| None);
let xtversion = XTVERSION.get_or_init(WString::new);
use std::sync::atomic::Ordering::{Acquire, Release};