diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6782b4b74..603934c81 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ fish 3.8.0 (released ???) 10198 10200 10201 10204 10210 10214 10219 10223 10227 10232 10235 10237 10243 10244 10245 10246 10251 10260 10267 10281 10347 10366 10368 10370 10371 10263 10270 10272 10276 10277 10278 10279 10291 10293 10305 10306 10309 10316 10317 10327 10328 10329 10330 10336 10340 - 10345 10346 10353 10354 10356 10372 10373 3299 10360 + 10345 10346 10353 10354 10356 10372 10373 3299 10360 10359 The entirety of fish's C++ code has been ported to Rust (:issue:`9512`). This means a large change in dependencies and how to build fish. @@ -21,6 +21,24 @@ Packagers should see the :ref:`For Distributors ` section at the Notable backwards-incompatible changes -------------------------------------- +- Fish now decodes keyboard input into human-readable key names. + To make this for for a wide range of terminals, fish asks terminals to speak several keyboard protocols, + including CSI u, XTerm's ``modifyOtherKeys`` and some progressive enhancements from the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). + Depending on terminal support, this allows to bind a lot more key combinations, + including arbitrary combinations of modifiers ``ctrl``, ``alt`` and ``shift``. + + This comes with a new syntax for specifying keys to builtin ``bind``. + The new syntax introduces modifier names and names for some keys that don't have an obvious and printable Unicode code point. + The old syntax remains mostly supported but the new one is preferred. + - Existing bindings that use the new names have a different meaning now. + For example + - ``bind up 'do something'`` binds the up arrow key instead of a two-key sequence. + - ``bind ctrl-x,alt-c 'do something'`` binds a sequence of two keys. + Since ``,`` and ``-`` act as separators, there are some cases where they need to be written as ``comma`` and ``minus`` respectively. + - To minimize gratuitous breakage, the key argument to ``bind`` is parsed using the old syntax in two cases: + - If key starts with a raw escape character (``\e``) or a raw ASCII control character (``\c``). + - If key consists of exactly two characters, contains none of ``,`` or ``-`` and is not a named key. + - ``random`` now uses a different random number generator and so the values you get even with the same seed have changed. Notably, it will now work much more sensibly with very small seeds. The seed was never guaranteed to give the same result across systems, @@ -39,7 +57,7 @@ Notable backwards-incompatible changes Notable improvements and fixes ------------------------------ - New function ``fish_should_add_to_history`` can be overridden to decide whether a command should be added to the history (:issue:`10302`). -- :kbd:`Control-C` during command input no longer prints ``^C`` and a new prompt but merely clears the command line. This restores the behavior from version 2.2. To revert to the old behavior use ``bind \cc __fish_cancel_commandline`` (:issue:`10213`). +- :kbd:`Control-C` during command input no longer prints ``^C`` and a new prompt but merely clears the command line. This restores the behavior from version 2.2. To revert to the old behavior use ``bind ctrl-c __fish_cancel_commandline`` (:issue:`10213`). - The :kbd:`Control-R` history search now uses glob syntax (:issue:`10131`). - The :kbd:`Control-R` history search now operates only on the line at cursor, making it easier to quickly compose a multi-line commandline by recalling previous commands. @@ -48,6 +66,12 @@ Deprecations and removed features - ``commandline --tokenize`` (short option ``-o``) has been deprecated in favor of ``commandline --tokens-expanded`` (short option ``-x``) which expands variables and other shell expressions, removing the need to use "eval" in custom completions (:issue:`10212`). - A new feature flag, ``remove-percent-self`` (see ``status features``) disables PID expansion of ``%self`` which has been supplanted by ``$fish_pid`` (:issue:`10262`). +- Specifying key names as terminfo name (``bind -k``) is deprecated and may be removed in a future version. +- Flow control -- which if enabled by ``stty ixon ixoff`` allows to pause terminal input with ``ctrl-s`` and resume it with ``ctrl-q`` -- now works only while fish is executing an external command. +- When a terminal pastes text into fish using bracketed paste, fish used to switch to a special ``paste`` bind mode. + This bind mode has been removed. The behavior on paste is currently not meant to be configurable. +- When fish is stopped or terminated by a signal that cannot be caught (SIGSTOP or SIGKILL), it may leave the terminal in a state where keypresses with modifiers are sent as CSI u sequences instead of traditional control characters or escape sequecnes (that are recognized by bash/readline). If this happens, you can use the ``reset`` command from ``ncurses`` to restore the terminal state. +- ``fish_key_reader --verbose`` is now ignored, so it no longer shows raw byte values or timing information. Since fish now decodes keys, this should no longer be necessary. Scripting improvements ---------------------- @@ -78,13 +102,14 @@ Interactive improvements New or improved bindings ^^^^^^^^^^^^^^^^^^^^^^^^ -- Bindings can now mix special input functions and shell commands, so ``bind \cg expand-abbr "commandline -i \n"`` works as expected (:issue:`8186`). +- Bindings can now mix special input functions and shell commands, so ``bind ctrl-g expand-abbr "commandline -i \n"`` works as expected (:issue:`8186`). - When the cursor is on a command that resolves to an executable script, :kbd:`Alt-O` will now open that script in your editor (:issue:`10266`). - Two improvements to the :kbd:`Alt-E` binding which edits the commandline in an external editor: - The editor's cursor position is copied back to fish. This is currently supported for Vim and Kakoune. - Cursor position synchronization is only supported for a set of known editors. This has been extended by also resolving aliases. For example use ``complete --wraps my-vim vim`` to synchronize cursors when `EDITOR=my-vim`. - ``backward-kill-path-component`` and friends now treat ``#`` as part of a path component (:issue:`10271`). - The ``E`` binding in vi mode now correctly handles the last character of the word, by jumping to the next word (:issue:`9700`). +- If the terminal supports shifted key codes from the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), ``shift-enter`` now inserts a newline instead of executing the command line. - Vi mode has seen some improvements but continues to suffer from the lack of people working on it. - Insert-mode :kbd:`Control-N` accepts autosuggestions (:issue:`10339`). - Outside insert mode, the cursor will no longer be placed beyond the last character on the commandline. @@ -103,6 +128,8 @@ Completions Improved terminal support ^^^^^^^^^^^^^^^^^^^^^^^^^ - Fish now sets the terminal window title unconditionally (:issue:`10037`). +- Focus reporting is enabled unconditionally, not just inside tmux. + To use it, define functions that handle events ``fish_focus_in`` and ``fish_focus_out``. Other improvements ------------------ diff --git a/build_tools/pexpect_helper.py b/build_tools/pexpect_helper.py index 05bee90b4..649c5c40c 100644 --- a/build_tools/pexpect_helper.py +++ b/build_tools/pexpect_helper.py @@ -34,7 +34,12 @@ def get_prompt_re(counter): """Return a regular expression for matching a with a given prompt counter.""" return re.compile( r"""(?:\r\n?|^) # beginning of line - (?:\x1b[\d\[KB(m]*)* # optional colors + (?:\x1b[\d[KB(m]*)* # optional colors + (?:\x1b[\?2004h) # Bracketed paste + (?:\x1b[>4;1m) # XTerm's modifyOtherKeys + (?:\x1b[>5u) # CSI u with kitty progressive enhancement + (?:\x1b=) # set application keypad mode, so the keypad keys send unique codes + (?:\x1b[\?1004h)? # enable focus notify (?:\[.\]\ )? # optional vi mode prompt """ + (r"prompt\ %d>" % counter) # prompt with counter diff --git a/cmake/Tests.cmake b/cmake/Tests.cmake index ad11bfd5f..16fdf9c94 100644 --- a/cmake/Tests.cmake +++ b/cmake/Tests.cmake @@ -1,13 +1,6 @@ # This adds ctest support to the project enable_testing() -# By default, ctest runs tests serially -if(NOT CTEST_PARALLEL_LEVEL) - include(ProcessorCount) - ProcessorCount(CORES) - set(CTEST_PARALLEL_LEVEL ${CORES}) -endif() - # Put in a tests folder to reduce the top level targets in IDEs. set(CMAKE_FOLDER tests) @@ -24,8 +17,6 @@ set(SKIP_RETURN_CODE 125) # running `make test` does not require any of the binaries to be built before testing. # * The only way to have a test depend on a binary is to add a fake test with a name like # "build_fish" that executes CMake recursively to build the `fish` target. -# * It is not possible to set top-level CTest options/settings such as CTEST_PARALLEL_LEVEL from -# within the CMake configuration file. # * Circling back to the point about individual tests not being actual Makefile targets, CMake does # not offer any way to execute a named test via the `make`/`ninja`/whatever interface; the only # way to manually invoke test `foo` is to to manually run `ctest` and specify a regex matching @@ -33,7 +24,7 @@ set(SKIP_RETURN_CODE 125) # The top-level test target is "fish_run_tests". add_custom_target(fish_run_tests - COMMAND env CTEST_PARALLEL_LEVEL=${CTEST_PARALLEL_LEVEL} FISH_FORCE_COLOR=1 + COMMAND env FISH_FORCE_COLOR=1 FISH_SOURCE_DIR=${CMAKE_SOURCE_DIR} ${CMAKE_CTEST_COMMAND} --force-new-ctest-process # --verbose --output-on-failure --progress diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 9b4f166cf..a40438d25 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -7,49 +7,67 @@ Synopsis .. synopsis:: - bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] [-k | --key] SEQUENCE COMMAND ... - bind [(-M | --mode) MODE] [-k | --key] [--preset] [--user] SEQUENCE - bind (-K | --key-names) [-a | --all] [--preset] [--user] + bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] KEYS COMMAND ... + bind [(-M | --mode) MODE] [--preset] [--user] [KEYS] + bind [-a | --all] [--preset] [--user] bind (-f | --function-names) bind (-L | --list-modes) - bind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | [-k | --key] SEQUENCE ... + bind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | KEYS ... Description ----------- -``bind`` manages bindings. +``bind`` manages key bindings. -It can add bindings if given a SEQUENCE of characters to bind to. These should be written as :ref:`fish escape sequences `. The most important of these are ``\c`` for the control key, and ``\e`` for escape, and because of historical reasons also the Alt key (sometimes also called "Meta"). +If both ``KEYS`` and ``COMMAND`` are given, ``bind`` adds (or replaces) a binding in ``MODE``. +If only ``KEYS`` is given, any existing binding in the given ``MODE`` will be printed. -For example, :kbd:`Alt`\ +\ :kbd:`W` can be written as ``\ew``, and :kbd:`Control`\ +\ :kbd:`X` (^X) can be written as ``\cx``. Note that Alt-based key bindings are case sensitive and Control-based key bindings are not. This is a constraint of text-based terminals, not ``fish``. +``KEYS`` is a comma-separated list of key names. +Modifier keys can be specified by prefixing a key name with a combination of ``ctrl-``, ``alt-`` and ``shift-``. +For example, :kbd:`Alt`\ +\ :kbd:`w` is written as ``alt-w``. +Key names are case-sensitive; for example ``alt-W`` is the same as ``alt-shift-w``. -The generic key binding that matches if no other binding does can be set by specifying a ``SEQUENCE`` of the empty string (``''``). For most key bindings, it makes sense to bind this to the ``self-insert`` function (i.e. ``bind '' self-insert``). This will insert any keystrokes not specifically bound to into the editor. Non-printable characters are ignored by the editor, so this will not result in control sequences being inserted. +Some keys have names, usually because they don't have an obvious printable character representation. +They are: -If the ``-k`` switch is used, the name of a key (such as 'down', 'up' or 'backspace') is used instead of a sequence. The names used are the same as the corresponding curses variables, but without the 'key\_' prefix. (See ``terminfo(5)`` for more information, or use ``bind --key-names`` for a list of all available named keys). Normally this will print an error if the current ``$TERM`` entry doesn't have a given key, unless the ``-s`` switch is given. +``plus`` (``+``), +``minus`` (``-``), +``comma`` (``,``), +``backspace``, +``delete``, +``escape``, +``enter``, +the arrow keys ``up``, ``down``, ``left`` and ``right``, +``pageup``, +``pagedown``, +``home``, +``end``, +``insert``, +``tab``, +``space`` and +``F1`` through ``F12``. -To find out what sequence a key combination sends, you can use :doc:`fish_key_reader `. +An empty value (``''``) for ``KEYS`` designates the generic binding. For most bind modes, it makes sense to bind this to the ``self-insert`` function (i.e. ``bind '' self-insert``). This will insert any keystrokes not specifically bound to into the editor. Non-printable characters are ignored by the editor, so this will not result in control sequences being inserted. + +To find the name of a key combination you can use :doc:`fish_key_reader `. ``COMMAND`` can be any fish command, but it can also be one of a set of special input functions. These include functions for moving the cursor, operating on the kill-ring, performing tab completion, etc. Use ``bind --function-names`` or :ref:`see below ` for a list of these input functions. .. note:: If a script changes the commandline, it should finish by calling the ``repaint`` special input function. -If no ``SEQUENCE`` is provided, all bindings (or just the bindings in the given ``MODE``) are printed. If ``SEQUENCE`` is provided but no ``COMMAND``, just the binding matching that sequence is printed. +If no ``KEYS`` argument is provided, all bindings (in the given ``MODE``) are printed. If ``KEYS`` is provided but no ``COMMAND``, just the binding matching that sequence is printed. Key bindings may use "modes", which mimics vi's modal input behavior. The default mode is "default". Every key binding applies to a single mode; you can specify which one with ``-M MODE``. If the key binding should change the mode, you can specify the new mode with ``-m NEW_MODE``. The mode can be viewed and changed via the ``$fish_bind_mode`` variable. If you want to change the mode from inside a fish function, use ``set fish_bind_mode MODE``. +To bind a sequence of keys, separate them with ``,``. + To save custom key bindings, put the ``bind`` statements into :ref:`config.fish `. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists. Options ------- The following options are available: -**-k** or **--key** - Specify a key name, such as 'left' or 'backspace' instead of a character sequence - -**-K** or **--key-names** - Display a list of available key names. Specifying **-a** or **--all** includes keys that don't have a known mapping - **-f** or **--function-names** Display a list of available input functions @@ -69,7 +87,7 @@ The following options are available: Specifying **-a** or **--all** without **-M** or **--mode** erases all binds in all modes regardless of sequence. **-a** or **--all** - See **--erase** and **--key-names** + See **--erase** **--preset** and **--user** Specify if bind should operate on user or preset bindings. @@ -349,7 +367,7 @@ Examples Exit the shell when :kbd:`Control`\ +\ :kbd:`D` is pressed:: - bind \cd 'exit' + bind ctrl-d 'exit' Perform a history search when :kbd:`Page Up` is pressed:: diff --git a/doc_src/cmds/fish_key_reader.rst b/doc_src/cmds/fish_key_reader.rst index 851ff3daa..709764835 100644 --- a/doc_src/cmds/fish_key_reader.rst +++ b/doc_src/cmds/fish_key_reader.rst @@ -15,16 +15,11 @@ Description :program:`fish_key_reader` is used to explain how you would bind a certain key sequence. By default, it prints the :doc:`bind ` command for one key sequence read interactively over standard input. -If the character sequence matches a special key name (see ``bind --key-names``), both ``bind CHARS ...`` and ``bind -k KEYNAME ...`` usage will be shown. In verbose mode (enabled by passing ``--verbose``), additional details about the characters received, such as the delay between chars, are written to standard error. - The following options are available: **-c** or **--continuous** Begins a session where multiple key sequences can be inspected. By default the program exits after capturing a single key sequence. -**-V** or **--verbose** - Tells fish_key_reader to output timing information and explain the sequence in more detail. - **-h** or **--help** Displays help about using this command. @@ -34,8 +29,6 @@ The following options are available: Usage Notes ----------- -In verbose mode, the delay in milliseconds since the previous character was received is included in the diagnostic information written to standard error. This information may be useful to determine the optimal ``fish_escape_delay_ms`` setting or learn the amount of lag introduced by tools like ``ssh``, ``mosh`` or ``tmux``. - ``fish_key_reader`` intentionally disables handling of many signals. To terminate ``fish_key_reader`` in ``--continuous`` mode do: - press :kbd:`Control`\ +\ :kbd:`C` twice, or @@ -51,12 +44,4 @@ Example > fish_key_reader Press a key: # press up-arrow - bind \e\[A 'do something' - - > fish_key_reader --verbose - Press a key: - # press alt+enter - hex: 1B char: \e - ( 0.027 ms) hex: D char: \cM (or \r) - bind \e\r 'do something' - + bind up 'do something' diff --git a/doc_src/interactive.rst b/doc_src/interactive.rst index ac8b43b6d..03b4964dd 100644 --- a/doc_src/interactive.rst +++ b/doc_src/interactive.rst @@ -534,38 +534,29 @@ Custom bindings In addition to the standard bindings listed here, you can also define your own with :doc:`bind `:: # Just clear the commandline on control-c - bind \cc 'commandline -r ""' + bind ctrl-c 'commandline -r ""' Put ``bind`` statements into :ref:`config.fish ` or a function called ``fish_user_key_bindings``. If you change your mind on a binding and want to go back to fish's default, you can simply erase it again:: - bind --erase \cc + bind --erase ctrl-c Fish remembers its preset bindings and so it will take effect again. This saves you from having to remember what it was before and add it again yourself. If you use :ref:`vi bindings `, note that ``bind`` will by default bind keys in :ref:`command mode `. To bind something in :ref:`insert mode `:: - bind --mode insert \cc 'commandline -r ""' + bind --mode insert ctrl-c 'commandline -r ""' .. _interactive-key-sequences: Key sequences """"""""""""" -The terminal tells fish which keys you pressed by sending some sequences of bytes to describe that key. For some keys, this is easy - pressing :kbd:`a` simply means the terminal sends "a". In others it's more complicated and terminals disagree on which they send. +To find out the name of a key, you can use :doc:`fish_key_reader `. -In these cases, :doc:`fish_key_reader ` can tell you how to write the key sequence for your terminal. Just start it and press the keys you are interested in:: - - > fish_key_reader # pressing control-c + > fish_key_reader # Press Alt + right-arrow Press a key: - Press [ctrl-C] again to exit - bind \cC 'do something' - - > fish_key_reader # pressing the right-arrow - Press a key: - bind \e\[C 'do something' - Note that some key combinations are indistinguishable or unbindable. For instance control-i *is the same* as the tab key. This is a terminal limitation that fish can't do anything about. When ``fish_key_reader`` prints the same sequence for two different keys, then that is because your terminal sends the same sequence for them. Also, :kbd:`Escape` is the same thing as :kbd:`Alt` in a terminal. To distinguish between pressing :kbd:`Escape` and then another key, and pressing :kbd:`Alt` and that key (or an escape sequence the key sends), fish waits for a certain time after seeing an escape character. This is configurable via the :envvar:`fish_escape_delay_ms` variable. @@ -576,10 +567,10 @@ If you want to be able to press :kbd:`Escape` and then a character and have it c Similarly, to disambiguate *other* keypresses where you've bound a subsequence and a longer sequence, fish has :envvar:`fish_sequence_key_delay_ms`:: - # This binds "jk" to switch to normal mode in vi mode. + # This binds the sequence j,k to switch to normal mode in vi mode. # If you kept it like that, every time you press "j", # fish would wait for a "k" or other key to disambiguate - bind -M insert -m default jk cancel repaint-mode + bind -M insert -m default j,k cancel repaint-mode # After setting this, fish only waits 200ms for the "k", # or decides to treat the "j" as a separate sequence, inserting it. diff --git a/share/completions/bind.fish b/share/completions/bind.fish index ec4fb172d..2daa9f9df 100644 --- a/share/completions/bind.fish +++ b/share/completions/bind.fish @@ -57,5 +57,21 @@ complete -c bind -s s -l silent -d 'Operate silently' complete -c bind -l preset -d 'Operate on preset bindings' complete -c bind -l user -d 'Operate on user bindings' -complete -c bind -n __fish_bind_test1 -a '(bind --key-names)' -d 'Key name' -x complete -c bind -n __fish_bind_test2 -a '(bind --function-names)' -d 'Function name' -x + +function __fish_bind_complete + argparse M/mode= m/sets-mode= preset user s/silent \ + a/all function-names list-modes e/erase -- (commandline -xpc)[2..] 2>/dev/null + or return 1 + set -l token (commandline -ct) + if test (count $argv) = 0 && set -l prefix (string match -r -- '(.*,)?(ctrl-|alt-|shift-)*' $token) + printf '%sctrl-\tCtrl modifier…\n' $prefix + printf '%salt-\tAlt modifier…\n' $prefix + printf '%sshift-\tShift modifier…\n' $prefix + set -l key_names plus minus comma backspace delete escape \ + enter up down left right pageup pagedown home end insert tab \ + space F(seq 12) + printf '%s\tNamed key\n' $prefix$key_names + end +end +complete -c bind -k -a '(__fish_bind_complete)' -f diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index 843c6ddbf..c00c7bd5b 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -191,52 +191,6 @@ end" >$__fish_config_dir/config.fish # Load key bindings __fish_reload_key_bindings - # Enable bracketed paste exception when running unit tests so we don't have to add - # the sequences to bind.expect - if not set -q FISH_UNIT_TESTS_RUNNING - # Enable bracketed paste before every prompt (see __fish_shared_bindings for the bindings). - # We used to do this for read, but that would break non-interactive use and - # compound commandlines like `read; cat`, because - # it won't disable it after the read. - function __fish_enable_bracketed_paste --on-event fish_prompt - printf "\e[?2004h" - end - - # Disable BP before every command because that might not support it. - function __fish_disable_bracketed_paste --on-event fish_preexec --on-event fish_exit - printf "\e[?2004l" - end - - # Tell the terminal we support BP. Since we are in __f_c_i, the first fish_prompt - # has already fired. - # But only if we're interactive, in case we are in `read` - status is-interactive - and __fish_enable_bracketed_paste - end - - # Similarly, enable TMUX's focus reporting when in tmux. - # This will be handled by - # - The keybindings (reading the sequence and triggering an event) - # - Any listeners (like the vi-cursor) - if set -q TMUX - and not set -q FISH_UNIT_TESTS_RUNNING - # Allow overriding these - we're called very late, - # and so it's otherwise awkward to disable focus reporting again. - not functions -q __fish_enable_focus - and function __fish_enable_focus --on-event fish_postexec - echo -n \e\[\?1004h - end - not functions -q __fish_disable_focus - and function __fish_disable_focus --on-event fish_preexec - echo -n \e\[\?1004l - end - # Note: Don't call this initially because, even though we're in a fish_prompt event, - # tmux reacts sooo quickly that we'll still get a sequence before we're prepared for it. - # So this means that we won't get focus events until you've run at least one command, but that's preferable - # to always seeing `^[[I` when starting fish. - # __fish_enable_focus - end - # Detect whether the terminal reflows on its own # If it does we shouldn't do it. # Allow $fish_handle_reflow to override it. diff --git a/share/functions/__fish_edit_command_if_at_cursor.fish b/share/functions/__fish_edit_command_if_at_cursor.fish index 37c1af424..e44229790 100644 --- a/share/functions/__fish_edit_command_if_at_cursor.fish +++ b/share/functions/__fish_edit_command_if_at_cursor.fish @@ -18,8 +18,6 @@ function __fish_edit_command_if_at_cursor --description 'If cursor is at the com set -l editor (__fish_anyeditor) or return 0 # We already printed a warning, so tell the caller to finish. - __fish_disable_bracketed_paste $editor $command_path - __fish_enable_bracketed_paste return 0 end diff --git a/share/functions/__fish_paste.fish b/share/functions/__fish_paste.fish new file mode 100644 index 000000000..d520f75b0 --- /dev/null +++ b/share/functions/__fish_paste.fish @@ -0,0 +1,47 @@ +function __fish_paste + # Also split on \r, otherwise it looks confusing + set -l data (string split \r -- $argv[1] | string split \n) + + if commandline --search-field >/dev/null + commandline --search-field -i -- $data + return + end + + # If the current token has an unmatched single-quote, + # escape all single-quotes (and backslashes) in the paste, + # in order to turn it into a single literal token. + # + # This eases pasting non-code (e.g. markdown or git commitishes). + set -l quote_state (__fish_tokenizer_state -- (commandline -ct | string collect)) + if contains -- $quote_state single single-escaped + if status test-feature regex-easyesc + set data (string replace -ra "(['\\\])" '\\\\$1' -- $data) + else + set data (string replace -ra "(['\\\])" '\\\\\\\$1' -- $data) + end + else if not contains -- $quote_state double double-escaped + and set -q data[2] + # Leading whitespace in subsequent lines is unneded, since fish + # already indents. Also gets rid of tabs (issue #5274). + set -l tmp + for line in $data + switch $quote_state + case normal + set -a tmp (string trim -l -- $line) + case single single-escaped double double-escaped escaped + set -a tmp $line + end + set quote_state (__fish_tokenizer_state -i $quote_state -- $line) + end + set data $data[1] $tmp[2..] + end + if not string length -q -- (commandline -c) + # If we're at the beginning of the first line, trim whitespace from the start, + # so we don't trigger ignoring history. + set data[1] (string trim -l -- $data[1]) + end + + if test -n "$data" + commandline -i -- $data + end +end diff --git a/share/functions/__fish_shared_key_bindings.fish b/share/functions/__fish_shared_key_bindings.fish index 7b931e51d..8ed0c16d8 100644 --- a/share/functions/__fish_shared_key_bindings.fish +++ b/share/functions/__fish_shared_key_bindings.fish @@ -1,4 +1,5 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mode" + set -l legacy_bind bind # These are some bindings that are supposed to be shared between vi mode and default mode. # They are supposed to be unrelated to text-editing (or movement). # This takes $argv so the vi-bindings can pass the mode they are valid in. @@ -9,164 +10,128 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod return 1 end - bind --preset $argv \cy yank + bind --preset $argv ctrl-y yank or return # protect against invalid $argv - bind --preset $argv \ey yank-pop + bind --preset $argv alt-y yank-pop # Left/Right arrow - bind --preset $argv -k right forward-char - bind --preset $argv -k left backward-char - bind --preset $argv \e\[C forward-char - bind --preset $argv \e\[D backward-char + bind --preset $argv right forward-char + bind --preset $argv left backward-char + $legacy_bind --preset $argv -k right forward-char + $legacy_bind --preset $argv -k left backward-char + $legacy_bind --preset $argv \e\[C forward-char + $legacy_bind --preset $argv \e\[D backward-char # Some terminals output these when they're in in keypad mode. - bind --preset $argv \eOC forward-char - bind --preset $argv \eOD backward-char + $legacy_bind --preset $argv \eOC forward-char + $legacy_bind --preset $argv \eOD backward-char # Ctrl-left/right - these also work in vim. - bind --preset $argv \e\[1\;5C forward-word - bind --preset $argv \e\[1\;5D backward-word + bind --preset $argv ctrl-right forward-word + bind --preset $argv ctrl-left backward-word + $legacy_bind --preset $argv \e\[1\;5C forward-word + $legacy_bind --preset $argv \e\[1\;5D backward-word - bind --preset $argv -k ppage beginning-of-history - bind --preset $argv -k npage end-of-history + bind --preset $argv pageup beginning-of-history + bind --preset $argv pagedown end-of-history + $legacy_bind --preset $argv -k ppage beginning-of-history + $legacy_bind --preset $argv -k npage end-of-history # Interaction with the system clipboard. - bind --preset $argv \cx fish_clipboard_copy - bind --preset $argv \cv fish_clipboard_paste + bind --preset $argv ctrl-x fish_clipboard_copy + bind --preset $argv ctrl-v fish_clipboard_paste - bind --preset $argv \e cancel - bind --preset $argv \t complete - bind --preset $argv \cs pager-toggle-search + bind --preset $argv escape cancel + bind --preset $argv ctrl-\[ cancel + bind --preset $argv tab complete + bind --preset $argv ctrl-i complete + bind --preset $argv ctrl-s pager-toggle-search # shift-tab does a tab complete followed by a search. - bind --preset $argv --key btab complete-and-search - bind --preset $argv -k sdc history-pager-delete or backward-delete-char # shifted delete + bind --preset $argv shift-tab complete-and-search + $legacy_bind --preset $argv -k btab complete-and-search + bind --preset $argv shift-delete history-pager-delete or backward-delete-char + $legacy_bind --preset $argv -k sdc history-pager-delete or backward-delete-char - bind --preset $argv -k down down-or-search - bind --preset $argv -k up up-or-search - bind --preset $argv \e\[A up-or-search - bind --preset $argv \e\[B down-or-search - bind --preset $argv \eOA up-or-search - bind --preset $argv \eOB down-or-search + bind --preset $argv down down-or-search + $legacy_bind --preset $argv -k down down-or-search + bind --preset $argv up up-or-search + $legacy_bind --preset $argv -k up up-or-search + $legacy_bind --preset $argv \e\[A up-or-search + $legacy_bind --preset $argv \e\[B down-or-search + $legacy_bind --preset $argv \eOA up-or-search + $legacy_bind --preset $argv \eOB down-or-search - bind --preset $argv -k sright forward-bigword - bind --preset $argv -k sleft backward-bigword + 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 - # Alt-left/Alt-right - bind --preset $argv \e\eOC nextd-or-forward-word - bind --preset $argv \e\eOD prevd-or-backward-word - bind --preset $argv \e\e\[C nextd-or-forward-word - bind --preset $argv \e\e\[D prevd-or-backward-word - bind --preset $argv \eO3C nextd-or-forward-word - bind --preset $argv \eO3D prevd-or-backward-word - bind --preset $argv \e\[3C nextd-or-forward-word - bind --preset $argv \e\[3D prevd-or-backward-word - bind --preset $argv \e\[1\;3C nextd-or-forward-word - bind --preset $argv \e\[1\;3D prevd-or-backward-word - bind --preset $argv \e\[1\;9C nextd-or-forward-word #iTerm2 - bind --preset $argv \e\[1\;9D prevd-or-backward-word #iTerm2 + bind --preset $argv alt-right nextd-or-forward-word + bind --preset $argv alt-left prevd-or-backward-word + $legacy_bind --preset $argv \e\eOC nextd-or-forward-word + $legacy_bind --preset $argv \e\eOD prevd-or-backward-word + $legacy_bind --preset $argv \e\e\[C nextd-or-forward-word + $legacy_bind --preset $argv \e\e\[D prevd-or-backward-word + $legacy_bind --preset $argv \eO3C nextd-or-forward-word + $legacy_bind --preset $argv \eO3D prevd-or-backward-word + $legacy_bind --preset $argv \e\[3C nextd-or-forward-word + $legacy_bind --preset $argv \e\[3D prevd-or-backward-word + $legacy_bind --preset $argv \e\[1\;3C nextd-or-forward-word + $legacy_bind --preset $argv \e\[1\;3D prevd-or-backward-word + $legacy_bind --preset $argv \e\[1\;9C nextd-or-forward-word #iTerm2 + $legacy_bind --preset $argv \e\[1\;9D prevd-or-backward-word #iTerm2 - # Alt-up/Alt-down - bind --preset $argv \e\eOA history-token-search-backward - bind --preset $argv \e\eOB history-token-search-forward - bind --preset $argv \e\e\[A history-token-search-backward - bind --preset $argv \e\e\[B history-token-search-forward - bind --preset $argv \eO3A history-token-search-backward - bind --preset $argv \eO3B history-token-search-forward - bind --preset $argv \e\[3A history-token-search-backward - bind --preset $argv \e\[3B history-token-search-forward - bind --preset $argv \e\[1\;3A history-token-search-backward - bind --preset $argv \e\[1\;3B history-token-search-forward - bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2 - bind --preset $argv \e\[1\;9B history-token-search-forward # iTerm2 + bind --preset $argv alt-up history-token-search-backward + bind --preset $argv alt-down history-token-search-forward + $legacy_bind --preset $argv \e\eOA history-token-search-backward + $legacy_bind --preset $argv \e\eOB history-token-search-forward + $legacy_bind --preset $argv \e\e\[A history-token-search-backward + $legacy_bind --preset $argv \e\e\[B history-token-search-forward + $legacy_bind --preset $argv \eO3A history-token-search-backward + $legacy_bind --preset $argv \eO3B history-token-search-forward + $legacy_bind --preset $argv \e\[3A history-token-search-backward + $legacy_bind --preset $argv \e\[3B history-token-search-forward + $legacy_bind --preset $argv \e\[1\;3A history-token-search-backward + $legacy_bind --preset $argv \e\[1\;3B history-token-search-forward + $legacy_bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2 + $legacy_bind --preset $argv \e\[1\;9B history-token-search-forward # iTerm2 # Bash compatibility # https://github.com/fish-shell/fish-shell/issues/89 - bind --preset $argv \e. history-token-search-backward + bind --preset $argv alt-. history-token-search-backward - bind --preset $argv \el __fish_list_current_token - bind --preset $argv \eo __fish_preview_current_file - bind --preset $argv \ew __fish_whatis_current_token - bind --preset $argv \cl clear-screen - bind --preset $argv \cc cancel-commandline - bind --preset $argv \cu backward-kill-line - bind --preset $argv \cw backward-kill-path-component - bind --preset $argv \e\[F end-of-line - bind --preset $argv \e\[H beginning-of-line + bind --preset $argv alt-l __fish_list_current_token + bind --preset $argv alt-o __fish_preview_current_file + bind --preset $argv alt-w __fish_whatis_current_token + bind --preset $argv ctrl-l clear-screen + bind --preset $argv ctrl-c cancel-commandline + bind --preset $argv ctrl-u backward-kill-line + bind --preset $argv ctrl-w backward-kill-path-component + bind --preset $argv end end-of-line + $legacy_bind --preset $argv \e\[F end-of-line + bind --preset $argv home beginning-of-line + $legacy_bind --preset $argv \e\[H beginning-of-line - bind --preset $argv \ed 'set -l cmd (commandline); if test -z "$cmd"; echo; dirh; commandline -f repaint; else; commandline -f kill-word; end' - bind --preset $argv \cd delete-or-exit + bind --preset $argv alt-d 'set -l cmd (commandline); if test -z "$cmd"; echo; dirh; commandline -f repaint; else; commandline -f kill-word; end' + bind --preset $argv ctrl-d delete-or-exit - bind --preset $argv \es 'for cmd in sudo doas please; if command -q $cmd; fish_commandline_prepend $cmd; break; end; end' + bind --preset $argv alt-s 'for cmd in sudo doas please; if command -q $cmd; fish_commandline_prepend $cmd; break; end; end' # Allow reading manpages by pressing F1 (many GUI applications) or Alt+h (like in zsh). - bind --preset $argv -k f1 __fish_man_page - bind --preset $argv \eh __fish_man_page + bind --preset $argv F1 __fish_man_page + $legacy_bind --preset $argv -k f1 __fish_man_page + bind --preset $argv alt-h __fish_man_page # This will make sure the output of the current command is paged using the default pager when # you press Meta-p. # If none is set, less will be used. - bind --preset $argv \ep __fish_paginate + bind --preset $argv alt-p __fish_paginate # Make it easy to turn an unexecuted command into a comment in the shell history. Also, # remove the commenting chars so the command can be further edited then executed. - bind --preset $argv \e\# __fish_toggle_comment_commandline + bind --preset $argv alt-# __fish_toggle_comment_commandline - # The [meta-e] and [meta-v] keystrokes invoke an external editor on the command buffer. - bind --preset $argv \ee edit_command_buffer - bind --preset $argv \ev edit_command_buffer - - # Tmux' focus events. - # Exclude paste mode because that should get _everything_ literally. - for mode in (bind --list-modes | string match -v paste) - # We only need the in-focus event currently (to redraw the vi-cursor). - bind --preset -M $mode \e\[I 'emit fish_focus_in' - bind --preset -M $mode \e\[O false - bind --preset -M $mode \e\[\?1004h false - end - - # Support for "bracketed paste" - # The way it works is that we acknowledge our support by printing - # \e\[?2004h - # then the terminal will "bracket" every paste in - # \e\[200~ and \e\[201~ - # Every character in between those two will be part of the paste and should not cause a binding to execute (like \n executing commands). - # - # We enable it after every command and disable it before (in __fish_config_interactive.fish) - # - # Support for this seems to be ubiquitous - emacs enables it unconditionally (!) since 25.1 - # (though it only supports it since then, it seems to be the last term to gain support). - # - # NOTE: This is more of a "security" measure than a proper feature. - # The better way to paste remains the `fish_clipboard_paste` function (bound to \cv by default). - # We don't disable highlighting here, so it will be redone after every character (which can be slow), - # and it doesn't handle "paste-stop" sequences in the paste (which the terminal needs to strip). - # - # See http://thejh.net/misc/website-terminal-copy-paste. - - # Bind the starting sequence in every bind mode, even user-defined ones. - # Exclude paste mode or there'll be an additional binding after switching between emacs and vi - for mode in (bind --list-modes | string match -v paste) - bind --preset -M $mode -m paste \e\[200~ "__fish_start_bracketed_paste $mode" - end - # This sequence ends paste-mode and returns to the previous mode we have saved before. - bind --preset -M paste \e\[201~ __fish_stop_bracketed_paste - # In paste-mode, everything self-inserts except for the sequence to get out of it - bind --preset -M paste "" self-insert - # Pass through formatting control characters else they may be dropped - # on some terminals. - bind --preset -M paste \b 'commandline -i \b' - bind --preset -M paste \t 'commandline -i \t' - bind --preset -M paste \v 'commandline -i \v' - # Without this, a \r will overwrite the other text, rendering it invisible - which makes the exercise kinda pointless. - bind --preset -M paste \r "commandline -i \n" - - # We usually just pass the text through as-is to facilitate pasting code, - # but when the current token contains an unbalanced single-quote (`'`), - # we escape all single-quotes and backslashes, effectively turning the paste - # into one literal token, to facilitate pasting non-code (e.g. markdown or git commitishes) - bind --preset -M paste "'" "__fish_commandline_insert_escaped \' \$__fish_paste_quoted" - bind --preset -M paste \\ "__fish_commandline_insert_escaped \\\ \$__fish_paste_quoted" - # Only insert spaces if we're either quoted or not at the beginning of the commandline - # - this strips leading spaces if they would trigger histignore. - bind --preset -M paste " " self-insert-notfirst + # These keystrokes invoke an external editor on the command buffer. + bind --preset $argv alt-e edit_command_buffer + bind --preset $argv alt-v edit_command_buffer # Bindings that are shared in text-insertion modes. if not set -l index (contains --index -- -M $argv) @@ -183,48 +148,27 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod bind --preset $argv "&" self-insert expand-abbr bind --preset $argv ">" self-insert expand-abbr bind --preset $argv "<" self-insert expand-abbr + bind --preset $argv shift-enter expand-abbr "commandline -i \n" # Shift+Return as sent with XTerm.vt100.formatOtherKeys: 0 - bind --preset $argv \e\[27\;2\;13~ expand-abbr "commandline -i \n" + $legacy_bind --preset $argv \e\[27\;2\;13~ expand-abbr "commandline -i \n" # Shift+Return CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1 - bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n" - bind --preset $argv \e\n expand-abbr "commandline -i \n" - bind --preset $argv \e\r expand-abbr "commandline -i \n" + $legacy_bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n" + bind --preset $argv alt-enter expand-abbr "commandline -i \n" # Closing a command substitution expands abbreviations bind --preset $argv ")" self-insert expand-abbr # Ctrl-space inserts space without expanding abbrs + bind --preset $argv ctrl-space 'test -n "$(commandline)" && commandline -i " "' bind --preset $argv -k nul 'test -n "$(commandline)" && commandline -i " "' - # Shift-space (CSI u escape sequence) behaves like space because it's easy to mistype. - bind --preset $argv \e\[32\;2u 'commandline -i " "; commandline -f expand-abbr' + # Shift-space behaves like space because it's easy to mistype. + bind --preset $argv shift-space 'commandline -i " "; commandline -f expand-abbr' + $legacy_bind --preset $argv \e\[32\;2u 'commandline -i " "; commandline -f expand-abbr' # CSI u escape sequence - bind --preset $argv \n execute - bind --preset $argv \r execute + bind --preset $argv enter execute + bind --preset $argv ctrl-j execute + bind --preset $argv ctrl-m execute # Make Control+Return behave like Return because it's easy to mistype after accepting an autosuggestion. - bind --preset $argv \e\[27\;5\;13~ execute # Sent with XTerm.vt100.formatOtherKeys: 0 - bind --preset $argv \e\[13\;5u execute # CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1 + bind --preset $argv ctrl-enter execute + $legacy_bind --preset $argv \e\[27\;5\;13~ execute # Sent with XTerm.vt100.formatOtherKeys: 0 + $legacy_bind --preset $argv \e\[13\;5u execute # CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1 end end - -function __fish_commandline_insert_escaped --description 'Insert the first arg escaped if a second arg is given' - if set -q argv[2] - commandline -i \\$argv[1] - else - commandline -i $argv[1] - end -end - -function __fish_start_bracketed_paste - # Save the last bind mode so we can restore it. - set -g __fish_last_bind_mode $argv[1] - # If the token is currently single-quoted, - # we escape single-quotes (and backslashes). - string match -q 'single*' (__fish_tokenizer_state -- (commandline -ct | string collect)) - and set -g __fish_paste_quoted 1 - commandline -f begin-undo-group -end - -function __fish_stop_bracketed_paste - # Restore the last bind mode. - set fish_bind_mode $__fish_last_bind_mode - set -e __fish_paste_quoted - commandline -f end-undo-group -end diff --git a/share/functions/edit_command_buffer.fish b/share/functions/edit_command_buffer.fish index 6e382b361..e7dd0f655 100644 --- a/share/functions/edit_command_buffer.fish +++ b/share/functions/edit_command_buffer.fish @@ -86,13 +86,10 @@ function edit_command_buffer --description 'Edit the command buffer in an extern set -a editor $f end - __fish_disable_bracketed_paste $editor - set -l editor_status $status - __fish_enable_bracketed_paste # Here we're checking the exit status of the editor. - if test $editor_status -eq 0 -a -s $f + if test $status -eq 0 -a -s $f # Set the command to the output of the edited command and move the cursor to the # end of the edited command. commandline -r -- (command cat $f) diff --git a/share/functions/fish_clipboard_paste.fish b/share/functions/fish_clipboard_paste.fish index 0b46b7bdb..3e3dbf8a9 100644 --- a/share/functions/fish_clipboard_paste.fish +++ b/share/functions/fish_clipboard_paste.fish @@ -23,49 +23,5 @@ function fish_clipboard_paste return end - # Also split on \r, otherwise it looks confusing - set data (string split \r -- $data | string split \n) - - if commandline --search-field >/dev/null - commandline --search-field -i -- $data - return - end - - # If the current token has an unmatched single-quote, - # escape all single-quotes (and backslashes) in the paste, - # in order to turn it into a single literal token. - # - # This eases pasting non-code (e.g. markdown or git commitishes). - set -l quote_state (__fish_tokenizer_state -- (commandline -ct | string collect)) - if contains -- $quote_state single single-escaped - if status test-feature regex-easyesc - set data (string replace -ra "(['\\\])" '\\\\$1' -- $data) - else - set data (string replace -ra "(['\\\])" '\\\\\\\$1' -- $data) - end - else if not contains -- $quote_state double double-escaped - and set -q data[2] - # Leading whitespace in subsequent lines is unneded, since fish - # already indents. Also gets rid of tabs (issue #5274). - set -l tmp - for line in $data - switch $quote_state - case normal - set -a tmp (string trim -l -- $line) - case single single-escaped double double-escaped escaped - set -a tmp $line - end - set quote_state (__fish_tokenizer_state -i $quote_state -- $line) - end - set data $data[1] $tmp[2..] - end - if not string length -q -- (commandline -c) - # If we're at the beginning of the first line, trim whitespace from the start, - # so we don't trigger ignoring history. - set data[1] (string trim -l -- $data[1]) - end - - if test -n "$data" - commandline -i -- $data - end + __fish_paste $data end diff --git a/share/functions/fish_default_key_bindings.fish b/share/functions/fish_default_key_bindings.fish index 2a4836d3e..45aed532c 100644 --- a/share/functions/fish_default_key_bindings.fish +++ b/share/functions/fish_default_key_bindings.fish @@ -1,4 +1,5 @@ function fish_default_key_bindings -d "emacs-like key binds" + set -l legacy_bind bind if contains -- -h $argv or contains -- --help $argv echo "Sorry but this function doesn't support -h or --help" @@ -30,65 +31,65 @@ function fish_default_key_bindings -d "emacs-like key binds" __fish_shared_key_bindings $argv or return # protect against invalid $argv - bind --preset $argv \ck kill-line + bind --preset $argv ctrl-k kill-line - bind --preset $argv \eOC forward-char - bind --preset $argv \eOD backward-char - bind --preset $argv \e\[C forward-char - bind --preset $argv \e\[D backward-char - bind --preset $argv -k right forward-char - bind --preset $argv -k left backward-char + bind --preset $argv right forward-char + bind --preset $argv left backward-char + $legacy_bind --preset $argv \eOC forward-char + $legacy_bind --preset $argv \eOD backward-char + $legacy_bind --preset $argv \e\[C forward-char + $legacy_bind --preset $argv \e\[D backward-char + $legacy_bind --preset $argv -k right forward-char + $legacy_bind --preset $argv -k left backward-char - bind --preset $argv -k dc delete-char - bind --preset $argv -k backspace backward-delete-char - bind --preset $argv \x7f backward-delete-char + bind --preset $argv delete delete-char + bind --preset $argv backspace backward-delete-char + bind --preset $argv shift-backspace backward-delete-char # for PuTTY # https://github.com/fish-shell/fish-shell/issues/180 - bind --preset $argv \e\[1~ beginning-of-line - bind --preset $argv \e\[3~ delete-char - bind --preset $argv \e\[4~ end-of-line + $legacy_bind --preset $argv \e\[1~ beginning-of-line + $legacy_bind --preset $argv \e\[3~ delete-char + $legacy_bind --preset $argv \e\[4~ end-of-line - bind --preset $argv -k home beginning-of-line - bind --preset $argv -k end end-of-line + bind --preset $argv home beginning-of-line + $legacy_bind --preset $argv -k home beginning-of-line + bind --preset $argv end end-of-line + $legacy_bind --preset $argv -k end end-of-line - bind --preset $argv \ca beginning-of-line - bind --preset $argv \ce end-of-line - bind --preset $argv \ch backward-delete-char - bind --preset $argv \cp up-or-search - bind --preset $argv \cn down-or-search - bind --preset $argv \cf forward-char - bind --preset $argv \cb backward-char - bind --preset $argv \ct transpose-chars - bind --preset $argv \cg cancel - bind --preset $argv \c_ undo - bind --preset $argv \cz undo - bind --preset $argv \e/ redo - bind --preset $argv \et transpose-words - bind --preset $argv \eu upcase-word + bind --preset $argv ctrl-a beginning-of-line + bind --preset $argv ctrl-e end-of-line + bind --preset $argv ctrl-h backward-delete-char + bind --preset $argv ctrl-p up-or-search + bind --preset $argv ctrl-n down-or-search + bind --preset $argv ctrl-f forward-char + bind --preset $argv ctrl-b backward-char + bind --preset $argv ctrl-t transpose-chars + bind --preset $argv ctrl-g cancel + bind --preset $argv ctrl-/ undo + bind --preset $argv ctrl-_ undo # XTerm idiosyncracy, can get rid of this once we go full CSI u + bind --preset $argv ctrl-z undo + bind --preset $argv alt-/ redo + bind --preset $argv alt-t transpose-words + bind --preset $argv alt-u upcase-word - # This clashes with __fish_list_current_token - # bind --preset $argv \el downcase-word - bind --preset $argv \ec capitalize-word - # One of these is alt+backspace. - bind --preset $argv \e\x7f backward-kill-word - bind --preset $argv \e\b backward-kill-word - if not test "$TERM_PROGRAM" = Apple_Terminal - bind --preset $argv \eb backward-word - bind --preset $argv \ef forward-word - else + bind --preset $argv alt-c capitalize-word + bind --preset $argv alt-backspace backward-kill-word + bind --preset $argv alt-b backward-word + bind --preset $argv alt-f forward-word + if test "$TERM_PROGRAM" = Apple_Terminal # Terminal.app sends \eb for alt+left, \ef for alt+right. # Yeah. - bind --preset $argv \eb prevd-or-backward-word - bind --preset $argv \ef nextd-or-forward-word + $legacy_bind --preset $argv alt-b prevd-or-backward-word + $legacy_bind --preset $argv alt-f nextd-or-forward-word end - bind --preset $argv \e\< beginning-of-buffer - bind --preset $argv \e\> end-of-buffer + bind --preset $argv alt-\< beginning-of-buffer + bind --preset $argv alt-\> end-of-buffer - bind --preset $argv \ed kill-word + bind --preset $argv alt-d kill-word - bind --preset $argv \cr history-pager + bind --preset $argv ctrl-r history-pager # term-specific special bindings switch "$TERM" @@ -96,16 +97,16 @@ function fish_default_key_bindings -d "emacs-like key binds" # suckless and bash/zsh/fish have a different approach to how the terminal should be configured; # the major effect is that several keys do not work as intended. # This is a workaround, there will be additions in he future. - bind --preset $argv \e\[P delete-char - bind --preset $argv \e\[Z up-line + $legacy_bind --preset $argv \e\[P delete-char + $legacy_bind --preset $argv \e\[Z up-line case 'rxvt*' - bind --preset $argv \e\[8~ end-of-line - bind --preset $argv \eOc forward-word - bind --preset $argv \eOd backward-word + $legacy_bind --preset $argv \e\[8~ end-of-line + $legacy_bind --preset $argv \eOc forward-word + $legacy_bind --preset $argv \eOd backward-word case xterm-256color # Microsoft's conemu uses xterm-256color plus # the following to tell a console to paste: - bind --preset $argv \e\x20ep fish_clipboard_paste + $legacy_bind --preset $argv \e\x20ep fish_clipboard_paste end set -e -g fish_cursor_selection_mode diff --git a/share/functions/fish_vi_key_bindings.fish b/share/functions/fish_vi_key_bindings.fish index f0ebf834b..6463ff3e9 100644 --- a/share/functions/fish_vi_key_bindings.fish +++ b/share/functions/fish_vi_key_bindings.fish @@ -1,4 +1,5 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' + set -l legacy_bind bind if contains -- -h $argv or contains -- --help $argv echo "Sorry but this function doesn't support -h or --help" >&2 @@ -37,7 +38,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' set -l init_mode insert # These are only the special vi-style keys # not end/home, we share those. - set -l eol_keys \$ g\$ + set -l eol_keys \$ g,\$ set -l bol_keys \^ 0 g\^ if contains -- $argv[1] insert default visual @@ -56,7 +57,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' # Add a way to switch from insert to normal (command) mode. # Note if we are paging, we want to stay in insert mode # See #2871 - bind -s --preset -M insert \e ' + set -l on_escape ' if commandline -P commandline -f cancel else @@ -67,23 +68,26 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' commandline -f repaint-mode end ' + bind -s --preset -M insert escape $on_escape + bind -s --preset -M insert ctrl-\[ $on_escape # Default (command) mode - bind -s --preset :q exit - bind -s --preset -m insert \cc cancel-commandline repaint-mode + bind -s --preset :,q exit + bind -s --preset -m insert ctrl-c cancel-commandline repaint-mode bind -s --preset -M default h backward-char bind -s --preset -M default l forward-char - bind -s --preset -m insert \n execute - bind -s --preset -m insert \r execute + bind -s --preset -m insert enter execute + bind -s --preset -m insert ctrl-j execute + bind -s --preset -m insert ctrl-m execute bind -s --preset -m insert o 'set fish_cursor_end_mode exclusive' insert-line-under repaint-mode - bind -s --preset -m insert O 'set fish_cursor_end_modefish_cursor_end_modeexclusive' insert-line-over repaint-mode + bind -s --preset -m insert O 'set fish_cursor_end_mode exclusive' insert-line-over repaint-mode bind -s --preset -m insert i repaint-mode bind -s --preset -m insert I beginning-of-line repaint-mode bind -s --preset -m insert a 'set fish_cursor_end_mode exclusive' forward-single-char repaint-mode bind -s --preset -m insert A 'set fish_cursor_end_mode exclusive' end-of-line repaint-mode bind -s --preset -m visual v begin-selection repaint-mode - bind -s --preset gg beginning-of-buffer + bind -s --preset g,g beginning-of-buffer bind -s --preset G end-of-buffer for key in $eol_keys @@ -94,7 +98,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' end bind -s --preset u undo - bind -s --preset \cr redo + bind -s --preset ctrl-r redo bind -s --preset [ history-token-search-backward bind -s --preset ] history-token-search-forward @@ -104,14 +108,14 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind -s --preset j down-or-search bind -s --preset b backward-word bind -s --preset B backward-bigword - bind -s --preset ge backward-word - bind -s --preset gE backward-bigword + bind -s --preset g,e backward-word + bind -s --preset g,E backward-bigword bind -s --preset w forward-word forward-single-char bind -s --preset W forward-bigword forward-single-char bind -s --preset e 'set fish_cursor_end_mode exclusive' forward-single-char forward-word backward-char 'set fish_cursor_end_mode inclusive' bind -s --preset E 'set fish_cursor_end_mode exclusive' forward-single-char forward-bigword backward-char 'set fish_cursor_end_mode inclusive' - bind -s --preset -M insert \cn accept-autosuggestion + bind -s --preset -M insert ctrl-n accept-autosuggestion # Vi/Vim doesn't support these keys in insert mode but that seems silly so we do so anyway. bind -s --preset -M insert -k home beginning-of-line @@ -128,104 +132,105 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind -s --preset -M default -k dc delete-char 'set fish_cursor_end_mode exclusive' forward-single-char backward-char 'set fish_cursor_end_mode inclusive' # Backspace deletes a char in insert mode, but not in normal/default mode. - bind -s --preset -M insert -k backspace backward-delete-char - bind -s --preset -M default -k backspace backward-char - bind -s --preset -M insert \ch backward-delete-char - bind -s --preset -M default \ch backward-char - bind -s --preset -M insert \x7f backward-delete-char - bind -s --preset -M default \x7f backward-char + bind -s --preset -M insert backspace backward-delete-char + bind -s --preset -M insert shift-backspace backward-delete-char + $legacy_bind -s --preset -M insert -k backspace backward-delete-char + bind -s --preset -M default backspace backward-char + $legacy_bind -s --preset -M default -k backspace backward-char + bind -s --preset -M insert ctrl-h backward-delete-char + bind -s --preset -M default ctrl-h backward-char - bind -s --preset dd kill-whole-line + bind -s --preset d,d kill-whole-line bind -s --preset D kill-line - bind -s --preset d\$ kill-line - bind -s --preset d\^ backward-kill-line - bind -s --preset d0 backward-kill-line - bind -s --preset dw kill-word - bind -s --preset dW kill-bigword - bind -s --preset diw forward-single-char forward-single-char backward-word kill-word - bind -s --preset diW forward-single-char forward-single-char backward-bigword kill-bigword - bind -s --preset daw forward-single-char forward-single-char backward-word kill-word - bind -s --preset daW forward-single-char forward-single-char backward-bigword kill-bigword - bind -s --preset de kill-word - bind -s --preset dE kill-bigword - bind -s --preset db backward-kill-word - bind -s --preset dB backward-kill-bigword - bind -s --preset dge backward-kill-word - bind -s --preset dgE backward-kill-bigword - bind -s --preset df begin-selection forward-jump kill-selection end-selection - bind -s --preset dt begin-selection forward-jump backward-char kill-selection end-selection - bind -s --preset dF begin-selection backward-jump kill-selection end-selection - bind -s --preset dT begin-selection backward-jump forward-single-char kill-selection end-selection - bind -s --preset dh backward-char delete-char - bind -s --preset dl delete-char - bind -s --preset di backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection - bind -s --preset da backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection - bind -s --preset 'd;' begin-selection repeat-jump kill-selection end-selection - bind -s --preset 'd,' begin-selection repeat-jump-reverse kill-selection end-selection + bind -s --preset d,\$ kill-line + bind -s --preset d,\^ backward-kill-line + bind -s --preset d,0 backward-kill-line + bind -s --preset d,w kill-word + bind -s --preset d,W kill-bigword + bind -s --preset d,i,w forward-single-char forward-single-char backward-word kill-word + bind -s --preset d,i,W forward-single-char forward-single-char backward-bigword kill-bigword + bind -s --preset d,a,w forward-single-char forward-single-char backward-word kill-word + bind -s --preset d,a,W forward-single-char forward-single-char backward-bigword kill-bigword + bind -s --preset d,e kill-word + bind -s --preset d,E kill-bigword + bind -s --preset d,b backward-kill-word + bind -s --preset d,B backward-kill-bigword + bind -s --preset d,g,e backward-kill-word + bind -s --preset d,g,E backward-kill-bigword + bind -s --preset d,f begin-selection forward-jump kill-selection end-selection + bind -s --preset d,t begin-selection forward-jump backward-char kill-selection end-selection + bind -s --preset d,F begin-selection backward-jump kill-selection end-selection + bind -s --preset d,T begin-selection backward-jump forward-single-char kill-selection end-selection + bind -s --preset d,h backward-char delete-char + bind -s --preset d,l delete-char + bind -s --preset d,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection + bind -s --preset d,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection + bind -s --preset 'd,;' begin-selection repeat-jump kill-selection end-selection + bind -s --preset 'd,comma' begin-selection repeat-jump-reverse kill-selection end-selection bind -s --preset -m insert s delete-char repaint-mode bind -s --preset -m insert S kill-inner-line repaint-mode - bind -s --preset -m insert cc kill-inner-line repaint-mode + bind -s --preset -m insert c,c kill-inner-line repaint-mode bind -s --preset -m insert C kill-line repaint-mode - bind -s --preset -m insert c\$ kill-line repaint-mode - bind -s --preset -m insert c\^ backward-kill-line repaint-mode - bind -s --preset -m insert c0 backward-kill-line repaint-mode - bind -s --preset -m insert cw kill-word repaint-mode - bind -s --preset -m insert cW kill-bigword repaint-mode - bind -s --preset -m insert ciw forward-single-char forward-single-char backward-word kill-word repaint-mode - bind -s --preset -m insert ciW forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode - bind -s --preset -m insert caw forward-single-char forward-single-char backward-word kill-word repaint-mode - bind -s --preset -m insert caW forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode - bind -s --preset -m insert ce kill-word repaint-mode - bind -s --preset -m insert cE kill-bigword repaint-mode - bind -s --preset -m insert cb backward-kill-word repaint-mode - bind -s --preset -m insert cB backward-kill-bigword repaint-mode - bind -s --preset -m insert cge backward-kill-word repaint-mode - bind -s --preset -m insert cgE backward-kill-bigword repaint-mode - bind -s --preset -m insert cf begin-selection forward-jump kill-selection end-selection repaint-mode - bind -s --preset -m insert ct begin-selection forward-jump backward-char kill-selection end-selection repaint-mode - bind -s --preset -m insert cF begin-selection backward-jump kill-selection end-selection repaint-mode - bind -s --preset -m insert cT begin-selection backward-jump forward-single-char kill-selection end-selection repaint-mode - bind -s --preset -m insert ch backward-char begin-selection kill-selection end-selection repaint-mode - bind -s --preset -m insert cl begin-selection kill-selection end-selection repaint-mode - bind -s --preset -m insert ci backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode - bind -s --preset -m insert ca backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode + bind -s --preset -m insert c,\$ kill-line repaint-mode + bind -s --preset -m insert c,\^ backward-kill-line repaint-mode + bind -s --preset -m insert c,0 backward-kill-line repaint-mode + bind -s --preset -m insert c,w kill-word repaint-mode + bind -s --preset -m insert c,W kill-bigword repaint-mode + bind -s --preset -m insert c,i,w forward-single-char forward-single-char backward-word kill-word repaint-mode + bind -s --preset -m insert c,i,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode + bind -s --preset -m insert c,a,w forward-single-char forward-single-char backward-word kill-word repaint-mode + bind -s --preset -m insert c,a,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode + bind -s --preset -m insert c,e kill-word repaint-mode + bind -s --preset -m insert c,E kill-bigword repaint-mode + bind -s --preset -m insert c,b backward-kill-word repaint-mode + bind -s --preset -m insert c,B backward-kill-bigword repaint-mode + bind -s --preset -m insert c,g,e backward-kill-word repaint-mode + bind -s --preset -m insert c,g,E backward-kill-bigword repaint-mode + bind -s --preset -m insert c,f begin-selection forward-jump kill-selection end-selection repaint-mode + bind -s --preset -m insert c,t begin-selection forward-jump backward-char kill-selection end-selection repaint-mode + bind -s --preset -m insert c,F begin-selection backward-jump kill-selection end-selection repaint-mode + bind -s --preset -m insert c,T begin-selection backward-jump forward-single-char kill-selection end-selection repaint-mode + bind -s --preset -m insert c,h backward-char begin-selection kill-selection end-selection repaint-mode + bind -s --preset -m insert c,l begin-selection kill-selection end-selection repaint-mode + bind -s --preset -m insert c,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode + bind -s --preset -m insert c,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode bind -s --preset '~' togglecase-char forward-single-char - bind -s --preset gu downcase-word - bind -s --preset gU upcase-word + bind -s --preset g,u downcase-word + bind -s --preset g,U upcase-word bind -s --preset J end-of-line delete-char bind -s --preset K 'man (commandline -t) 2>/dev/null; or echo -n \a' bind -s --preset yy kill-whole-line yank - for seq in '"*yy' '"*Y' '"+yy' '"+Y' + for seq in '",*,y,y' '",*,Y' '"+,y,y' '",+,Y' bind -s --preset $seq fish_clipboard_copy end bind -s --preset Y kill-whole-line yank - bind -s --preset y\$ kill-line yank - bind -s --preset y\^ backward-kill-line yank - bind -s --preset y0 backward-kill-line yank - bind -s --preset yw kill-word yank - bind -s --preset yW kill-bigword yank - bind -s --preset yiw forward-single-char forward-single-char backward-word kill-word yank - bind -s --preset yiW forward-single-char forward-single-char backward-bigword kill-bigword yank - bind -s --preset yaw forward-single-char forward-single-char backward-word kill-word yank - bind -s --preset yaW forward-single-char forward-single-char backward-bigword kill-bigword yank - bind -s --preset ye kill-word yank - bind -s --preset yE kill-bigword yank - bind -s --preset yb backward-kill-word yank - bind -s --preset yB backward-kill-bigword yank - bind -s --preset yge backward-kill-word yank - bind -s --preset ygE backward-kill-bigword yank - bind -s --preset yf begin-selection forward-jump kill-selection yank end-selection - bind -s --preset yt begin-selection forward-jump-till kill-selection yank end-selection - bind -s --preset yF begin-selection backward-jump kill-selection yank end-selection - bind -s --preset yT begin-selection backward-jump-till kill-selection yank end-selection - bind -s --preset yh backward-char begin-selection kill-selection yank end-selection - bind -s --preset yl begin-selection kill-selection yank end-selection - bind -s --preset yi backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection - bind -s --preset ya backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection + bind -s --preset y,\$ kill-line yank + bind -s --preset y,\^ backward-kill-line yank + bind -s --preset y,0 backward-kill-line yank + bind -s --preset y,w kill-word yank + bind -s --preset y,W kill-bigword yank + bind -s --preset y,i,w forward-single-char forward-single-char backward-word kill-word yank + bind -s --preset y,i,W forward-single-char forward-single-char backward-bigword kill-bigword yank + bind -s --preset y,a,w forward-single-char forward-single-char backward-word kill-word yank + bind -s --preset y,a,W forward-single-char forward-single-char backward-bigword kill-bigword yank + bind -s --preset y,e kill-word yank + bind -s --preset y,E kill-bigword yank + bind -s --preset y,b backward-kill-word yank + bind -s --preset y,B backward-kill-bigword yank + bind -s --preset y,g,e backward-kill-word yank + bind -s --preset y,g,E backward-kill-bigword yank + bind -s --preset y,f begin-selection forward-jump kill-selection yank end-selection + bind -s --preset y,t begin-selection forward-jump-till kill-selection yank end-selection + bind -s --preset y,F begin-selection backward-jump kill-selection yank end-selection + bind -s --preset y,T begin-selection backward-jump-till kill-selection yank end-selection + bind -s --preset y,h backward-char begin-selection kill-selection yank end-selection + bind -s --preset y,l begin-selection kill-selection yank end-selection + bind -s --preset y,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection + bind -s --preset y,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection bind -s --preset f forward-jump bind -s --preset F backward-jump @@ -240,32 +245,40 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' # \ so there's no need to go back a char, just paste it without moving bind -s --preset p 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_modefish_cursor_end_modeinclusive' yank bind -s --preset P yank - bind -s --preset gp yank-pop + bind -s --preset g,p yank-pop # same vim 'pasting' note as upper - bind -s --preset '"*p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste - bind -s --preset '"*P' fish_clipboard_paste - bind -s --preset '"+p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste - bind -s --preset '"+P' fish_clipboard_paste + bind -s --preset '",*,p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste + bind -s --preset '",*,P' fish_clipboard_paste + bind -s --preset '",+,p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste + bind -s --preset '",+,P' fish_clipboard_paste # # Lowercase r, enters replace_one mode # bind -s --preset -m replace_one r repaint-mode bind -s --preset -M replace_one -m default '' delete-char self-insert backward-char repaint-mode - bind -s --preset -M replace_one -m default \r 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode' - bind -s --preset -M replace_one -m default \e cancel repaint-mode + bind -s --preset -M replace_one -m default enter 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode' + bind -s --preset -M replace_one -m default ctrl-j 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode' + bind -s --preset -M replace_one -m default ctrl-m 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode' + bind -s --preset -M replace_one -m default escape cancel repaint-mode + bind -s --preset -M replace_one -m default ctrl-\[ cancel repaint-mode # # Uppercase R, enters replace mode # bind -s --preset -m replace R repaint-mode bind -s --preset -M replace '' delete-char self-insert - bind -s --preset -M replace -m insert \r execute repaint-mode - bind -s --preset -M replace -m default \e cancel repaint-mode + bind -s --preset -M replace -m insert enter execute repaint-mode + bind -s --preset -M replace -m insert ctrl-j execute repaint-mode + bind -s --preset -M replace -m insert ctrl-m execute repaint-mode + bind -s --preset -M replace -m default escape cancel repaint-mode + bind -s --preset -M replace -m default ctrl-\[ cancel repaint-mode # in vim (and maybe in vi), deletes the changes # but this binding just move cursor backward, not delete the changes - bind -s --preset -M replace -k backspace backward-char + bind -s --preset -M replace backspace backward-char + bind -s --preset -M replace shift-backspace backward-char + $legacy_bind -s --preset -M replace -k backspace backward-char # # visual mode @@ -278,8 +291,8 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind -s --preset -M visual b backward-word bind -s --preset -M visual B backward-bigword - bind -s --preset -M visual ge backward-word - bind -s --preset -M visual gE backward-bigword + bind -s --preset -M visual g,e backward-word + bind -s --preset -M visual g,E backward-bigword bind -s --preset -M visual w forward-word bind -s --preset -M visual W forward-bigword bind -s --preset -M visual e 'set fish_cursor_end_mode exclusive' forward-single-char forward-word backward-char 'set fish_cursor_end_mode inclusive' @@ -304,12 +317,13 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind -s --preset -M visual -m default x kill-selection end-selection repaint-mode bind -s --preset -M visual -m default X kill-whole-line end-selection repaint-mode bind -s --preset -M visual -m default y kill-selection yank end-selection repaint-mode - bind -s --preset -M visual -m default '"*y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" - bind -s --preset -M visual -m default '"+y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" + bind -s --preset -M visual -m default '",*,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" + bind -s --preset -M visual -m default '",+,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode" bind -s --preset -M visual -m default '~' togglecase-selection end-selection repaint-mode - bind -s --preset -M visual -m default \cc end-selection repaint-mode - bind -s --preset -M visual -m default \e end-selection repaint-mode + bind -s --preset -M visual -m default ctrl-c end-selection repaint-mode + bind -s --preset -M visual -m default escape end-selection repaint-mode + bind -s --preset -M visual -m default ctrl-\[ end-selection repaint-mode # Make it easy to turn an unexecuted command into a comment in the shell history. Also, remove # the commenting chars so the command can be further edited then executed. diff --git a/src/bin/fish_key_reader.rs b/src/bin/fish_key_reader.rs index 384e06384..0b1ac8dee 100644 --- a/src/bin/fish_key_reader.rs +++ b/src/bin/fish_key_reader.rs @@ -7,12 +7,7 @@ //! //! Type "exit" or "quit" to terminate the program. -use core::panic; -use std::{ - ops::ControlFlow, - os::unix::prelude::OsStrExt, - time::{Duration, Instant}, -}; +use std::{ops::ControlFlow, os::unix::prelude::OsStrExt}; use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR}; @@ -22,16 +17,15 @@ builtins::shared::BUILTIN_ERR_UNKNOWN, common::{shell_modes, str2wcstring, PROGRAM_NAME}, env::env_init, - eprintf, - fallback::fish_wcwidth, - fprintf, + eprintf, fprintf, input::input_terminfo_get_name, input_common::{CharEvent, InputEventQueue, InputEventQueuer}, + key::Key, panic::panic_handler, print_help::print_help, printf, proc::set_interactive_session, - reader::{check_exit_loop_maybe_warning, reader_init, reader_test_and_clear_interrupted}, + reader::{check_exit_loop_maybe_warning, reader_init}, signal::signal_set_handlers, threads, topic_monitor::topic_monitor_init, @@ -40,15 +34,15 @@ }; /// Return true if the recent sequence of characters indicates the user wants to exit the program. -fn should_exit(recent_chars: &mut Vec, c: char) -> bool { - let c = if c < '\u{80}' { c as u8 } else { 0 }; - - recent_chars.push(c); +fn should_exit(recent_keys: &mut Vec, key: Key) -> bool { + recent_keys.push(key); for evt in [VINTR, VEOF] { let modes = shell_modes(); - if c == modes.c_cc[evt] { - if recent_chars.iter().rev().nth(1) == Some(&modes.c_cc[evt]) { + let cc = Key::from_single_byte(modes.c_cc[evt]); + + if key == cc { + if recent_keys.iter().rev().nth(1) == Some(&cc) { return true; } eprintf!( @@ -59,7 +53,15 @@ fn should_exit(recent_chars: &mut Vec, c: char) -> bool { } } - recent_chars.ends_with(b"exit") || recent_chars.ends_with(b"quit") + let Some(tail) = recent_keys + .len() + .checked_sub(4) + .and_then(|start| recent_keys.get(start..)) + else { + return false; + }; + let tail = tail.iter().map(|c| c.codepoint); + tail.clone().eq("exit".chars()) || tail.eq("quit".chars()) } /// Return the name if the recent sequence of characters matches a known terminfo sequence. @@ -80,116 +82,17 @@ fn sequence_name(recent_chars: &mut Vec, c: char) -> Option { input_terminfo_get_name(&str2wcstring(recent_chars)) } -/// Return true if the character must be escaped when used in the sequence of chars to be bound in -/// a `bind` command. -fn must_escape(c: char) -> bool { - "[]()<>{}*\\?$#;&|'\"".contains(c) -} - -fn ctrl_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) { - let ctrl_symbolic_names: [&wstr; 29] = { - std::array::from_fn(|i| match i { - 8 => L!("\\b"), - 9 => L!("\\t"), - 10 => L!("\\n"), - 13 => L!("\\r"), - 27 => L!("\\e"), - 28 => L!("\\x1c"), - _ => L!(""), - }) - }; - - let c = u8::try_from(c).unwrap(); - let cu = usize::from(c); - - if !ctrl_symbolic_names[cu].is_empty() { - if bind_friendly { - sprintf!(=> buf, "%s", ctrl_symbolic_names[cu]); - } else { - sprintf!(=> buf, "\\c%c (or %ls)", char::from(c + 0x40), ctrl_symbolic_names[cu]); - } - } else { - sprintf!(=> buf, "\\c%c", char::from(c + 0x40)); - } -} - -fn space_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) { - if bind_friendly { - sprintf!(=> buf, "\\x%X", u32::from(c)); - } else { - sprintf!(=> buf, "\\x%X (aka \"space\")", u32::from(c)); - } -} - -fn del_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) { - if bind_friendly { - sprintf!(=> buf, "\\x%X", u32::from(c)); - } else { - sprintf!(=> buf, "\\x%X (aka \"del\")", u32::from(c)); - } -} - -fn ascii_printable_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) { - if bind_friendly && must_escape(c) { - sprintf!(=> buf, "\\%c", c); - } else { - sprintf!(=> buf, "%c", c); - } -} - -/// Convert a wide-char to a symbol that can be used in our output. -fn char_to_symbol(c: char, bind_friendly: bool) -> WString { - let mut buff = WString::new(); - let buf = &mut buff; - if c == '\x1b' { - // Escape - this is *technically* also \c[ - buf.push_str("\\e"); - } else if c < ' ' { - // ASCII control character - ctrl_to_symbol(buf, c, bind_friendly); - } else if c == ' ' { - // the "space" character - space_to_symbol(buf, c, bind_friendly); - } else if c == '\x7F' { - // the "del" character - del_to_symbol(buf, c, bind_friendly); - } else if c < '\u{80}' { - // ASCII characters that are not control characters - ascii_printable_to_symbol(buf, c, bind_friendly); - } else if fish_wcwidth(c) > 0 { - sprintf!(=> buf, "%lc", c); - } else if c <= '\u{FFFF}' { - // BMP Unicode chararacter - sprintf!(=> buf, "\\u%04X", u32::from(c)); - } else { - sprintf!(=> buf, "\\U%06X", u32::from(c)); - } - buff -} - -fn add_char_to_bind_command(c: char, bind_chars: &mut Vec) { - bind_chars.push(c); -} - -fn output_bind_command(bind_chars: &mut Vec) { +fn output_bind_command(bind_chars: &mut Vec<(Key, WString)>) { if !bind_chars.is_empty() { printf!("bind "); - for &bind_char in &*bind_chars { - printf!("%s", char_to_symbol(bind_char, true)); + for (key, _seq) in &*bind_chars { + printf!("%s", key); } printf!(" 'do something'\n"); bind_chars.clear(); } } -fn output_info_about_char(c: char) { - eprintf!( - "hex: %4X char: %ls\n", - u32::from(c), - char_to_symbol(c, false) - ); -} - fn output_matching_key_name(recent_chars: &mut Vec, c: char) -> bool { if let Some(name) = sequence_name(recent_chars, c) { printf!("bind -k %ls 'do something'\n", name); @@ -198,72 +101,29 @@ fn output_matching_key_name(recent_chars: &mut Vec, c: char) -> bool { false } -fn output_elapsed_time(prev_timestamp: Instant, first_char_seen: bool, verbose: bool) -> Instant { - // How much time has passed since the previous char was received in microseconds. - let now = Instant::now(); - let delta = now - prev_timestamp; - - if verbose { - if delta >= Duration::from_millis(200) && first_char_seen { - eprintf!("\n"); - } - if delta >= Duration::from_millis(1000) { - eprintf!(" "); - } else { - eprintf!( - "(%3lld.%03lld ms) ", - u64::try_from(delta.as_millis()).unwrap(), - u64::try_from(delta.as_micros() % 1000).unwrap() - ); - } - } - now -} - /// Process the characters we receive as the user presses keys. -fn process_input(continuous_mode: bool, verbose: bool) -> i32 { +fn process_input(continuous_mode: bool) -> i32 { let mut first_char_seen = false; - let mut prev_timestamp = Instant::now() - .checked_sub(Duration::from_millis(1000)) - .unwrap_or(Instant::now()); let mut queue = InputEventQueue::new(STDIN_FILENO); let mut bind_chars = vec![]; let mut recent_chars1 = vec![]; let mut recent_chars2 = vec![]; eprintf!("Press a key:\n"); - while !check_exit_loop_maybe_warning(None) { - let evt = if reader_test_and_clear_interrupted() != 0 { - Some(CharEvent::from_char(char::from(shell_modes().c_cc[VINTR]))) - } else { - queue.readch_timed_esc() - }; + while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) { + let evt = queue.readch(); - if evt.as_ref().is_none_or(|evt| !evt.is_char()) { - output_bind_command(&mut bind_chars); - if first_char_seen && !continuous_mode { - return 0; - } + let CharEvent::Key(kevt) = evt else { continue; - } - let evt = evt.unwrap(); - - let c = evt.get_char().unwrap(); - prev_timestamp = output_elapsed_time(prev_timestamp, first_char_seen, verbose); - // Hack for #3189. Do not suggest \c@ as the binding for nul, because a string containing - // nul cannot be passed to builtin_bind since it uses C strings. We'll output the name of - // this key (nul) elsewhere. - if c != '\0' { - add_char_to_bind_command(c, &mut bind_chars); - } - if verbose { - output_info_about_char(c); - } + }; + let c = kevt.key.codepoint; + bind_chars.push((kevt.key, kevt.seq)); + output_bind_command(&mut bind_chars); if output_matching_key_name(&mut recent_chars1, c) { output_bind_command(&mut bind_chars); } - if continuous_mode && should_exit(&mut recent_chars2, c) { + if continuous_mode && should_exit(&mut recent_chars2, kevt.key) { eprintf!("\nExiting at your request.\n"); break; } @@ -274,7 +134,7 @@ fn process_input(continuous_mode: bool, verbose: bool) -> i32 { } /// Setup our environment (e.g., tty modes), process key strokes, then reset the environment. -fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> i32 { +fn setup_and_process_keys(continuous_mode: bool) -> i32 { set_interactive_session(true); topic_monitor_init(); threads::init(); @@ -298,16 +158,16 @@ fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> i32 { eprintf!("\n"); } - process_input(continuous_mode, verbose) + process_input(continuous_mode) } -fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> ControlFlow { +fn parse_flags(continuous_mode: &mut bool) -> ControlFlow { let short_opts: &wstr = L!("+chvV"); let long_opts: &[woption] = &[ wopt(L!("continuous"), woption_argument_t::no_argument, 'c'), wopt(L!("help"), woption_argument_t::no_argument, 'h'), wopt(L!("version"), woption_argument_t::no_argument, 'v'), - wopt(L!("verbose"), woption_argument_t::no_argument, 'V'), + wopt(L!("verbose"), woption_argument_t::no_argument, 'V'), // Removed ]; let args: Vec = std::env::args_os() @@ -335,9 +195,7 @@ fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> ControlFlow { - *verbose = true; - } + 'V' => {} '?' => { printf!( "%s", @@ -369,9 +227,8 @@ fn main() { fn throwing_main() -> i32 { let mut continuous_mode = false; - let mut verbose = false; - if let ControlFlow::Break(i) = parse_flags(&mut continuous_mode, &mut verbose) { + if let ControlFlow::Break(i) = parse_flags(&mut continuous_mode) { return i; } @@ -380,5 +237,5 @@ fn throwing_main() -> i32 { return 1; } - setup_and_process_keys(continuous_mode, verbose) + setup_and_process_keys(continuous_mode) } diff --git a/src/builtins/bind.rs b/src/builtins/bind.rs index 0912476b5..3ddb73b82 100644 --- a/src/builtins/bind.rs +++ b/src/builtins/bind.rs @@ -6,9 +6,10 @@ }; use crate::highlight::{colorize, highlight_shell}; use crate::input::{ - input_function_get_names, input_mappings, input_terminfo_get_name, input_terminfo_get_names, + input_function_get_names, input_mappings, input_terminfo_get_names, input_terminfo_get_sequence, GetSequenceError, InputMappingSet, }; +use crate::key::{self, canonicalize_raw_escapes, parse_keys, Key}; use crate::nix::isatty; use std::sync::MutexGuard; @@ -75,7 +76,7 @@ fn new() -> BuiltinBind { /// Returns false if no binding with that sequence and mode exists. fn list_one( &self, - seq: &wstr, + seq: &[Key], bind_mode: &wstr, user: bool, parser: &Parser, @@ -83,11 +84,16 @@ fn list_one( ) -> bool { let mut ecmds: &[_] = &[]; let mut sets_mode = None; + let mut terminfo_name = None; let mut out = WString::new(); - if !self - .input_mappings - .get(seq, bind_mode, &mut ecmds, user, &mut sets_mode) - { + if !self.input_mappings.get( + seq, + bind_mode, + &mut ecmds, + user, + &mut sets_mode, + &mut terminfo_name, + ) { return false; } @@ -109,16 +115,22 @@ fn list_one( } } - // Append the name. - if let Some(tname) = input_terminfo_get_name(seq) { + if let Some(tname) = terminfo_name { // Note that we show -k here because we have an input key name. out.push_str(" -k "); out.push_utfstr(&tname); } else { - // No key name, so no -k; we show the escape sequence directly. - let eseq = escape(seq); out.push(' '); - out.push_utfstr(&eseq); + // Append the name. + for (i, key) in seq.iter().enumerate() { + if i != 0 { + out.push(key::KEY_SEPARATOR); + } + out.push_utfstr(&WString::from(*key)); + } + if seq.is_empty() { + out.push_str("''"); + } } // Now show the list of commands. @@ -144,7 +156,7 @@ fn list_one( // Returns false only if neither exists. fn list_one_user_andor_preset( &self, - seq: &wstr, + seq: &[Key], bind_mode: &wstr, user: bool, preset: bool, @@ -224,41 +236,53 @@ fn add( cmds: &[&wstr], mode: WString, sets_mode: Option, - terminfo: bool, + is_terminfo_key: bool, user: bool, streams: &mut IoStreams, ) -> bool { let cmds = cmds.iter().map(|&s| s.to_owned()).collect(); - if terminfo { - if let Some(seq2) = self.get_terminfo_sequence(seq, streams) { - self.input_mappings.add(seq2, cmds, mode, sets_mode, user); - } else { - return true; - } - } else { - self.input_mappings - .add(seq.to_owned(), cmds, mode, sets_mode, user) - } + let Some(key_seq) = self.compute_seq(streams, seq) else { + return true; + }; + self.input_mappings.add( + key_seq, + is_terminfo_key.then(|| seq.to_owned()), + cmds, + mode, + sets_mode, + user, + ); false } + fn compute_seq(&self, streams: &mut IoStreams, seq: &wstr) -> Option> { + if self.opts.use_terminfo { + let Some(tinfo_seq) = self.get_terminfo_sequence(seq, streams) else { + // get_terminfo_sequence already printed the error. + return None; + }; + Some(canonicalize_raw_escapes( + tinfo_seq.chars().map(Key::from_single_char).collect(), + )) + } else { + match parse_keys(seq) { + Ok(keys) => Some(keys), + Err(err) => { + streams.err.append(sprintf!("bind: %s\n", err)); + None + } + } + } + } + /// Erase specified key bindings /// /// @param seq /// an array of all key bindings to erase /// @param all /// if specified, _all_ key bindings will be erased - /// @param use_terminfo - /// Whether to look use terminfo -k name /// - fn erase( - &mut self, - seq: &[&wstr], - all: bool, - use_terminfo: bool, - user: bool, - streams: &mut IoStreams, - ) -> bool { + fn erase(&mut self, seq: &[&wstr], all: bool, user: bool, streams: &mut IoStreams) -> bool { let mode = if self.opts.bind_mode_given { Some(self.opts.bind_mode.as_utfstr()) } else { @@ -270,21 +294,15 @@ fn erase( return false; } - let mut res = false; let mode = mode.unwrap_or(DEFAULT_BIND_MODE); for s in seq { - if use_terminfo { - if let Some(seq2) = self.get_terminfo_sequence(s, streams) { - self.input_mappings.erase(&seq2, mode, user); - } else { - res = true; - } - } else { - self.input_mappings.erase(s, mode, user); - } + let Some(s) = self.compute_seq(streams, s) else { + return true; + }; + self.input_mappings.erase(&s, mode, user); } - res + false } fn insert( @@ -331,14 +349,8 @@ fn insert( self.list(bind_mode, true, parser, streams); } } else if arg_count == 1 { - let seq = if self.opts.use_terminfo { - let Some(seq2) = self.get_terminfo_sequence(argv[optind], streams) else { - // get_terminfo_sequence already printed the error. - return true; - }; - seq2 - } else { - argv[optind].to_owned() + let Some(seq) = self.compute_seq(streams, argv[optind]) else { + return true; }; if !self.list_one_user_andor_preset( @@ -360,9 +372,15 @@ fn insert( cmd, eseq )); + } else if seq.len() == 1 { + streams.err.append(wgettext_fmt!( + "%ls: No binding found for key '%ls'\n", + cmd, + eseq + )); } else { streams.err.append(wgettext_fmt!( - "%ls: No binding found for sequence '%ls'\n", + "%ls: No binding found for key sequence '%ls'\n", cmd, eseq )); @@ -372,8 +390,9 @@ fn insert( } } else { // Actually insert! + let seq = argv[optind]; if self.add( - argv[optind], + seq, &argv[optind + 1..], self.opts.bind_mode.to_owned(), self.opts.sets_bind_mode.to_owned(), @@ -527,7 +546,6 @@ pub fn bind( if self.erase( &argv[optind..], self.opts.all, - self.opts.use_terminfo, true, /* user */ streams, ) { @@ -538,7 +556,6 @@ pub fn bind( if self.erase( &argv[optind..], self.opts.all, - self.opts.use_terminfo, false, /* user */ streams, ) { diff --git a/src/builtins/fg.rs b/src/builtins/fg.rs index 76b11b9c8..b5f2838a0 100644 --- a/src/builtins/fg.rs +++ b/src/builtins/fg.rs @@ -1,6 +1,8 @@ //! Implementation of the fg builtin. use crate::fds::make_fd_blocking; +use crate::input_common::terminal_protocols_disable_scoped; +use crate::proc::is_interactive_session; use crate::reader::reader_write_title; use crate::tokenizer::tok_command; use crate::wutil::perror; @@ -155,6 +157,8 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Optio } } } + let _terminal_protocols = (is_interactive_session() && job.group().wants_terminal()) + .then(terminal_protocols_disable_scoped); let mut transfer = TtyTransfer::new(); transfer.to_job_group(job.group.as_ref().unwrap()); let resumed = job.resume(); diff --git a/src/builtins/read.rs b/src/builtins/read.rs index 2bccd3edb..f397a4ec6 100644 --- a/src/builtins/read.rs +++ b/src/builtins/read.rs @@ -7,15 +7,16 @@ use crate::common::str2wcstring; use crate::common::unescape_string; use crate::common::valid_var_name; -use crate::common::ScopeGuard; use crate::common::UnescapeStringStyle; use crate::env::EnvMode; use crate::env::Environment; use crate::env::READ_BYTE_LIMIT; use crate::env::{EnvVar, EnvVarFlags}; +use crate::input_common::terminal_protocols_enable_scoped; use crate::libc::MB_CUR_MAX; use crate::nix::isatty; use crate::reader::commandline_set_buffer; +use crate::reader::reader_current_data; use crate::reader::ReaderConfig; use crate::reader::{reader_pop, reader_push, reader_readline}; use crate::tokenizer::Tokenizer; @@ -26,9 +27,7 @@ use crate::wutil::encoding::mbrtowc; use crate::wutil::encoding::zero_mbstate; use crate::wutil::perror; -use crate::wutil::write_to_fd; use libc::SEEK_CUR; -use libc::STDOUT_FILENO; use std::os::fd::RawFd; use std::sync::atomic::Ordering; @@ -592,12 +591,9 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Opt let stream_stdin_is_a_tty = isatty(streams.stdin_fd); - let _maybe_disable_bracketed_paste = stream_stdin_is_a_tty.then(|| { - let _ = write_to_fd(b"\x1b[?2004h", STDOUT_FILENO); - ScopeGuard::new((), |()| { - let _ = write_to_fd(b"\x1b[?2004l", STDOUT_FILENO); - }) - }); + // Enable terminal protocols if noninteractive. + let _terminal_protocols = (stream_stdin_is_a_tty && reader_current_data().is_none()) + .then(terminal_protocols_enable_scoped); // Normally, we either consume a line of input or all available input. But if we are reading a // line at a time, we need a middle ground where we only consume as many lines as we need to diff --git a/src/exec.rs b/src/exec.rs index a7a45f3aa..dc8429e12 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -24,6 +24,9 @@ #[cfg(FISH_USE_POSIX_SPAWN)] use crate::fork_exec::spawn::PosixSpawner; use crate::function::{self, FunctionProperties}; +use crate::input_common::{ + terminal_protocols_disable, terminal_protocols_disable_scoped, TERMINAL_PROTOCOLS, +}; use crate::io::{ BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe, IoStreams, OutputStream, SeparatedBuffer, StringOutputStream, @@ -39,7 +42,7 @@ print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType, TtyTransfer, INVALID_PID, }; -use crate::reader::{reader_run_count, restore_term_mode}; +use crate::reader::{reader_current_data, reader_run_count, restore_term_mode}; use crate::redirection::{dup2_list_resolve_chain, Dup2List}; use crate::threads::{iothread_perform_cant_wait, is_forked_child}; use crate::timer::push_timer; @@ -72,6 +75,15 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool { return true; } + let _terminal_protocols_disabled = ( + // If interactive or inside noninteractive builtin read. + reader_current_data().is_some() && + // If we try to start an external process. + job.group().wants_terminal() + && TERMINAL_PROTOCOLS.get().borrow().is_some() + ) + .then(terminal_protocols_disable_scoped); + // Handle an exec call. if job.processes()[0].typ == ProcessType::exec { // If we are interactive, perhaps disallow exec if there are background jobs. @@ -439,6 +451,9 @@ fn launch_process_nofork(vars: &EnvStack, p: &Process) -> ! { // Ensure the terminal modes are what they were before we changed them. restore_term_mode(); + if reader_current_data().is_some() && TERMINAL_PROTOCOLS.get().borrow().is_some() { + terminal_protocols_disable(); + } // Bounce to launch_process. This never returns. safe_launch_process(p, &actual_cmd, &argv, &*envp); } diff --git a/src/flog.rs b/src/flog.rs index 48e3690ae..af3763ba5 100644 --- a/src/flog.rs +++ b/src/flog.rs @@ -126,6 +126,7 @@ pub fn all_categories() -> Vec<&'static category_t> { (fd_monitor, "fd-monitor", "FD monitor events"); (term_support, "term-support", "Terminal feature detection"); + (term_protocols, "term-protocols", "Terminal protocol negotiation"); (reader, "reader", "The interactive reader/input system"); (reader_render, "reader-render", "Rendering the command line"); diff --git a/src/input.rs b/src/input.rs index 9506c2201..29e16a112 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,6 +6,7 @@ use crate::input_common::{ CharEvent, CharInputStyle, InputEventQueuer, ReadlineCmd, R_END_INPUT_FUNCTIONS, }; +use crate::key::{self, canonicalize_raw_escapes, ctrl, Key}; use crate::parser::Parser; use crate::proc::job_reap; use crate::reader::{ @@ -33,7 +34,7 @@ #[derive(Debug, Clone)] pub struct InputMappingName { - pub seq: WString, + pub seq: Vec, pub mode: WString, } @@ -41,7 +42,7 @@ pub struct InputMappingName { #[derive(Debug, Clone)] struct InputMapping { /// Character sequence which generates this event. - seq: WString, + seq: Vec, /// Commands that should be evaluated by this mapping. commands: Vec, /// We wish to preserve the user-specified order. This is just an incrementing value. @@ -50,15 +51,18 @@ struct InputMapping { mode: WString, /// New mode that should be switched to after command evaluation, or None to leave the mode unchanged. sets_mode: Option, + /// Whether this sequence was specified via its terminfo name. + terminfo_name: Option, } impl InputMapping { /// Create a new mapping. fn new( - seq: WString, + seq: Vec, commands: Vec, mode: WString, sets_mode: Option, + terminfo_name: Option, ) -> InputMapping { static LAST_INPUT_MAP_SPEC_ORDER: AtomicU32 = AtomicU32::new(0); let specification_order = 1 + LAST_INPUT_MAP_SPEC_ORDER.fetch_add(1, Ordering::Relaxed); @@ -72,6 +76,7 @@ fn new( specification_order, mode, sets_mode, + terminfo_name, } } @@ -115,6 +120,8 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat const INPUT_FUNCTION_METADATA: &[InputFunctionMetadata] = &[ // NULL makes it unusable - this is specially inserted when we detect mouse input make_md(L!(""), ReadlineCmd::DisableMouseTracking), + make_md(L!(""), ReadlineCmd::FocusIn), + make_md(L!(""), ReadlineCmd::FocusOut), make_md(L!("accept-autosuggestion"), ReadlineCmd::AcceptAutosuggestion), make_md(L!("and"), ReadlineCmd::FuncAnd), make_md(L!("backward-bigword"), ReadlineCmd::BackwardBigword), @@ -274,7 +281,8 @@ impl InputMappingSet { /// Adds an input mapping. pub fn add( &mut self, - sequence: WString, + sequence: Vec, + terminfo_name: Option, commands: Vec, mode: WString, sets_mode: Option, @@ -299,20 +307,28 @@ pub fn add( } // Add a new mapping, using the next order. - let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode); + let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode, terminfo_name); input_mapping_insert_sorted(ml, new_mapping); } // Like add(), but takes a single command. pub fn add1( &mut self, - sequence: WString, + sequence: Vec, + terminfo_name: Option, command: WString, mode: WString, sets_mode: Option, user: bool, ) { - self.add(sequence, vec![command], mode, sets_mode, user); + self.add( + sequence, + terminfo_name, + vec![command], + mode, + sets_mode, + user, + ); } } @@ -330,35 +346,44 @@ pub fn init_input() { // If we have no keybindings, add a few simple defaults. if input_mapping.preset_mapping_list.is_empty() { // Helper for adding. - let mut add = |seq: &str, cmd: &str| { + let mut add = |key: Vec, cmd: &str| { let mode = DEFAULT_BIND_MODE.to_owned(); let sets_mode = Some(DEFAULT_BIND_MODE.to_owned()); - input_mapping.add1(seq.into(), cmd.into(), mode, sets_mode, false); + input_mapping.add1(key, None, cmd.into(), mode, sets_mode, false); }; - add("", "self-insert"); - add("\n", "execute"); - add("\r", "execute"); - add("\t", "complete"); - add("\x03", "cancel-commandline"); - add("\x04", "exit"); - add("\x05", "bind"); - // ctrl-s - add("\x13", "pager-toggle-search"); - // ctrl-u - add("\x15", "backward-kill-line"); - // del/backspace - add("\x7f", "backward-delete-char"); + add(vec![], "self-insert"); + add(vec![Key::from_raw(key::Enter)], "execute"); + add(vec![Key::from_raw(key::Tab)], "complete"); + add(vec![ctrl('c')], "cancel-commandline"); + add(vec![ctrl('d')], "exit"); + add(vec![ctrl('e')], "bind"); + add(vec![ctrl('s')], "pager-toggle-search"); + add(vec![ctrl('u')], "backward-kill-line"); + add(vec![Key::from_raw(key::Backspace)], "backward-delete-char"); // Arrows - can't have functions, so *-or-search isn't available. - add("\x1B[A", "up-line"); - add("\x1B[B", "down-line"); - add("\x1B[C", "forward-char"); - add("\x1B[D", "backward-char"); - // emacs-style ctrl-p/n/b/f - add("\x10", "up-line"); - add("\x0e", "down-line"); - add("\x02", "backward-char"); - add("\x06", "forward-char"); + add(vec![Key::from_raw(key::Up)], "up-line"); + add(vec![Key::from_raw(key::Down)], "down-line"); + add(vec![Key::from_raw(key::Right)], "forward-char"); + add(vec![Key::from_raw(key::Left)], "backward-char"); + // Emacs style + add(vec![ctrl('p')], "up-line"); + add(vec![ctrl('n')], "down-line"); + add(vec![ctrl('b')], "backward-char"); + add(vec![ctrl('f')], "forward-char"); + + let mut add_legacy = |escape_sequence: &str, cmd: &str| { + add( + canonicalize_raw_escapes( + escape_sequence.chars().map(Key::from_single_char).collect(), + ), + cmd, + ); + }; + add_legacy("\x1B[A", "up-line"); + add_legacy("\x1B[B", "down-line"); + add_legacy("\x1B[C", "forward-char"); + add_legacy("\x1B[D", "backward-char"); } } @@ -370,6 +395,7 @@ pub fn init_input() { pub struct Inputter { in_fd: RawFd, queue: VecDeque, + paste_buffer: Option>, // We need a parser to evaluate bindings. parser: Rc, input_function_args: Vec, @@ -417,7 +443,7 @@ fn select_interrupted(&mut self) { if reader_reading_interrupted() != 0 { let vintr = shell_modes().c_cc[libc::VINTR]; if vintr != 0 { - self.push_front(CharEvent::from_char(vintr.into())); + self.push_front(CharEvent::from_key(Key::from_single_byte(vintr))); } return; } @@ -427,6 +453,23 @@ fn select_interrupted(&mut self) { fn uvar_change_notified(&mut self) { self.parser.sync_uvars_and_fire(true /* always */); } + + fn paste_start_buffering(&mut self) { + self.paste_buffer = Some(vec![]); + } + fn paste_is_buffering(&self) -> bool { + self.paste_buffer.is_some() + } + fn paste_commit(&mut self) { + let buffer = self.paste_buffer.take().unwrap(); + self.push_front(CharEvent::Command(sprintf!( + "__fish_paste %s", + escape(&str2wcstring(&buffer)) + ))); + } + fn paste_push_char(&mut self, b: u8) { + self.paste_buffer.as_mut().unwrap().push(b) + } } impl Inputter { @@ -435,6 +478,7 @@ pub fn new(parser: Rc, in_fd: RawFd) -> Inputter { Inputter { in_fd, queue: VecDeque::new(), + paste_buffer: None, parser, input_function_args: Vec::new(), function_status: false, @@ -464,9 +508,12 @@ fn function_push_args(&mut self, code: ReadlineCmd) { let arg: char; loop { let evt = self.readch(); - if let Some(c) = evt.get_char() { - arg = c; - break; + if let Some(kevt) = evt.get_key() { + if let Some(c) = kevt.key.codepoint_text() { + // TODO forward the whole key + arg = c; + break; + } } skipped.push(evt); } @@ -492,7 +539,16 @@ fn mapping_execute(&mut self, m: &InputMapping) { let evt = match input_function_get_code(cmd) { Some(code) => { self.function_push_args(code); - CharEvent::from_readline_seq(code, m.seq.clone()) + // At this point, the sequence is only used for reinserting the keys into + // the event queue for self-insert. Modifiers make no sense here so drop them. + CharEvent::from_readline_seq( + code, + m.seq + .iter() + .filter(|key| key.modifiers.is_none()) + .map(|key| key.codepoint) + .collect(), + ) } None => CharEvent::Command(cmd.clone()), }; @@ -539,6 +595,8 @@ struct EventQueuePeeker<'q> { /// The current index. This never exceeds peeked.len(). idx: usize, + /// The current index within a the raw characters within a single key event. + subidx: usize, /// The queue from which to read more events. event_queue: &'q mut Inputter, @@ -550,6 +608,7 @@ fn new(event_queue: &mut Inputter) -> EventQueuePeeker { peeked: Vec::new(), had_timeout: false, idx: 0, + subidx: 0, event_queue, } } @@ -566,12 +625,13 @@ fn next(&mut self) -> CharEvent { } let res = self.peeked[self.idx].clone(); self.idx += 1; + self.subidx = 0; res } /// Check if the next event is the given character. This advances the index on success only. /// If \p escaped is set, then return false if this (or any other) character had a timeout. - fn next_is_char(&mut self, c: char, escaped: bool) -> bool { + fn next_is_char(&mut self, key: Key, escaped: bool) -> bool { assert!( self.idx <= self.peeked.len(), "Index must not be larger than dequeued event count" @@ -583,36 +643,74 @@ fn next_is_char(&mut self, c: char, escaped: bool) -> bool { // Grab a new event if we have exhausted what we have already peeked. // Use either readch or readch_timed, per our param. if self.idx == self.peeked.len() { - let newevt: CharEvent; - if !escaped { - if let Some(mevt) = self.event_queue.readch_timed_sequence_key() { - newevt = mevt; - } else { - self.had_timeout = true; - return false; - } - } else if let Some(mevt) = self.event_queue.readch_timed_esc() { - newevt = mevt; + let Some(newevt) = (if escaped { + self.event_queue.readch_timed_esc() } else { + self.event_queue.readch_timed_sequence_key() + }) else { self.had_timeout = true; return false; - } + }; self.peeked.push(newevt); } // Now we have peeked far enough; check the event. // If it matches the char, then increment the index. - if self.peeked[self.idx].get_char() == Some(c) { + let evt = &self.peeked[self.idx]; + let Some(kevt) = evt.get_key() else { + return false; + }; + if kevt.key == key { self.idx += 1; + self.subidx = 0; return true; } + let actual_seq = kevt.seq.as_char_slice(); + if !actual_seq.is_empty() { + let seq_char = actual_seq[self.subidx]; + FLOG!( + reader, + "match mapping's", + key, + format!("against actual char {}", u32::from(seq_char)), + ); + if Key::from_single_char(seq_char) == key { + self.subidx += 1; + if self.subidx == actual_seq.len() { + self.idx += 1; + self.subidx = 0; + } + FLOG!(reader, "matched legacy sequence"); + return true; + } + if key.modifiers.alt + && !key.modifiers.ctrl + && !key.modifiers.shift + && seq_char == '\x1b' + { + if self.subidx + 1 == actual_seq.len() { + self.idx += 1; + self.subidx = 0; + FLOG!(reader, "matched escape prefix of legacy alt sequence"); + return self.next_is_char(Key::from_raw(key.codepoint), true); + } else if actual_seq + .get(self.subidx + 1) + .cloned() + .map(|c| Key::from_single_char(c).codepoint) + == Some(key.codepoint) + { + self.subidx += 2; + if self.subidx == actual_seq.len() { + self.idx += 1; + self.subidx = 0; + } + FLOG!(reader, "matched legacy alt sequence"); + return true; + } + } + } false } - /// \return the current index. - fn len(&self) -> usize { - self.idx - } - /// Consume all events up to the current index. /// Remaining events are returned to the queue. fn consume(mut self) { @@ -620,6 +718,7 @@ fn consume(mut self) { self.event_queue.insert_front(self.peeked.drain(self.idx..)); self.peeked.clear(); self.idx = 0; + self.subidx = 0; } /// Test if any of our peeked events are readline or check_exit. @@ -632,82 +731,42 @@ fn char_sequence_interrupted(&self) -> bool { /// Reset our index back to 0. fn restart(&mut self) { self.idx = 0; + self.subidx = 0; } } impl Drop for EventQueuePeeker<'_> { fn drop(&mut self) { assert!( - self.idx == 0, + self.idx == 0 && self.subidx == 0, "Events left on the queue - missing restart or consume?", ); self.event_queue.insert_front(self.peeked.drain(self.idx..)); } } -/// Try reading a mouse-tracking CSI sequence, using the given \p peeker. -/// Events are left on the peeker and the caller must restart or consume it. -/// \return true if matched, false if not. -fn have_mouse_tracking_csi(peeker: &mut EventQueuePeeker) -> bool { - // Maximum length of any CSI is NPAR (which is nominally 16), although this does not account for - // user input intermixed with pseudo input generated by the tty emulator. - // Check for the CSI first. - if !peeker.next_is_char('\x1b', false) || !peeker.next_is_char('[', true /* escaped */) { - return false; - } - - let mut next = peeker.next().get_char(); - let length; - if next == Some('M') { - // Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6 chars - // (although in mode 1005, the characters may be unicode and not necessarily just one byte - // long) reporting the button that was clicked and its location. - length = 6; - } else if next == Some('<') { - // Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters for button - // code, Px, and Py, ending with 'M' for button press or 'm' for button release. - loop { - next = peeker.next().get_char(); - if next == Some('M') || next == Some('m') { - // However much we've read, we've consumed the CSI in its entirety. - length = peeker.len(); - break; - } - if peeker.len() >= 16 { - // This is likely a malformed mouse-reporting CSI but we can't do anything about it. - return false; - } - } - } else if next == Some('t') { - // VT200 button released in mouse highlighting mode at valid text location. 5 chars. - length = 5; - } else if next == Some('T') { - // VT200 button released in mouse highlighting mode past end-of-line. 9 characters. - length = 9; - } else { - return false; - } - - // Consume however many characters it takes to prevent the mouse tracking sequence from reaching - // the prompt, dependent on the class of mouse reporting as detected above. - while peeker.len() < length { - let _ = peeker.next(); - } - true -} - /// \return true if a given \p peeker matches a given sequence of char events given by \p str. -fn try_peek_sequence(peeker: &mut EventQueuePeeker, str: &wstr) -> bool { - assert!(!str.is_empty(), "Empty string passed to try_peek_sequence"); - let mut prev = '\0'; - for c in str.chars() { +fn try_peek_sequence(peeker: &mut EventQueuePeeker, seq: &[Key]) -> bool { + assert!( + !seq.is_empty(), + "Empty sequence passed to try_peek_sequence" + ); + let mut prev = Key::from_raw(key::Invalid); + for key in seq { // If we just read an escape, we need to add a timeout for the next char, // to distinguish between the actual escape key and an "alt"-modifier. - let escaped = prev == '\x1B'; - if !peeker.next_is_char(c, escaped) { + let escaped = prev == Key::from_raw(key::Escape); + if !peeker.next_is_char(*key, escaped) { return false; } - prev = c; + prev = *key; + } + if peeker.subidx != 0 { + FLOG!( + reader, + "legacy binding matched prefix of key encoding but did not consume all of it" + ); + return false; } true } @@ -739,7 +798,7 @@ fn find_mapping(vars: &dyn Environment, peeker: &mut EventQueuePeeker) -> Option if try_peek_sequence(peeker, &m.seq) { // A binding for just escape should also be deferred // so escape sequences take precedence. - if m.seq == "\x1B" { + if m.seq == vec![Key::from_raw(key::Escape)] { if escape.is_none() { escape = Some(m); } @@ -771,27 +830,12 @@ fn find_mapping(vars: &dyn Environment, peeker: &mut EventQueuePeeker) -> Option fn mapping_execute_matching_or_generic(&mut self) { let vars = self.parser.vars_ref(); let mut peeker = EventQueuePeeker::new(self); - // Check for mouse-tracking CSI before mappings to prevent the generic mapping handler from - // taking over. - if have_mouse_tracking_csi(&mut peeker) { - // fish recognizes but does not actually support mouse reporting. We never turn it on, and - // it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn - // it off before exiting. We turn it off here to avoid wasting resources. - // - // Since this is only called when we detect an incoming mouse reporting payload, we know the - // terminal emulator supports mouse reporting, so no terminfo checks. - FLOG!(reader, "Disabling mouse tracking"); - - // We shouldn't directly manipulate stdout from here, so we ask the reader to do it. - // writembs(outputter_t::stdoutput(), "\x1B[?1000l"); - peeker.consume(); - self.push_front(CharEvent::from_readline(ReadlineCmd::DisableMouseTracking)); - return; - } - peeker.restart(); - // Check for ordinary mappings. if let Some(mapping) = Self::find_mapping(&*vars, &mut peeker) { + FLOG!( + reader, + format!("Found mapping {:?} from {:?}", &mapping, &peeker.peeked) + ); peeker.consume(); self.mapping_execute(&mapping); return; @@ -839,14 +883,7 @@ fn read_characters_no_readline(&mut self) -> CharEvent { evt_to_return } - /// Read a character from stdin. Try to convert some escape sequences into character constants, - /// but do not permanently block the escape character. - /// - /// This is performed in the same way vim does it, i.e. if an escape character is read, wait for - /// more input for a short time (a few milliseconds). If more input is available, it is assumed - /// to be an escape sequence for a special character (such as an arrow key), and readch attempts - /// to parse it. If no more input follows after the escape key, it is assumed to be an actual - /// escape key press, and is returned as such. + /// Read a key from stdin. pub fn read_char(&mut self) -> CharEvent { // Clear the interrupted flag. reader_reset_interrupted(); @@ -867,8 +904,8 @@ pub fn read_char(&mut self) -> CharEvent { // Hackish: mark the input style. if readline_event.cmd == ReadlineCmd::SelfInsertNotFirst { - if let CharEvent::Char(cevt) = &mut res { - cevt.input_style = CharInputStyle::NotFirst; + if let CharEvent::Key(kevt) = &mut res { + kevt.input_style = CharInputStyle::NotFirst; } } return res; @@ -897,7 +934,17 @@ pub fn read_char(&mut self) -> CharEvent { // Allow the reader to check for exit conditions. return evt; } - CharEvent::Char(ref _cevt) => { + CharEvent::Key(ref kevt) => { + FLOG!( + reader, + "Read char", + kevt.key, + format!( + "-- {:?} -- {:?}", + kevt.key, + kevt.seq.chars().map(u32::from).collect::>() + ) + ); self.push_front(evt); self.mapping_execute_matching_or_generic(); } @@ -941,7 +988,7 @@ pub fn clear(&mut self, mode: Option<&wstr>, user: bool) { } /// Erase binding for specified key sequence. - pub fn erase(&mut self, sequence: &wstr, mode: &wstr, user: bool) -> bool { + pub fn erase(&mut self, sequence: &[Key], mode: &wstr, user: bool) -> bool { // Clear cached mappings. self.all_mappings_cache = RefCell::new(None); @@ -965,11 +1012,12 @@ pub fn erase(&mut self, sequence: &wstr, mode: &wstr, user: bool) -> bool { /// it exists, false if not. pub fn get<'a>( &'a self, - sequence: &wstr, + sequence: &[Key], mode: &wstr, out_cmds: &mut &'a [WString], user: bool, out_sets_mode: &mut Option<&'a wstr>, + out_terminfo_name: &mut Option, ) -> bool { let ml = if user { &self.mapping_list @@ -980,6 +1028,7 @@ pub fn get<'a>( if m.seq == sequence && m.mode == mode { *out_cmds = &m.commands; *out_sets_mode = m.sets_mode.as_deref(); + *out_terminfo_name = m.terminfo_name.clone(); return true; } } diff --git a/src/input_common.rs b/src/input_common.rs index b2f95c550..1ad64df06 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -1,13 +1,25 @@ -use crate::common::{fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked}; +use libc::STDOUT_FILENO; +use once_cell::sync::OnceCell; + +use crate::common::{ + fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, ScopeGuard, + ScopeGuarding, +}; use crate::env::{EnvStack, Environment}; use crate::fd_readable_set::FdReadableSet; use crate::flog::FLOG; -use crate::reader::reader_current_data; -use crate::threads::{iothread_port, iothread_service_main}; +use crate::key::{ + self, alt, canonicalize_control_char, canonicalize_keyed_control_char, function_key, shift, + Key, Modifiers, +}; +use crate::proc::is_interactive_session; +use crate::reader::{reader_current_data, reader_test_and_clear_interrupted}; +use crate::threads::{iothread_port, iothread_service_main, MainThread}; use crate::universal_notifier::default_notifier; use crate::wchar::{encode_byte_to_char, prelude::*}; -use crate::wutil::encoding::{mbrtowc, zero_mbstate}; -use crate::wutil::fish_wcstol; +use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate}; +use crate::wutil::{fish_wcstol, write_to_fd}; +use std::cell::RefCell; use std::collections::VecDeque; use std::os::fd::RawFd; use std::ptr; @@ -111,6 +123,8 @@ pub enum ReadlineCmd { EndUndoGroup, RepeatJump, DisableMouseTracking, + FocusIn, + FocusOut, // ncurses uses the obvious name ClearScreenAndRepaint, // NOTE: This one has to be last. @@ -121,7 +135,7 @@ pub enum ReadlineCmd { #[derive(Debug, Clone)] pub enum CharEventType { /// A character was entered. - Char(char), + Char(Key), /// A readline event. Readline(ReadlineCmd), @@ -147,9 +161,9 @@ pub struct ReadlineCmdEvent { } #[derive(Debug, Clone)] -pub struct PlainCharEvent { +pub struct KeyEvent { // The key. - pub char: char, + pub key: Key, // The style to use when inserting characters into the command line. pub input_style: CharInputStyle, /// The sequence of characters in the input mapping which generated this event. @@ -161,7 +175,7 @@ pub struct PlainCharEvent { #[derive(Debug, Clone)] pub enum CharEvent { /// A character was entered. - Char(PlainCharEvent), + Key(KeyEvent), /// A readline event. Readline(ReadlineCmdEvent), @@ -179,7 +193,7 @@ pub enum CharEvent { impl CharEvent { pub fn is_char(&self) -> bool { - matches!(self, CharEvent::Char(_)) + matches!(self, CharEvent::Key(_)) } pub fn is_eof(&self) -> bool { @@ -198,6 +212,20 @@ pub fn is_readline_or_command(&self) -> bool { matches!(self, CharEvent::Readline(_) | CharEvent::Command(_)) } + pub fn get_char(&self) -> char { + let CharEvent::Key(kevt) = self else { + panic!("Not a char type"); + }; + kevt.key.codepoint + } + + pub fn get_key(&self) -> Option<&KeyEvent> { + match self { + CharEvent::Key(kevt) => Some(kevt), + _ => None, + } + } + pub fn get_readline(&self) -> ReadlineCmd { let CharEvent::Readline(c) = self else { panic!("Not a readline type"); @@ -213,19 +241,24 @@ pub fn get_command(&self) -> Option<&wstr> { } pub fn from_char(c: char) -> CharEvent { - Self::from_char_seq(c, WString::new()) + Self::from_key(Key::from_raw(c)) } - pub fn get_char(&self) -> Option { - match self { - CharEvent::Char(cevt) => Some(cevt.char), - _ => None, - } + pub fn from_key(key: Key) -> CharEvent { + Self::from_key_seq(key, WString::new()) + } + + pub fn from_key_seq(key: Key, seq: WString) -> CharEvent { + CharEvent::Key(KeyEvent { + key, + input_style: CharInputStyle::Normal, + seq, + }) } pub fn from_char_seq(c: char, seq: WString) -> CharEvent { - CharEvent::Char(PlainCharEvent { - char: c, + CharEvent::Key(KeyEvent { + key: Key::from_raw(c), input_style: CharInputStyle::Normal, seq, }) @@ -272,9 +305,11 @@ enum ReadbResult { // Our ioport reported a change, so service main thread requests. IOPortNotified, + + NothingToRead, } -fn readb(in_fd: RawFd) -> ReadbResult { +fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult { assert!(in_fd >= 0, "Invalid in fd"); let mut fdset = FdReadableSet::new(); loop { @@ -293,7 +328,11 @@ fn readb(in_fd: RawFd) -> ReadbResult { } // Here's where we call select(). - let select_res = fdset.check_readable(FdReadableSet::kNoTimeout); + let select_res = fdset.check_readable(if blocking { + FdReadableSet::kNoTimeout + } else { + 0 + }); if select_res < 0 { let err = errno::errno().0; if err == libc::EINTR || err == libc::EAGAIN { @@ -305,12 +344,15 @@ fn readb(in_fd: RawFd) -> ReadbResult { } } - // select() did not return an error, so we may have a readable fd. - // The priority order is: uvars, stdin, ioport. - // Check to see if we want a universal variable barrier. - if let Some(notifier_fd) = notifier_fd { - if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) { - return ReadbResult::UvarNotified; + if blocking { + // select() did not return an error, so we may have a readable fd. + // The priority order is: uvars, stdin, ioport. + // Check to see if we want a universal variable barrier. + if let Some(notifier_fd) = notifier_fd { + if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) + { + return ReadbResult::UvarNotified; + } } } @@ -324,6 +366,9 @@ fn readb(in_fd: RawFd) -> ReadbResult { // The common path is to return a u8. return ReadbResult::Byte(arr[0]); } + if !blocking { + return ReadbResult::NothingToRead; + } // Check for iothread completions only if there is no data to be read from the stdin. // This gives priority to the foreground. @@ -383,6 +428,111 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) { } } +pub static TERMINAL_PROTOCOLS: MainThread>> = + MainThread::new(RefCell::new(None)); + +pub fn terminal_protocols_enable() { + assert!(TERMINAL_PROTOCOLS.get().borrow().is_none()); + TERMINAL_PROTOCOLS + .get() + .replace(Some(TerminalProtocols::new())); +} + +pub fn terminal_protocols_disable() { + assert!(TERMINAL_PROTOCOLS.get().borrow().is_some()); + TERMINAL_PROTOCOLS.get().replace(None); +} + +pub fn terminal_protocols_enable_scoped() -> impl ScopeGuarding { + terminal_protocols_enable(); + ScopeGuard::new((), |()| terminal_protocols_disable()) +} + +pub fn terminal_protocols_disable_scoped() -> impl ScopeGuarding { + terminal_protocols_disable(); + ScopeGuard::new((), |()| { + // If a child is stopped, this will already be enabled. + if TERMINAL_PROTOCOLS.get().borrow().is_none() { + terminal_protocols_enable() + } + }) +} + +pub struct TerminalProtocols {} + +impl TerminalProtocols { + fn new() -> Self { + terminal_protocols_enable_impl(); + TerminalProtocols {} + } +} + +impl Drop for TerminalProtocols { + fn drop(&mut self) { + terminal_protocols_disable_impl(); + } +} + +fn terminal_protocols_enable_impl() { + // Interactive or inside builtin read. + assert!(is_interactive_session() || reader_current_data().is_some()); + + let mut sequences = concat!( + "\x1b[?2004h", // Bracketed paste + "\x1b[>4;1m", // XTerm's modifyOtherKeys + "\x1b[>5u", // CSI u with kitty progressive enhancement + "\x1b=", // set application keypad mode, so the keypad keys send unique codes + "\x1b[?1004h", // enable focus notify + ); + + // Note: Don't call this initially because, even though we're in a fish_prompt event, + // tmux reacts sooo quickly that we'll still get a sequence before we're prepared for it. + // So this means that we won't get focus events until you've run at least one command, + // but that's preferable to always seeing "^[[I" when starting fish. + static FIRST_CALL_HACK: OnceCell<()> = OnceCell::new(); + if FIRST_CALL_HACK.get().is_none() { + sequences = sequences.strip_suffix("\x1b[?1004h").unwrap(); + } + FIRST_CALL_HACK.get_or_init(|| ()); + + FLOG!( + term_protocols, + format!( + "Enabling extended keys, bracketed paste and focus reporting: {:?}", + sequences + ) + ); + let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO); +} + +fn terminal_protocols_disable_impl() { + // Interactive or inside builtin read. + assert!(is_interactive_session() || reader_current_data().is_some()); + let sequences = concat!( + "\x1b[?2004l", + "\x1b[>4;0m", + "\x1b[<1u", // Konsole breaks unless we pass an explicit number of entries to pop. + "\x1b>", + "\x1b[?1004l", + ); + FLOG!( + term_protocols, + format!( + "Disabling extended keys, bracketed paste and focus reporting: {:?}", + sequences + ) + ); + let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO); +} + +fn parse_mask(mask: u32) -> Modifiers { + Modifiers { + ctrl: (mask & 4) != 0, + alt: (mask & 2) != 0, + shift: (mask & 1) != 0, + } +} + /// A trait which knows how to produce a stream of input events. /// Note this is conceptually a "base class" with override points. pub trait InputEventQueuer { @@ -395,10 +545,7 @@ fn try_pop(&mut self) -> Option { /// convert them to a wchar_t. Conversion is done using mbrtowc. If a character has previously /// been read and then 'unread' using \c input_common_unreadch, that character is returned. fn readch(&mut self) -> CharEvent { - let mut res: char = '\0'; let mut state = zero_mbstate(); - let mut bytes = [0; 64 * 16]; - let mut num_bytes = 0; loop { // Do we have something enqueued already? // Note this may be initially true, or it may become true through calls to @@ -413,14 +560,13 @@ fn readch(&mut self) -> CharEvent { return mevt; } - let rr = readb(self.get_in_fd()); + let rr = readb(self.get_in_fd(), /*blocking=*/ true); match rr { ReadbResult::Eof => { return CharEvent::Eof; } ReadbResult::Interrupted => { - // FIXME: here signals may break multibyte sequences. self.select_interrupted(); } @@ -433,58 +579,405 @@ fn readch(&mut self) -> CharEvent { } ReadbResult::Byte(read_byte) => { - if crate::libc::MB_CUR_MAX() == 1 { - // single-byte locale, all values are legal - // FIXME: this looks wrong, this falsely assumes that - // the single-byte locale is compatible with Unicode upper-ASCII. - res = read_byte.into(); - return CharEvent::from_char(res); + let mut have_escape_prefix = false; + let mut buffer = vec![read_byte]; + let key_with_escape = if read_byte == 0x1b { + self.parse_escape_sequence(&mut buffer, &mut have_escape_prefix) + } else { + canonicalize_control_char(read_byte) + }; + if self.paste_is_buffering() { + if read_byte != 0x1b { + self.paste_push_char(read_byte); + } + continue; } - let mut codepoint = u32::from(res); - let sz = unsafe { - mbrtowc( - std::ptr::addr_of_mut!(codepoint).cast(), - std::ptr::addr_of!(read_byte).cast(), - 1, + let mut seq = WString::new(); + let mut key = key_with_escape; + let mut consumed = 0; + for i in 0..buffer.len() { + self.parse_codepoint( &mut state, - ) - } as isize; - match sz { - -1 => { - FLOG!(reader, "Illegal input"); - return CharEvent::from_check_exit(); - } - -2 => { - // Sequence not yet complete. - bytes[num_bytes] = read_byte; - num_bytes += 1; + &mut key, + &mut seq, + &buffer, + i, + &mut consumed, + &mut have_escape_prefix, + ); + } + return if let Some(key) = key { + CharEvent::from_key_seq(key, seq) + } else { + self.insert_front(seq.chars().skip(1).map(CharEvent::from_char)); + let Some(c) = seq.chars().next() else { continue; - } - 0 => { - // Actual nul char. - return CharEvent::from_char('\0'); - } - _ => (), - } - if let Some(res) = char::from_u32(codepoint) { - // Sequence complete. - if !fish_reserved_codepoint(res) { - return CharEvent::from_char(res); - } - } - bytes[num_bytes] = read_byte; - num_bytes += 1; - for &b in &bytes[1..num_bytes] { - let c = CharEvent::from_char(encode_byte_to_char(b)); - self.push_back(c); - } - let res = CharEvent::from_char(encode_byte_to_char(bytes[0])); - return res; + }; + CharEvent::from_key_seq(Key::from_raw(c), seq) + }; } + ReadbResult::NothingToRead => unreachable!(), } } } + fn try_readb(&mut self, buffer: &mut Vec) -> Option { + let ReadbResult::Byte(next) = readb(self.get_in_fd(), /*blocking=*/ false) else { + return None; + }; + buffer.push(next); + Some(next) + } + + fn parse_escape_sequence( + &mut self, + buffer: &mut Vec, + have_escape_prefix: &mut bool, + ) -> Option { + let Some(next) = self.try_readb(buffer) else { + if !self.paste_is_buffering() { + return Some(Key { + modifiers: Modifiers::default(), + codepoint: key::Escape, + }); + } + return None; + }; + if next == b'[' { + // potential CSI + return Some(self.parse_csi(buffer).unwrap_or(alt('['))); + } + if next == b'O' { + // potential SS3 + return Some(self.parse_ss3(buffer).unwrap_or(alt('O'))); + } + match canonicalize_control_char(next) { + Some(mut key) => { + key.modifiers.alt = true; + Some(key) + } + None => { + *have_escape_prefix = true; + None + } + } + } + + fn parse_codepoint( + &mut self, + state: &mut mbstate_t, + out_key: &mut Option, + out_seq: &mut WString, + buffer: &[u8], + i: usize, + consumed: &mut usize, + have_escape_prefix: &mut bool, + ) { + let mut res: char = '\0'; + let read_byte = buffer[i]; + if crate::libc::MB_CUR_MAX() == 1 { + // single-byte locale, all values are legal + // FIXME: this looks wrong, this falsely assumes that + // the single-byte locale is compatible with Unicode upper-ASCII. + res = read_byte.into(); + out_seq.push(res); + return; + } + let mut codepoint = u32::from(res); + let sz = unsafe { + mbrtowc( + std::ptr::addr_of_mut!(codepoint).cast(), + std::ptr::addr_of!(read_byte).cast(), + 1, + state, + ) + } as isize; + match sz { + -1 => { + FLOG!(reader, "Illegal input"); + *consumed += 1; + self.push_front(CharEvent::from_check_exit()); + return; + } + -2 => { + // Sequence not yet complete. + return; + } + 0 => { + // Actual nul char. + *consumed += 1; + out_seq.push('\0'); + return; + } + _ => (), + } + if let Some(res) = char::from_u32(codepoint) { + // Sequence complete. + if !fish_reserved_codepoint(res) { + if *have_escape_prefix && i != 0 { + *have_escape_prefix = false; + *out_key = Some(alt(res)); + } + *consumed += 1; + out_seq.push(res); + return; + } + } + for &b in &buffer[*consumed..i] { + out_seq.push(encode_byte_to_char(b)); + *consumed += 1; + } + } + + fn parse_csi(&mut self, buffer: &mut Vec) -> Option { + let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff); + let mut params = [[0_u32; 16]; 4]; + let mut c = next_char(self); + let private_mode; + if matches!(c, b'?' | b'<' | b'=' | b'>') { + // private mode + private_mode = Some(c); + c = next_char(self); + } else { + private_mode = None; + } + let mut count = 0; + let mut subcount = 0; + while count < 16 && c >= 0x30 && c <= 0x3f { + if c.is_ascii_digit() { + params[count][subcount] = params[count][subcount] * 10 + u32::from(c - b'0'); + } else if c == b':' && subcount < 3 { + subcount += 1; + } else if c == b';' { + count += 1; + subcount = 0; + } else { + return None; + } + c = next_char(self); + } + if c != b'$' && !(0x40..=0x7e).contains(&c) { + return None; + } + + let masked_key = |mut codepoint, shifted_codepoint| { + let mask = params[1][0].saturating_sub(1); + let mut modifiers = parse_mask(mask); + if let Some(shifted_codepoint) = shifted_codepoint { + if shifted_codepoint != '\0' && modifiers.shift { + modifiers.shift = false; + codepoint = shifted_codepoint; + } + } + Key { + modifiers, + codepoint, + } + }; + + let key = match c { + b'$' => { + if private_mode == Some(b'?') && next_char(self) == b'y' { + // DECRPM + return None; + } + match params[0][0] { + 23 | 24 => shift( + char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(), // rxvt style + ), + _ => return None, + } + } + b'A' => masked_key(key::Up, None), + b'B' => masked_key(key::Down, None), + b'C' => masked_key(key::Right, None), + b'D' => masked_key(key::Left, None), + b'E' => masked_key('5', None), // Numeric keypad + b'F' => masked_key(key::End, None), // PC/xterm style + b'H' => masked_key(key::Home, None), // PC/xterm style + b'M' | b'm' => { + self.disable_mouse_tracking(); + let sgr = private_mode == Some(b'<'); + if !sgr && c == b'm' { + return None; + } + // Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters + // for button code, Px, and Py, ending with 'M' for button press or 'm' for + // button release. + if sgr { + return None; + } + // Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6 + // chars (although in mode 1005, the characters may be unicode and not necessarily + // just one byte long) reporting the button that was clicked and its location. + let _ = next_char(self); + let _ = next_char(self); + let _ = next_char(self); + return None; + } + b't' => { + self.disable_mouse_tracking(); + // VT200 button released in mouse highlighting mode at valid text location. 5 chars. + let _ = next_char(self); + let _ = next_char(self); + return None; + } + b'T' => { + self.disable_mouse_tracking(); + // VT200 button released in mouse highlighting mode past end-of-line. 9 characters. + for _ in 0..7 { + let _ = next_char(self); + } + return None; + } + b'P' => masked_key(function_key(1), None), + b'Q' => masked_key(function_key(2), None), + b'R' => masked_key(function_key(3), None), + b'S' => masked_key(function_key(4), None), + b'~' => match params[0][0] { + 1 => masked_key(key::Home, None), // VT220/tmux style + 2 => masked_key(key::Insert, None), + 3 => masked_key(key::Delete, None), + 4 => masked_key(key::End, None), // VT220/tmux style + 5 => masked_key(key::PageUp, None), + 6 => masked_key(key::PageDown, None), + 7 => masked_key(key::Home, None), // rxvt style + 8 => masked_key(key::End, None), // rxvt style + 11..=15 => masked_key( + char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(), + None, + ), + 17..=21 => masked_key( + char::from_u32(u32::from(function_key(6)) + params[0][0] - 17).unwrap(), + None, + ), + 23 | 24 => masked_key( + char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(), + None, + ), + 25 | 26 => { + shift(char::from_u32(u32::from(function_key(3)) + params[0][0] - 25).unwrap()) + } // rxvt style + 28 | 29 => { + shift(char::from_u32(u32::from(function_key(5)) + params[0][0] - 28).unwrap()) + } // rxvt style + 31 | 32 => { + shift(char::from_u32(u32::from(function_key(7)) + params[0][0] - 31).unwrap()) + } // rxvt style + 33 | 34 => { + shift(char::from_u32(u32::from(function_key(9)) + params[0][0] - 33).unwrap()) + } // rxvt style + 200 => { + self.paste_start_buffering(); + self.push_front(CharEvent::from_readline(ReadlineCmd::BeginUndoGroup)); + return Some(Key::from_raw(key::Invalid)); + } + 201 => { + self.push_front(CharEvent::from_readline(ReadlineCmd::EndUndoGroup)); + self.paste_commit(); + return Some(Key::from_raw(key::Invalid)); + } + _ => return None, + }, + b'u' => { + // Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here. + let key = match params[0][0] { + 57414 => key::Enter, + 57417 => key::Left, + 57418 => key::Right, + 57419 => key::Up, + 57420 => key::Down, + 57421 => key::PageUp, + 57422 => key::PageDown, + 57423 => key::Home, + 57424 => key::End, + 57425 => key::Insert, + 57426 => key::Delete, + cp => canonicalize_keyed_control_char(char::from_u32(cp).unwrap()), + }; + masked_key( + key, + Some(canonicalize_keyed_control_char( + char::from_u32(params[0][1]).unwrap(), + )), + ) + } + b'Z' => shift(key::Tab), + b'I' => { + self.push_front(CharEvent::from_readline(ReadlineCmd::FocusIn)); + return Some(Key::from_raw(key::Invalid)); + } + b'O' => { + self.push_front(CharEvent::from_readline(ReadlineCmd::FocusOut)); + return Some(Key::from_raw(key::Invalid)); + } + _ => return None, + }; + Some(key) + } + + fn disable_mouse_tracking(&mut self) { + // fish recognizes but does not actually support mouse reporting. We never turn it on, and + // it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn + // it off before exiting. We turn it off here to avoid wasting resources. + // + // Since this is only called when we detect an incoming mouse reporting payload, we know the + // terminal emulator supports mouse reporting, so no terminfo checks. + FLOG!(reader, "Disabling mouse tracking"); + + // We shouldn't directly manipulate stdout from here, so we ask the reader to do it. + // writembs(outputter_t::stdoutput(), "\x1B[?1000l"); + self.push_front(CharEvent::from_readline(ReadlineCmd::DisableMouseTracking)); + } + + fn parse_ss3(&mut self, buffer: &mut Vec) -> Option { + let mut raw_mask = 0; + let mut code = b'0'; + loop { + raw_mask = raw_mask * 10 + u32::from(code - b'0'); + code = self.try_readb(buffer).unwrap_or(0xff); + if !(b'0'..=b'9').contains(&code) { + break; + } + } + let modifiers = parse_mask(raw_mask.saturating_sub(1)); + #[rustfmt::skip] + let key = match code { + b' ' => Key{modifiers, codepoint: key::Space}, + b'A' => Key{modifiers, codepoint: key::Up}, + b'B' => Key{modifiers, codepoint: key::Down}, + b'C' => Key{modifiers, codepoint: key::Right}, + b'D' => Key{modifiers, codepoint: key::Left}, + b'F' => Key{modifiers, codepoint: key::End}, + b'H' => Key{modifiers, codepoint: key::Home}, + b'I' => Key{modifiers, codepoint: key::Tab}, + b'M' => Key{modifiers, codepoint: key::Enter}, + b'P' => Key{modifiers, codepoint: function_key(1)}, + b'Q' => Key{modifiers, codepoint: function_key(2)}, + b'R' => Key{modifiers, codepoint: function_key(3)}, + b'S' => Key{modifiers, codepoint: function_key(4)}, + b'X' => Key{modifiers, codepoint: '='}, + b'j' => Key{modifiers, codepoint: '*'}, + b'k' => Key{modifiers, codepoint: '+'}, + b'l' => Key{modifiers, codepoint: ','}, + b'm' => Key{modifiers, codepoint: '-'}, + b'n' => Key{modifiers, codepoint: '.'}, + b'o' => Key{modifiers, codepoint: '/'}, + b'p' => Key{modifiers, codepoint: '0'}, + b'q' => Key{modifiers, codepoint: '1'}, + b'r' => Key{modifiers, codepoint: '2'}, + b's' => Key{modifiers, codepoint: '3'}, + b't' => Key{modifiers, codepoint: '4'}, + b'u' => Key{modifiers, codepoint: '5'}, + b'v' => Key{modifiers, codepoint: '6'}, + b'w' => Key{modifiers, codepoint: '7'}, + b'x' => Key{modifiers, codepoint: '8'}, + b'y' => Key{modifiers, codepoint: '9'}, + _ => return None, + }; + Some(key) + } + fn readch_timed_esc(&mut self) -> Option { self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed)) } @@ -556,6 +1049,24 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option { /// Return the fd corresponding to stdin. fn get_in_fd(&self) -> RawFd; + // Support for "bracketed paste" + // The way it works is that we acknowledge our support by printing + // \e\[?2004h + // then the terminal will "bracket" every paste in + // \e\[200~ and \e\[201~ + // Every character in between those two will be part of the paste and should not cause a binding to execute (like \n executing commands). + // + // We enable it after every command and disable it before, see the terminal protocols logic. + // + // Support for this seems to be ubiquitous - emacs enables it unconditionally (!) since 25.1 + // (though it only supports it since then, it seems to be the last term to gain support). + // + // See http://thejh.net/misc/website-terminal-copy-paste. + fn paste_start_buffering(&mut self); + fn paste_is_buffering(&self) -> bool; + fn paste_push_char(&mut self, _b: u8) {} + fn paste_commit(&mut self); + /// Enqueue a character or a readline function to the queue of unread characters that /// readch will return before actually reading from fd 0. fn push_back(&mut self, ch: CharEvent) { @@ -622,8 +1133,8 @@ fn drop_leading_readline_events(&mut self) { /// nothing. fn prepare_to_select(&mut self) {} - /// Override point for when when select() is interrupted by a signal. The default does nothing. - fn select_interrupted(&mut self) {} + /// Called when select() is interrupted by a signal. + fn select_interrupted(&mut self); /// Override point for when when select() is interrupted by the universal variable notifier. /// The default does nothing. @@ -639,6 +1150,7 @@ fn has_lookahead(&self) -> bool { pub struct InputEventQueue { queue: VecDeque, in_fd: RawFd, + is_in_bracketed_paste: bool, } impl InputEventQueue { @@ -646,6 +1158,7 @@ pub fn new(in_fd: RawFd) -> InputEventQueue { InputEventQueue { queue: VecDeque::new(), in_fd, + is_in_bracketed_paste: false, } } } @@ -662,4 +1175,23 @@ fn get_queue_mut(&mut self) -> &mut VecDeque { fn get_in_fd(&self) -> RawFd { self.in_fd } + + fn select_interrupted(&mut self) { + if reader_test_and_clear_interrupted() != 0 { + let vintr = shell_modes().c_cc[libc::VINTR]; + if vintr != 0 { + self.push_front(CharEvent::from_key(Key::from_single_byte(vintr))); + } + } + } + + fn paste_start_buffering(&mut self) { + self.is_in_bracketed_paste = true; + } + fn paste_is_buffering(&self) -> bool { + self.is_in_bracketed_paste + } + fn paste_commit(&mut self) { + self.is_in_bracketed_paste = false; + } } diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 000000000..0d43cb754 --- /dev/null +++ b/src/key.rs @@ -0,0 +1,430 @@ +use std::ops; +use std::rc::Rc; + +use libc::VERASE; + +use crate::{ + fallback::fish_wcwidth, reader::TERMINAL_MODE_ON_STARTUP, wchar::prelude::*, wutil::fish_wcstoi, +}; + +pub(crate) const Backspace: char = '\u{F500}'; // below ENCODE_DIRECT_BASE +pub(crate) const Delete: char = '\u{F501}'; +pub(crate) const Escape: char = '\u{F502}'; +pub(crate) const Enter: char = '\u{F503}'; +pub(crate) const Up: char = '\u{F504}'; +pub(crate) const Down: char = '\u{F505}'; +pub(crate) const Left: char = '\u{F506}'; +pub(crate) const Right: char = '\u{F507}'; +pub(crate) const PageUp: char = '\u{F508}'; +pub(crate) const PageDown: char = '\u{F509}'; +pub(crate) const Home: char = '\u{F50a}'; +pub(crate) const End: char = '\u{F50b}'; +pub(crate) const Insert: char = '\u{F50c}'; +pub(crate) const Tab: char = '\u{F50d}'; +pub(crate) const Space: char = '\u{F50e}'; +pub(crate) const Invalid: char = '\u{F50f}'; +pub(crate) fn function_key(n: u32) -> char { + assert!((1..=12).contains(&n)); + char::from_u32(u32::from(Invalid) + n).unwrap() +} + +const NAMED_KEYS_RANGE: ops::Range = 0xF500..(Invalid as u32 + 12); + +const KEY_NAMES: &[(char, &wstr)] = &[ + ('+', L!("plus")), + ('-', L!("minus")), + (',', L!("comma")), + (Backspace, L!("backspace")), + (Delete, L!("delete")), + (Escape, L!("escape")), + (Enter, L!("enter")), + (Up, L!("up")), + (Down, L!("down")), + (Left, L!("left")), + (Right, L!("right")), + (PageUp, L!("pageup")), + (PageDown, L!("pagedown")), + (Home, L!("home")), + (End, L!("end")), + (Insert, L!("insert")), + (Tab, L!("tab")), + (Space, L!("space")), +]; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Modifiers { + pub ctrl: bool, + pub alt: bool, + pub shift: bool, +} + +impl Modifiers { + const fn new() -> Self { + Modifiers { + ctrl: false, + alt: false, + shift: false, + } + } + pub(crate) fn is_some(&self) -> bool { + self.ctrl || self.alt || self.shift + } + pub(crate) fn is_none(&self) -> bool { + !self.is_some() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Key { + pub modifiers: Modifiers, + pub codepoint: char, +} + +impl Key { + pub(crate) fn from_raw(codepoint: char) -> Self { + Self { + modifiers: Modifiers::default(), + codepoint, + } + } +} + +pub(crate) const fn ctrl(codepoint: char) -> Key { + let mut modifiers = Modifiers::new(); + modifiers.ctrl = true; + Key { + modifiers, + codepoint, + } +} + +pub(crate) const fn alt(codepoint: char) -> Key { + let mut modifiers = Modifiers::new(); + modifiers.alt = true; + Key { + modifiers, + codepoint, + } +} + +pub(crate) const fn shift(codepoint: char) -> Key { + let mut modifiers = Modifiers::new(); + modifiers.shift = true; + Key { + modifiers, + codepoint, + } +} + +impl Key { + pub fn from_single_char(c: char) -> Self { + u8::try_from(c) + .map(Key::from_single_byte) + .unwrap_or(Key::from_raw(c)) + } + pub fn from_single_byte(c: u8) -> Self { + canonicalize_control_char(c).unwrap_or(Key::from_raw(char::from(c))) + } +} + +pub fn canonicalize_control_char(c: u8) -> Option { + let codepoint = canonicalize_keyed_control_char(char::from(c)); + if u32::from(codepoint) > 255 { + return Some(Key { + modifiers: Modifiers::default(), + codepoint, + }); + } + + if c < 32 { + return Some(ctrl(canonicalize_unkeyed_control_char(c))); + } + + None +} + +fn ascii_control(c: char) -> char { + char::from_u32(u32::from(c) & 0o37).unwrap() +} + +pub(crate) fn canonicalize_keyed_control_char(c: char) -> char { + if c == ascii_control('m') || c == ascii_control('j') { + return Enter; + } + if c == ascii_control('i') { + return Tab; + } + if c == ' ' { + return Space; + } + if c == char::from(TERMINAL_MODE_ON_STARTUP.lock().unwrap().c_cc[VERASE]) { + return Backspace; + } + if c == char::from(127) { + // when it's not backspace + return Delete; + } + if c == '\x1b' { + return Escape; + } + c +} + +pub(crate) fn canonicalize_unkeyed_control_char(c: u8) -> char { + if c == 0 { + // For legacy terminals we have to make a decision here; they send NUL on Ctrl-2, + // Ctrl-Shift-2 or Ctrl-Backtick, but the most straightforward way is Ctrl-Space. + return Space; + } + // Represent Ctrl-letter combinations in lower-case, to be clear + // that Shift is not involved. + if c < 27 { + return char::from(c - 1 + b'a'); + } + // Represent Ctrl-symbol combinations in "upper-case", as they are + // traditionally-rendered. + assert!(c < 32); + return char::from(c - 1 + b'A'); +} + +pub(crate) fn canonicalize_key(mut key: Key) -> Result { + // Leave raw escapes to disambiguate from named escape. + if key.codepoint != '\x1b' { + key.codepoint = canonicalize_keyed_control_char(key.codepoint); + if key.codepoint < ' ' { + key.codepoint = canonicalize_unkeyed_control_char(u8::try_from(key.codepoint).unwrap()); + if key.modifiers.ctrl { + return Err(wgettext_fmt!( + "Cannot add control modifier to control character '%s'", + key + )); + } + key.modifiers.ctrl = true; + } + } + if key.modifiers.shift { + if key.codepoint.is_ascii_alphabetic() { + // Shift + ASCII letters is just the uppercase letter. + key.modifiers.shift = false; + key.codepoint = key.codepoint.to_ascii_uppercase(); + } else if !NAMED_KEYS_RANGE.contains(&u32::from(key.codepoint)) { + // Shift + any other printable character is not allowed. + return Err(wgettext_fmt!( + "Shift modifier is only supported on special keys and lowercase ASCII, not '%s'", + key, + )); + } + } + Ok(key) +} + +pub const KEY_SEPARATOR: char = ','; + +pub(crate) fn parse_keys(value: &wstr) -> Result, WString> { + let mut res = vec![]; + if value.is_empty() { + return Ok(res); + } + let first = value.as_char_slice()[0]; + if value.len() == 1 { + // Hack: allow singular comma. + res.push(canonicalize_key(Key::from_raw(first)).unwrap()); + } else if (value.len() == 2 + && !value.contains('-') + && !value.contains(KEY_SEPARATOR) + && !KEY_NAMES.iter().any(|(_codepoint, name)| name == value)) + || (first == '\x1b' || first == ascii_control(first)) + { + // Hack: treat as legacy syntax (meaning: not comma separated) if + // 1. it doesn't contain '-' or ',' and is short enough to probably not be a key name. + // 2. it starts with raw escape (\e) or a raw ASCII control character (\c). + for c in value.chars() { + res.push(canonicalize_key(Key::from_raw(c)).unwrap()); + } + } else { + for full_key_name in value.split(KEY_SEPARATOR) { + if full_key_name == "-" { + // Hack: allow singular minus. + res.push(canonicalize_key(Key::from_raw('-')).unwrap()); + continue; + } + let mut modifiers = Modifiers::default(); + let num_keys = full_key_name.split('-').count(); + let mut components = full_key_name.split('-'); + for _i in 0..num_keys.checked_sub(1).unwrap() { + let modifier = components.next().unwrap(); + match modifier { + _ if modifier == "ctrl" => modifiers.ctrl = true, + _ if modifier == "alt" => modifiers.alt = true, + _ if modifier == "shift" => modifiers.shift = true, + _ => { + return Err(wgettext_fmt!( + "unknown modififer '%s' in '%s'", + modifier, + full_key_name + )) + } + } + } + let key_name = components.next().unwrap(); + let codepoint = KEY_NAMES + .iter() + .find_map(|(codepoint, name)| (name == key_name).then_some(*codepoint)) + .or_else(|| (key_name.len() == 1).then(|| key_name.as_char_slice()[0])); + let key = if let Some(codepoint) = codepoint { + canonicalize_key(Key { + modifiers, + codepoint, + })? + } else if codepoint.is_none() && key_name.starts_with('F') && key_name.len() <= 3 { + let num = key_name.strip_prefix('F'); + let codepoint = match fish_wcstoi(num) { + Ok(n) if (1..=12).contains(&n) => function_key(u32::try_from(n).unwrap()), + _ => { + return Err(wgettext_fmt!( + "only F1 through F12 are supported, not '%s'", + num, + full_key_name + )); + } + }; + Key { + modifiers, + codepoint, + } + } else { + return Err(wgettext_fmt!("cannot parse key '%s'", full_key_name)); + }; + res.push(key); + } + } + Ok(canonicalize_raw_escapes(res)) +} + +pub(crate) fn canonicalize_raw_escapes(keys: Vec) -> Vec { + // Historical bindings use \ek to mean alt-k. Canonicalize them. + if !keys.iter().any(|key| key.codepoint == '\x1b') { + return keys; + } + let mut canonical = vec![]; + let mut had_literal_escape = false; + for mut key in keys { + if had_literal_escape { + had_literal_escape = false; + if key.modifiers.alt { + canonical.push(Key::from_raw(Escape)); + } else { + key.modifiers.alt = true; + if key.codepoint == '\x1b' { + key.codepoint = Escape; + } + } + } else if key.codepoint == '\x1b' { + had_literal_escape = true; + continue; + } + canonical.push(key); + } + if had_literal_escape { + canonical.push(Key::from_raw(Escape)); + } + canonical +} + +impl Key { + pub(crate) fn codepoint_text(&self) -> Option { + if self.modifiers.is_some() { + return None; + } + let c = self.codepoint; + if c == Space { + return Some(' '); + } + if c == Enter { + return Some('\n'); + } + if c == Tab { + return Some('\t'); + } + if NAMED_KEYS_RANGE.contains(&u32::from(c)) || u32::from(c) <= 27 { + return None; + } + Some(c) + } +} + +impl std::fmt::Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + WString::from(*self).fmt(f) + } +} + +impl printf_compat::args::ToArg<'static> for Key { + fn to_arg(self) -> printf_compat::args::Arg<'static> { + printf_compat::args::Arg::BoxedStr(Rc::new(WString::from(self).into_boxed_utfstr())) + } +} + +impl From for WString { + fn from(key: Key) -> Self { + let name = KEY_NAMES + .iter() + .find_map(|&(codepoint, name)| (codepoint == key.codepoint).then(|| name.to_owned())) + .or_else(|| { + (function_key(1)..=function_key(12)) + .contains(&key.codepoint) + .then(|| { + sprintf!( + "F%d", + u32::from(key.codepoint) - u32::from(function_key(1)) + 1 + ) + }) + }); + let mut res = name.unwrap_or_else(|| char_to_symbol(key.codepoint)); + + if key.modifiers.shift { + res.insert_utfstr(0, L!("shift-")); + } + if key.modifiers.alt { + res.insert_utfstr(0, L!("alt-")); + } + if key.modifiers.ctrl { + res.insert_utfstr(0, L!("ctrl-")); + } + + res + } +} + +/// Return true if the character must be escaped when used in the sequence of chars to be bound in +/// a `bind` command. +fn must_escape(c: char) -> bool { + "[]()<>{}*\\?$#;&|'\"".contains(c) +} + +fn ascii_printable_to_symbol(buf: &mut WString, c: char) { + if must_escape(c) { + sprintf!(=> buf, "\\%c", c); + } else { + sprintf!(=> buf, "%c", c); + } +} + +/// Convert a wide-char to a symbol that can be used in our output. +fn char_to_symbol(c: char) -> WString { + let mut buff = WString::new(); + let buf = &mut buff; + assert!(c >= ' '); + if c < '\u{80}' { + // ASCII characters that are not control characters + ascii_printable_to_symbol(buf, c); + } else if fish_wcwidth(c) > 0 { + sprintf!(=> buf, "%lc", c); + } else if c <= '\u{FFFF}' { + // BMP Unicode chararacter + sprintf!(=> buf, "\\u%04X", u32::from(c)); + } else { + sprintf!(=> buf, "\\U%06X", u32::from(c)); + } + buff +} diff --git a/src/lib.rs b/src/lib.rs index 66338cf4b..fae8dc16a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,7 @@ pub mod input_common; pub mod io; pub mod job_group; +pub mod key; pub mod kill; #[allow(non_snake_case)] pub mod libc; diff --git a/src/proc.rs b/src/proc.rs index 0acff52eb..bd3e9098a 100644 --- a/src/proc.rs +++ b/src/proc.rs @@ -11,6 +11,7 @@ use crate::event::{self, Event}; use crate::flog::{FLOG, FLOGF}; use crate::global_safety::RelaxedAtomicBool; +use crate::input_common::terminal_protocols_enable; use crate::io::IoChain; use crate::job_group::{JobGroup, MaybeJobId}; use crate::parse_tree::ParsedSourceRef; @@ -1412,6 +1413,9 @@ fn process_mark_finished_children(parser: &Parser, block_ok: bool) { let status = ProcStatus::from_waitpid(statusv); handle_child_status(j, proc, &status); if status.stopped() { + if is_interactive_session() && j.group().wants_terminal() { + terminal_protocols_enable(); + } j.group().set_is_foreground(false); } if status.continued() { diff --git a/src/reader.rs b/src/reader.rs index bde049217..9e3309a13 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -69,7 +69,9 @@ }; use crate::input::init_input; use crate::input::Inputter; -use crate::input_common::{CharEvent, CharInputStyle, ReadlineCmd}; +use crate::input_common::{ + terminal_protocols_enable_scoped, CharEvent, CharInputStyle, ReadlineCmd, +}; use crate::io::IoChain; use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; use crate::libc::MB_CUR_MAX; @@ -133,7 +135,7 @@ enum ExitState { Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); /// Mode on startup, which we restore on exit. -static TERMINAL_MODE_ON_STARTUP: Lazy> = +pub static TERMINAL_MODE_ON_STARTUP: Lazy> = Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); /// Mode we use to execute programs. @@ -784,10 +786,15 @@ pub fn reader_init() -> impl ScopeGuarding { // Set up our fixed terminal modes once, // so we don't get flow control just because we inherited it. - if is_interactive_session() && unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } { - term_donate(/*quiet=*/ true); + let mut terminal_protocols = None; + if is_interactive_session() { + terminal_protocols = Some(terminal_protocols_enable_scoped()); + if unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } { + term_donate(/*quiet=*/ true); + } } - ScopeGuard::new((), |()| { + ScopeGuard::new((), move |()| { + let _terminal_protocols = terminal_protocols; restore_term_mode(); }) } @@ -1912,26 +1919,23 @@ fn readline(&mut self, nchars: Option) -> Option { CharEvent::Command(command) => { zelf.run_input_command_scripts(&command); } - CharEvent::Char(cevt) => { + CharEvent::Key(kevt) => { // Ordinary char. - let c = cevt.char; - if cevt.input_style == CharInputStyle::NotFirst + if kevt.input_style == CharInputStyle::NotFirst && zelf.active_edit_line().1.position() == 0 { // This character is skipped. - } else if c.is_control() { - // This can happen if the user presses a control char we don't recognize. No - // reason to report this to the user unless they've enabled debugging output. - FLOG!(reader, wgettext_fmt!("Unknown key binding 0x%X", c)); } else { // Regular character. let (elt, _el) = zelf.active_edit_line(); - zelf.insert_char(elt, c); + if let Some(c) = kevt.key.codepoint_text() { + zelf.insert_char(elt, c); - if elt == EditableLineTag::Commandline { - zelf.clear_pager(); - // We end history search. We could instead update the search string. - zelf.history_search.reset(); + if elt == EditableLineTag::Commandline { + zelf.clear_pager(); + // We end history search. We could instead update the search string. + zelf.history_search.reset(); + } } } rls.last_cmd = None; @@ -2036,7 +2040,7 @@ fn read_normal_chars(&mut self, rls: &mut ReadlineLoopState) -> Option Option { + event::fire_generic(self.parser(), L!("fish_focus_in").to_owned(), vec![]); + } + rl::FocusOut => { + event::fire_generic(self.parser(), L!("fish_focus_out").to_owned(), vec![]); + } rl::ClearScreenAndRepaint => { self.parser().libdata_mut().pods.is_repaint = true; let clear = screen_clear(); diff --git a/src/tests/input.rs b/src/tests/input.rs index b74a92185..4a5906dbe 100644 --- a/src/tests/input.rs +++ b/src/tests/input.rs @@ -1,5 +1,6 @@ use crate::input::{input_mappings, Inputter, DEFAULT_BIND_MODE}; use crate::input_common::{CharEvent, ReadlineCmd}; +use crate::key::Key; use crate::parser::Parser; use crate::tests::prelude::*; use crate::wchar::prelude::*; @@ -15,8 +16,9 @@ fn test_input() { // Ensure sequences are order independent. Here we add two bindings where the first is a prefix // of the second, and then emit the second key list. The second binding should be invoked, not // the first! - let prefix_binding = WString::from_str("qqqqqqqa"); - let desired_binding = prefix_binding.clone() + "a"; + let prefix_binding: Vec = "qqqqqqqa".chars().map(Key::from_raw).collect(); + let mut desired_binding = prefix_binding.clone(); + desired_binding.push(Key::from_raw('a')); let default_mode = || DEFAULT_BIND_MODE.to_owned(); @@ -24,6 +26,7 @@ fn test_input() { let mut input_mapping = input_mappings(); input_mapping.add1( prefix_binding, + None, WString::from_str("up-line"), default_mode(), None, @@ -31,6 +34,7 @@ fn test_input() { ); input_mapping.add1( desired_binding.clone(), + None, WString::from_str("down-line"), default_mode(), None, @@ -39,8 +43,8 @@ fn test_input() { } // Push the desired binding to the queue. - for c in desired_binding.chars() { - input.queue_char(CharEvent::from_char(c)); + for c in desired_binding { + input.queue_char(CharEvent::from_key(c)); } // Now test. diff --git a/src/tests/input_common.rs b/src/tests/input_common.rs index d4fc5cee0..d3a73dcbf 100644 --- a/src/tests/input_common.rs +++ b/src/tests/input_common.rs @@ -7,10 +7,10 @@ fn test_push_front_back() { queue.push_front(CharEvent::from_char('b')); queue.push_back(CharEvent::from_char('c')); queue.push_back(CharEvent::from_char('d')); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'b'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'c'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'd'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); assert!(queue.try_pop().is_none()); } @@ -27,15 +27,15 @@ fn test_promote_interruptions_to_front() { assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Undo); assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Redo); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'b'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'c'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'd'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); assert!(!queue.has_lookahead()); queue.push_back(CharEvent::from_char('e')); queue.promote_interruptions_to_front(); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'e'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'e'); assert!(!queue.has_lookahead()); } @@ -51,9 +51,9 @@ fn test_insert_front() { CharEvent::from_char('C'), ]; queue.insert_front(events); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'A'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'B'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'C'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'b'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'A'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'B'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'C'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); } diff --git a/src/tests/key.rs b/src/tests/key.rs new file mode 100644 index 000000000..d3b75773d --- /dev/null +++ b/src/tests/key.rs @@ -0,0 +1,13 @@ +use crate::key::{self, ctrl, parse_keys, Key}; +use crate::wchar::prelude::*; + +#[test] +fn test_parse_key() { + assert_eq!( + parse_keys(L!("escape")), + Ok(vec![Key::from_raw(key::Escape)]) + ); + assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::Escape)])); + assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')])); + assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')])); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0dd06fff8..21f7360ad 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -12,6 +12,7 @@ mod history; mod input; mod input_common; +mod key; mod pager; mod parse_util; mod parser; diff --git a/src/wchar_ext.rs b/src/wchar_ext.rs index efa48e3e3..45c463caa 100644 --- a/src/wchar_ext.rs +++ b/src/wchar_ext.rs @@ -277,6 +277,16 @@ fn starts_with(&self, prefix: Prefix) -> bool { iter_prefixes_iter(prefix.chars(), self.as_char_slice().iter().copied()) } + fn strip_prefix(&self, prefix: Prefix) -> &wstr { + let iter = prefix.chars(); + let prefix_len = iter.clone().count(); + if iter_prefixes_iter(iter, self.as_char_slice().iter().copied()) { + self.slice_from(prefix_len) + } else { + self.slice_from(0) + } + } + /// \return whether we end with a given Suffix. /// The Suffix can be a char, a &str, a &wstr, or a &WString. fn ends_with(&self, suffix: Suffix) -> bool { diff --git a/tests/checks/abbr.fish b/tests/checks/abbr.fish index 3275022b4..810b24868 100644 --- a/tests/checks/abbr.fish +++ b/tests/checks/abbr.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Universal abbreviations are imported. set -U _fish_abbr_cuckoo somevalue diff --git a/tests/checks/alias.fish b/tests/checks/alias.fish index de725979d..f4145ac92 100644 --- a/tests/checks/alias.fish +++ b/tests/checks/alias.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Avoid regressions of issue \#3860 wherein the first word of the alias ends with a semicolon function foo echo ran foo diff --git a/tests/checks/andandoror.fish b/tests/checks/andandoror.fish index 806315699..1492aad71 100644 --- a/tests/checks/andandoror.fish +++ b/tests/checks/andandoror.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # "Basic && and || support" echo first && echo second diff --git a/tests/checks/andor.fish b/tests/checks/andor.fish index c228c9ccf..8e9fc3c33 100644 --- a/tests/checks/andor.fish +++ b/tests/checks/andor.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs set -xl LANG C # uniform quotes diff --git a/tests/checks/argparse.fish b/tests/checks/argparse.fish index 7b5745792..e21e6da21 100644 --- a/tests/checks/argparse.fish +++ b/tests/checks/argparse.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs ########## # NOTE: This uses argparse, which touches the local variables. diff --git a/tests/checks/bad-option.fish b/tests/checks/bad-option.fish index df47065f6..1df4328ff 100644 --- a/tests/checks/bad-option.fish +++ b/tests/checks/bad-option.fish @@ -1,2 +1,2 @@ -#RUN: %fish -Z +#RUN: %fish -Z | %filter-ctrlseqs # CHECKERR: {{.*fish}}: {{unrecognized option: Z|invalid option -- '?Z'?|unknown option -- Z|illegal option -- Z|-Z: unknown option}} diff --git a/tests/checks/basic.fish b/tests/checks/basic.fish index eedc04035..9297e15c1 100644 --- a/tests/checks/basic.fish +++ b/tests/checks/basic.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # # Test function, loops, conditionals and some basic elements # diff --git a/tests/checks/bind.fish b/tests/checks/bind.fish index 486f2e4a2..b71785620 100644 --- a/tests/checks/bind.fish +++ b/tests/checks/bind.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Test various `bind` command invocations. This is meant to verify that # invalid flags, mode names, etc. are caught as well as to verify that valid # ones are allowed. @@ -14,102 +14,103 @@ bind -M bind-mode \cX true # This should succeed and result in a success, zero, status. bind -M bind_mode \cX true -### HACK: All full bind listings need to have the \x7f -> backward-delete-char -# binding explicitly removed, because on some systems that's backspace, on others not. # Listing bindings -bind | string match -v '*backward-delete-char' -bind --user --preset | string match -v '*backward-delete-char' +bind | string match -v '*escape,\\[*' # Hide legacy bindings. +bind --user --preset | string match -v '*escape,\\[*' # CHECK: bind --preset '' self-insert -# CHECK: bind --preset \n execute -# CHECK: bind --preset \r execute -# CHECK: bind --preset \t complete -# CHECK: bind --preset \cc cancel-commandline -# CHECK: bind --preset \cd exit -# CHECK: bind --preset \ce bind -# CHECK: bind --preset \cs pager-toggle-search -# CHECK: bind --preset \cu backward-kill-line -# CHECK: bind --preset \e\[A up-line -# CHECK: bind --preset \e\[B down-line -# CHECK: bind --preset \e\[C forward-char -# CHECK: bind --preset \e\[D backward-char -# CHECK: bind --preset \cp up-line -# CHECK: bind --preset \cn down-line -# CHECK: bind --preset \cb backward-char -# CHECK: bind --preset \cf forward-char -# CHECK: bind -M bind_mode \cx true +# CHECK: bind --preset enter execute +# CHECK: bind --preset tab complete +# CHECK: bind --preset ctrl-c cancel-commandline +# CHECK: bind --preset ctrl-d exit +# CHECK: bind --preset ctrl-e bind +# CHECK: bind --preset ctrl-s pager-toggle-search +# CHECK: bind --preset ctrl-u backward-kill-line +# CHECK: bind --preset backspace backward-delete-char +# CHECK: bind --preset up up-line +# CHECK: bind --preset down down-line +# CHECK: bind --preset right forward-char +# CHECK: bind --preset left backward-char +# CHECK: bind --preset ctrl-p up-line +# CHECK: bind --preset ctrl-n down-line +# CHECK: bind --preset ctrl-b backward-char +# CHECK: bind --preset ctrl-f forward-char +# CHECK: bind -M bind_mode ctrl-x true # CHECK: bind --preset '' self-insert -# CHECK: bind --preset \n execute -# CHECK: bind --preset \r execute -# CHECK: bind --preset \t complete -# CHECK: bind --preset \cc cancel-commandline -# CHECK: bind --preset \cd exit -# CHECK: bind --preset \ce bind -# CHECK: bind --preset \cs pager-toggle-search -# CHECK: bind --preset \cu backward-kill-line -# CHECK: bind --preset \e\[A up-line -# CHECK: bind --preset \e\[B down-line -# CHECK: bind --preset \e\[C forward-char -# CHECK: bind --preset \e\[D backward-char -# CHECK: bind --preset \cp up-line -# CHECK: bind --preset \cn down-line -# CHECK: bind --preset \cb backward-char -# CHECK: bind --preset \cf forward-char -# CHECK: bind -M bind_mode \cx true +# CHECK: bind --preset enter execute +# CHECK: bind --preset tab complete +# CHECK: bind --preset ctrl-c cancel-commandline +# CHECK: bind --preset ctrl-d exit +# CHECK: bind --preset ctrl-e bind +# CHECK: bind --preset ctrl-s pager-toggle-search +# CHECK: bind --preset ctrl-u backward-kill-line +# CHECK: bind --preset backspace backward-delete-char +# CHECK: bind --preset up up-line +# CHECK: bind --preset down down-line +# CHECK: bind --preset right forward-char +# CHECK: bind --preset left backward-char +# CHECK: bind --preset ctrl-p up-line +# CHECK: bind --preset ctrl-n down-line +# CHECK: bind --preset ctrl-b backward-char +# CHECK: bind --preset ctrl-f forward-char +# CHECK: bind -M bind_mode ctrl-x true # Preset only -bind --preset | string match -v '*backward-delete-char' +bind --preset | string match -v '*escape,\\[*' # CHECK: bind --preset '' self-insert -# CHECK: bind --preset \n execute -# CHECK: bind --preset \r execute -# CHECK: bind --preset \t complete -# CHECK: bind --preset \cc cancel-commandline -# CHECK: bind --preset \cd exit -# CHECK: bind --preset \ce bind -# CHECK: bind --preset \cs pager-toggle-search -# CHECK: bind --preset \cu backward-kill-line -# CHECK: bind --preset \e\[A up-line -# CHECK: bind --preset \e\[B down-line -# CHECK: bind --preset \e\[C forward-char -# CHECK: bind --preset \e\[D backward-char -# CHECK: bind --preset \cp up-line -# CHECK: bind --preset \cn down-line -# CHECK: bind --preset \cb backward-char -# CHECK: bind --preset \cf forward-char +# CHECK: bind --preset enter execute +# CHECK: bind --preset tab complete +# CHECK: bind --preset ctrl-c cancel-commandline +# CHECK: bind --preset ctrl-d exit +# CHECK: bind --preset ctrl-e bind +# CHECK: bind --preset ctrl-s pager-toggle-search +# CHECK: bind --preset ctrl-u backward-kill-line +# CHECK: bind --preset backspace backward-delete-char +# CHECK: bind --preset up up-line +# CHECK: bind --preset down down-line +# CHECK: bind --preset right forward-char +# CHECK: bind --preset left backward-char +# CHECK: bind --preset ctrl-p up-line +# CHECK: bind --preset ctrl-n down-line +# CHECK: bind --preset ctrl-b backward-char +# CHECK: bind --preset ctrl-f forward-char # User only -bind --user | string match -v '*backward-delete-char' -# CHECK: bind -M bind_mode \cx true +bind --user | string match -v '*escape,\\[*' +# CHECK: bind -M bind_mode ctrl-x true # Adding bindings -bind \t 'echo banana' -bind | string match -v '*backward-delete-char' +bind tab 'echo banana' +bind | string match -v '*escape,\\[*' # CHECK: bind --preset '' self-insert -# CHECK: bind --preset \n execute -# CHECK: bind --preset \r execute -# CHECK: bind --preset \t complete -# CHECK: bind --preset \cc cancel-commandline -# CHECK: bind --preset \cd exit -# CHECK: bind --preset \ce bind -# CHECK: bind --preset \cs pager-toggle-search -# CHECK: bind --preset \cu backward-kill-line -# CHECK: bind --preset \e\[A up-line -# CHECK: bind --preset \e\[B down-line -# CHECK: bind --preset \e\[C forward-char -# CHECK: bind --preset \e\[D backward-char -# CHECK: bind --preset \cp up-line -# CHECK: bind --preset \cn down-line -# CHECK: bind --preset \cb backward-char -# CHECK: bind --preset \cf forward-char -# CHECK: bind -M bind_mode \cx true -# CHECK: bind \t 'echo banana' +# CHECK: bind --preset enter execute +# CHECK: bind --preset tab complete +# CHECK: bind --preset ctrl-c cancel-commandline +# CHECK: bind --preset ctrl-d exit +# CHECK: bind --preset ctrl-e bind +# CHECK: bind --preset ctrl-s pager-toggle-search +# CHECK: bind --preset ctrl-u backward-kill-line +# CHECK: bind --preset backspace backward-delete-char +# CHECK: bind --preset up up-line +# CHECK: bind --preset down down-line +# CHECK: bind --preset right forward-char +# CHECK: bind --preset left backward-char +# CHECK: bind --preset ctrl-p up-line +# CHECK: bind --preset ctrl-n down-line +# CHECK: bind --preset ctrl-b backward-char +# CHECK: bind --preset ctrl-f forward-char +# CHECK: bind -M bind_mode ctrl-x true +# CHECK: bind tab 'echo banana' # Erasing bindings -bind --erase \t -bind \t -bind \t 'echo wurst' -# CHECK: bind --preset \t complete -bind --erase --user --preset \t -bind \t -# CHECKERR: bind: No binding found for sequence '\t' +bind --erase tab +bind tab +bind tab 'echo wurst' +# CHECK: bind --preset tab complete +bind --erase --user --preset tab +bind tab +# CHECKERR: bind: No binding found for key 'tab' + +bind ctrl-\b +# CHECKERR: bind: Cannot add control modifier to control character 'ctrl-h' exit 0 diff --git a/tests/checks/braces.fish b/tests/checks/braces.fish index 13dea02c3..30b82cf9f 100644 --- a/tests/checks/braces.fish +++ b/tests/checks/braces.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs echo x-{1} #CHECK: x-{1} @@ -22,7 +22,7 @@ echo foo-{""} # still expands to foo-{} #CHECK: foo-{} echo foo-{$undefinedvar} # still expands to nothing -#CHECK: +#CHECK: echo foo-{,,,} # four empty items in the braces. #CHECK: foo- foo- foo- foo- diff --git a/tests/checks/broken-config.fish b/tests/checks/broken-config.fish index 28c471d56..55ea784a0 100644 --- a/tests/checks/broken-config.fish +++ b/tests/checks/broken-config.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs begin set -l dir $PWD/(dirname (status -f)) set -gx XDG_CONFIG_HOME $dir/broken-config/ diff --git a/tests/checks/builtinbuiltin.fish b/tests/checks/builtinbuiltin.fish index 7c8e7193d..43c5fb0af 100644 --- a/tests/checks/builtinbuiltin.fish +++ b/tests/checks/builtinbuiltin.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Tests for the "builtin" builtin/keyword. builtin -q string; and echo String exists #CHECK: String exists diff --git a/tests/checks/caller-exit.fish b/tests/checks/caller-exit.fish index d145589bc..6a4a88e36 100644 --- a/tests/checks/caller-exit.fish +++ b/tests/checks/caller-exit.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs echo (function foo1 --on-job-exit caller; end; functions --handlers-type caller-exit | grep foo) # CHECK: caller-exit foo1 echo (function foo2 --on-job-exit caller; end; functions --handlers-type process-exit | grep foo) diff --git a/tests/checks/caller-observer.fish b/tests/checks/caller-observer.fish index 3585ed3a8..66a90dc89 100644 --- a/tests/checks/caller-observer.fish +++ b/tests/checks/caller-observer.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Verify the '--on-job-exit caller' misfeature. function make_call_observer -a type diff --git a/tests/checks/cd.fish b/tests/checks/cd.fish index 98a7757df..cacf8a6e6 100644 --- a/tests/checks/cd.fish +++ b/tests/checks/cd.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs set -g fish (realpath $fish) diff --git a/tests/checks/check-all-fish-files.fish b/tests/checks/check-all-fish-files.fish index 018c3d43c..067f7836e 100644 --- a/tests/checks/check-all-fish-files.fish +++ b/tests/checks/check-all-fish-files.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs # Test ALL THE FISH FILES # in share/, that is - the tests are exempt because they contain syntax errors, on purpose diff --git a/tests/checks/check-completions.fish b/tests/checks/check-completions.fish index bffa5e344..3bf2ec377 100644 --- a/tests/checks/check-completions.fish +++ b/tests/checks/check-completions.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs # Test all completions where the command exists # No output is good output diff --git a/tests/checks/check-translations.fish b/tests/checks/check-translations.fish index ba34ab2ad..ec0c0428f 100644 --- a/tests/checks/check-translations.fish +++ b/tests/checks/check-translations.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs #REQUIRES: msgfmt --help set -l fail_count 0 diff --git a/tests/checks/cmdsub-limit.fish b/tests/checks/cmdsub-limit.fish index ab3ee9bbf..f53811384 100644 --- a/tests/checks/cmdsub-limit.fish +++ b/tests/checks/cmdsub-limit.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # This tests various corner cases involving command substitution. diff --git a/tests/checks/cmdsub.fish b/tests/checks/cmdsub.fish index 84eb94bed..9e2424f00 100644 --- a/tests/checks/cmdsub.fish +++ b/tests/checks/cmdsub.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs echo $(echo 1\n2) # CHECK: 1 2 diff --git a/tests/checks/command-1.fish b/tests/checks/command-1.fish index d8ca460e5..273d861f4 100644 --- a/tests/checks/command-1.fish +++ b/tests/checks/command-1.fish @@ -1,2 +1,2 @@ -#RUN: %fish -c "echo 1.2.3.4." +#RUN: %fish -c "echo 1.2.3.4." | %filter-ctrlseqs # CHECK: 1.2.3.4. diff --git a/tests/checks/command-2.fish b/tests/checks/command-2.fish index c0b4ef83d..c9a64de1d 100644 --- a/tests/checks/command-2.fish +++ b/tests/checks/command-2.fish @@ -1,3 +1,3 @@ -#RUN: %fish -c "echo 1.2.3.4." -c "echo 5.6.7.8." +#RUN: %fish -c "echo 1.2.3.4." -c "echo 5.6.7.8." | %filter-ctrlseqs # CHECK: 1.2.3.4. # CHECK: 5.6.7.8. diff --git a/tests/checks/command-not-found.fish b/tests/checks/command-not-found.fish index 0253bfc0e..7f7e6ed80 100644 --- a/tests/checks/command-not-found.fish +++ b/tests/checks/command-not-found.fish @@ -1,18 +1,18 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs set -g PATH $fish -c "nonexistent-command-1234 banana rama" #CHECKERR: fish: Unknown command: nonexistent-command-1234 -#CHECKERR: fish: +#CHECKERR: fish: #CHECKERR: nonexistent-command-1234 banana rama #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ $fish -C 'function fish_command_not_found; echo cmd-not-found; end' -ic "nonexistent-command-1234 1 2 3 4" #CHECKERR: cmd-not-found -#CHECKERR: fish: +#CHECKERR: fish: #CHECKERR: nonexistent-command-1234 1 2 3 4 #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ $fish -C 'function fish_command_not_found; echo command-not-found $argv; end' -c "nonexistent-command-abcd foo bar baz" #CHECKERR: command-not-found nonexistent-command-abcd foo bar baz -#CHECKERR: fish: +#CHECKERR: fish: #CHECKERR: nonexistent-command-abcd foo bar baz #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ diff --git a/tests/checks/command-vars-persist.fish b/tests/checks/command-vars-persist.fish index c3670dd84..8662f9fea 100644 --- a/tests/checks/command-vars-persist.fish +++ b/tests/checks/command-vars-persist.fish @@ -1,2 +1,2 @@ -#RUN: %fish -c 'set foo bar' -c 'echo $foo' +#RUN: %fish -c 'set foo bar' -c 'echo $foo' | %filter-ctrlseqs # CHECK: bar diff --git a/tests/checks/commandline.fish b/tests/checks/commandline.fish index 6562d5873..3ebf4d207 100644 --- a/tests/checks/commandline.fish +++ b/tests/checks/commandline.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs commandline --input "echo foo | bar" --is-valid and echo Valid diff --git a/tests/checks/complete-group-order.fish b/tests/checks/complete-group-order.fish index 6bb9a7f95..de50a48d7 100644 --- a/tests/checks/complete-group-order.fish +++ b/tests/checks/complete-group-order.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs function fooc; true; end; diff --git a/tests/checks/complete.fish b/tests/checks/complete.fish index 1270d1713..228d69713 100644 --- a/tests/checks/complete.fish +++ b/tests/checks/complete.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs function complete_test_alpha1 echo $argv end diff --git a/tests/checks/complete_directories.fish b/tests/checks/complete_directories.fish index 681a8c633..65d9b4206 100644 --- a/tests/checks/complete_directories.fish +++ b/tests/checks/complete_directories.fish @@ -1,4 +1,4 @@ -#RUN: %fish --interactive %s +#RUN: %fish --interactive %s | %filter-ctrlseqs # ^ interactive so we can do `complete` mkdir -p __fish_complete_directories/ cd __fish_complete_directories diff --git a/tests/checks/contains_opt.fish b/tests/checks/contains_opt.fish index 3d4c66592..cb04179d2 100644 --- a/tests/checks/contains_opt.fish +++ b/tests/checks/contains_opt.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs function commandline if test $argv[1] = -ct echo --long4\n-4 diff --git a/tests/checks/count.fish b/tests/checks/count.fish index c66afbfcd..0902178a3 100644 --- a/tests/checks/count.fish +++ b/tests/checks/count.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Validate the behavior of the `count` command. # no args diff --git a/tests/checks/deep-cmdsub.fish b/tests/checks/deep-cmdsub.fish index 5a6ac7966..d4e64d8d0 100644 --- a/tests/checks/deep-cmdsub.fish +++ b/tests/checks/deep-cmdsub.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Ensure we don't hang on deep command substitutions - see #6503. diff --git a/tests/checks/default-setup-path.fish b/tests/checks/default-setup-path.fish index 8345abcae..09750b7a5 100644 --- a/tests/checks/default-setup-path.fish +++ b/tests/checks/default-setup-path.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs if command -q getconf # (no env -u, some systems don't support that) diff --git a/tests/checks/directory-redirect.fish b/tests/checks/directory-redirect.fish index b3219b6fb..38d930cb6 100644 --- a/tests/checks/directory-redirect.fish +++ b/tests/checks/directory-redirect.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs begin end >. status -b; and echo "status -b returned true after bad redirect on a begin block" diff --git a/tests/checks/disown-parent.fish b/tests/checks/disown-parent.fish index adfe8e344..3ce48b78c 100644 --- a/tests/checks/disown-parent.fish +++ b/tests/checks/disown-parent.fish @@ -1,4 +1,4 @@ -# RUN: env fish_test_helper=%fish_test_helper %fish %s +# RUN: env fish_test_helper=%fish_test_helper %fish %s | %filter-ctrlseqs # Ensure that a job which attempts to disown itself does not explode. # Here fish_test_helper is the process group leader; we attempt to disown diff --git a/tests/checks/empty.fish b/tests/checks/empty.fish index aa25cdb60..0eecc256d 100644 --- a/tests/checks/empty.fish +++ b/tests/checks/empty.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # See issue 5692 diff --git a/tests/checks/env.fish b/tests/checks/env.fish index 4741a45f6..c8464129f 100644 --- a/tests/checks/env.fish +++ b/tests/checks/env.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs function getenvs env | string match FISH_ENV_TEST_\* diff --git a/tests/checks/eval.fish b/tests/checks/eval.fish index 9552f5a4e..ecef046c0 100644 --- a/tests/checks/eval.fish +++ b/tests/checks/eval.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Regression test for issue #4443 eval set -l previously_undefined foo echo $previously_undefined diff --git a/tests/checks/exec.fish b/tests/checks/exec.fish index ed7d8d5a2..e1e7d4dd8 100644 --- a/tests/checks/exec.fish +++ b/tests/checks/exec.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs exec cat /dev/null echo $status # CHECK: 2 diff --git a/tests/checks/expansion.fish b/tests/checks/expansion.fish index 1b6511ad4..e48dbc030 100644 --- a/tests/checks/expansion.fish +++ b/tests/checks/expansion.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # caret position (#5812) printf '<%s>\n' ($fish -c ' $f[a]' 2>&1) diff --git a/tests/checks/fds.fish b/tests/checks/fds.fish index de581e4b2..e32bb76d9 100644 --- a/tests/checks/fds.fish +++ b/tests/checks/fds.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C "set helper %fish_test_helper" %s +# RUN: %fish -C "set helper %fish_test_helper" %s | %filter-ctrlseqs # Check that we don't leave stray FDs. diff --git a/tests/checks/features-ampersand-nobg-in-token1.fish b/tests/checks/features-ampersand-nobg-in-token1.fish index bc352bb9e..9f1b0e5b9 100644 --- a/tests/checks/features-ampersand-nobg-in-token1.fish +++ b/tests/checks/features-ampersand-nobg-in-token1.fish @@ -1,4 +1,4 @@ -#RUN: %fish --features=ampersand-nobg-in-token -C 'set -g fish_indent %fish_indent' %s +#RUN: %fish --features=ampersand-nobg-in-token -C 'set -g fish_indent %fish_indent' %s | %filter-ctrlseqs echo no&background # CHECK: no&background diff --git a/tests/checks/features-nocaret1.fish b/tests/checks/features-nocaret1.fish index c0b18c671..04ebcf5dd 100644 --- a/tests/checks/features-nocaret1.fish +++ b/tests/checks/features-nocaret1.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'no-stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status' +#RUN: %fish --features 'no-stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status' | %filter-ctrlseqs # CHECK: nocaret: 0 diff --git a/tests/checks/features-nocaret2.fish b/tests/checks/features-nocaret2.fish index 7aa21462b..82579a0c1 100644 --- a/tests/checks/features-nocaret2.fish +++ b/tests/checks/features-nocaret2.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status' +#RUN: %fish --features 'stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status' | %filter-ctrlseqs # CHECK: nocaret: 0 diff --git a/tests/checks/features-nocaret3.fish b/tests/checks/features-nocaret3.fish index ed4575452..b4e06e695 100644 --- a/tests/checks/features-nocaret3.fish +++ b/tests/checks/features-nocaret3.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'no-stderr-nocaret' -c 'echo -n careton:; echo ^/dev/null' +#RUN: %fish --features 'no-stderr-nocaret' -c 'echo -n careton:; echo ^/dev/null' | %filter-ctrlseqs # CHECK: careton:^/dev/null diff --git a/tests/checks/features-nocaret4.fish b/tests/checks/features-nocaret4.fish index 1f538cdb5..c02a9a160 100644 --- a/tests/checks/features-nocaret4.fish +++ b/tests/checks/features-nocaret4.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features ' stderr-nocaret' -c 'echo -n "caretoff: "; echo ^/dev/null' +#RUN: %fish --features ' stderr-nocaret' -c 'echo -n "caretoff: "; echo ^/dev/null' | %filter-ctrlseqs # CHECK: caretoff: ^/dev/null diff --git a/tests/checks/features-percent-self1.fish b/tests/checks/features-percent-self1.fish index 0c36161d0..f6f6c4262 100644 --- a/tests/checks/features-percent-self1.fish +++ b/tests/checks/features-percent-self1.fish @@ -1,4 +1,4 @@ -#RUN: %fish --features=remove-percent-self %s +#RUN: %fish --features=remove-percent-self %s | %filter-ctrlseqs echo %self # CHECK: %self diff --git a/tests/checks/features-percent-self2.fish b/tests/checks/features-percent-self2.fish index e315a752d..6737ee3f2 100644 --- a/tests/checks/features-percent-self2.fish +++ b/tests/checks/features-percent-self2.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'remove-percent-self' -c 'status test-feature remove-percent-self; echo remove-percent-self: $status' +#RUN: %fish --features 'remove-percent-self' -c 'status test-feature remove-percent-self; echo remove-percent-self: $status' | %filter-ctrlseqs # CHECK: remove-percent-self: 0 diff --git a/tests/checks/features-qmark1.fish b/tests/checks/features-qmark1.fish index b0ebdb03d..7191372bb 100644 --- a/tests/checks/features-qmark1.fish +++ b/tests/checks/features-qmark1.fish @@ -1,3 +1,3 @@ # Explicitly overriding HOME/XDG_CONFIG_HOME is only required if not invoking via `make test` -# RUN: %fish --features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"' +# RUN: %fish --features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"' | %filter-ctrlseqs #CHECK: qmarkon: 1 diff --git a/tests/checks/features-qmark2.fish b/tests/checks/features-qmark2.fish index 36dd56528..5a8424ac1 100644 --- a/tests/checks/features-qmark2.fish +++ b/tests/checks/features-qmark2.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'qmark-noglob' -C 'string match --quiet "??" ab ; echo "qmarkoff: $status"' +#RUN: %fish --features 'qmark-noglob' -C 'string match --quiet "??" ab ; echo "qmarkoff: $status"' | %filter-ctrlseqs # CHECK: qmarkoff: 1 diff --git a/tests/checks/features-string-backslashes-off.fish b/tests/checks/features-string-backslashes-off.fish index 233cdcfc4..3af9b8196 100644 --- a/tests/checks/features-string-backslashes-off.fish +++ b/tests/checks/features-string-backslashes-off.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'no-regex-easyesc' -C 'string replace -ra "\\\\" "\\\\\\\\" -- "a\b\c"' +#RUN: %fish --features 'no-regex-easyesc' -C 'string replace -ra "\\\\" "\\\\\\\\" -- "a\b\c"' | %filter-ctrlseqs # CHECK: a\b\c diff --git a/tests/checks/features-string-backslashes.fish b/tests/checks/features-string-backslashes.fish index 596a12bd1..9fd5d1c4e 100644 --- a/tests/checks/features-string-backslashes.fish +++ b/tests/checks/features-string-backslashes.fish @@ -1,2 +1,2 @@ -#RUN: %fish --features 'regex-easyesc' -C 'string replace -ra "\\\\" "\\\\\\\\" -- "a\b\c"' +#RUN: %fish --features 'regex-easyesc' -C 'string replace -ra "\\\\" "\\\\\\\\" -- "a\b\c"' | %filter-ctrlseqs # CHECK: a\\b\\c diff --git a/tests/checks/fish_add_path.fish b/tests/checks/fish_add_path.fish index d47fef959..f10da460a 100644 --- a/tests/checks/fish_add_path.fish +++ b/tests/checks/fish_add_path.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # # This deals with $PATH manipulation. We need to be careful not to step on anything. diff --git a/tests/checks/fish_exit.fish b/tests/checks/fish_exit.fish index 77d063c3d..a40bb9a29 100644 --- a/tests/checks/fish_exit.fish +++ b/tests/checks/fish_exit.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # fish_exit fires successfully. echo 'function do_exit --on-event fish_exit; echo "fish_exiting $fish_pid"; end' > /tmp/test_exit.fish diff --git a/tests/checks/fish_user_paths.fish b/tests/checks/fish_user_paths.fish index 2ffd7a735..803fffd12 100644 --- a/tests/checks/fish_user_paths.fish +++ b/tests/checks/fish_user_paths.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # # This deals with $PATH manipulation. We need to be careful not to step on anything. diff --git a/tests/checks/for.fish b/tests/checks/for.fish index 139ab1ff9..ad106f48f 100644 --- a/tests/checks/for.fish +++ b/tests/checks/for.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # A for-loop-variable is a local variable in the enclosing scope. set -g i global diff --git a/tests/checks/function-definition.fish b/tests/checks/function-definition.fish index 9ee933f05..443de84b8 100644 --- a/tests/checks/function-definition.fish +++ b/tests/checks/function-definition.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs function stuff --argument a b c # This is a comment diff --git a/tests/checks/function.fish b/tests/checks/function.fish index e665e89ec..b901c754f 100644 --- a/tests/checks/function.fish +++ b/tests/checks/function.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs function t --argument-names a b c echo t end diff --git a/tests/checks/functions.fish b/tests/checks/functions.fish index 4ed058ea7..82812371f 100644 --- a/tests/checks/functions.fish +++ b/tests/checks/functions.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Test the `functions` builtin function f1 diff --git a/tests/checks/git.fish b/tests/checks/git.fish index 7e9629f0c..d45f59765 100644 --- a/tests/checks/git.fish +++ b/tests/checks/git.fish @@ -1,4 +1,4 @@ -#RUN: %fish -i %s +#RUN: %fish -i %s | %filter-ctrlseqs # Note: ^ this is interactive so we test interactive behavior, # e.g. the fish_git_prompt variable handlers test `status is-interactive`. #REQUIRES: command -v git @@ -198,4 +198,3 @@ end $fish -c 'complete -C "git -C ./.gi"' # CHECK: ./.git/ Directory - diff --git a/tests/checks/glob.fish b/tests/checks/glob.fish index bb75d23e0..e160cbd63 100644 --- a/tests/checks/glob.fish +++ b/tests/checks/glob.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs set -l oldpwd $PWD cd (mktemp -d) diff --git a/tests/checks/history.fish b/tests/checks/history.fish index 53a62d463..42307b14c 100644 --- a/tests/checks/history.fish +++ b/tests/checks/history.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Verify that specifying unexpected options or arguments results in an error. # First using the legacy, now deprecated, long options to specify a diff --git a/tests/checks/indent.fish b/tests/checks/indent.fish index b93028be0..0014e719e 100644 --- a/tests/checks/indent.fish +++ b/tests/checks/indent.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish_indent %fish_indent' %s +# RUN: %fish -C 'set -g fish_indent %fish_indent' %s | %filter-ctrlseqs # Test file for fish_indent # Note that littlecheck ignores leading whitespace, so we have to use {{ }} to explicitly match it. @@ -73,7 +73,7 @@ end | cat | cat | begin ; echo hi ; end | begin ; begin ; echo hi ; end ; end ar #CHECK: begin #CHECK: {{ }}echo hi -#CHECK: +#CHECK: #CHECK: end | cat | cat | begin #CHECK: {{ }}echo hi #CHECK: end | begin @@ -99,7 +99,7 @@ end #CHECK: {{ }}{{ }}echo sup #CHECK: {{ }}case beta gamma #CHECK: {{ }}{{ }}echo hi -#CHECK: +#CHECK: #CHECK: end echo -n ' @@ -117,15 +117,15 @@ function hello_world ' | $fish_indent #CHECK: function hello_world -#CHECK: +#CHECK: #CHECK: {{ }}begin #CHECK: {{ }}{{ }}echo hi #CHECK: {{ }}end | cat -#CHECK: +#CHECK: #CHECK: {{ }}echo sup #CHECK: {{ }}echo sup #CHECK: {{ }}echo hello -#CHECK: +#CHECK: #CHECK: {{ }}echo hello #CHECK: end @@ -149,13 +149,13 @@ qqq end' | $fish_indent #CHECK: echo alpha #comment1 #CHECK: #comment2 -#CHECK: +#CHECK: #CHECK: #comment3 #CHECK: for i in abc #comment1 #CHECK: {{ }}#comment2 #CHECK: {{ }}echo hi #CHECK: end -#CHECK: +#CHECK: #CHECK: switch foo #abc #CHECK: {{ }}# bar #CHECK: {{ }}case bar @@ -299,26 +299,26 @@ echo bye #CHECK: {{ }}echo yes #CHECK: en\ #CHECK: d -#CHECK: +#CHECK: #CHECK: while true #CHECK: {{ }}builtin yes #CHECK: end -#CHECK: +#CHECK: #CHECK: alpha | beta -#CHECK: +#CHECK: #CHECK: gamma | \ #CHECK: # comment3 #CHECK: delta -#CHECK: +#CHECK: #CHECK: if true #CHECK: {{ }}echo abc #CHECK: end -#CHECK: +#CHECK: #CHECK: if false # comment4 #CHECK: {{ }}and true && false #CHECK: {{ }}echo abc #CHECK: end -#CHECK: +#CHECK: #CHECK: echo hi | #CHECK: {{ }}echo bye diff --git a/tests/checks/init-command-2.fish b/tests/checks/init-command-2.fish index 0e603deb2..901e4d3ea 100644 --- a/tests/checks/init-command-2.fish +++ b/tests/checks/init-command-2.fish @@ -1,3 +1,3 @@ -#RUN: %fish -C 'echo init-command' -C 'echo 2nd init-command' +#RUN: %fish -C 'echo init-command' -C 'echo 2nd init-command' | %filter-ctrlseqs # CHECK: init-command # CHECK: 2nd init-command diff --git a/tests/checks/init-command-mix-ordering.fish b/tests/checks/init-command-mix-ordering.fish index 8c54be6e6..954a94f75 100644 --- a/tests/checks/init-command-mix-ordering.fish +++ b/tests/checks/init-command-mix-ordering.fish @@ -1,3 +1,3 @@ -#RUN: %fish -c 'echo command' -C 'echo init-command' +#RUN: %fish -c 'echo command' -C 'echo init-command' | %filter-ctrlseqs # CHECK: init-command # CHECK: command diff --git a/tests/checks/init-command-mix.fish b/tests/checks/init-command-mix.fish index a8cd076bf..e36b8334f 100644 --- a/tests/checks/init-command-mix.fish +++ b/tests/checks/init-command-mix.fish @@ -1,3 +1,3 @@ -#RUN: %fish -C 'echo init-command' -c 'echo command' +#RUN: %fish -C 'echo init-command' -c 'echo command' | %filter-ctrlseqs # CHECK: init-command # CHECK: command diff --git a/tests/checks/init-command.fish b/tests/checks/init-command.fish index 0a7875e1a..6af8ab797 100644 --- a/tests/checks/init-command.fish +++ b/tests/checks/init-command.fish @@ -1,2 +1,2 @@ -#RUN: %fish -C 'echo init-command' +#RUN: %fish -C 'echo init-command' | %filter-ctrlseqs # CHECK: init-command diff --git a/tests/checks/init-unreadable-cwd.fish b/tests/checks/init-unreadable-cwd.fish index c30843a22..2aa0a8086 100644 --- a/tests/checks/init-unreadable-cwd.fish +++ b/tests/checks/init-unreadable-cwd.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # Test that fish doesn't crash if cwd is unreadable at the start (#6597) set -l oldpwd $PWD diff --git a/tests/checks/invocation.fish b/tests/checks/invocation.fish index 94915937c..702eadd5b 100644 --- a/tests/checks/invocation.fish +++ b/tests/checks/invocation.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs $fish -c "echo 1.2.3.4." # CHECK: 1.2.3.4. @@ -107,4 +107,3 @@ $fish --no-config -c 'echo notprinted | and true' # Regression test for a hang. echo "set -L" | $fish > /dev/null - diff --git a/tests/checks/job-control-noninteractive.fish b/tests/checks/job-control-noninteractive.fish index da4566306..2540bd5e2 100644 --- a/tests/checks/job-control-noninteractive.fish +++ b/tests/checks/job-control-noninteractive.fish @@ -1,4 +1,4 @@ -#RUN: env fth=%fish_test_helper fish=%fish %fish %s +#RUN: env fth=%fish_test_helper fish=%fish %fish %s | %filter-ctrlseqs # Ensure job control works in non-interactive environments. diff --git a/tests/checks/job-control-not-a-tty.fish b/tests/checks/job-control-not-a-tty.fish index 80b780549..e9d301f57 100644 --- a/tests/checks/job-control-not-a-tty.fish +++ b/tests/checks/job-control-not-a-tty.fish @@ -1,4 +1,4 @@ -#RUN: echo 'status job-control full; command echo A ; echo B;' | %fish +#RUN: echo 'status job-control full; command echo A ; echo B;' | %fish | %filter-ctrlseqs # Regression test for #6573. diff --git a/tests/checks/job-ids.fish b/tests/checks/job-ids.fish index e94d2d800..39190d03a 100644 --- a/tests/checks/job-ids.fish +++ b/tests/checks/job-ids.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Ensure that job IDs are sequential. diff --git a/tests/checks/jobs-are-escaped.fish b/tests/checks/jobs-are-escaped.fish index 76a8722c1..df59b2d57 100644 --- a/tests/checks/jobs-are-escaped.fish +++ b/tests/checks/jobs-are-escaped.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Ensure that jobs are printed with new lines escaped diff --git a/tests/checks/jobs.fish b/tests/checks/jobs.fish index 7f85f0861..d3f4ddaeb 100644 --- a/tests/checks/jobs.fish +++ b/tests/checks/jobs.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Verify zombies are not left by disown (#7183, #5342) # Do this first to avoid colliding with the other disowned processes below, which may diff --git a/tests/checks/line-continuation.fish b/tests/checks/line-continuation.fish index 37e11a41a..3d46b5b8f 100644 --- a/tests/checks/line-continuation.fish +++ b/tests/checks/line-continuation.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs ech\ o echo #CHECK: echo diff --git a/tests/checks/line-number.fish b/tests/checks/line-number.fish index c5832db9a..2714dbe25 100644 --- a/tests/checks/line-number.fish +++ b/tests/checks/line-number.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # # These lines left around because we need the line numbers. # This file in general requires careful editing in the middle, I recommend appending. diff --git a/tests/checks/locale-numeric.fish b/tests/checks/locale-numeric.fish index 91b00bcc8..0d797b68c 100644 --- a/tests/checks/locale-numeric.fish +++ b/tests/checks/locale-numeric.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # musl currently does not have a `locale` command, so we skip this test there. # REQUIRES: command -v locale # We need a comma-using locale we know. diff --git a/tests/checks/locale.fish b/tests/checks/locale.fish index 5f8cf6290..7b4349557 100644 --- a/tests/checks/locale.fish +++ b/tests/checks/locale.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C "set fish %fish" %s +#RUN: %fish -C "set fish %fish" %s | %filter-ctrlseqs # This hangs when running on github actions with tsan for unknown reasons, # see #7934. #REQUIRES: test -z "$GITHUB_WORKFLOW" diff --git a/tests/checks/loops.fish b/tests/checks/loops.fish index aea9d7e3a..f889a937f 100644 --- a/tests/checks/loops.fish +++ b/tests/checks/loops.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs function never_runs while false diff --git a/tests/checks/math.fish b/tests/checks/math.fish index bcfadd955..c62d2b183 100644 --- a/tests/checks/math.fish +++ b/tests/checks/math.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # OpenBSD doesn't do hex numbers in str/wcstod (like C99 requires). # So let's skip this. #REQUIRES: test "$(uname)" != OpenBSD diff --git a/tests/checks/no-config.fish b/tests/checks/no-config.fish index 622617d6c..0a1e64a4f 100644 --- a/tests/checks/no-config.fish +++ b/tests/checks/no-config.fish @@ -1,4 +1,4 @@ -#RUN: %fish --no-config %s +#RUN: %fish --no-config %s | %filter-ctrlseqs functions | string match help # CHECK: help diff --git a/tests/checks/no-execute.fish b/tests/checks/no-execute.fish index 1e5f54e01..867b0d619 100644 --- a/tests/checks/no-execute.fish +++ b/tests/checks/no-execute.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs # Test that fish -n doesn't check for command existence - function autoloading throws a wrench in that. echo "type foo" | $fish -n diff --git a/tests/checks/noshebang.fish b/tests/checks/noshebang.fish index 008723f35..9bc9ae59d 100644 --- a/tests/checks/noshebang.fish +++ b/tests/checks/noshebang.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Do not run under sanitizers in CI, as they intercept a busted posix_spawn # which mishandles shebangless scripts. diff --git a/tests/checks/not.fish b/tests/checks/not.fish index 77a03c419..3a121d227 100644 --- a/tests/checks/not.fish +++ b/tests/checks/not.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs not true echo $status diff --git a/tests/checks/nuls.fish b/tests/checks/nuls.fish index fa2194787..560fee041 100644 --- a/tests/checks/nuls.fish +++ b/tests/checks/nuls.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # NUL-handling # This one actually prints a NUL diff --git a/tests/checks/path.fish b/tests/checks/path.fish index f8c326acc..95135e4ba 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # The "path" builtin for dealing with paths # Extension - for figuring out the file extension of a given path. diff --git a/tests/checks/pipeline-pgroup.fish b/tests/checks/pipeline-pgroup.fish index 07da18829..e94eab239 100644 --- a/tests/checks/pipeline-pgroup.fish +++ b/tests/checks/pipeline-pgroup.fish @@ -1,4 +1,4 @@ -# RUN: env fth=%fish_test_helper fish=%fish %fish %s +# RUN: env fth=%fish_test_helper fish=%fish %fish %s | %filter-ctrlseqs status job-control full diff --git a/tests/checks/pipestatus.fish b/tests/checks/pipestatus.fish index bfe799306..5a093fa31 100644 --- a/tests/checks/pipestatus.fish +++ b/tests/checks/pipestatus.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # pipestatus variable - builtins only false | false | false diff --git a/tests/checks/print-help.fish b/tests/checks/print-help.fish index fcd0e1327..1d12ec184 100644 --- a/tests/checks/print-help.fish +++ b/tests/checks/print-help.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Test redirecting builtin help with a pipe set -lx __fish_data_dir (mktemp -d) diff --git a/tests/checks/printf.fish b/tests/checks/printf.fish index f1cc238e1..9664116b4 100644 --- a/tests/checks/printf.fish +++ b/tests/checks/printf.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs printf "%d %d\n" 1 2 3 # CHECK: 1 2 diff --git a/tests/checks/prompt.fish b/tests/checks/prompt.fish index a600dfff0..b9be6362f 100644 --- a/tests/checks/prompt.fish +++ b/tests/checks/prompt.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs prompt_pwd -d 1 /foo/bar/baz # CHECK: /f/b/baz diff --git a/tests/checks/psub.fish b/tests/checks/psub.fish index d277072e2..e8cda45dd 100644 --- a/tests/checks/psub.fish +++ b/tests/checks/psub.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs set -l filename (echo foo | psub --testing) test -f $filename or echo 'psub is not a regular file' >&2 diff --git a/tests/checks/random.fish b/tests/checks/random.fish index 712313686..591d79ed2 100644 --- a/tests/checks/random.fish +++ b/tests/checks/random.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs set -l max 9223372036854775807 set -l close_max 9223372036854775806 set -l min -9223372036854775807 diff --git a/tests/checks/rc-returned.fish b/tests/checks/rc-returned.fish index 240e28ea1..88aead42c 100644 --- a/tests/checks/rc-returned.fish +++ b/tests/checks/rc-returned.fish @@ -1,2 +1,2 @@ -#RUN: %fish -c '%fish -c false; echo RC: $status' +#RUN: %fish -c '%fish -c false; echo RC: $status' | %filter-ctrlseqs # CHECK: RC: 1 diff --git a/tests/checks/read.fish b/tests/checks/read.fish index b921b5600..1c8c1dbaa 100644 --- a/tests/checks/read.fish +++ b/tests/checks/read.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C "set fish %fish" %s +# RUN: %fish -C "set fish %fish" %s | %filter-ctrlseqs # Set term again explicitly to ensure behavior. set -gx TERM xterm # Read with no vars is not an error @@ -155,7 +155,7 @@ echo $foo echo newline | read -lz foo echo $foo #CHECK: newline -#CHECK: +#CHECK: echo -n 'test ing' | read -lz foo bar print_vars foo bar #CHECK: 1 'test' 1 'ing' @@ -303,7 +303,7 @@ echo $foo echo $bar #CHECK: b echo $baz -#CHECK: +#CHECK: # Multi-char delimiters with -d echo a...b...c | read -l -d "..." a b c diff --git a/tests/checks/realpath.fish b/tests/checks/realpath.fish index 2f4dfbf54..b521c8632 100644 --- a/tests/checks/realpath.fish +++ b/tests/checks/realpath.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # $XDG_DATA_HOME can itself be a relative path. So force it to an absolute # path so we can remove it from any resolved paths below. This is needed # because the contents of the builtin realpath.out file can't include any $PWD diff --git a/tests/checks/redirect.fish b/tests/checks/redirect.fish index d4d9628f5..f94726192 100644 --- a/tests/checks/redirect.fish +++ b/tests/checks/redirect.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs function outnerr command echo out $argv diff --git a/tests/checks/regex-import.fish b/tests/checks/regex-import.fish index 4905a79d0..56dc0a0f9 100644 --- a/tests/checks/regex-import.fish +++ b/tests/checks/regex-import.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Tests for importing named regex groups as fish variables # Invalid variable name? diff --git a/tests/checks/return.fish b/tests/checks/return.fish index 299051e38..cb49fcc74 100644 --- a/tests/checks/return.fish +++ b/tests/checks/return.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs # Some tests of the "return" builtin. $fish -c 'return 5' diff --git a/tests/checks/scoping.fish b/tests/checks/scoping.fish index f2547580a..480776cdc 100644 --- a/tests/checks/scoping.fish +++ b/tests/checks/scoping.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Test scoping rules for functions and status set -e smurf diff --git a/tests/checks/self-signal-usr1.fish b/tests/checks/self-signal-usr1.fish index 15d45ee6b..261189fc3 100644 --- a/tests/checks/self-signal-usr1.fish +++ b/tests/checks/self-signal-usr1.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # See #6397 diff --git a/tests/checks/set.fish b/tests/checks/set.fish index 731cadbfe..0b1b8347a 100644 --- a/tests/checks/set.fish +++ b/tests/checks/set.fish @@ -1,5 +1,5 @@ # Explicitly overriding HOME/XDG_CONFIG_HOME is only required if not invoking via `make test` -# RUN: env FISH=%fish %fish %s +# RUN: env FISH=%fish %fish %s | %filter-ctrlseqs # Environment variable tests # Test if variables can be properly set diff --git a/tests/checks/setenv.fish b/tests/checks/setenv.fish index f65fa74c1..528c68528 100644 --- a/tests/checks/setenv.fish +++ b/tests/checks/setenv.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Verify the correct behavior of the `setenv` compatibility shim. diff --git a/tests/checks/sigint.fish b/tests/checks/sigint.fish index 7838ece07..25fa56247 100644 --- a/tests/checks/sigint.fish +++ b/tests/checks/sigint.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C "set -g helper %fish_test_helper; set -g fish %fish" %s +#RUN: %fish -C "set -g helper %fish_test_helper; set -g fish %fish" %s | %filter-ctrlseqs # Check that nohup is propagated. set output_path (mktemp) diff --git a/tests/checks/sigint2.fish b/tests/checks/sigint2.fish index 0160cf3b5..17643f76c 100644 --- a/tests/checks/sigint2.fish +++ b/tests/checks/sigint2.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C "set helper %fish_test_helper" %s +#RUN: %fish -C "set helper %fish_test_helper" %s | %filter-ctrlseqs # This hangs on OpenBSD #REQUIRES: test "$(uname)" != OpenBSD diff --git a/tests/checks/signal.fish b/tests/checks/signal.fish index 20d0fd6c2..88a7601e1 100644 --- a/tests/checks/signal.fish +++ b/tests/checks/signal.fish @@ -1,4 +1,4 @@ -# RUN: env fish_test_helper=%fish_test_helper %fish -C 'set -l fish %fish' %s +# RUN: env fish_test_helper=%fish_test_helper %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs $fish -c 'function main; exit 4; true; end; main' echo $status diff --git a/tests/checks/slices.fish b/tests/checks/slices.fish index 44b25870b..c971a959b 100644 --- a/tests/checks/slices.fish +++ b/tests/checks/slices.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs set n 10 set test (seq $n) echo $test[1..$n] # normal range diff --git a/tests/checks/stack-overflow.fish b/tests/checks/stack-overflow.fish index de2189012..f83392467 100644 --- a/tests/checks/stack-overflow.fish +++ b/tests/checks/stack-overflow.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Check that we correctly detect an overflow of fish's function stack. # This is dependent on the internal max stack depth of course. diff --git a/tests/checks/status-command.fish b/tests/checks/status-command.fish index c6105dea2..acf4c9b62 100644 --- a/tests/checks/status-command.fish +++ b/tests/checks/status-command.fish @@ -1,4 +1,4 @@ -#RUN: env FISH_PATH=%fish FILE_PATH=%s %fish %s +#RUN: env FISH_PATH=%fish FILE_PATH=%s %fish %s | %filter-ctrlseqs status line-number # CHECK: 3 diff --git a/tests/checks/status-value.fish b/tests/checks/status-value.fish index ce02a45ad..56c00b9a5 100644 --- a/tests/checks/status-value.fish +++ b/tests/checks/status-value.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # Empty commands should be 123 set empty_var diff --git a/tests/checks/status.fish b/tests/checks/status.fish index 733dd2260..5c1ad447f 100644 --- a/tests/checks/status.fish +++ b/tests/checks/status.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs status -b and echo '"status -b" unexpectedly returned true at top level' diff --git a/tests/checks/string-advanced.fish b/tests/checks/string-advanced.fish index 22102f776..22e5c9ffa 100644 --- a/tests/checks/string-advanced.fish +++ b/tests/checks/string-advanced.fish @@ -1,3 +1,3 @@ -#RUN: %fish --features regex-easyesc %s +#RUN: %fish --features regex-easyesc %s | %filter-ctrlseqs string replace -r 'a(.*)' '\U$0\E' abc # CHECK: ABC diff --git a/tests/checks/string.fish b/tests/checks/string.fish index b24e5c706..b56bd7c0e 100644 --- a/tests/checks/string.fish +++ b/tests/checks/string.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Tests for string builtin. Mostly taken from man page examples. string match -r -v "c.*" dog can cat diz; and echo "exit 0" diff --git a/tests/checks/switch.fish b/tests/checks/switch.fish index b5393c30e..b6ea3cfcc 100644 --- a/tests/checks/switch.fish +++ b/tests/checks/switch.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C "set fish %fish" %s +#RUN: %fish -C "set fish %fish" %s | %filter-ctrlseqs # Check that switch with an argument expanding to nothing still works. switch $foo case a diff --git a/tests/checks/symlinks-not-overwritten.fish b/tests/checks/symlinks-not-overwritten.fish index b5c6c5a27..7427ff32e 100644 --- a/tests/checks/symlinks-not-overwritten.fish +++ b/tests/checks/symlinks-not-overwritten.fish @@ -1,5 +1,5 @@ # Explicitly overriding HOME/XDG_CONFIG_HOME is only required if not invoking via `make test` -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs mkdir -p $XDG_CONFIG_HOME/fish diff --git a/tests/checks/syntax-error-location.fish b/tests/checks/syntax-error-location.fish index 9821d752d..61b7057c5 100644 --- a/tests/checks/syntax-error-location.fish +++ b/tests/checks/syntax-error-location.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish %fish' %s +#RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # A $status used as a command should not impact the location of other errors. echo 'echo foo | exec grep # this exec is not allowed! diff --git a/tests/checks/test.fish b/tests/checks/test.fish index 5a1cb09b4..0fc9552b2 100644 --- a/tests/checks/test.fish +++ b/tests/checks/test.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # # Tests for the `test` builtin, aka `[`. test inf -gt 0 diff --git a/tests/checks/threads.fish b/tests/checks/threads.fish index 024bf77db..0eb47f5ac 100644 --- a/tests/checks/threads.fish +++ b/tests/checks/threads.fish @@ -1,3 +1,3 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Check we can handle more than 64 threads (if not this HANGS!): true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true (true ))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) diff --git a/tests/checks/time.fish b/tests/checks/time.fish index f4fe070e1..0af7ad762 100644 --- a/tests/checks/time.fish +++ b/tests/checks/time.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -l fish %fish' %s +#RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs time sleep 0 # These are a tad awkward because it picks the correct unit and adapts whitespace. diff --git a/tests/checks/tmux-abbr.fish b/tests/checks/tmux-abbr.fish index c8af9fe51..055f9dbf0 100644 --- a/tests/checks/tmux-abbr.fish +++ b/tests/checks/tmux-abbr.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux set -g isolated_tmux_fish_extra_args -C ' diff --git a/tests/checks/tmux-bind.fish b/tests/checks/tmux-bind.fish index 54c8f4f6f..3ec9aa158 100644 --- a/tests/checks/tmux-bind.fish +++ b/tests/checks/tmux-bind.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux isolated-tmux-start diff --git a/tests/checks/tmux-commandline.fish b/tests/checks/tmux-commandline.fish index 533546f92..a006be308 100644 --- a/tests/checks/tmux-commandline.fish +++ b/tests/checks/tmux-commandline.fish @@ -1,9 +1,9 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux isolated-tmux-start -isolated-tmux send-keys 'bind \eg "commandline -p -C -- -4"' Enter C-l +isolated-tmux send-keys 'bind alt-g "commandline -p -C -- -4"' Enter C-l isolated-tmux send-keys 'echo bar|cat' \eg foo tmux-sleep isolated-tmux capture-pane -p diff --git a/tests/checks/tmux-complete.fish b/tests/checks/tmux-complete.fish index 81ab019d1..9902d3973 100644 --- a/tests/checks/tmux-complete.fish +++ b/tests/checks/tmux-complete.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux #REQUIRES: uname -r | grep -qv Microsoft # disable on github actions because it's flakey @@ -85,7 +85,7 @@ tmux-sleep isolated-tmux capture-pane -p # CHECK: prompt 6> echo suggest this -isolated-tmux send-keys C-u 'bind \cs forward-single-char' Enter C-l +isolated-tmux send-keys C-u 'bind ctrl-s forward-single-char' Enter C-l isolated-tmux send-keys 'echo suggest thi' tmux-sleep isolated-tmux send-keys C-s diff --git a/tests/checks/tmux-history-search.fish b/tests/checks/tmux-history-search.fish index 102afb033..e2d7704bd 100644 --- a/tests/checks/tmux-history-search.fish +++ b/tests/checks/tmux-history-search.fish @@ -1,13 +1,13 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux # disable on github actions because it's flakey #REQUIRES: test -z "$CI" # The default history-pager-delete binding is shift-delete which -# may not have a terminfo entry for screen-256color, so rebind to F1. +# won't work on terminals that don't support CSI u, so rebind. set -g isolated_tmux_fish_extra_args -C ' set -g fish_autosuggestion_enabled 0 - bind -k f1 history-pager-delete or backward-delete-char + bind alt-d history-pager-delete or backward-delete-char ' isolated-tmux-start @@ -55,7 +55,7 @@ isolated-tmux send-keys C-e C-u C-r tmux-sleep isolated-tmux send-keys "echo sdifjsdoifjsdoifj" tmux-sleep -isolated-tmux send-keys F1 +isolated-tmux send-keys M-d # alt-d tmux-sleep isolated-tmux capture-pane -p | grep "(no matches)" # CHECK: (no matches) diff --git a/tests/checks/tmux-prompt.fish b/tests/checks/tmux-prompt.fish index e1f129e56..90c049250 100644 --- a/tests/checks/tmux-prompt.fish +++ b/tests/checks/tmux-prompt.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs #REQUIRES: command -v tmux set -g isolated_tmux_fish_extra_args -C ' diff --git a/tests/checks/trace.fish b/tests/checks/trace.fish index 737655fc8..126ec0aea 100644 --- a/tests/checks/trace.fish +++ b/tests/checks/trace.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs echo untraced # CHECK: untraced diff --git a/tests/checks/trap.fish b/tests/checks/trap.fish index 0b7def74d..2970ddafe 100644 --- a/tests/checks/trap.fish +++ b/tests/checks/trap.fish @@ -1,4 +1,4 @@ -# RUN: env fth=%fish_test_helper %fish %s +# RUN: env fth=%fish_test_helper %fish %s | %filter-ctrlseqs set -g SIGUSR1_COUNT 0 diff --git a/tests/checks/trap_print.fish b/tests/checks/trap_print.fish index 0319a9bf8..a8ae83fa2 100644 --- a/tests/checks/trap_print.fish +++ b/tests/checks/trap_print.fish @@ -1,4 +1,4 @@ -# RUN: env fth=%fish_test_helper %fish %s +# RUN: env fth=%fish_test_helper %fish %s | %filter-ctrlseqs # These tests check how installed trap handlers are listed. Run separately from the main trap # handler to ensure a clean environment. diff --git a/tests/checks/type.fish b/tests/checks/type.fish index a872b5798..116b8a724 100644 --- a/tests/checks/type.fish +++ b/tests/checks/type.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # # Tests for the `type` builtin # First type --query, which is the most important part. diff --git a/tests/checks/ulimit.fish b/tests/checks/ulimit.fish index ad3b2f7a2..6c7dc98d3 100644 --- a/tests/checks/ulimit.fish +++ b/tests/checks/ulimit.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs ulimit --core-size #CHECK: {{unlimited|\d+}} diff --git a/tests/checks/umask.fish b/tests/checks/umask.fish index 6bc3f895f..0d31cd48f 100644 --- a/tests/checks/umask.fish +++ b/tests/checks/umask.fish @@ -1,4 +1,4 @@ -# RUN: %fish -C 'set -g fish %fish' %s +# RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs # Test the umask command. In particular the symbolic modes since they've been # broken for four years (see issue #738) at the time I added these tests. diff --git a/tests/checks/variable-assignment.fish b/tests/checks/variable-assignment.fish index d5fb6b6ef..2f8d2bfd8 100644 --- a/tests/checks/variable-assignment.fish +++ b/tests/checks/variable-assignment.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # erase all lowercase variables to make sure they don't break our tests for varname in (set -xn | string match -r '^[a-z].*') diff --git a/tests/checks/vars_as_commands.fish b/tests/checks/vars_as_commands.fish index 0c74fa747..03ce91a59 100644 --- a/tests/checks/vars_as_commands.fish +++ b/tests/checks/vars_as_commands.fish @@ -1,4 +1,4 @@ -#RUN: %fish -C 'set -g fish (builtin realpath %fish)' %s +#RUN: %fish -C 'set -g fish (builtin realpath %fish)' %s | %filter-ctrlseqs # Test that using variables as command names work correctly. $EMPTY_VARIABLE diff --git a/tests/checks/version.fish b/tests/checks/version.fish index 2c9424dec..bc77ad08a 100644 --- a/tests/checks/version.fish +++ b/tests/checks/version.fish @@ -1,2 +1,2 @@ -#RUN: %fish -v +#RUN: %fish -v | %filter-ctrlseqs # CHECK: fish, version {{[-.ga-f0-9]*(irty)?}} diff --git a/tests/checks/wait.fish b/tests/checks/wait.fish index db28c179f..c7bdf63fb 100644 --- a/tests/checks/wait.fish +++ b/tests/checks/wait.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Ensure that we can wait for stuff. status job-control full diff --git a/tests/checks/wildcard.fish b/tests/checks/wildcard.fish index ee9e4d667..f7b52d964 100644 --- a/tests/checks/wildcard.fish +++ b/tests/checks/wildcard.fish @@ -1,4 +1,4 @@ -# RUN: %fish %s +# RUN: %fish %s | %filter-ctrlseqs # Ensure that, if variable expansion results in multiple strings # and one of them fails a glob, that we don't fail the entire expansion. diff --git a/tests/checks/wraps.fish b/tests/checks/wraps.fish index 9c93e3420..76197644c 100644 --- a/tests/checks/wraps.fish +++ b/tests/checks/wraps.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs # Validate some things about command wrapping. set -g LANG C # For predictable error messages. diff --git a/tests/checks/zero_based_array.fish b/tests/checks/zero_based_array.fish index 20992674a..0e20949c0 100644 --- a/tests/checks/zero_based_array.fish +++ b/tests/checks/zero_based_array.fish @@ -1,4 +1,4 @@ -#RUN: %fish %s +#RUN: %fish %s | %filter-ctrlseqs echo $foo[0] #CHECKERR: {{.*}}checks/zero_based_array.fish (line {{\d+}}): array indices start at 1, not 0. #CHECKERR: echo $foo[0] diff --git a/tests/filter-ctrlseqs.sh b/tests/filter-ctrlseqs.sh new file mode 100755 index 000000000..9e3bf1cb6 --- /dev/null +++ b/tests/filter-ctrlseqs.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +escape=$(printf '\033') + +"$1" -c 'cat | string replace -ra $argv[1] ""' -- "\ +""$escape\[\?2004h""\ +""$escape\[>4;1m""\ +""$escape\[>5u""\ +""$escape=""\ +""($escape\[\?1004h)?""\ +""|""\ +""$escape\[\?2004l""\ +""$escape\[>4;0m""\ +""$escape\[<1u""\ +""$escape>""\ +""$escape\[\?1004l" diff --git a/tests/pexpects/bind.py b/tests/pexpects/bind.py index 67bac5286..9f1139689 100644 --- a/tests/pexpects/bind.py +++ b/tests/pexpects/bind.py @@ -64,7 +64,7 @@ expect_prompt("\r\nmno pqrt\r\n") # Now test that exactly the expected bind modes are defined sendline("bind --list-modes") -expect_prompt("\r\ndefault\r\npaste", unmatched="Unexpected bind modes") +expect_prompt("\r\ndefault", unmatched="Unexpected bind modes") # Test vi key bindings. # This should leave vi mode in the insert state. @@ -244,7 +244,7 @@ expect_prompt("foo") # Now test that exactly the expected bind modes are defined sendline("bind --list-modes") expect_prompt( - "\r\ndefault\r\ninsert\r\npaste\r\nreplace\r\nreplace_one\r\nvisual\r\n", + "default\r\ninsert\r\nreplace\r\nreplace_one\r\nvisual\r\n", unmatched="Unexpected vi bind modes", ) @@ -273,36 +273,14 @@ expect_prompt( "\r\ndef abc\r\n", unmatched="emacs transpose words fail, 200ms timeout: no delay" ) -# Same test as above but with a slight delay less than the escape timeout. -send("echo ghi jkl") -send("\033") -sleep(0.020) -send("t\r") -expect_prompt( - "\r\njkl ghi\r\n", - unmatched="emacs transpose words fail, 200ms timeout: short delay", -) - -# Now test with a delay > the escape timeout. The transposition should not -# occur and the "t" should become part of the text that is echoed. -send("echo mno pqr") -send("\033") -sleep(0.350) -send("t\r") -# emacs transpose words, 100ms timeout: long delay -expect_prompt( - "\r\nmno pqrt\r\n", - unmatched="emacs transpose words fail, 200ms timeout: long delay", -) - # Verify special characters, such as \cV, are not intercepted by the kernel # tty driver. Rather, they can be bound and handled by fish. -sendline("bind \\cV 'echo ctrl-v seen'") +sendline("bind ctrl-v 'echo ctrl-v seen'") expect_prompt() send("\026\r") expect_prompt("ctrl-v seen", unmatched="ctrl-v not seen") -send("bind \\cO 'echo ctrl-o seen'\r") +send("bind ctrl-o 'echo ctrl-o seen'\r") expect_prompt() send("\017\r") expect_prompt("ctrl-o seen", unmatched="ctrl-o not seen") @@ -346,7 +324,7 @@ sendline("qqqecho qqq") expect_prompt("qqq", unmatched="Leading qs not stripped") # Test bigword with single-character words. -sendline("bind \cg kill-bigword") +sendline("bind ctrl-g kill-bigword") expect_prompt() send("a b c d\x01") # ctrl-a, move back to the beginning of the line send("\x07") # ctrl-g, kill bigword @@ -355,12 +333,12 @@ expect_prompt("\nb c d") # Test that overriding the escape binding works # and does not inhibit other escape sequences (up-arrow in this case). -sendline("bind \\x1b 'echo foo'") +sendline("bind escape 'echo foo'") expect_prompt() send("\x1b") expect_str("foo") send("\x1b[A") -expect_str("bind \\x1b 'echo foo'") +expect_str("bind escape 'echo foo'") sendline("") expect_prompt() @@ -370,7 +348,7 @@ sendline("echo") expect_prompt("\nb c d") # Check that ctrl-z can be bound -sendline('bind \cz "echo bound ctrl-z"') +sendline('bind ctrl-z "echo bound ctrl-z"') expect_prompt() send("\x1A") expect_str("bound ctrl-z") @@ -382,7 +360,7 @@ expect_prompt("fooBAR") # Check that the builtin version of `exit` works # (for obvious reasons this MUST BE LAST) -sendline("function myexit; echo exit; exit; end; bind \cz myexit") +sendline("function myexit; echo exit; exit; end; bind ctrl-z myexit") expect_prompt() send("\x1A") expect_str("exit") diff --git a/tests/pexpects/bind_mode_events.py b/tests/pexpects/bind_mode_events.py index 26271b31f..2ba4d0ce0 100644 --- a/tests/pexpects/bind_mode_events.py +++ b/tests/pexpects/bind_mode_events.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 from pexpect_helper import SpawnedProc import os +import sys import signal +import platform + +# Fails on macOS CI +if "CI" in os.environ and platform.system() == "Darwin": + sys.exit(127) sp = SpawnedProc() send, sendline, sleep, expect_prompt = sp.send, sp.sendline, sp.sleep, sp.expect_prompt @@ -11,7 +17,7 @@ send("set -g fish_key_bindings fish_vi_key_bindings\r") expect_prompt() send("echo ready to go\r") -expect_prompt("\r\nready to go\r\n") +expect_prompt(f"\r\nready to go\r\n") send( "function add_change --on-variable fish_bind_mode ; set -g MODE_CHANGES $MODE_CHANGES $fish_bind_mode ; end\r" ) @@ -19,19 +25,19 @@ expect_prompt() # normal mode send("\033") -sleep(0.050) +sleep(10 if "CI" in os.environ else 1) # insert mode send("i") -sleep(0.050) +sleep(10 if "CI" in os.environ else 1) # back to normal mode send("\033") -sleep(0.050) +sleep(10 if "CI" in os.environ else 1) # insert mode again send("i") -sleep(0.050) +sleep(10 if "CI" in os.environ else 1) send("echo mode changes: $MODE_CHANGES\r") expect_prompt("\r\nmode changes: default insert default insert\r\n") diff --git a/tests/pexpects/commandline.py b/tests/pexpects/commandline.py index ff75c050f..0ac3eeadb 100644 --- a/tests/pexpects/commandline.py +++ b/tests/pexpects/commandline.py @@ -62,7 +62,7 @@ expect_re("prompt [0-9]+>whatever") # Test --current-process output send(control("u")) -sendline(r"bind \cb 'set tmp (commandline --current-process)'") +sendline(r"bind ctrl-b 'set tmp (commandline --current-process)'") expect_prompt() send("echo process1; echo process2") send(control("a")) @@ -79,7 +79,7 @@ sendline('echo "process extent is [$tmp]"') expect_str("process extent is [echo process # comment]") # DISABLED because it keeps failing under ASAN -# sendline(r"bind \cb 'set tmp (commandline --current-process | count)'") +# sendline(r"bind ctrl-b 'set tmp (commandline --current-process | count)'") # sendline(r'commandline "echo line1 \\" "# comment" "line2"') # send(control("b")) # send(control("u") * 6) diff --git a/tests/pexpects/complete.py b/tests/pexpects/complete.py index 9f3b8ac45..6976c3fd3 100644 --- a/tests/pexpects/complete.py +++ b/tests/pexpects/complete.py @@ -53,7 +53,7 @@ sendline("") # Check cancelling completion acceptance # (bind cancel to something else so we don't have to mess with the escape delay) -sendline("bind \cg cancel") +sendline("bind ctrl-g cancel") sendline("complete -c echo -x -a 'foooo bar'") send("echo fo\t") send("\x07") @@ -70,7 +70,7 @@ send("\x07") sendline("bar") expect_re("foooo bar") -sendline("bind \cg 'commandline -f cancel; commandline \"\"'") +sendline("bind ctrl-g 'commandline -f cancel; commandline \"\"'") send("echo fo\t") expect_re("foooo") send("\x07") diff --git a/tests/pexpects/fg.py b/tests/pexpects/fg.py index 0405a1782..17b480e1a 100644 --- a/tests/pexpects/fg.py +++ b/tests/pexpects/fg.py @@ -22,11 +22,11 @@ testproc = "sleep 500" if platform.system() != "NetBSD" else "cat" sendline(testproc) sendline("set -l foo bar; echo $foo") expect_str("") -sleep(0.2) +sleep(1.2) # ctrl-z - send job to background send("\x1A") -sleep(0.2) +sleep(1.2) expect_prompt() sendline("set -l foo bar; echo $foo") expect_str("bar") @@ -80,7 +80,7 @@ sendline("jobs") expect_prompt("jobs: There are no jobs") # Regression test for #2214: foregrounding from a key binding works! -sendline(r"bind \cr 'fg >/dev/null 2>/dev/null'") +sendline(r"bind ctrl-r 'fg >/dev/null 2>/dev/null'") expect_prompt() sendline("$fish_test_helper print_stop_cont") sleep(0.2) diff --git a/tests/pexpects/fkr.py b/tests/pexpects/fkr.py index 9c29e9bb1..d8c081801 100644 --- a/tests/pexpects/fkr.py +++ b/tests/pexpects/fkr.py @@ -29,38 +29,32 @@ expect_str("Press a key:") # Is a single control char echoed correctly? send("\x07") -expect_str("char: \\cG\r\nbind \\cG 'do something'\r\n") +expect_str("bind ctrl-g 'do something'\r\n") # Is a non-ASCII UTF-8 sequence prefaced by an escape char handled correctly? sleep(0.020) -# send "\x1B\xE1\x88\xB4" -send("\x1B\u1234") -expect_str("char: ሴ\r\nbind \\eሴ 'do something'\r\n") +send("\x1B") +expect_str("bind escape 'do something'\r\n") +send("\u1234") +expect_str("bind ሴ 'do something'\r\n") # Is a NULL char echoed correctly? sleep(0.020) send("\x00") -expect_str("char: \\c@\r\nbind -k nul 'do something'\r\n") +expect_str("bind ctrl-space 'do something'\r\n") -# Ensure we only name the sequence if we match all of it. -# Otherwise we end up calling escape+backspace "backspace"! send("\x1b\x7f") -expect_str("char: \\e\r\n") -expect_str("char: \\x7F") -expect_str("""(aka "del")\r\nbind \\e\\x7F 'do something'\r\n""") +expect_str("bind alt-backspace 'do something'\r\n") send("\x1c") -expect_str(r"char: \c\ (or \x1c)") -expect_str(r"bind \x1c 'do something'") +expect_str(r"bind ctrl-\\ 'do something'") # Does it keep running if handed control sequences in the wrong order? send("\x03") sleep(0.010) send("\x04") -expect_str("char: \\cD\r\n") -# Now send a second [ctrl-D]. Does that terminate the process like it should? +# Now send a second ctrl-d. Does that terminate the process like it should? sleep(0.050) send("\x04\x04") -expect_str("char: \\cD\r\n") expect_str("Exiting at your request.\r\n") diff --git a/tests/pexpects/signals.py b/tests/pexpects/signals.py index 4f8fc1480..c3cb4af94 100644 --- a/tests/pexpects/signals.py +++ b/tests/pexpects/signals.py @@ -13,10 +13,14 @@ send, sendline, sleep, expect_prompt, expect_re, expect_str = ( from time import sleep import os +import platform import signal import subprocess import sys +if platform.system() == "FreeBSD": # Spurious failure. + sys.exit(127) + expect_prompt() # Verify that SIGINT inside a command sub cancels it. diff --git a/tests/pexpects/terminal.py b/tests/pexpects/terminal.py index a8b0309ca..89f9e88c3 100644 --- a/tests/pexpects/terminal.py +++ b/tests/pexpects/terminal.py @@ -24,6 +24,7 @@ expect_prompt() # See if $LINES/$COLUMNS change in response to sigwinch, also in a --on-signal function sendline("function on-winch --on-signal winch; echo $LINES $COLUMNS; end") expect_prompt() +sleep(4) sp.spawn.setwinsize(40, 50) expect_str("40 50") sendline("echo $LINES $COLUMNS") @@ -54,8 +55,9 @@ expect_prompt("0") # a pexpect issue? # So disable it everywhere but linux for now. if platform.system() in ["Linux"]: - # Confirm flow control in the shell is disabled - we should ignore the ctrl-s in there. - sendline("echo hello\x13hello") + # Flow control does not work in CSI u mode, but it works while we are running an external process. + sendline("sleep 2") + sendline("hello\x13hello") # This should not match because we should not get any output. # Unfortunately we have to wait for the timeout to expire - set it to a second. expect_str("hellohello", timeout=1, shouldfail=True) diff --git a/tests/pexpects/torn_escapes.py b/tests/pexpects/torn_escapes.py index 1927268f6..4e1feb2b8 100644 --- a/tests/pexpects/torn_escapes.py +++ b/tests/pexpects/torn_escapes.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import os +import platform import signal +import sys from pexpect_helper import SpawnedProc @@ -13,6 +15,10 @@ send, sendline, sleep, expect_prompt, expect_str, expect_re = ( sp.expect_str, sp.expect_re, ) + +if platform.system() == "FreeBSD": # Spurious failure. + sys.exit(127) + # Ensure that signals don't tear escape sequences. See #8628. expect_prompt() @@ -52,10 +58,11 @@ sendline( ) expect_prompt() -sendline(r"bind abc\edef wacky_handler") +sendline(r"bind a,b,c,\e,d,e,f wacky_handler") expect_prompt() # We can respond to SIGUSR1. +sleep(1) os.kill(sp.spawn.pid, signal.SIGUSR1) expect_str(r"Got SIGUSR1 1") sendline(r"") diff --git a/tests/test.fish b/tests/test.fish index a51aa2822..05df28a1d 100644 --- a/tests/test.fish +++ b/tests/test.fish @@ -42,6 +42,7 @@ if set -q files_to_test[1] --progress $force_color \ -s fish=../test/root/bin/fish \ -s fish_test_helper=../test/root/bin/fish_test_helper \ + -s filter-ctrlseqs='../tests/filter-ctrlseqs.sh ../test/root/bin/fish' \ $files_to_test set -l littlecheck_status $status