From 790beedbb0b5efaebece496902036995b718b80c Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 19 Nov 2025 13:26:54 +0100 Subject: [PATCH] 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 --- CHANGELOG.rst | 1 + doc_src/cmds/status.rst | 7 ++++ doc_src/terminal-compatibility.rst | 17 +++++++--- po/de.po | 3 ++ po/en.po | 3 ++ po/fr.po | 3 ++ po/pl.po | 3 ++ po/pt_BR.po | 3 ++ po/sv.po | 3 ++ po/zh_CN.po | 3 ++ po/zh_TW.po | 3 ++ share/completions/status.fish | 2 ++ share/functions/__fish_per_os_bind.fish | 12 +++++++ .../functions/__fish_shared_key_bindings.fish | 33 +++++-------------- .../functions/fish_default_key_bindings.fish | 18 +++------- share/functions/fish_in_macos_terminal.fish | 3 ++ src/builtins/status.rs | 20 +++++++---- src/input_common.rs | 14 +++++--- src/reader/reader.rs | 2 ++ src/terminal.rs | 2 +- src/tty_handoff.rs | 4 +++ 21 files changed, 106 insertions(+), 53 deletions(-) create mode 100644 share/functions/__fish_per_os_bind.fish create mode 100644 share/functions/fish_in_macos_terminal.fish diff --git a/CHANGELOG.rst b/CHANGELOG.rst index caa70a2c5..bec1fa9c2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `. - New :ref:`feature flag ` ``omit-term-workarounds`` can be turned on to prevent fish from trying to work around incompatible terminals. For distributors and developers diff --git a/doc_src/cmds/status.rst b/doc_src/cmds/status.rst index 6fae22a9b..b68575e5f 100644 --- a/doc_src/cmds/status.rst +++ b/doc_src/cmds/status.rst @@ -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 `, so before the first ``fish_prompt`` or ``fish_read`` :ref:`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 `. + Like :ref:`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* diff --git a/doc_src/terminal-compatibility.rst b/doc_src/terminal-compatibility.rst index d23225ad8..3882949ff 100644 --- a/doc_src/terminal-compatibility.rst +++ b/doc_src/terminal-compatibility.rst @@ -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 ` 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 ` + + The response's second parameter is ignored. + * ``query-os-name`` (for :ref:`status terminal-os `) + + Terminals running on Unix should respond with the hex encoding of ``uname=$(uname)`` as second parameter. .. _term-compat-dcs-gnu-screen: diff --git a/po/de.po b/po/de.po index 72d93cb66..95dad21b2 100644 --- a/po/de.po +++ b/po/de.po @@ -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 "" diff --git a/po/en.po b/po/en.po index 2d01cf442..a32a4d97e 100644 --- a/po/en.po +++ b/po/en.po @@ -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 "" diff --git a/po/fr.po b/po/fr.po index 1bd1257ce..8b953984c 100644 --- a/po/fr.po +++ b/po/fr.po @@ -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 "" diff --git a/po/pl.po b/po/pl.po index 275ca666d..2657f965a 100644 --- a/po/pl.po +++ b/po/pl.po @@ -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 "" diff --git a/po/pt_BR.po b/po/pt_BR.po index 1c12bf793..26f606cbe 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -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 "" diff --git a/po/sv.po b/po/sv.po index 701839caf..3a9a7ac9f 100644 --- a/po/sv.po +++ b/po/sv.po @@ -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 "" diff --git a/po/zh_CN.po b/po/zh_CN.po index c35d9d3df..7c46fe167 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -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 "打印当前运行的脚本的路径 (不含文件名)" diff --git a/po/zh_TW.po b/po/zh_TW.po index da64b4b5a..529984297 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -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 "印出目前執行的命令稿路徑(不包括檔名)" diff --git a/share/completions/status.fish b/share/completions/status.fish index c3b5a3be6..348da9549 100644 --- a/share/completions/status.fish +++ b/share/completions/status.fish @@ -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"' diff --git a/share/functions/__fish_per_os_bind.fish b/share/functions/__fish_per_os_bind.fish new file mode 100644 index 000000000..e8b57a35e --- /dev/null +++ b/share/functions/__fish_per_os_bind.fish @@ -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 diff --git a/share/functions/__fish_shared_key_bindings.fish b/share/functions/__fish_shared_key_bindings.fish index b702fc67a..8fef567f1 100644 --- a/share/functions/__fish_shared_key_bindings.fish +++ b/share/functions/__fish_shared_key_bindings.fish @@ -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 diff --git a/share/functions/fish_default_key_bindings.fish b/share/functions/fish_default_key_bindings.fish index ae86a8a1c..c6806e02f 100644 --- a/share/functions/fish_default_key_bindings.fish +++ b/share/functions/fish_default_key_bindings.fish @@ -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 diff --git a/share/functions/fish_in_macos_terminal.fish b/share/functions/fish_in_macos_terminal.fish new file mode 100644 index 000000000..2668a0958 --- /dev/null +++ b/share/functions/fish_in_macos_terminal.fish @@ -0,0 +1,3 @@ +function fish_in_macos_terminal + test "$(status terminal-os || echo uname="$(__fish_uname)")" = uname=Darwin +end diff --git a/src/builtins/status.rs b/src/builtins/status.rs index f7d395a7e..86fbd64ef 100644 --- a/src/builtins/status.rs +++ b/src/builtins/status.rs @@ -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())] +} diff --git a/src/input_common.rs b/src/input_common.rs index aa227f552..22264d32a 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -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) -> Option { 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) -> Option { 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; } diff --git a/src/reader/reader.rs b/src/reader/reader.rs index 5d3a45a69..02ec7c7c8 100644 --- a/src/reader/reader.rs +++ b/src/reader/reader.rs @@ -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 } diff --git a/src/terminal.rs b/src/terminal.rs index 79ee105f7..3b4ae03f2 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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), diff --git a/src/tty_handoff.rs b/src/tty_handoff.rs index 6fe68c8dd..b88087649 100644 --- a/src/tty_handoff.rs +++ b/src/tty_handoff.rs @@ -47,6 +47,9 @@ pub fn maybe_set_scroll_content_up_capability() { }); } +pub static TERMINAL_OS_NAME: OnceCell> = OnceCell::new(); +pub(crate) const XTGETTCAP_QUERY_OS_NAME: &str = "query-os-name"; + pub static XTVERSION: OnceCell = 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};