From ebdc3a0393db08d3647070a35cf8e37274b42d42 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Mon, 6 Jan 2025 21:09:41 +0100 Subject: [PATCH] Swap alt-{left,right,backspace,delete} with ctrl-* on macOS See https://github.com/fish-shell/fish-shell/issues/ 10926 --- doc_src/cmds/status.rst | 5 ++ share/completions/status.fish | 1 + .../functions/__fish_config_interactive.fish | 7 +++ .../functions/__fish_shared_key_bindings.fish | 40 +++++++++++--- .../functions/fish_default_key_bindings.fish | 32 +++++++++-- src/builtins/status.rs | 9 ++++ src/input_common.rs | 53 ++++++++++++++++++- src/reader.rs | 15 +++++- 8 files changed, 146 insertions(+), 16 deletions(-) diff --git a/doc_src/cmds/status.rst b/doc_src/cmds/status.rst index f85df7c61..a148ccf5c 100644 --- a/doc_src/cmds/status.rst +++ b/doc_src/cmds/status.rst @@ -29,6 +29,7 @@ Synopsis status job-control CONTROL_TYPE status features status test-feature FEATURE + status client-os status buildinfo Description @@ -98,6 +99,10 @@ The following operations (subcommands) are available: **test-feature** *FEATURE* Returns 0 when FEATURE is enabled, 1 if it is disabled, and 2 if it is not recognized. +**client-os** + Print the name of the OS (**bsd**, **macos**, **linux** or **unknown**) that the terminal emulator is running on. + When running inside SSH, this will be the client OS. + **buildinfo** This prints information on how fish was build - which architecture, which build system or profile was used, etc. This is mainly useful for debugging. diff --git a/share/completions/status.fish b/share/completions/status.fish index 1ca40874b..84b9be037 100644 --- a/share/completions/status.fish +++ b/share/completions/status.fish @@ -16,6 +16,7 @@ complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_com # The subcommands that are not "is-something" which don't change the fish state. complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a buildinfo -d "Print information on how this version fish was built" +complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a client-os -d "Print the name of the OS the terminal is running on" complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a current-command -d "Print the name of the currently running command or function" complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a current-commandline -d "Print the currently running command with its arguments" complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a current-filename -d "Print the filename of the currently running script" diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index 7148a74c7..72d014e2c 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -217,6 +217,13 @@ end" >$__fish_config_dir/config.fish end __fish_update_cwd_osc # Run once because we might have already inherited a PWD from an old tab + if set -q TMUX + # NOTE This will be stale when the next tmux client attaches to fish's window. + # This isn't oo bad since we only use this to detect the client OS. + # TODO we should have a client scope. + set -g __fish_tmux_client_tty (tmux display-message -p '#{client_tty}' 2>/dev/null) + end + # Bump this whenever some code below needs to run once when upgrading to a new version. # The universal variable __fish_initialized is initialized in share/config.fish. set __fish_initialized 3800 diff --git a/share/functions/__fish_shared_key_bindings.fish b/share/functions/__fish_shared_key_bindings.fish index 2677edda5..28c8dca6b 100644 --- a/share/functions/__fish_shared_key_bindings.fish +++ b/share/functions/__fish_shared_key_bindings.fish @@ -21,8 +21,20 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod $legacy_bind --preset $argv -k left backward-char # Ctrl-left/right - these also work in vim. - bind --preset $argv ctrl-right forward-word - bind --preset $argv ctrl-left backward-word + bind --preset $argv ctrl-right ' + if test (status client-os) = macos + commandline -f forward-token + else + commandline -f forward-word + end + ' + bind --preset $argv ctrl-left ' + if test (status client-os) = macos + commandline -f backward-token + else + commandline -f backward-word + end + ' bind --preset $argv pageup beginning-of-history bind --preset $argv pagedown end-of-history @@ -49,13 +61,25 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod bind --preset $argv up up-or-search $legacy_bind --preset $argv -k up up-or-search - bind --preset $argv shift-right forward-bigword - bind --preset $argv shift-left backward-bigword - $legacy_bind --preset $argv -k sright forward-bigword - $legacy_bind --preset $argv -k sleft backward-bigword + bind --preset $argv shift-right forward-token + bind --preset $argv shift-left backward-token + $legacy_bind --preset $argv -k sright forward-token + $legacy_bind --preset $argv -k sleft backward-token - bind --preset $argv alt-right nextd-or-forward-token - bind --preset $argv alt-left prevd-or-backward-token + bind --preset $argv alt-right ' + if test (status client-os) = macos + commandline -f nextd-or-forward-word + else + nextd-or-forward-token + end + ' + bind --preset $argv alt-left ' + if test (status client-os) = macos + commandline -f prevd-or-backward-word + else + prevd-or-backward-token + end + ' bind --preset $argv alt-up history-token-search-backward bind --preset $argv alt-down history-token-search-forward diff --git a/share/functions/fish_default_key_bindings.fish b/share/functions/fish_default_key_bindings.fish index a64c628d9..63efef077 100644 --- a/share/functions/fish_default_key_bindings.fish +++ b/share/functions/fish_default_key_bindings.fish @@ -57,10 +57,34 @@ function fish_default_key_bindings -d "emacs-like key binds" bind --preset $argv alt-u upcase-word bind --preset $argv alt-c capitalize-word - bind --preset $argv alt-backspace 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 + bind --preset $argv alt-backspace ' + if test (status client-os) = macos + commandline -f backward-kill-word + else + commandline -f backward-kill-token + end + ' + bind --preset $argv ctrl-backspace ' + if test (status client-os) = macos + commandline -f backward-kill-token + else + commandline -f backward-kill-word + end + ' + bind --preset $argv alt-delete ' + if test (status client-os) = macos + commandline -f kill-word + else + commandline -f kill-token + end + ' + bind --preset $argv ctrl-delete ' + if test (status client-os) = macos + commandline -f kill-token + else + commandline -f kill-word + end + ' bind --preset $argv alt-b backward-word bind --preset $argv alt-f forward-word if test "$TERM_PROGRAM" = Apple_Terminal diff --git a/src/builtins/status.rs b/src/builtins/status.rs index 06a76ee41..886c1c847 100644 --- a/src/builtins/status.rs +++ b/src/builtins/status.rs @@ -1,8 +1,10 @@ use std::os::unix::prelude::*; +use std::sync::atomic::Ordering; use super::prelude::*; use crate::common::{get_executable_path, str2wcstring, PROGRAM_NAME}; use crate::future_feature_flags::{self as features, feature_test}; +use crate::input_common::{ClientOS, CLIENT_OS}; use crate::proc::{ get_job_control_mode, get_login, is_interactive_session, set_job_control_mode, JobControl, }; @@ -58,6 +60,7 @@ enum StatusCmd { STATUS_TEST_FEATURE, STATUS_CURRENT_COMMANDLINE, STATUS_BUILDINFO, + STATUS_CLIENT_OS, } str_enum!( @@ -65,6 +68,7 @@ enum StatusCmd { (STATUS_BASENAME, "basename"), (STATUS_BASENAME, "current-basename"), (STATUS_BUILDINFO, "buildinfo"), + (STATUS_CLIENT_OS, "client-os"), (STATUS_CURRENT_CMD, "current-command"), (STATUS_CURRENT_COMMANDLINE, "current-commandline"), (STATUS_DIRNAME, "current-dirname"), @@ -599,6 +603,11 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O streams.out.appendln(path); } } + STATUS_CLIENT_OS => { + let os: ClientOS = + unsafe { std::mem::transmute(CLIENT_OS.load(Ordering::Relaxed)) }; + streams.out.appendln(format!("{}", os)); + } STATUS_SET_JOB_CONTROL | STATUS_FEATURES | STATUS_TEST_FEATURE => { unreachable!("") } diff --git a/src/input_common.rs b/src/input_common.rs index 3d2d3243e..37d9d6544 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -24,7 +24,7 @@ use std::os::fd::RawFd; use std::os::unix::ffi::OsStrExt; use std::ptr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; // The range of key codes for inputrc-style keyboard functions. pub const R_END_INPUT_FUNCTIONS: usize = (ReadlineCmd::ReverseRepeatJump as usize) + 1; @@ -450,6 +450,31 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) { pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); pub(crate) static CURSOR_UP_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub(crate) enum ClientOS { + Bsd, + Linux, + macOS, + Unknown, +} + +impl std::fmt::Display for ClientOS { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + ClientOS::Bsd => "bsd", + ClientOS::Linux => "linux", + ClientOS::macOS => "macos", + ClientOS::Unknown => "unknown", + }; + f.write_str(name) + } +} + +// As first approximation, use the target OS. This should be overwritten by the XTGETTCAP +// query later. +pub(crate) static CLIENT_OS: AtomicU8 = AtomicU8::new(ClientOS::Unknown as _); + static KITTY_KEYBOARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); macro_rules! kitty_progressive_enhancements { @@ -468,7 +493,7 @@ pub(crate) fn enable_kitty_progressive_enhancements() -> bool { true } -static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false); +pub(crate) static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false); pub static IN_MIDNIGHT_COMMANDER_PRE_CSI_U: RelaxedAtomicBool = RelaxedAtomicBool::new(false); static IN_ITERM_PRE_CSI_U: RelaxedAtomicBool = RelaxedAtomicBool::new(false); @@ -485,6 +510,20 @@ pub fn terminal_protocol_hacks() { version < (99, 5, 6) }), ); + CLIENT_OS.store( + (if cfg!(target_os = "macos") + || var_os("LC_TERMINAL").is_some_and(|term| term.as_os_str().as_bytes() == b"iTerm2") + { + ClientOS::macOS + } else if cfg!(bsd) { + ClientOS::Bsd + } else if cfg!(target_os = "linux") { + ClientOS::Linux + } else { + ClientOS::Unknown + }) as _, + Ordering::Relaxed, + ); } fn parse_version(version: &wstr) -> Option<(i64, i64, i64)> { @@ -1320,6 +1359,16 @@ fn parse_dcs(&mut self, buffer: &mut Vec) -> Option { CURSOR_UP_SUPPORTED.store(true); FLOG!(reader, "Cursor up is supported"); } + if key == b"kitty-query-os_name" { + let os = match &value[..] { + b"bsd" => ClientOS::Bsd, + b"macos" => ClientOS::macOS, + b"linux" => ClientOS::Linux, + _ => ClientOS::Unknown, + }; + CLIENT_OS.store(os as _, Ordering::Relaxed); + FLOG!(reader, "Client OS: ", os); + } return None; } diff --git a/src/reader.rs b/src/reader.rs index 9b675b75e..ae29a78da 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -2479,7 +2479,7 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo self.stop_waiting_for_cursor_position(); } ImplicitEvent::SynchronizedOutputSupported => { - if query_capabilities_via_dcs() { + if query_capabilities_via_dcs(self) { self.save_screen_state(); } } @@ -2506,7 +2506,7 @@ fn xtgettcap(out: &mut impl Write, cap: &str) { let _ = write!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap)); } -fn query_capabilities_via_dcs() -> bool { +fn query_capabilities_via_dcs(reader: &Reader) -> bool { static QUERIED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); if QUERIED.load() { return false; @@ -2518,6 +2518,17 @@ fn query_capabilities_via_dcs() -> bool { let _ = out.write(b"\x1b[?1049h"); // enable alternative screen buffer xtgettcap(out.by_ref(), "indn"); xtgettcap(out.by_ref(), "cuu"); + // Query the name of the Client OS. + xtgettcap(out.by_ref(), "kitty-query-os_name"); + if let Some(client_tty) = reader.vars().get_unless_empty(L!("__fish_tmux_client_tty")) { + if let Ok(mut client_tty) = wopen_cloexec( + &client_tty.as_list()[0], + OFlag::O_WRONLY, + Mode::from_bits_truncate(0o666), + ) { + xtgettcap(&mut client_tty, "kitty-query-os_name"); + } + } let _ = out.write(b"\x1b[?1049l"); // disable alternative screen buffer let _ = out.write(b"\x1b[?2026l"); // end synchronized update out.end_buffering();