Extended & human-friendly keys

See the changelog additions for user-visible changes.

Since we enable/disable terminal protocols whenever we pass terminal ownership,
tests can no longer run in parallel on the same terminal.

For the same reason, readline shortcuts in the gdb REPL will not work anymore.
As a remedy, use gdbserver, or lobby for CSI u support in libreadline.

Add sleep to some tests, otherwise they fall (both in CI and locally).

There are two weird failures on FreeBSD remaining, disable them for now
https://github.com/fish-shell/fish-shell/pull/10359/checks?check_run_id=23330096362

Design and implementation borrows heavily from Kakoune.

In future, we should try to implement more of the kitty progressive
enhancements.

Closes #10359
This commit is contained in:
Johannes Altmanninger 2024-03-30 16:10:12 +01:00
parent 8ada027f05
commit 8bf8b10f68
180 changed files with 2203 additions and 1304 deletions

View file

@ -12,7 +12,7 @@ fish 3.8.0 (released ???)
10198 10200 10201 10204 10210 10214 10219 10223 10227 10232 10235 10237 10243 10244 10245 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 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 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`). 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. This means a large change in dependencies and how to build fish.
@ -21,6 +21,24 @@ Packagers should see the :ref:`For Distributors <rust-packaging>` section at the
Notable backwards-incompatible changes 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. - ``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. Notably, it will now work much more sensibly with very small seeds.
The seed was never guaranteed to give the same result across systems, The seed was never guaranteed to give the same result across systems,
@ -39,7 +57,7 @@ Notable backwards-incompatible changes
Notable improvements and fixes 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`). - 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 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. - 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`). - ``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`). - 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 Scripting improvements
---------------------- ----------------------
@ -78,13 +102,14 @@ Interactive improvements
New or improved bindings 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`). - 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: - 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. - 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`. - 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`). - ``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`). - 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. - 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`). - 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. - 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 Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
- Fish now sets the terminal window title unconditionally (:issue:`10037`). - 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 Other improvements
------------------ ------------------

View file

@ -34,7 +34,12 @@ def get_prompt_re(counter):
"""Return a regular expression for matching a with a given prompt counter.""" """Return a regular expression for matching a with a given prompt counter."""
return re.compile( return re.compile(
r"""(?:\r\n?|^) # beginning of line 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 (?:\[.\]\ )? # optional vi mode prompt
""" """
+ (r"prompt\ %d>" % counter) # prompt with counter + (r"prompt\ %d>" % counter) # prompt with counter

View file

@ -1,13 +1,6 @@
# This adds ctest support to the project # This adds ctest support to the project
enable_testing() 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. # Put in a tests folder to reduce the top level targets in IDEs.
set(CMAKE_FOLDER tests) 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. # 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 # * 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. # "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 # * 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 # 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 # 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". # The top-level test target is "fish_run_tests".
add_custom_target(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} FISH_SOURCE_DIR=${CMAKE_SOURCE_DIR}
${CMAKE_CTEST_COMMAND} --force-new-ctest-process # --verbose ${CMAKE_CTEST_COMMAND} --force-new-ctest-process # --verbose
--output-on-failure --progress --output-on-failure --progress

View file

@ -7,49 +7,67 @@ Synopsis
.. synopsis:: .. synopsis::
bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] [-k | --key] SEQUENCE COMMAND ... bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] KEYS COMMAND ...
bind [(-M | --mode) MODE] [-k | --key] [--preset] [--user] SEQUENCE bind [(-M | --mode) MODE] [--preset] [--user] [KEYS]
bind (-K | --key-names) [-a | --all] [--preset] [--user] bind [-a | --all] [--preset] [--user]
bind (-f | --function-names) bind (-f | --function-names)
bind (-L | --list-modes) 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 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 <escapes>`. 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 <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 <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 <special-input-functions>` for a list of these input functions. ``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 <special-input-functions>` for a list of these input functions.
.. note:: .. note::
If a script changes the commandline, it should finish by calling the ``repaint`` special input function. 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``. 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 <configuration>`. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists. To save custom key bindings, put the ``bind`` statements into :ref:`config.fish <configuration>`. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists.
Options Options
------- -------
The following options are available: 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** **-f** or **--function-names**
Display a list of available input functions 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. Specifying **-a** or **--all** without **-M** or **--mode** erases all binds in all modes regardless of sequence.
**-a** or **--all** **-a** or **--all**
See **--erase** and **--key-names** See **--erase**
**--preset** and **--user** **--preset** and **--user**
Specify if bind should operate on user or preset bindings. 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:: 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:: Perform a history search when :kbd:`Page Up` is pressed::

View file

@ -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 <bind>` command for one key sequence read interactively over standard input. :program:`fish_key_reader` is used to explain how you would bind a certain key sequence. By default, it prints the :doc:`bind <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: The following options are available:
**-c** or **--continuous** **-c** or **--continuous**
Begins a session where multiple key sequences can be inspected. By default the program exits after capturing a single key sequence. 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** **-h** or **--help**
Displays help about using this command. Displays help about using this command.
@ -34,8 +29,6 @@ The following options are available:
Usage Notes 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: ``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 - press :kbd:`Control`\ +\ :kbd:`C` twice, or
@ -51,12 +44,4 @@ Example
> fish_key_reader > fish_key_reader
Press a key: Press a key:
# press up-arrow # press up-arrow
bind \e\[A 'do something' bind up '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'

View file

@ -534,38 +534,29 @@ Custom bindings
In addition to the standard bindings listed here, you can also define your own with :doc:`bind <cmds/bind>`:: In addition to the standard bindings listed here, you can also define your own with :doc:`bind <cmds/bind>`::
# Just clear the commandline on control-c # Just clear the commandline on control-c
bind \cc 'commandline -r ""' bind ctrl-c 'commandline -r ""'
Put ``bind`` statements into :ref:`config.fish <configuration>` or a function called ``fish_user_key_bindings``. Put ``bind`` statements into :ref:`config.fish <configuration>` 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:: 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. 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 <vi-mode>`, note that ``bind`` will by default bind keys in :ref:`command mode <vi-mode-command>`. To bind something in :ref:`insert mode <vi-mode-insert>`:: If you use :ref:`vi bindings <vi-mode>`, note that ``bind`` will by default bind keys in :ref:`command mode <vi-mode-command>`. To bind something in :ref:`insert mode <vi-mode-insert>`::
bind --mode insert \cc 'commandline -r ""' bind --mode insert ctrl-c 'commandline -r ""'
.. _interactive-key-sequences: .. _interactive-key-sequences:
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 <cmds/fish_key_reader>`.
In these cases, :doc:`fish_key_reader <cmds/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 # Press Alt + right-arrow
> fish_key_reader # pressing control-c
Press a key: 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. 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. 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`:: 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", # If you kept it like that, every time you press "j",
# fish would wait for a "k" or other key to disambiguate # 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", # After setting this, fish only waits 200ms for the "k",
# or decides to treat the "j" as a separate sequence, inserting it. # or decides to treat the "j" as a separate sequence, inserting it.

View file

@ -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 preset -d 'Operate on preset bindings'
complete -c bind -l user -d 'Operate on user 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 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

View file

@ -191,52 +191,6 @@ end" >$__fish_config_dir/config.fish
# Load key bindings # Load key bindings
__fish_reload_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 # Detect whether the terminal reflows on its own
# If it does we shouldn't do it. # If it does we shouldn't do it.
# Allow $fish_handle_reflow to override it. # Allow $fish_handle_reflow to override it.

View file

@ -18,8 +18,6 @@ function __fish_edit_command_if_at_cursor --description 'If cursor is at the com
set -l editor (__fish_anyeditor) set -l editor (__fish_anyeditor)
or return 0 # We already printed a warning, so tell the caller to finish. or return 0 # We already printed a warning, so tell the caller to finish.
__fish_disable_bracketed_paste
$editor $command_path $editor $command_path
__fish_enable_bracketed_paste
return 0 return 0
end end

View file

@ -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

View file

@ -1,4 +1,5 @@
function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mode" 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. # 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). # 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. # 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 return 1
end end
bind --preset $argv \cy yank bind --preset $argv ctrl-y yank
or return # protect against invalid $argv or return # protect against invalid $argv
bind --preset $argv \ey yank-pop bind --preset $argv alt-y yank-pop
# Left/Right arrow # Left/Right arrow
bind --preset $argv -k right forward-char bind --preset $argv right forward-char
bind --preset $argv -k left backward-char bind --preset $argv left backward-char
bind --preset $argv \e\[C forward-char $legacy_bind --preset $argv -k right forward-char
bind --preset $argv \e\[D backward-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. # Some terminals output these when they're in in keypad mode.
bind --preset $argv \eOC forward-char $legacy_bind --preset $argv \eOC forward-char
bind --preset $argv \eOD backward-char $legacy_bind --preset $argv \eOD backward-char
# Ctrl-left/right - these also work in vim. # Ctrl-left/right - these also work in vim.
bind --preset $argv \e\[1\;5C forward-word bind --preset $argv ctrl-right forward-word
bind --preset $argv \e\[1\;5D backward-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 pageup beginning-of-history
bind --preset $argv -k npage end-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. # Interaction with the system clipboard.
bind --preset $argv \cx fish_clipboard_copy bind --preset $argv ctrl-x fish_clipboard_copy
bind --preset $argv \cv fish_clipboard_paste bind --preset $argv ctrl-v fish_clipboard_paste
bind --preset $argv \e cancel bind --preset $argv escape cancel
bind --preset $argv \t complete bind --preset $argv ctrl-\[ cancel
bind --preset $argv \cs pager-toggle-search 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. # shift-tab does a tab complete followed by a search.
bind --preset $argv --key btab complete-and-search bind --preset $argv shift-tab complete-and-search
bind --preset $argv -k sdc history-pager-delete or backward-delete-char # shifted delete $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 down down-or-search
bind --preset $argv -k up up-or-search $legacy_bind --preset $argv -k down down-or-search
bind --preset $argv \e\[A up-or-search bind --preset $argv up up-or-search
bind --preset $argv \e\[B down-or-search $legacy_bind --preset $argv -k up up-or-search
bind --preset $argv \eOA up-or-search $legacy_bind --preset $argv \e\[A up-or-search
bind --preset $argv \eOB down-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 shift-right forward-bigword
bind --preset $argv -k sleft backward-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 alt-right nextd-or-forward-word
bind --preset $argv \e\eOC nextd-or-forward-word bind --preset $argv alt-left prevd-or-backward-word
bind --preset $argv \e\eOD prevd-or-backward-word $legacy_bind --preset $argv \e\eOC nextd-or-forward-word
bind --preset $argv \e\e\[C nextd-or-forward-word $legacy_bind --preset $argv \e\eOD prevd-or-backward-word
bind --preset $argv \e\e\[D prevd-or-backward-word $legacy_bind --preset $argv \e\e\[C nextd-or-forward-word
bind --preset $argv \eO3C nextd-or-forward-word $legacy_bind --preset $argv \e\e\[D prevd-or-backward-word
bind --preset $argv \eO3D prevd-or-backward-word $legacy_bind --preset $argv \eO3C nextd-or-forward-word
bind --preset $argv \e\[3C nextd-or-forward-word $legacy_bind --preset $argv \eO3D prevd-or-backward-word
bind --preset $argv \e\[3D prevd-or-backward-word $legacy_bind --preset $argv \e\[3C nextd-or-forward-word
bind --preset $argv \e\[1\;3C nextd-or-forward-word $legacy_bind --preset $argv \e\[3D prevd-or-backward-word
bind --preset $argv \e\[1\;3D prevd-or-backward-word $legacy_bind --preset $argv \e\[1\;3C nextd-or-forward-word
bind --preset $argv \e\[1\;9C nextd-or-forward-word #iTerm2 $legacy_bind --preset $argv \e\[1\;3D prevd-or-backward-word
bind --preset $argv \e\[1\;9D prevd-or-backward-word #iTerm2 $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 alt-up history-token-search-backward
bind --preset $argv \e\eOA history-token-search-backward bind --preset $argv alt-down history-token-search-forward
bind --preset $argv \e\eOB history-token-search-forward $legacy_bind --preset $argv \e\eOA history-token-search-backward
bind --preset $argv \e\e\[A history-token-search-backward $legacy_bind --preset $argv \e\eOB history-token-search-forward
bind --preset $argv \e\e\[B history-token-search-forward $legacy_bind --preset $argv \e\e\[A history-token-search-backward
bind --preset $argv \eO3A history-token-search-backward $legacy_bind --preset $argv \e\e\[B history-token-search-forward
bind --preset $argv \eO3B history-token-search-forward $legacy_bind --preset $argv \eO3A history-token-search-backward
bind --preset $argv \e\[3A history-token-search-backward $legacy_bind --preset $argv \eO3B history-token-search-forward
bind --preset $argv \e\[3B history-token-search-forward $legacy_bind --preset $argv \e\[3A history-token-search-backward
bind --preset $argv \e\[1\;3A history-token-search-backward $legacy_bind --preset $argv \e\[3B history-token-search-forward
bind --preset $argv \e\[1\;3B history-token-search-forward $legacy_bind --preset $argv \e\[1\;3A history-token-search-backward
bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2 $legacy_bind --preset $argv \e\[1\;3B history-token-search-forward
bind --preset $argv \e\[1\;9B history-token-search-forward # iTerm2 $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 # Bash compatibility
# https://github.com/fish-shell/fish-shell/issues/89 # 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 alt-l __fish_list_current_token
bind --preset $argv \eo __fish_preview_current_file bind --preset $argv alt-o __fish_preview_current_file
bind --preset $argv \ew __fish_whatis_current_token bind --preset $argv alt-w __fish_whatis_current_token
bind --preset $argv \cl clear-screen bind --preset $argv ctrl-l clear-screen
bind --preset $argv \cc cancel-commandline bind --preset $argv ctrl-c cancel-commandline
bind --preset $argv \cu backward-kill-line bind --preset $argv ctrl-u backward-kill-line
bind --preset $argv \cw backward-kill-path-component bind --preset $argv ctrl-w backward-kill-path-component
bind --preset $argv \e\[F end-of-line bind --preset $argv end end-of-line
bind --preset $argv \e\[H beginning-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 alt-d '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 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). # 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 F1 __fish_man_page
bind --preset $argv \eh __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 # This will make sure the output of the current command is paged using the default pager when
# you press Meta-p. # you press Meta-p.
# If none is set, less will be used. # 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, # 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. # 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. # These keystrokes invoke an external editor on the command buffer.
bind --preset $argv \ee edit_command_buffer bind --preset $argv alt-e edit_command_buffer
bind --preset $argv \ev edit_command_buffer bind --preset $argv alt-v 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
# Bindings that are shared in text-insertion modes. # Bindings that are shared in text-insertion modes.
if not set -l index (contains --index -- -M $argv) 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 ">" 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 # 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 # Shift+Return CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1
bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n" $legacy_bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n"
bind --preset $argv \e\n expand-abbr "commandline -i \n" bind --preset $argv alt-enter expand-abbr "commandline -i \n"
bind --preset $argv \e\r expand-abbr "commandline -i \n"
# Closing a command substitution expands abbreviations # Closing a command substitution expands abbreviations
bind --preset $argv ")" self-insert expand-abbr bind --preset $argv ")" self-insert expand-abbr
# Ctrl-space inserts space without expanding abbrs # 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 " "' 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. # Shift-space behaves like space because it's easy to mistype.
bind --preset $argv \e\[32\;2u 'commandline -i " "; commandline -f expand-abbr' 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 enter execute
bind --preset $argv \r 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. # 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 ctrl-enter execute
bind --preset $argv \e\[13\;5u execute # CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1 $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
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

View file

@ -86,13 +86,10 @@ function edit_command_buffer --description 'Edit the command buffer in an extern
set -a editor $f set -a editor $f
end end
__fish_disable_bracketed_paste
$editor $editor
set -l editor_status $status
__fish_enable_bracketed_paste
# Here we're checking the exit status of the editor. # 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 # Set the command to the output of the edited command and move the cursor to the
# end of the edited command. # end of the edited command.
commandline -r -- (command cat $f) commandline -r -- (command cat $f)

View file

@ -23,49 +23,5 @@ function fish_clipboard_paste
return return
end end
# Also split on \r, otherwise it looks confusing __fish_paste $data
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
end end

View file

@ -1,4 +1,5 @@
function fish_default_key_bindings -d "emacs-like key binds" function fish_default_key_bindings -d "emacs-like key binds"
set -l legacy_bind bind
if contains -- -h $argv if contains -- -h $argv
or contains -- --help $argv or contains -- --help $argv
echo "Sorry but this function doesn't support -h or --help" 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 __fish_shared_key_bindings $argv
or return # protect against invalid $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 right forward-char
bind --preset $argv \eOD backward-char bind --preset $argv left backward-char
bind --preset $argv \e\[C forward-char $legacy_bind --preset $argv \eOC forward-char
bind --preset $argv \e\[D backward-char $legacy_bind --preset $argv \eOD backward-char
bind --preset $argv -k right forward-char $legacy_bind --preset $argv \e\[C forward-char
bind --preset $argv -k left backward-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 delete delete-char
bind --preset $argv -k backspace backward-delete-char bind --preset $argv backspace backward-delete-char
bind --preset $argv \x7f backward-delete-char bind --preset $argv shift-backspace backward-delete-char
# for PuTTY # for PuTTY
# https://github.com/fish-shell/fish-shell/issues/180 # https://github.com/fish-shell/fish-shell/issues/180
bind --preset $argv \e\[1~ beginning-of-line $legacy_bind --preset $argv \e\[1~ beginning-of-line
bind --preset $argv \e\[3~ delete-char $legacy_bind --preset $argv \e\[3~ delete-char
bind --preset $argv \e\[4~ end-of-line $legacy_bind --preset $argv \e\[4~ end-of-line
bind --preset $argv -k home beginning-of-line bind --preset $argv home beginning-of-line
bind --preset $argv -k end end-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 ctrl-a beginning-of-line
bind --preset $argv \ce end-of-line bind --preset $argv ctrl-e end-of-line
bind --preset $argv \ch backward-delete-char bind --preset $argv ctrl-h backward-delete-char
bind --preset $argv \cp up-or-search bind --preset $argv ctrl-p up-or-search
bind --preset $argv \cn down-or-search bind --preset $argv ctrl-n down-or-search
bind --preset $argv \cf forward-char bind --preset $argv ctrl-f forward-char
bind --preset $argv \cb backward-char bind --preset $argv ctrl-b backward-char
bind --preset $argv \ct transpose-chars bind --preset $argv ctrl-t transpose-chars
bind --preset $argv \cg cancel bind --preset $argv ctrl-g cancel
bind --preset $argv \c_ undo bind --preset $argv ctrl-/ undo
bind --preset $argv \cz undo bind --preset $argv ctrl-_ undo # XTerm idiosyncracy, can get rid of this once we go full CSI u
bind --preset $argv \e/ redo bind --preset $argv ctrl-z undo
bind --preset $argv \et transpose-words bind --preset $argv alt-/ redo
bind --preset $argv \eu upcase-word bind --preset $argv alt-t transpose-words
bind --preset $argv alt-u upcase-word
# This clashes with __fish_list_current_token bind --preset $argv alt-c capitalize-word
# bind --preset $argv \el downcase-word bind --preset $argv alt-backspace backward-kill-word
bind --preset $argv \ec capitalize-word bind --preset $argv alt-b backward-word
# One of these is alt+backspace. bind --preset $argv alt-f forward-word
bind --preset $argv \e\x7f backward-kill-word if test "$TERM_PROGRAM" = Apple_Terminal
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
# Terminal.app sends \eb for alt+left, \ef for alt+right. # Terminal.app sends \eb for alt+left, \ef for alt+right.
# Yeah. # Yeah.
bind --preset $argv \eb prevd-or-backward-word $legacy_bind --preset $argv alt-b prevd-or-backward-word
bind --preset $argv \ef nextd-or-forward-word $legacy_bind --preset $argv alt-f nextd-or-forward-word
end end
bind --preset $argv \e\< beginning-of-buffer bind --preset $argv alt-\< beginning-of-buffer
bind --preset $argv \e\> end-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 # term-specific special bindings
switch "$TERM" 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; # 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. # the major effect is that several keys do not work as intended.
# This is a workaround, there will be additions in he future. # This is a workaround, there will be additions in he future.
bind --preset $argv \e\[P delete-char $legacy_bind --preset $argv \e\[P delete-char
bind --preset $argv \e\[Z up-line $legacy_bind --preset $argv \e\[Z up-line
case 'rxvt*' case 'rxvt*'
bind --preset $argv \e\[8~ end-of-line $legacy_bind --preset $argv \e\[8~ end-of-line
bind --preset $argv \eOc forward-word $legacy_bind --preset $argv \eOc forward-word
bind --preset $argv \eOd backward-word $legacy_bind --preset $argv \eOd backward-word
case xterm-256color case xterm-256color
# Microsoft's conemu uses xterm-256color plus # Microsoft's conemu uses xterm-256color plus
# the following to tell a console to paste: # 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 end
set -e -g fish_cursor_selection_mode set -e -g fish_cursor_selection_mode

View file

@ -1,4 +1,5 @@
function fish_vi_key_bindings --description 'vi-like key bindings for fish' function fish_vi_key_bindings --description 'vi-like key bindings for fish'
set -l legacy_bind bind
if contains -- -h $argv if contains -- -h $argv
or contains -- --help $argv or contains -- --help $argv
echo "Sorry but this function doesn't support -h or --help" >&2 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 set -l init_mode insert
# These are only the special vi-style keys # These are only the special vi-style keys
# not end/home, we share those. # not end/home, we share those.
set -l eol_keys \$ g\$ set -l eol_keys \$ g,\$
set -l bol_keys \^ 0 g\^ set -l bol_keys \^ 0 g\^
if contains -- $argv[1] insert default visual 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. # Add a way to switch from insert to normal (command) mode.
# Note if we are paging, we want to stay in insert mode # Note if we are paging, we want to stay in insert mode
# See #2871 # See #2871
bind -s --preset -M insert \e ' set -l on_escape '
if commandline -P if commandline -P
commandline -f cancel commandline -f cancel
else else
@ -67,23 +68,26 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
commandline -f repaint-mode commandline -f repaint-mode
end end
' '
bind -s --preset -M insert escape $on_escape
bind -s --preset -M insert ctrl-\[ $on_escape
# Default (command) mode # Default (command) mode
bind -s --preset :q exit bind -s --preset :,q exit
bind -s --preset -m insert \cc cancel-commandline repaint-mode bind -s --preset -m insert ctrl-c cancel-commandline repaint-mode
bind -s --preset -M default h backward-char bind -s --preset -M default h backward-char
bind -s --preset -M default l forward-char bind -s --preset -M default l forward-char
bind -s --preset -m insert \n execute bind -s --preset -m insert enter execute
bind -s --preset -m insert \r 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_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 repaint-mode
bind -s --preset -m insert I beginning-of-line 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' 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 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 -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 bind -s --preset G end-of-buffer
for key in $eol_keys for key in $eol_keys
@ -94,7 +98,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
end end
bind -s --preset u undo 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-backward
bind -s --preset ] history-token-search-forward 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 j down-or-search
bind -s --preset b backward-word bind -s --preset b backward-word
bind -s --preset B backward-bigword bind -s --preset B backward-bigword
bind -s --preset ge backward-word bind -s --preset g,e backward-word
bind -s --preset gE backward-bigword bind -s --preset g,E backward-bigword
bind -s --preset w forward-word forward-single-char bind -s --preset w forward-word forward-single-char
bind -s --preset W forward-bigword 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-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 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. # 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 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' 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. # 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 insert backspace backward-delete-char
bind -s --preset -M default -k backspace backward-char bind -s --preset -M insert shift-backspace backward-delete-char
bind -s --preset -M insert \ch backward-delete-char $legacy_bind -s --preset -M insert -k backspace backward-delete-char
bind -s --preset -M default \ch backward-char bind -s --preset -M default backspace backward-char
bind -s --preset -M insert \x7f backward-delete-char $legacy_bind -s --preset -M default -k backspace backward-char
bind -s --preset -M default \x7f 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\$ kill-line bind -s --preset d,\$ kill-line
bind -s --preset d\^ backward-kill-line bind -s --preset d,\^ backward-kill-line
bind -s --preset d0 backward-kill-line bind -s --preset d,0 backward-kill-line
bind -s --preset dw kill-word bind -s --preset d,w kill-word
bind -s --preset dW kill-bigword bind -s --preset d,W kill-bigword
bind -s --preset diw forward-single-char forward-single-char backward-word kill-word bind -s --preset d,i,w 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 d,i,W 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 d,a,w 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 d,a,W forward-single-char forward-single-char backward-bigword kill-bigword
bind -s --preset de kill-word bind -s --preset d,e kill-word
bind -s --preset dE kill-bigword bind -s --preset d,E kill-bigword
bind -s --preset db backward-kill-word bind -s --preset d,b backward-kill-word
bind -s --preset dB backward-kill-bigword bind -s --preset d,B backward-kill-bigword
bind -s --preset dge backward-kill-word bind -s --preset d,g,e backward-kill-word
bind -s --preset dgE backward-kill-bigword bind -s --preset d,g,E backward-kill-bigword
bind -s --preset df begin-selection forward-jump kill-selection end-selection bind -s --preset d,f 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 d,t 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 d,F 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 d,T begin-selection backward-jump forward-single-char kill-selection end-selection
bind -s --preset dh backward-char delete-char bind -s --preset d,h backward-char delete-char
bind -s --preset dl delete-char bind -s --preset d,l 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 d,i 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,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,;' 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,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 delete-char repaint-mode
bind -s --preset -m insert S kill-inner-line 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\$ 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 c,\^ backward-kill-line repaint-mode
bind -s --preset -m insert c0 backward-kill-line repaint-mode bind -s --preset -m insert c,0 backward-kill-line repaint-mode
bind -s --preset -m insert cw kill-word repaint-mode bind -s --preset -m insert c,w kill-word repaint-mode
bind -s --preset -m insert cW kill-bigword repaint-mode bind -s --preset -m insert c,W 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 c,i,w 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 c,i,W 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 c,a,w 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 c,a,W 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 c,e kill-word repaint-mode
bind -s --preset -m insert cE kill-bigword repaint-mode bind -s --preset -m insert c,E kill-bigword repaint-mode
bind -s --preset -m insert cb backward-kill-word repaint-mode bind -s --preset -m insert c,b backward-kill-word repaint-mode
bind -s --preset -m insert cB backward-kill-bigword repaint-mode bind -s --preset -m insert c,B backward-kill-bigword repaint-mode
bind -s --preset -m insert cge backward-kill-word repaint-mode bind -s --preset -m insert c,g,e backward-kill-word repaint-mode
bind -s --preset -m insert cgE backward-kill-bigword repaint-mode bind -s --preset -m insert c,g,E 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 c,f 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 c,t 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 c,F 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 c,T 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 c,h 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 c,l 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 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 ca backward-jump 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 '~' togglecase-char forward-single-char
bind -s --preset gu downcase-word bind -s --preset g,u downcase-word
bind -s --preset gU upcase-word bind -s --preset g,U upcase-word
bind -s --preset J end-of-line delete-char 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 K 'man (commandline -t) 2>/dev/null; or echo -n \a'
bind -s --preset yy kill-whole-line yank 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 bind -s --preset $seq fish_clipboard_copy
end end
bind -s --preset Y kill-whole-line yank bind -s --preset Y kill-whole-line yank
bind -s --preset y\$ kill-line yank bind -s --preset y,\$ kill-line yank
bind -s --preset y\^ backward-kill-line yank bind -s --preset y,\^ backward-kill-line yank
bind -s --preset y0 backward-kill-line yank bind -s --preset y,0 backward-kill-line yank
bind -s --preset yw kill-word yank bind -s --preset y,w kill-word yank
bind -s --preset yW kill-bigword yank bind -s --preset y,W kill-bigword yank
bind -s --preset yiw forward-single-char forward-single-char backward-word kill-word yank bind -s --preset y,i,w 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 y,i,W 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 y,a,w 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 y,a,W forward-single-char forward-single-char backward-bigword kill-bigword yank
bind -s --preset ye kill-word yank bind -s --preset y,e kill-word yank
bind -s --preset yE kill-bigword yank bind -s --preset y,E kill-bigword yank
bind -s --preset yb backward-kill-word yank bind -s --preset y,b backward-kill-word yank
bind -s --preset yB backward-kill-bigword yank bind -s --preset y,B backward-kill-bigword yank
bind -s --preset yge backward-kill-word yank bind -s --preset y,g,e backward-kill-word yank
bind -s --preset ygE backward-kill-bigword yank bind -s --preset y,g,E backward-kill-bigword yank
bind -s --preset yf begin-selection forward-jump kill-selection yank end-selection bind -s --preset y,f 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 y,t 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 y,F 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 y,T 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 y,h backward-char begin-selection kill-selection yank end-selection
bind -s --preset yl begin-selection kill-selection yank end-selection bind -s --preset y,l 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 y,i 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,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 forward-jump
bind -s --preset F backward-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 # \ 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 '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 P yank
bind -s --preset gp yank-pop bind -s --preset g,p yank-pop
# same vim 'pasting' note as upper # 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' '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' 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' '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' fish_clipboard_paste
# #
# Lowercase r, enters replace_one mode # Lowercase r, enters replace_one mode
# #
bind -s --preset -m replace_one r repaint-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 '' 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 enter '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 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 # Uppercase R, enters replace mode
# #
bind -s --preset -m replace R repaint-mode bind -s --preset -m replace R repaint-mode
bind -s --preset -M replace '' delete-char self-insert 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 insert enter execute repaint-mode
bind -s --preset -M replace -m default \e cancel 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), <BS> deletes the changes # in vim (and maybe in vi), <BS> deletes the changes
# but this binding just move cursor backward, not delete 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 # 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-word
bind -s --preset -M visual B backward-bigword bind -s --preset -M visual B backward-bigword
bind -s --preset -M visual ge backward-word bind -s --preset -M visual g,e backward-word
bind -s --preset -M visual gE backward-bigword bind -s --preset -M visual g,E backward-bigword
bind -s --preset -M visual w forward-word bind -s --preset -M visual w forward-word
bind -s --preset -M visual W forward-bigword 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' 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-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 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 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 '~' 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 ctrl-c end-selection repaint-mode
bind -s --preset -M visual -m default \e 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 # 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. # the commenting chars so the command can be further edited then executed.

View file

@ -7,12 +7,7 @@
//! //!
//! Type "exit" or "quit" to terminate the program. //! Type "exit" or "quit" to terminate the program.
use core::panic; use std::{ops::ControlFlow, os::unix::prelude::OsStrExt};
use std::{
ops::ControlFlow,
os::unix::prelude::OsStrExt,
time::{Duration, Instant},
};
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR}; use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
@ -22,16 +17,15 @@ use fish::{
builtins::shared::BUILTIN_ERR_UNKNOWN, builtins::shared::BUILTIN_ERR_UNKNOWN,
common::{shell_modes, str2wcstring, PROGRAM_NAME}, common::{shell_modes, str2wcstring, PROGRAM_NAME},
env::env_init, env::env_init,
eprintf, eprintf, fprintf,
fallback::fish_wcwidth,
fprintf,
input::input_terminfo_get_name, input::input_terminfo_get_name,
input_common::{CharEvent, InputEventQueue, InputEventQueuer}, input_common::{CharEvent, InputEventQueue, InputEventQueuer},
key::Key,
panic::panic_handler, panic::panic_handler,
print_help::print_help, print_help::print_help,
printf, printf,
proc::set_interactive_session, 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, signal::signal_set_handlers,
threads, threads,
topic_monitor::topic_monitor_init, topic_monitor::topic_monitor_init,
@ -40,15 +34,15 @@ use fish::{
}; };
/// Return true if the recent sequence of characters indicates the user wants to exit the program. /// Return true if the recent sequence of characters indicates the user wants to exit the program.
fn should_exit(recent_chars: &mut Vec<u8>, c: char) -> bool { fn should_exit(recent_keys: &mut Vec<Key>, key: Key) -> bool {
let c = if c < '\u{80}' { c as u8 } else { 0 }; recent_keys.push(key);
recent_chars.push(c);
for evt in [VINTR, VEOF] { for evt in [VINTR, VEOF] {
let modes = shell_modes(); let modes = shell_modes();
if c == modes.c_cc[evt] { let cc = Key::from_single_byte(modes.c_cc[evt]);
if recent_chars.iter().rev().nth(1) == Some(&modes.c_cc[evt]) {
if key == cc {
if recent_keys.iter().rev().nth(1) == Some(&cc) {
return true; return true;
} }
eprintf!( eprintf!(
@ -59,7 +53,15 @@ fn should_exit(recent_chars: &mut Vec<u8>, 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. /// 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<u8>, c: char) -> Option<WString> {
input_terminfo_get_name(&str2wcstring(recent_chars)) 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 fn output_bind_command(bind_chars: &mut Vec<(Key, WString)>) {
/// 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<char>) {
bind_chars.push(c);
}
fn output_bind_command(bind_chars: &mut Vec<char>) {
if !bind_chars.is_empty() { if !bind_chars.is_empty() {
printf!("bind "); printf!("bind ");
for &bind_char in &*bind_chars { for (key, _seq) in &*bind_chars {
printf!("%s", char_to_symbol(bind_char, true)); printf!("%s", key);
} }
printf!(" 'do something'\n"); printf!(" 'do something'\n");
bind_chars.clear(); 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<u8>, c: char) -> bool { fn output_matching_key_name(recent_chars: &mut Vec<u8>, c: char) -> bool {
if let Some(name) = sequence_name(recent_chars, c) { if let Some(name) = sequence_name(recent_chars, c) {
printf!("bind -k %ls 'do something'\n", name); printf!("bind -k %ls 'do something'\n", name);
@ -198,72 +101,29 @@ fn output_matching_key_name(recent_chars: &mut Vec<u8>, c: char) -> bool {
false 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. /// 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 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 queue = InputEventQueue::new(STDIN_FILENO);
let mut bind_chars = vec![]; let mut bind_chars = vec![];
let mut recent_chars1 = vec![]; let mut recent_chars1 = vec![];
let mut recent_chars2 = vec![]; let mut recent_chars2 = vec![];
eprintf!("Press a key:\n"); eprintf!("Press a key:\n");
while !check_exit_loop_maybe_warning(None) { while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
let evt = if reader_test_and_clear_interrupted() != 0 { let evt = queue.readch();
Some(CharEvent::from_char(char::from(shell_modes().c_cc[VINTR])))
} else {
queue.readch_timed_esc()
};
if evt.as_ref().is_none_or(|evt| !evt.is_char()) { let CharEvent::Key(kevt) = evt else {
output_bind_command(&mut bind_chars);
if first_char_seen && !continuous_mode {
return 0;
}
continue; continue;
} };
let evt = evt.unwrap(); let c = kevt.key.codepoint;
bind_chars.push((kevt.key, kevt.seq));
let c = evt.get_char().unwrap(); output_bind_command(&mut bind_chars);
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);
}
if output_matching_key_name(&mut recent_chars1, c) { if output_matching_key_name(&mut recent_chars1, c) {
output_bind_command(&mut bind_chars); 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"); eprintf!("\nExiting at your request.\n");
break; 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. /// 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); set_interactive_session(true);
topic_monitor_init(); topic_monitor_init();
threads::init(); threads::init();
@ -298,16 +158,16 @@ fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> i32 {
eprintf!("\n"); eprintf!("\n");
} }
process_input(continuous_mode, verbose) process_input(continuous_mode)
} }
fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> ControlFlow<i32> { fn parse_flags(continuous_mode: &mut bool) -> ControlFlow<i32> {
let short_opts: &wstr = L!("+chvV"); let short_opts: &wstr = L!("+chvV");
let long_opts: &[woption] = &[ let long_opts: &[woption] = &[
wopt(L!("continuous"), woption_argument_t::no_argument, 'c'), wopt(L!("continuous"), woption_argument_t::no_argument, 'c'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'), wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("version"), woption_argument_t::no_argument, 'v'), 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<WString> = std::env::args_os() let args: Vec<WString> = std::env::args_os()
@ -335,9 +195,7 @@ fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> ControlFlow<i3
); );
return ControlFlow::Break(0); return ControlFlow::Break(0);
} }
'V' => { 'V' => {}
*verbose = true;
}
'?' => { '?' => {
printf!( printf!(
"%s", "%s",
@ -369,9 +227,8 @@ fn main() {
fn throwing_main() -> i32 { fn throwing_main() -> i32 {
let mut continuous_mode = false; 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; return i;
} }
@ -380,5 +237,5 @@ fn throwing_main() -> i32 {
return 1; return 1;
} }
setup_and_process_keys(continuous_mode, verbose) setup_and_process_keys(continuous_mode)
} }

View file

@ -6,9 +6,10 @@ use crate::common::{
}; };
use crate::highlight::{colorize, highlight_shell}; use crate::highlight::{colorize, highlight_shell};
use crate::input::{ 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, input_terminfo_get_sequence, GetSequenceError, InputMappingSet,
}; };
use crate::key::{self, canonicalize_raw_escapes, parse_keys, Key};
use crate::nix::isatty; use crate::nix::isatty;
use std::sync::MutexGuard; use std::sync::MutexGuard;
@ -75,7 +76,7 @@ impl BuiltinBind {
/// Returns false if no binding with that sequence and mode exists. /// Returns false if no binding with that sequence and mode exists.
fn list_one( fn list_one(
&self, &self,
seq: &wstr, seq: &[Key],
bind_mode: &wstr, bind_mode: &wstr,
user: bool, user: bool,
parser: &Parser, parser: &Parser,
@ -83,11 +84,16 @@ impl BuiltinBind {
) -> bool { ) -> bool {
let mut ecmds: &[_] = &[]; let mut ecmds: &[_] = &[];
let mut sets_mode = None; let mut sets_mode = None;
let mut terminfo_name = None;
let mut out = WString::new(); let mut out = WString::new();
if !self if !self.input_mappings.get(
.input_mappings seq,
.get(seq, bind_mode, &mut ecmds, user, &mut sets_mode) bind_mode,
{ &mut ecmds,
user,
&mut sets_mode,
&mut terminfo_name,
) {
return false; return false;
} }
@ -109,16 +115,22 @@ impl BuiltinBind {
} }
} }
// Append the name. if let Some(tname) = terminfo_name {
if let Some(tname) = input_terminfo_get_name(seq) {
// Note that we show -k here because we have an input key name. // Note that we show -k here because we have an input key name.
out.push_str(" -k "); out.push_str(" -k ");
out.push_utfstr(&tname); out.push_utfstr(&tname);
} else { } else {
// No key name, so no -k; we show the escape sequence directly.
let eseq = escape(seq);
out.push(' '); 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. // Now show the list of commands.
@ -144,7 +156,7 @@ impl BuiltinBind {
// Returns false only if neither exists. // Returns false only if neither exists.
fn list_one_user_andor_preset( fn list_one_user_andor_preset(
&self, &self,
seq: &wstr, seq: &[Key],
bind_mode: &wstr, bind_mode: &wstr,
user: bool, user: bool,
preset: bool, preset: bool,
@ -224,41 +236,53 @@ impl BuiltinBind {
cmds: &[&wstr], cmds: &[&wstr],
mode: WString, mode: WString,
sets_mode: Option<WString>, sets_mode: Option<WString>,
terminfo: bool, is_terminfo_key: bool,
user: bool, user: bool,
streams: &mut IoStreams, streams: &mut IoStreams,
) -> bool { ) -> bool {
let cmds = cmds.iter().map(|&s| s.to_owned()).collect(); let cmds = cmds.iter().map(|&s| s.to_owned()).collect();
if terminfo { let Some(key_seq) = self.compute_seq(streams, seq) else {
if let Some(seq2) = self.get_terminfo_sequence(seq, streams) { return true;
self.input_mappings.add(seq2, cmds, mode, sets_mode, user); };
} else { self.input_mappings.add(
return true; key_seq,
} is_terminfo_key.then(|| seq.to_owned()),
} else { cmds,
self.input_mappings mode,
.add(seq.to_owned(), cmds, mode, sets_mode, user) sets_mode,
} user,
);
false false
} }
fn compute_seq(&self, streams: &mut IoStreams, seq: &wstr) -> Option<Vec<Key>> {
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 /// Erase specified key bindings
/// ///
/// @param seq /// @param seq
/// an array of all key bindings to erase /// an array of all key bindings to erase
/// @param all /// @param all
/// if specified, _all_ key bindings will be erased /// if specified, _all_ key bindings will be erased
/// @param use_terminfo
/// Whether to look use terminfo -k name
/// ///
fn erase( fn erase(&mut self, seq: &[&wstr], all: bool, user: bool, streams: &mut IoStreams) -> bool {
&mut self,
seq: &[&wstr],
all: bool,
use_terminfo: bool,
user: bool,
streams: &mut IoStreams,
) -> bool {
let mode = if self.opts.bind_mode_given { let mode = if self.opts.bind_mode_given {
Some(self.opts.bind_mode.as_utfstr()) Some(self.opts.bind_mode.as_utfstr())
} else { } else {
@ -270,21 +294,15 @@ impl BuiltinBind {
return false; return false;
} }
let mut res = false;
let mode = mode.unwrap_or(DEFAULT_BIND_MODE); let mode = mode.unwrap_or(DEFAULT_BIND_MODE);
for s in seq { for s in seq {
if use_terminfo { let Some(s) = self.compute_seq(streams, s) else {
if let Some(seq2) = self.get_terminfo_sequence(s, streams) { return true;
self.input_mappings.erase(&seq2, mode, user); };
} else { self.input_mappings.erase(&s, mode, user);
res = true;
}
} else {
self.input_mappings.erase(s, mode, user);
}
} }
res false
} }
fn insert( fn insert(
@ -331,14 +349,8 @@ impl BuiltinBind {
self.list(bind_mode, true, parser, streams); self.list(bind_mode, true, parser, streams);
} }
} else if arg_count == 1 { } else if arg_count == 1 {
let seq = if self.opts.use_terminfo { let Some(seq) = self.compute_seq(streams, argv[optind]) else {
let Some(seq2) = self.get_terminfo_sequence(argv[optind], streams) else { return true;
// get_terminfo_sequence already printed the error.
return true;
};
seq2
} else {
argv[optind].to_owned()
}; };
if !self.list_one_user_andor_preset( if !self.list_one_user_andor_preset(
@ -360,9 +372,15 @@ impl BuiltinBind {
cmd, cmd,
eseq eseq
)); ));
} else if seq.len() == 1 {
streams.err.append(wgettext_fmt!(
"%ls: No binding found for key '%ls'\n",
cmd,
eseq
));
} else { } else {
streams.err.append(wgettext_fmt!( streams.err.append(wgettext_fmt!(
"%ls: No binding found for sequence '%ls'\n", "%ls: No binding found for key sequence '%ls'\n",
cmd, cmd,
eseq eseq
)); ));
@ -372,8 +390,9 @@ impl BuiltinBind {
} }
} else { } else {
// Actually insert! // Actually insert!
let seq = argv[optind];
if self.add( if self.add(
argv[optind], seq,
&argv[optind + 1..], &argv[optind + 1..],
self.opts.bind_mode.to_owned(), self.opts.bind_mode.to_owned(),
self.opts.sets_bind_mode.to_owned(), self.opts.sets_bind_mode.to_owned(),
@ -527,7 +546,6 @@ impl BuiltinBind {
if self.erase( if self.erase(
&argv[optind..], &argv[optind..],
self.opts.all, self.opts.all,
self.opts.use_terminfo,
true, /* user */ true, /* user */
streams, streams,
) { ) {
@ -538,7 +556,6 @@ impl BuiltinBind {
if self.erase( if self.erase(
&argv[optind..], &argv[optind..],
self.opts.all, self.opts.all,
self.opts.use_terminfo,
false, /* user */ false, /* user */
streams, streams,
) { ) {

View file

@ -1,6 +1,8 @@
//! Implementation of the fg builtin. //! Implementation of the fg builtin.
use crate::fds::make_fd_blocking; 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::reader::reader_write_title;
use crate::tokenizer::tok_command; use crate::tokenizer::tok_command;
use crate::wutil::perror; 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(); let mut transfer = TtyTransfer::new();
transfer.to_job_group(job.group.as_ref().unwrap()); transfer.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume(); let resumed = job.resume();

View file

@ -7,15 +7,16 @@ use crate::common::scoped_push_replacer;
use crate::common::str2wcstring; use crate::common::str2wcstring;
use crate::common::unescape_string; use crate::common::unescape_string;
use crate::common::valid_var_name; use crate::common::valid_var_name;
use crate::common::ScopeGuard;
use crate::common::UnescapeStringStyle; use crate::common::UnescapeStringStyle;
use crate::env::EnvMode; use crate::env::EnvMode;
use crate::env::Environment; use crate::env::Environment;
use crate::env::READ_BYTE_LIMIT; use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags}; use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::terminal_protocols_enable_scoped;
use crate::libc::MB_CUR_MAX; use crate::libc::MB_CUR_MAX;
use crate::nix::isatty; use crate::nix::isatty;
use crate::reader::commandline_set_buffer; use crate::reader::commandline_set_buffer;
use crate::reader::reader_current_data;
use crate::reader::ReaderConfig; use crate::reader::ReaderConfig;
use crate::reader::{reader_pop, reader_push, reader_readline}; use crate::reader::{reader_pop, reader_push, reader_readline};
use crate::tokenizer::Tokenizer; use crate::tokenizer::Tokenizer;
@ -26,9 +27,7 @@ use crate::wutil;
use crate::wutil::encoding::mbrtowc; use crate::wutil::encoding::mbrtowc;
use crate::wutil::encoding::zero_mbstate; use crate::wutil::encoding::zero_mbstate;
use crate::wutil::perror; use crate::wutil::perror;
use crate::wutil::write_to_fd;
use libc::SEEK_CUR; use libc::SEEK_CUR;
use libc::STDOUT_FILENO;
use std::os::fd::RawFd; use std::os::fd::RawFd;
use std::sync::atomic::Ordering; 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 stream_stdin_is_a_tty = isatty(streams.stdin_fd);
let _maybe_disable_bracketed_paste = stream_stdin_is_a_tty.then(|| { // Enable terminal protocols if noninteractive.
let _ = write_to_fd(b"\x1b[?2004h", STDOUT_FILENO); let _terminal_protocols = (stream_stdin_is_a_tty && reader_current_data().is_none())
ScopeGuard::new((), |()| { .then(terminal_protocols_enable_scoped);
let _ = write_to_fd(b"\x1b[?2004l", STDOUT_FILENO);
})
});
// Normally, we either consume a line of input or all available input. But if we are reading a // 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 // line at a time, we need a middle ground where we only consume as many lines as we need to

View file

@ -24,6 +24,9 @@ use crate::fork_exec::postfork::{
#[cfg(FISH_USE_POSIX_SPAWN)] #[cfg(FISH_USE_POSIX_SPAWN)]
use crate::fork_exec::spawn::PosixSpawner; use crate::fork_exec::spawn::PosixSpawner;
use crate::function::{self, FunctionProperties}; use crate::function::{self, FunctionProperties};
use crate::input_common::{
terminal_protocols_disable, terminal_protocols_disable_scoped, TERMINAL_PROTOCOLS,
};
use crate::io::{ use crate::io::{
BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe, BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe,
IoStreams, OutputStream, SeparatedBuffer, StringOutputStream, IoStreams, OutputStream, SeparatedBuffer, StringOutputStream,
@ -39,7 +42,7 @@ use crate::proc::{
print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType, print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType,
TtyTransfer, INVALID_PID, 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::redirection::{dup2_list_resolve_chain, Dup2List};
use crate::threads::{iothread_perform_cant_wait, is_forked_child}; use crate::threads::{iothread_perform_cant_wait, is_forked_child};
use crate::timer::push_timer; use crate::timer::push_timer;
@ -72,6 +75,15 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
return true; 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. // Handle an exec call.
if job.processes()[0].typ == ProcessType::exec { if job.processes()[0].typ == ProcessType::exec {
// If we are interactive, perhaps disallow exec if there are background jobs. // 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. // Ensure the terminal modes are what they were before we changed them.
restore_term_mode(); 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. // Bounce to launch_process. This never returns.
safe_launch_process(p, &actual_cmd, &argv, &*envp); safe_launch_process(p, &actual_cmd, &argv, &*envp);
} }

View file

@ -126,6 +126,7 @@ pub mod categories {
(fd_monitor, "fd-monitor", "FD monitor events"); (fd_monitor, "fd-monitor", "FD monitor events");
(term_support, "term-support", "Terminal feature detection"); (term_support, "term-support", "Terminal feature detection");
(term_protocols, "term-protocols", "Terminal protocol negotiation");
(reader, "reader", "The interactive reader/input system"); (reader, "reader", "The interactive reader/input system");
(reader_render, "reader-render", "Rendering the command line"); (reader_render, "reader-render", "Rendering the command line");

View file

@ -6,6 +6,7 @@ use crate::flog::FLOG;
use crate::input_common::{ use crate::input_common::{
CharEvent, CharInputStyle, InputEventQueuer, ReadlineCmd, R_END_INPUT_FUNCTIONS, CharEvent, CharInputStyle, InputEventQueuer, ReadlineCmd, R_END_INPUT_FUNCTIONS,
}; };
use crate::key::{self, canonicalize_raw_escapes, ctrl, Key};
use crate::parser::Parser; use crate::parser::Parser;
use crate::proc::job_reap; use crate::proc::job_reap;
use crate::reader::{ use crate::reader::{
@ -33,7 +34,7 @@ pub const NUL_MAPPING_NAME: &wstr = L!("nul");
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct InputMappingName { pub struct InputMappingName {
pub seq: WString, pub seq: Vec<Key>,
pub mode: WString, pub mode: WString,
} }
@ -41,7 +42,7 @@ pub struct InputMappingName {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct InputMapping { struct InputMapping {
/// Character sequence which generates this event. /// Character sequence which generates this event.
seq: WString, seq: Vec<Key>,
/// Commands that should be evaluated by this mapping. /// Commands that should be evaluated by this mapping.
commands: Vec<WString>, commands: Vec<WString>,
/// We wish to preserve the user-specified order. This is just an incrementing value. /// We wish to preserve the user-specified order. This is just an incrementing value.
@ -50,15 +51,18 @@ struct InputMapping {
mode: WString, mode: WString,
/// New mode that should be switched to after command evaluation, or None to leave the mode unchanged. /// New mode that should be switched to after command evaluation, or None to leave the mode unchanged.
sets_mode: Option<WString>, sets_mode: Option<WString>,
/// Whether this sequence was specified via its terminfo name.
terminfo_name: Option<WString>,
} }
impl InputMapping { impl InputMapping {
/// Create a new mapping. /// Create a new mapping.
fn new( fn new(
seq: WString, seq: Vec<Key>,
commands: Vec<WString>, commands: Vec<WString>,
mode: WString, mode: WString,
sets_mode: Option<WString>, sets_mode: Option<WString>,
terminfo_name: Option<WString>,
) -> InputMapping { ) -> InputMapping {
static LAST_INPUT_MAP_SPEC_ORDER: AtomicU32 = AtomicU32::new(0); static LAST_INPUT_MAP_SPEC_ORDER: AtomicU32 = AtomicU32::new(0);
let specification_order = 1 + LAST_INPUT_MAP_SPEC_ORDER.fetch_add(1, Ordering::Relaxed); let specification_order = 1 + LAST_INPUT_MAP_SPEC_ORDER.fetch_add(1, Ordering::Relaxed);
@ -72,6 +76,7 @@ impl InputMapping {
specification_order, specification_order,
mode, mode,
sets_mode, sets_mode,
terminfo_name,
} }
} }
@ -115,6 +120,8 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
const INPUT_FUNCTION_METADATA: &[InputFunctionMetadata] = &[ const INPUT_FUNCTION_METADATA: &[InputFunctionMetadata] = &[
// NULL makes it unusable - this is specially inserted when we detect mouse input // NULL makes it unusable - this is specially inserted when we detect mouse input
make_md(L!(""), ReadlineCmd::DisableMouseTracking), 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!("accept-autosuggestion"), ReadlineCmd::AcceptAutosuggestion),
make_md(L!("and"), ReadlineCmd::FuncAnd), make_md(L!("and"), ReadlineCmd::FuncAnd),
make_md(L!("backward-bigword"), ReadlineCmd::BackwardBigword), make_md(L!("backward-bigword"), ReadlineCmd::BackwardBigword),
@ -274,7 +281,8 @@ impl InputMappingSet {
/// Adds an input mapping. /// Adds an input mapping.
pub fn add( pub fn add(
&mut self, &mut self,
sequence: WString, sequence: Vec<Key>,
terminfo_name: Option<WString>,
commands: Vec<WString>, commands: Vec<WString>,
mode: WString, mode: WString,
sets_mode: Option<WString>, sets_mode: Option<WString>,
@ -299,20 +307,28 @@ impl InputMappingSet {
} }
// Add a new mapping, using the next order. // 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); input_mapping_insert_sorted(ml, new_mapping);
} }
// Like add(), but takes a single command. // Like add(), but takes a single command.
pub fn add1( pub fn add1(
&mut self, &mut self,
sequence: WString, sequence: Vec<Key>,
terminfo_name: Option<WString>,
command: WString, command: WString,
mode: WString, mode: WString,
sets_mode: Option<WString>, sets_mode: Option<WString>,
user: bool, 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 we have no keybindings, add a few simple defaults.
if input_mapping.preset_mapping_list.is_empty() { if input_mapping.preset_mapping_list.is_empty() {
// Helper for adding. // Helper for adding.
let mut add = |seq: &str, cmd: &str| { let mut add = |key: Vec<Key>, cmd: &str| {
let mode = DEFAULT_BIND_MODE.to_owned(); let mode = DEFAULT_BIND_MODE.to_owned();
let sets_mode = Some(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(vec![], "self-insert");
add("\n", "execute"); add(vec![Key::from_raw(key::Enter)], "execute");
add("\r", "execute"); add(vec![Key::from_raw(key::Tab)], "complete");
add("\t", "complete"); add(vec![ctrl('c')], "cancel-commandline");
add("\x03", "cancel-commandline"); add(vec![ctrl('d')], "exit");
add("\x04", "exit"); add(vec![ctrl('e')], "bind");
add("\x05", "bind"); add(vec![ctrl('s')], "pager-toggle-search");
// ctrl-s add(vec![ctrl('u')], "backward-kill-line");
add("\x13", "pager-toggle-search"); add(vec![Key::from_raw(key::Backspace)], "backward-delete-char");
// ctrl-u
add("\x15", "backward-kill-line");
// del/backspace
add("\x7f", "backward-delete-char");
// Arrows - can't have functions, so *-or-search isn't available. // Arrows - can't have functions, so *-or-search isn't available.
add("\x1B[A", "up-line"); add(vec![Key::from_raw(key::Up)], "up-line");
add("\x1B[B", "down-line"); add(vec![Key::from_raw(key::Down)], "down-line");
add("\x1B[C", "forward-char"); add(vec![Key::from_raw(key::Right)], "forward-char");
add("\x1B[D", "backward-char"); add(vec![Key::from_raw(key::Left)], "backward-char");
// emacs-style ctrl-p/n/b/f // Emacs style
add("\x10", "up-line"); add(vec![ctrl('p')], "up-line");
add("\x0e", "down-line"); add(vec![ctrl('n')], "down-line");
add("\x02", "backward-char"); add(vec![ctrl('b')], "backward-char");
add("\x06", "forward-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 type CommandHandler<'a> = dyn FnMut(&[WString]) + 'a;
pub struct Inputter { pub struct Inputter {
in_fd: RawFd, in_fd: RawFd,
queue: VecDeque<CharEvent>, queue: VecDeque<CharEvent>,
paste_buffer: Option<Vec<u8>>,
// We need a parser to evaluate bindings. // We need a parser to evaluate bindings.
parser: Rc<Parser>, parser: Rc<Parser>,
input_function_args: Vec<char>, input_function_args: Vec<char>,
@ -417,7 +443,7 @@ impl InputEventQueuer for Inputter {
if reader_reading_interrupted() != 0 { if reader_reading_interrupted() != 0 {
let vintr = shell_modes().c_cc[libc::VINTR]; let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 { if vintr != 0 {
self.push_front(CharEvent::from_char(vintr.into())); self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
} }
return; return;
} }
@ -427,6 +453,23 @@ impl InputEventQueuer for Inputter {
fn uvar_change_notified(&mut self) { fn uvar_change_notified(&mut self) {
self.parser.sync_uvars_and_fire(true /* always */); 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 { impl Inputter {
@ -435,6 +478,7 @@ impl Inputter {
Inputter { Inputter {
in_fd, in_fd,
queue: VecDeque::new(), queue: VecDeque::new(),
paste_buffer: None,
parser, parser,
input_function_args: Vec::new(), input_function_args: Vec::new(),
function_status: false, function_status: false,
@ -464,9 +508,12 @@ impl Inputter {
let arg: char; let arg: char;
loop { loop {
let evt = self.readch(); let evt = self.readch();
if let Some(c) = evt.get_char() { if let Some(kevt) = evt.get_key() {
arg = c; if let Some(c) = kevt.key.codepoint_text() {
break; // TODO forward the whole key
arg = c;
break;
}
} }
skipped.push(evt); skipped.push(evt);
} }
@ -492,7 +539,16 @@ impl Inputter {
let evt = match input_function_get_code(cmd) { let evt = match input_function_get_code(cmd) {
Some(code) => { Some(code) => {
self.function_push_args(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()), None => CharEvent::Command(cmd.clone()),
}; };
@ -539,6 +595,8 @@ struct EventQueuePeeker<'q> {
/// The current index. This never exceeds peeked.len(). /// The current index. This never exceeds peeked.len().
idx: usize, 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. /// The queue from which to read more events.
event_queue: &'q mut Inputter, event_queue: &'q mut Inputter,
@ -550,6 +608,7 @@ impl EventQueuePeeker<'_> {
peeked: Vec::new(), peeked: Vec::new(),
had_timeout: false, had_timeout: false,
idx: 0, idx: 0,
subidx: 0,
event_queue, event_queue,
} }
} }
@ -566,12 +625,13 @@ impl EventQueuePeeker<'_> {
} }
let res = self.peeked[self.idx].clone(); let res = self.peeked[self.idx].clone();
self.idx += 1; self.idx += 1;
self.subidx = 0;
res res
} }
/// Check if the next event is the given character. This advances the index on success only. /// 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. /// 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!( assert!(
self.idx <= self.peeked.len(), self.idx <= self.peeked.len(),
"Index must not be larger than dequeued event count" "Index must not be larger than dequeued event count"
@ -583,36 +643,74 @@ impl EventQueuePeeker<'_> {
// Grab a new event if we have exhausted what we have already peeked. // Grab a new event if we have exhausted what we have already peeked.
// Use either readch or readch_timed, per our param. // Use either readch or readch_timed, per our param.
if self.idx == self.peeked.len() { if self.idx == self.peeked.len() {
let newevt: CharEvent; let Some(newevt) = (if escaped {
if !escaped { self.event_queue.readch_timed_esc()
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;
} else { } else {
self.event_queue.readch_timed_sequence_key()
}) else {
self.had_timeout = true; self.had_timeout = true;
return false; return false;
} };
self.peeked.push(newevt); self.peeked.push(newevt);
} }
// Now we have peeked far enough; check the event. // Now we have peeked far enough; check the event.
// If it matches the char, then increment the index. // 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.idx += 1;
self.subidx = 0;
return true; 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 false
} }
/// \return the current index.
fn len(&self) -> usize {
self.idx
}
/// Consume all events up to the current index. /// Consume all events up to the current index.
/// Remaining events are returned to the queue. /// Remaining events are returned to the queue.
fn consume(mut self) { fn consume(mut self) {
@ -620,6 +718,7 @@ impl EventQueuePeeker<'_> {
self.event_queue.insert_front(self.peeked.drain(self.idx..)); self.event_queue.insert_front(self.peeked.drain(self.idx..));
self.peeked.clear(); self.peeked.clear();
self.idx = 0; self.idx = 0;
self.subidx = 0;
} }
/// Test if any of our peeked events are readline or check_exit. /// Test if any of our peeked events are readline or check_exit.
@ -632,82 +731,42 @@ impl EventQueuePeeker<'_> {
/// Reset our index back to 0. /// Reset our index back to 0.
fn restart(&mut self) { fn restart(&mut self) {
self.idx = 0; self.idx = 0;
self.subidx = 0;
} }
} }
impl Drop for EventQueuePeeker<'_> { impl Drop for EventQueuePeeker<'_> {
fn drop(&mut self) { fn drop(&mut self) {
assert!( assert!(
self.idx == 0, self.idx == 0 && self.subidx == 0,
"Events left on the queue - missing restart or consume?", "Events left on the queue - missing restart or consume?",
); );
self.event_queue.insert_front(self.peeked.drain(self.idx..)); 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. /// \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 { fn try_peek_sequence(peeker: &mut EventQueuePeeker, seq: &[Key]) -> bool {
assert!(!str.is_empty(), "Empty string passed to try_peek_sequence"); assert!(
let mut prev = '\0'; !seq.is_empty(),
for c in str.chars() { "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, // 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. // to distinguish between the actual escape key and an "alt"-modifier.
let escaped = prev == '\x1B'; let escaped = prev == Key::from_raw(key::Escape);
if !peeker.next_is_char(c, escaped) { if !peeker.next_is_char(*key, escaped) {
return false; 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 true
} }
@ -739,7 +798,7 @@ impl Inputter {
if try_peek_sequence(peeker, &m.seq) { if try_peek_sequence(peeker, &m.seq) {
// A binding for just escape should also be deferred // A binding for just escape should also be deferred
// so escape sequences take precedence. // so escape sequences take precedence.
if m.seq == "\x1B" { if m.seq == vec![Key::from_raw(key::Escape)] {
if escape.is_none() { if escape.is_none() {
escape = Some(m); escape = Some(m);
} }
@ -771,27 +830,12 @@ impl Inputter {
fn mapping_execute_matching_or_generic(&mut self) { fn mapping_execute_matching_or_generic(&mut self) {
let vars = self.parser.vars_ref(); let vars = self.parser.vars_ref();
let mut peeker = EventQueuePeeker::new(self); 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. // Check for ordinary mappings.
if let Some(mapping) = Self::find_mapping(&*vars, &mut peeker) { if let Some(mapping) = Self::find_mapping(&*vars, &mut peeker) {
FLOG!(
reader,
format!("Found mapping {:?} from {:?}", &mapping, &peeker.peeked)
);
peeker.consume(); peeker.consume();
self.mapping_execute(&mapping); self.mapping_execute(&mapping);
return; return;
@ -839,14 +883,7 @@ impl Inputter {
evt_to_return evt_to_return
} }
/// Read a character from stdin. Try to convert some escape sequences into character constants, /// Read a key from stdin.
/// 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.
pub fn read_char(&mut self) -> CharEvent { pub fn read_char(&mut self) -> CharEvent {
// Clear the interrupted flag. // Clear the interrupted flag.
reader_reset_interrupted(); reader_reset_interrupted();
@ -867,8 +904,8 @@ impl Inputter {
// Hackish: mark the input style. // Hackish: mark the input style.
if readline_event.cmd == ReadlineCmd::SelfInsertNotFirst { if readline_event.cmd == ReadlineCmd::SelfInsertNotFirst {
if let CharEvent::Char(cevt) = &mut res { if let CharEvent::Key(kevt) = &mut res {
cevt.input_style = CharInputStyle::NotFirst; kevt.input_style = CharInputStyle::NotFirst;
} }
} }
return res; return res;
@ -897,7 +934,17 @@ impl Inputter {
// Allow the reader to check for exit conditions. // Allow the reader to check for exit conditions.
return evt; 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::<Vec<_>>()
)
);
self.push_front(evt); self.push_front(evt);
self.mapping_execute_matching_or_generic(); self.mapping_execute_matching_or_generic();
} }
@ -941,7 +988,7 @@ impl InputMappingSet {
} }
/// Erase binding for specified key sequence. /// 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. // Clear cached mappings.
self.all_mappings_cache = RefCell::new(None); self.all_mappings_cache = RefCell::new(None);
@ -965,11 +1012,12 @@ impl InputMappingSet {
/// it exists, false if not. /// it exists, false if not.
pub fn get<'a>( pub fn get<'a>(
&'a self, &'a self,
sequence: &wstr, sequence: &[Key],
mode: &wstr, mode: &wstr,
out_cmds: &mut &'a [WString], out_cmds: &mut &'a [WString],
user: bool, user: bool,
out_sets_mode: &mut Option<&'a wstr>, out_sets_mode: &mut Option<&'a wstr>,
out_terminfo_name: &mut Option<WString>,
) -> bool { ) -> bool {
let ml = if user { let ml = if user {
&self.mapping_list &self.mapping_list
@ -980,6 +1028,7 @@ impl InputMappingSet {
if m.seq == sequence && m.mode == mode { if m.seq == sequence && m.mode == mode {
*out_cmds = &m.commands; *out_cmds = &m.commands;
*out_sets_mode = m.sets_mode.as_deref(); *out_sets_mode = m.sets_mode.as_deref();
*out_terminfo_name = m.terminfo_name.clone();
return true; return true;
} }
} }

View file

@ -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::env::{EnvStack, Environment};
use crate::fd_readable_set::FdReadableSet; use crate::fd_readable_set::FdReadableSet;
use crate::flog::FLOG; use crate::flog::FLOG;
use crate::reader::reader_current_data; use crate::key::{
use crate::threads::{iothread_port, iothread_service_main}; 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::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*}; use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, zero_mbstate}; use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
use crate::wutil::fish_wcstol; use crate::wutil::{fish_wcstol, write_to_fd};
use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::os::fd::RawFd; use std::os::fd::RawFd;
use std::ptr; use std::ptr;
@ -111,6 +123,8 @@ pub enum ReadlineCmd {
EndUndoGroup, EndUndoGroup,
RepeatJump, RepeatJump,
DisableMouseTracking, DisableMouseTracking,
FocusIn,
FocusOut,
// ncurses uses the obvious name // ncurses uses the obvious name
ClearScreenAndRepaint, ClearScreenAndRepaint,
// NOTE: This one has to be last. // NOTE: This one has to be last.
@ -121,7 +135,7 @@ pub enum ReadlineCmd {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CharEventType { pub enum CharEventType {
/// A character was entered. /// A character was entered.
Char(char), Char(Key),
/// A readline event. /// A readline event.
Readline(ReadlineCmd), Readline(ReadlineCmd),
@ -147,9 +161,9 @@ pub struct ReadlineCmdEvent {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlainCharEvent { pub struct KeyEvent {
// The key. // The key.
pub char: char, pub key: Key,
// The style to use when inserting characters into the command line. // The style to use when inserting characters into the command line.
pub input_style: CharInputStyle, pub input_style: CharInputStyle,
/// The sequence of characters in the input mapping which generated this event. /// The sequence of characters in the input mapping which generated this event.
@ -161,7 +175,7 @@ pub struct PlainCharEvent {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CharEvent { pub enum CharEvent {
/// A character was entered. /// A character was entered.
Char(PlainCharEvent), Key(KeyEvent),
/// A readline event. /// A readline event.
Readline(ReadlineCmdEvent), Readline(ReadlineCmdEvent),
@ -179,7 +193,7 @@ pub enum CharEvent {
impl CharEvent { impl CharEvent {
pub fn is_char(&self) -> bool { pub fn is_char(&self) -> bool {
matches!(self, CharEvent::Char(_)) matches!(self, CharEvent::Key(_))
} }
pub fn is_eof(&self) -> bool { pub fn is_eof(&self) -> bool {
@ -198,6 +212,20 @@ impl CharEvent {
matches!(self, CharEvent::Readline(_) | CharEvent::Command(_)) 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 { pub fn get_readline(&self) -> ReadlineCmd {
let CharEvent::Readline(c) = self else { let CharEvent::Readline(c) = self else {
panic!("Not a readline type"); panic!("Not a readline type");
@ -213,19 +241,24 @@ impl CharEvent {
} }
pub fn from_char(c: char) -> CharEvent { 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<char> { pub fn from_key(key: Key) -> CharEvent {
match self { Self::from_key_seq(key, WString::new())
CharEvent::Char(cevt) => Some(cevt.char), }
_ => None,
} 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 { pub fn from_char_seq(c: char, seq: WString) -> CharEvent {
CharEvent::Char(PlainCharEvent { CharEvent::Key(KeyEvent {
char: c, key: Key::from_raw(c),
input_style: CharInputStyle::Normal, input_style: CharInputStyle::Normal,
seq, seq,
}) })
@ -272,9 +305,11 @@ enum ReadbResult {
// Our ioport reported a change, so service main thread requests. // Our ioport reported a change, so service main thread requests.
IOPortNotified, IOPortNotified,
NothingToRead,
} }
fn readb(in_fd: RawFd) -> ReadbResult { fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
assert!(in_fd >= 0, "Invalid in fd"); assert!(in_fd >= 0, "Invalid in fd");
let mut fdset = FdReadableSet::new(); let mut fdset = FdReadableSet::new();
loop { loop {
@ -293,7 +328,11 @@ fn readb(in_fd: RawFd) -> ReadbResult {
} }
// Here's where we call select(). // 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 { if select_res < 0 {
let err = errno::errno().0; let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN { 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. if blocking {
// The priority order is: uvars, stdin, ioport. // select() did not return an error, so we may have a readable fd.
// Check to see if we want a universal variable barrier. // The priority order is: uvars, stdin, ioport.
if let Some(notifier_fd) = notifier_fd { // Check to see if we want a universal variable barrier.
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) { if let Some(notifier_fd) = notifier_fd {
return ReadbResult::UvarNotified; 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. // The common path is to return a u8.
return ReadbResult::Byte(arr[0]); 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. // Check for iothread completions only if there is no data to be read from the stdin.
// This gives priority to the foreground. // 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<RefCell<Option<TerminalProtocols>>> =
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<Target = ()> {
terminal_protocols_enable();
ScopeGuard::new((), |()| terminal_protocols_disable())
}
pub fn terminal_protocols_disable_scoped() -> impl ScopeGuarding<Target = ()> {
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. /// A trait which knows how to produce a stream of input events.
/// Note this is conceptually a "base class" with override points. /// Note this is conceptually a "base class" with override points.
pub trait InputEventQueuer { pub trait InputEventQueuer {
@ -395,10 +545,7 @@ pub trait InputEventQueuer {
/// convert them to a wchar_t. Conversion is done using mbrtowc. If a character has previously /// 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. /// been read and then 'unread' using \c input_common_unreadch, that character is returned.
fn readch(&mut self) -> CharEvent { fn readch(&mut self) -> CharEvent {
let mut res: char = '\0';
let mut state = zero_mbstate(); let mut state = zero_mbstate();
let mut bytes = [0; 64 * 16];
let mut num_bytes = 0;
loop { loop {
// Do we have something enqueued already? // Do we have something enqueued already?
// Note this may be initially true, or it may become true through calls to // Note this may be initially true, or it may become true through calls to
@ -413,14 +560,13 @@ pub trait InputEventQueuer {
return mevt; return mevt;
} }
let rr = readb(self.get_in_fd()); let rr = readb(self.get_in_fd(), /*blocking=*/ true);
match rr { match rr {
ReadbResult::Eof => { ReadbResult::Eof => {
return CharEvent::Eof; return CharEvent::Eof;
} }
ReadbResult::Interrupted => { ReadbResult::Interrupted => {
// FIXME: here signals may break multibyte sequences.
self.select_interrupted(); self.select_interrupted();
} }
@ -433,58 +579,405 @@ pub trait InputEventQueuer {
} }
ReadbResult::Byte(read_byte) => { ReadbResult::Byte(read_byte) => {
if crate::libc::MB_CUR_MAX() == 1 { let mut have_escape_prefix = false;
// single-byte locale, all values are legal let mut buffer = vec![read_byte];
// FIXME: this looks wrong, this falsely assumes that let key_with_escape = if read_byte == 0x1b {
// the single-byte locale is compatible with Unicode upper-ASCII. self.parse_escape_sequence(&mut buffer, &mut have_escape_prefix)
res = read_byte.into(); } else {
return CharEvent::from_char(res); 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 mut seq = WString::new();
let sz = unsafe { let mut key = key_with_escape;
mbrtowc( let mut consumed = 0;
std::ptr::addr_of_mut!(codepoint).cast(), for i in 0..buffer.len() {
std::ptr::addr_of!(read_byte).cast(), self.parse_codepoint(
1,
&mut state, &mut state,
) &mut key,
} as isize; &mut seq,
match sz { &buffer,
-1 => { i,
FLOG!(reader, "Illegal input"); &mut consumed,
return CharEvent::from_check_exit(); &mut have_escape_prefix,
} );
-2 => { }
// Sequence not yet complete. return if let Some(key) = key {
bytes[num_bytes] = read_byte; CharEvent::from_key_seq(key, seq)
num_bytes += 1; } else {
self.insert_front(seq.chars().skip(1).map(CharEvent::from_char));
let Some(c) = seq.chars().next() else {
continue; continue;
} };
0 => { CharEvent::from_key_seq(Key::from_raw(c), seq)
// 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;
} }
ReadbResult::NothingToRead => unreachable!(),
} }
} }
} }
fn try_readb(&mut self, buffer: &mut Vec<u8>) -> Option<u8> {
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<u8>,
have_escape_prefix: &mut bool,
) -> Option<Key> {
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<Key>,
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<u8>) -> Option<Key> {
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<u8>) -> Option<Key> {
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<CharEvent> { fn readch_timed_esc(&mut self) -> Option<CharEvent> {
self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed)) self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed))
} }
@ -556,6 +1049,24 @@ pub trait InputEventQueuer {
/// Return the fd corresponding to stdin. /// Return the fd corresponding to stdin.
fn get_in_fd(&self) -> RawFd; 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 /// Enqueue a character or a readline function to the queue of unread characters that
/// readch will return before actually reading from fd 0. /// readch will return before actually reading from fd 0.
fn push_back(&mut self, ch: CharEvent) { fn push_back(&mut self, ch: CharEvent) {
@ -622,8 +1133,8 @@ pub trait InputEventQueuer {
/// nothing. /// nothing.
fn prepare_to_select(&mut self) {} fn prepare_to_select(&mut self) {}
/// Override point for when when select() is interrupted by a signal. The default does nothing. /// Called when select() is interrupted by a signal.
fn select_interrupted(&mut self) {} fn select_interrupted(&mut self);
/// Override point for when when select() is interrupted by the universal variable notifier. /// Override point for when when select() is interrupted by the universal variable notifier.
/// The default does nothing. /// The default does nothing.
@ -639,6 +1150,7 @@ pub trait InputEventQueuer {
pub struct InputEventQueue { pub struct InputEventQueue {
queue: VecDeque<CharEvent>, queue: VecDeque<CharEvent>,
in_fd: RawFd, in_fd: RawFd,
is_in_bracketed_paste: bool,
} }
impl InputEventQueue { impl InputEventQueue {
@ -646,6 +1158,7 @@ impl InputEventQueue {
InputEventQueue { InputEventQueue {
queue: VecDeque::new(), queue: VecDeque::new(),
in_fd, in_fd,
is_in_bracketed_paste: false,
} }
} }
} }
@ -662,4 +1175,23 @@ impl InputEventQueuer for InputEventQueue {
fn get_in_fd(&self) -> RawFd { fn get_in_fd(&self) -> RawFd {
self.in_fd 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;
}
} }

430
src/key.rs Normal file
View file

@ -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<u32> = 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<Key> {
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<Key, WString> {
// 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<Vec<Key>, 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<Key>) -> Vec<Key> {
// 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<char> {
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<Key> 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
}

View file

@ -59,6 +59,7 @@ pub mod input;
pub mod input_common; pub mod input_common;
pub mod io; pub mod io;
pub mod job_group; pub mod job_group;
pub mod key;
pub mod kill; pub mod kill;
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub mod libc; pub mod libc;

View file

@ -11,6 +11,7 @@ use crate::env::Statuses;
use crate::event::{self, Event}; use crate::event::{self, Event};
use crate::flog::{FLOG, FLOGF}; use crate::flog::{FLOG, FLOGF};
use crate::global_safety::RelaxedAtomicBool; use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::terminal_protocols_enable;
use crate::io::IoChain; use crate::io::IoChain;
use crate::job_group::{JobGroup, MaybeJobId}; use crate::job_group::{JobGroup, MaybeJobId};
use crate::parse_tree::ParsedSourceRef; 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); let status = ProcStatus::from_waitpid(statusv);
handle_child_status(j, proc, &status); handle_child_status(j, proc, &status);
if status.stopped() { if status.stopped() {
if is_interactive_session() && j.group().wants_terminal() {
terminal_protocols_enable();
}
j.group().set_is_foreground(false); j.group().set_is_foreground(false);
} }
if status.continued() { if status.continued() {

View file

@ -69,7 +69,9 @@ use crate::history::{
}; };
use crate::input::init_input; use crate::input::init_input;
use crate::input::Inputter; 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::io::IoChain;
use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate};
use crate::libc::MB_CUR_MAX; use crate::libc::MB_CUR_MAX;
@ -133,7 +135,7 @@ pub static SHELL_MODES: Lazy<Mutex<libc::termios>> =
Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() }));
/// Mode on startup, which we restore on exit. /// Mode on startup, which we restore on exit.
static TERMINAL_MODE_ON_STARTUP: Lazy<Mutex<libc::termios>> = pub static TERMINAL_MODE_ON_STARTUP: Lazy<Mutex<libc::termios>> =
Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() }));
/// Mode we use to execute programs. /// Mode we use to execute programs.
@ -784,10 +786,15 @@ pub fn reader_init() -> impl ScopeGuarding<Target = ()> {
// Set up our fixed terminal modes once, // Set up our fixed terminal modes once,
// so we don't get flow control just because we inherited it. // so we don't get flow control just because we inherited it.
if is_interactive_session() && unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } { let mut terminal_protocols = None;
term_donate(/*quiet=*/ true); 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(); restore_term_mode();
}) })
} }
@ -1912,26 +1919,23 @@ impl ReaderData {
CharEvent::Command(command) => { CharEvent::Command(command) => {
zelf.run_input_command_scripts(&command); zelf.run_input_command_scripts(&command);
} }
CharEvent::Char(cevt) => { CharEvent::Key(kevt) => {
// Ordinary char. // Ordinary char.
let c = cevt.char; if kevt.input_style == CharInputStyle::NotFirst
if cevt.input_style == CharInputStyle::NotFirst
&& zelf.active_edit_line().1.position() == 0 && zelf.active_edit_line().1.position() == 0
{ {
// This character is skipped. // 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 { } else {
// Regular character. // Regular character.
let (elt, _el) = zelf.active_edit_line(); 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 { if elt == EditableLineTag::Commandline {
zelf.clear_pager(); zelf.clear_pager();
// We end history search. We could instead update the search string. // We end history search. We could instead update the search string.
zelf.history_search.reset(); zelf.history_search.reset();
}
} }
} }
rls.last_cmd = None; rls.last_cmd = None;
@ -2036,7 +2040,7 @@ impl ReaderData {
while accumulated_chars.len() < limit { while accumulated_chars.len() < limit {
let evt = self.inputter.read_char(); let evt = self.inputter.read_char();
let CharEvent::Char(cevt) = &evt else { let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt); event_needing_handling = Some(evt);
break; break;
}; };
@ -2044,16 +2048,20 @@ impl ReaderData {
event_needing_handling = Some(evt); event_needing_handling = Some(evt);
break; break;
} }
if cevt.input_style == CharInputStyle::NotFirst if kevt.input_style == CharInputStyle::NotFirst
&& accumulated_chars.is_empty() && accumulated_chars.is_empty()
&& self.active_edit_line().1.position() == 0 && self.active_edit_line().1.position() == 0
{ {
// The cursor is at the beginning and nothing is accumulated, so skip this character. // The cursor is at the beginning and nothing is accumulated, so skip this character.
continue; continue;
} else {
accumulated_chars.push(cevt.char);
} }
if let Some(c) = kevt.key.codepoint_text() {
accumulated_chars.push(c);
} else {
continue;
};
if last_exec_count != self.exec_count() { if last_exec_count != self.exec_count() {
last_exec_count = self.exec_count(); last_exec_count = self.exec_count();
self.screen.save_status(); self.screen.save_status();
@ -3047,6 +3055,12 @@ impl ReaderData {
.borrow_mut() .borrow_mut()
.write_wstr(L!("\x1B[?1000l")); .write_wstr(L!("\x1B[?1000l"));
} }
rl::FocusIn => {
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 => { rl::ClearScreenAndRepaint => {
self.parser().libdata_mut().pods.is_repaint = true; self.parser().libdata_mut().pods.is_repaint = true;
let clear = screen_clear(); let clear = screen_clear();

View file

@ -1,5 +1,6 @@
use crate::input::{input_mappings, Inputter, DEFAULT_BIND_MODE}; use crate::input::{input_mappings, Inputter, DEFAULT_BIND_MODE};
use crate::input_common::{CharEvent, ReadlineCmd}; use crate::input_common::{CharEvent, ReadlineCmd};
use crate::key::Key;
use crate::parser::Parser; use crate::parser::Parser;
use crate::tests::prelude::*; use crate::tests::prelude::*;
use crate::wchar::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 // 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 // of the second, and then emit the second key list. The second binding should be invoked, not
// the first! // the first!
let prefix_binding = WString::from_str("qqqqqqqa"); let prefix_binding: Vec<Key> = "qqqqqqqa".chars().map(Key::from_raw).collect();
let desired_binding = prefix_binding.clone() + "a"; let mut desired_binding = prefix_binding.clone();
desired_binding.push(Key::from_raw('a'));
let default_mode = || DEFAULT_BIND_MODE.to_owned(); let default_mode = || DEFAULT_BIND_MODE.to_owned();
@ -24,6 +26,7 @@ fn test_input() {
let mut input_mapping = input_mappings(); let mut input_mapping = input_mappings();
input_mapping.add1( input_mapping.add1(
prefix_binding, prefix_binding,
None,
WString::from_str("up-line"), WString::from_str("up-line"),
default_mode(), default_mode(),
None, None,
@ -31,6 +34,7 @@ fn test_input() {
); );
input_mapping.add1( input_mapping.add1(
desired_binding.clone(), desired_binding.clone(),
None,
WString::from_str("down-line"), WString::from_str("down-line"),
default_mode(), default_mode(),
None, None,
@ -39,8 +43,8 @@ fn test_input() {
} }
// Push the desired binding to the queue. // Push the desired binding to the queue.
for c in desired_binding.chars() { for c in desired_binding {
input.queue_char(CharEvent::from_char(c)); input.queue_char(CharEvent::from_key(c));
} }
// Now test. // Now test.

View file

@ -7,10 +7,10 @@ fn test_push_front_back() {
queue.push_front(CharEvent::from_char('b')); queue.push_front(CharEvent::from_char('b'));
queue.push_back(CharEvent::from_char('c')); queue.push_back(CharEvent::from_char('c'));
queue.push_back(CharEvent::from_char('d')); 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(), 'b');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'a'); assert_eq!(queue.try_pop().unwrap().get_char(), 'a');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'c'); assert_eq!(queue.try_pop().unwrap().get_char(), 'c');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'd'); assert_eq!(queue.try_pop().unwrap().get_char(), 'd');
assert!(queue.try_pop().is_none()); 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::Undo);
assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Redo); 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(), 'a');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'b'); assert_eq!(queue.try_pop().unwrap().get_char(), 'b');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'c'); assert_eq!(queue.try_pop().unwrap().get_char(), 'c');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'd'); assert_eq!(queue.try_pop().unwrap().get_char(), 'd');
assert!(!queue.has_lookahead()); assert!(!queue.has_lookahead());
queue.push_back(CharEvent::from_char('e')); queue.push_back(CharEvent::from_char('e'));
queue.promote_interruptions_to_front(); 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()); assert!(!queue.has_lookahead());
} }
@ -51,9 +51,9 @@ fn test_insert_front() {
CharEvent::from_char('C'), CharEvent::from_char('C'),
]; ];
queue.insert_front(events); queue.insert_front(events);
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'A'); assert_eq!(queue.try_pop().unwrap().get_char(), 'A');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'B'); assert_eq!(queue.try_pop().unwrap().get_char(), 'B');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'C'); assert_eq!(queue.try_pop().unwrap().get_char(), 'C');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'a'); assert_eq!(queue.try_pop().unwrap().get_char(), 'a');
assert_eq!(queue.try_pop().unwrap().get_char().unwrap(), 'b'); assert_eq!(queue.try_pop().unwrap().get_char(), 'b');
} }

13
src/tests/key.rs Normal file
View file

@ -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')]));
}

View file

@ -12,6 +12,7 @@ mod highlight;
mod history; mod history;
mod input; mod input;
mod input_common; mod input_common;
mod key;
mod pager; mod pager;
mod parse_util; mod parse_util;
mod parser; mod parser;

View file

@ -277,6 +277,16 @@ pub trait WExt {
iter_prefixes_iter(prefix.chars(), self.as_char_slice().iter().copied()) iter_prefixes_iter(prefix.chars(), self.as_char_slice().iter().copied())
} }
fn strip_prefix<Prefix: IntoCharIter>(&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. /// \return whether we end with a given Suffix.
/// The Suffix can be a char, a &str, a &wstr, or a &WString. /// The Suffix can be a char, a &str, a &wstr, or a &WString.
fn ends_with<Suffix: IntoCharIter>(&self, suffix: Suffix) -> bool { fn ends_with<Suffix: IntoCharIter>(&self, suffix: Suffix) -> bool {

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Universal abbreviations are imported. # Universal abbreviations are imported.
set -U _fish_abbr_cuckoo somevalue set -U _fish_abbr_cuckoo somevalue

View file

@ -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 # Avoid regressions of issue \#3860 wherein the first word of the alias ends with a semicolon
function foo function foo
echo ran foo echo ran foo

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# "Basic && and || support" # "Basic && and || support"
echo first && echo second echo first && echo second

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
set -xl LANG C # uniform quotes set -xl LANG C # uniform quotes

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
########## ##########
# NOTE: This uses argparse, which touches the local variables. # NOTE: This uses argparse, which touches the local variables.

View file

@ -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}} # CHECKERR: {{.*fish}}: {{unrecognized option: Z|invalid option -- '?Z'?|unknown option -- Z|illegal option -- Z|-Z: unknown option}}

View file

@ -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 # Test function, loops, conditionals and some basic elements
# #

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Test various `bind` command invocations. This is meant to verify that # 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 # invalid flags, mode names, etc. are caught as well as to verify that valid
# ones are allowed. # ones are allowed.
@ -14,102 +14,103 @@ bind -M bind-mode \cX true
# This should succeed and result in a success, zero, status. # This should succeed and result in a success, zero, status.
bind -M bind_mode \cX true 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 # Listing bindings
bind | string match -v '*backward-delete-char' bind | string match -v '*escape,\\[*' # Hide legacy bindings.
bind --user --preset | string match -v '*backward-delete-char' bind --user --preset | string match -v '*escape,\\[*'
# CHECK: bind --preset '' self-insert # CHECK: bind --preset '' self-insert
# CHECK: bind --preset \n execute # CHECK: bind --preset enter execute
# CHECK: bind --preset \r execute # CHECK: bind --preset tab complete
# CHECK: bind --preset \t complete # CHECK: bind --preset ctrl-c cancel-commandline
# CHECK: bind --preset \cc cancel-commandline # CHECK: bind --preset ctrl-d exit
# CHECK: bind --preset \cd exit # CHECK: bind --preset ctrl-e bind
# CHECK: bind --preset \ce bind # CHECK: bind --preset ctrl-s pager-toggle-search
# CHECK: bind --preset \cs pager-toggle-search # CHECK: bind --preset ctrl-u backward-kill-line
# CHECK: bind --preset \cu backward-kill-line # CHECK: bind --preset backspace backward-delete-char
# CHECK: bind --preset \e\[A up-line # CHECK: bind --preset up up-line
# CHECK: bind --preset \e\[B down-line # CHECK: bind --preset down down-line
# CHECK: bind --preset \e\[C forward-char # CHECK: bind --preset right forward-char
# CHECK: bind --preset \e\[D backward-char # CHECK: bind --preset left backward-char
# CHECK: bind --preset \cp up-line # CHECK: bind --preset ctrl-p up-line
# CHECK: bind --preset \cn down-line # CHECK: bind --preset ctrl-n down-line
# CHECK: bind --preset \cb backward-char # CHECK: bind --preset ctrl-b backward-char
# CHECK: bind --preset \cf forward-char # CHECK: bind --preset ctrl-f forward-char
# CHECK: bind -M bind_mode \cx true # CHECK: bind -M bind_mode ctrl-x true
# CHECK: bind --preset '' self-insert # CHECK: bind --preset '' self-insert
# CHECK: bind --preset \n execute # CHECK: bind --preset enter execute
# CHECK: bind --preset \r execute # CHECK: bind --preset tab complete
# CHECK: bind --preset \t complete # CHECK: bind --preset ctrl-c cancel-commandline
# CHECK: bind --preset \cc cancel-commandline # CHECK: bind --preset ctrl-d exit
# CHECK: bind --preset \cd exit # CHECK: bind --preset ctrl-e bind
# CHECK: bind --preset \ce bind # CHECK: bind --preset ctrl-s pager-toggle-search
# CHECK: bind --preset \cs pager-toggle-search # CHECK: bind --preset ctrl-u backward-kill-line
# CHECK: bind --preset \cu backward-kill-line # CHECK: bind --preset backspace backward-delete-char
# CHECK: bind --preset \e\[A up-line # CHECK: bind --preset up up-line
# CHECK: bind --preset \e\[B down-line # CHECK: bind --preset down down-line
# CHECK: bind --preset \e\[C forward-char # CHECK: bind --preset right forward-char
# CHECK: bind --preset \e\[D backward-char # CHECK: bind --preset left backward-char
# CHECK: bind --preset \cp up-line # CHECK: bind --preset ctrl-p up-line
# CHECK: bind --preset \cn down-line # CHECK: bind --preset ctrl-n down-line
# CHECK: bind --preset \cb backward-char # CHECK: bind --preset ctrl-b backward-char
# CHECK: bind --preset \cf forward-char # CHECK: bind --preset ctrl-f forward-char
# CHECK: bind -M bind_mode \cx true # CHECK: bind -M bind_mode ctrl-x true
# Preset only # Preset only
bind --preset | string match -v '*backward-delete-char' bind --preset | string match -v '*escape,\\[*'
# CHECK: bind --preset '' self-insert # CHECK: bind --preset '' self-insert
# CHECK: bind --preset \n execute # CHECK: bind --preset enter execute
# CHECK: bind --preset \r execute # CHECK: bind --preset tab complete
# CHECK: bind --preset \t complete # CHECK: bind --preset ctrl-c cancel-commandline
# CHECK: bind --preset \cc cancel-commandline # CHECK: bind --preset ctrl-d exit
# CHECK: bind --preset \cd exit # CHECK: bind --preset ctrl-e bind
# CHECK: bind --preset \ce bind # CHECK: bind --preset ctrl-s pager-toggle-search
# CHECK: bind --preset \cs pager-toggle-search # CHECK: bind --preset ctrl-u backward-kill-line
# CHECK: bind --preset \cu backward-kill-line # CHECK: bind --preset backspace backward-delete-char
# CHECK: bind --preset \e\[A up-line # CHECK: bind --preset up up-line
# CHECK: bind --preset \e\[B down-line # CHECK: bind --preset down down-line
# CHECK: bind --preset \e\[C forward-char # CHECK: bind --preset right forward-char
# CHECK: bind --preset \e\[D backward-char # CHECK: bind --preset left backward-char
# CHECK: bind --preset \cp up-line # CHECK: bind --preset ctrl-p up-line
# CHECK: bind --preset \cn down-line # CHECK: bind --preset ctrl-n down-line
# CHECK: bind --preset \cb backward-char # CHECK: bind --preset ctrl-b backward-char
# CHECK: bind --preset \cf forward-char # CHECK: bind --preset ctrl-f forward-char
# User only # User only
bind --user | string match -v '*backward-delete-char' bind --user | string match -v '*escape,\\[*'
# CHECK: bind -M bind_mode \cx true # CHECK: bind -M bind_mode ctrl-x true
# Adding bindings # Adding bindings
bind \t 'echo banana' bind tab 'echo banana'
bind | string match -v '*backward-delete-char' bind | string match -v '*escape,\\[*'
# CHECK: bind --preset '' self-insert # CHECK: bind --preset '' self-insert
# CHECK: bind --preset \n execute # CHECK: bind --preset enter execute
# CHECK: bind --preset \r execute # CHECK: bind --preset tab complete
# CHECK: bind --preset \t complete # CHECK: bind --preset ctrl-c cancel-commandline
# CHECK: bind --preset \cc cancel-commandline # CHECK: bind --preset ctrl-d exit
# CHECK: bind --preset \cd exit # CHECK: bind --preset ctrl-e bind
# CHECK: bind --preset \ce bind # CHECK: bind --preset ctrl-s pager-toggle-search
# CHECK: bind --preset \cs pager-toggle-search # CHECK: bind --preset ctrl-u backward-kill-line
# CHECK: bind --preset \cu backward-kill-line # CHECK: bind --preset backspace backward-delete-char
# CHECK: bind --preset \e\[A up-line # CHECK: bind --preset up up-line
# CHECK: bind --preset \e\[B down-line # CHECK: bind --preset down down-line
# CHECK: bind --preset \e\[C forward-char # CHECK: bind --preset right forward-char
# CHECK: bind --preset \e\[D backward-char # CHECK: bind --preset left backward-char
# CHECK: bind --preset \cp up-line # CHECK: bind --preset ctrl-p up-line
# CHECK: bind --preset \cn down-line # CHECK: bind --preset ctrl-n down-line
# CHECK: bind --preset \cb backward-char # CHECK: bind --preset ctrl-b backward-char
# CHECK: bind --preset \cf forward-char # CHECK: bind --preset ctrl-f forward-char
# CHECK: bind -M bind_mode \cx true # CHECK: bind -M bind_mode ctrl-x true
# CHECK: bind \t 'echo banana' # CHECK: bind tab 'echo banana'
# Erasing bindings # Erasing bindings
bind --erase \t bind --erase tab
bind \t bind tab
bind \t 'echo wurst' bind tab 'echo wurst'
# CHECK: bind --preset \t complete # CHECK: bind --preset tab complete
bind --erase --user --preset \t bind --erase --user --preset tab
bind \t bind tab
# CHECKERR: bind: No binding found for sequence '\t' # CHECKERR: bind: No binding found for key 'tab'
bind ctrl-\b
# CHECKERR: bind: Cannot add control modifier to control character 'ctrl-h'
exit 0 exit 0

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
echo x-{1} echo x-{1}
#CHECK: x-{1} #CHECK: x-{1}
@ -22,7 +22,7 @@ echo foo-{""} # still expands to foo-{}
#CHECK: foo-{} #CHECK: foo-{}
echo foo-{$undefinedvar} # still expands to nothing echo foo-{$undefinedvar} # still expands to nothing
#CHECK: #CHECK:
echo foo-{,,,} # four empty items in the braces. echo foo-{,,,} # four empty items in the braces.
#CHECK: foo- foo- foo- foo- #CHECK: foo- foo- foo- foo-

View file

@ -1,4 +1,4 @@
#RUN: %fish -C 'set -g fish %fish' %s #RUN: %fish -C 'set -g fish %fish' %s | %filter-ctrlseqs
begin begin
set -l dir $PWD/(dirname (status -f)) set -l dir $PWD/(dirname (status -f))
set -gx XDG_CONFIG_HOME $dir/broken-config/ set -gx XDG_CONFIG_HOME $dir/broken-config/

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Tests for the "builtin" builtin/keyword. # Tests for the "builtin" builtin/keyword.
builtin -q string; and echo String exists builtin -q string; and echo String exists
#CHECK: String exists #CHECK: String exists

View file

@ -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) echo (function foo1 --on-job-exit caller; end; functions --handlers-type caller-exit | grep foo)
# CHECK: caller-exit foo1 # CHECK: caller-exit foo1
echo (function foo2 --on-job-exit caller; end; functions --handlers-type process-exit | grep foo) echo (function foo2 --on-job-exit caller; end; functions --handlers-type process-exit | grep foo)

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Verify the '--on-job-exit caller' misfeature. # Verify the '--on-job-exit caller' misfeature.
function make_call_observer -a type function make_call_observer -a type

View file

@ -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) set -g fish (realpath $fish)

View file

@ -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 # Test ALL THE FISH FILES
# in share/, that is - the tests are exempt because they contain syntax errors, on purpose # in share/, that is - the tests are exempt because they contain syntax errors, on purpose

View file

@ -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 # Test all completions where the command exists
# No output is good output # No output is good output

View file

@ -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 #REQUIRES: msgfmt --help
set -l fail_count 0 set -l fail_count 0

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
# This tests various corner cases involving command substitution. # This tests various corner cases involving command substitution.

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
echo $(echo 1\n2) echo $(echo 1\n2)
# CHECK: 1 2 # CHECK: 1 2

View file

@ -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. # CHECK: 1.2.3.4.

View file

@ -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: 1.2.3.4.
# CHECK: 5.6.7.8. # CHECK: 5.6.7.8.

View file

@ -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 set -g PATH
$fish -c "nonexistent-command-1234 banana rama" $fish -c "nonexistent-command-1234 banana rama"
#CHECKERR: fish: Unknown command: nonexistent-command-1234 #CHECKERR: fish: Unknown command: nonexistent-command-1234
#CHECKERR: fish: #CHECKERR: fish:
#CHECKERR: nonexistent-command-1234 banana rama #CHECKERR: nonexistent-command-1234 banana rama
#CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^
$fish -C 'function fish_command_not_found; echo cmd-not-found; end' -ic "nonexistent-command-1234 1 2 3 4" $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: cmd-not-found
#CHECKERR: fish: #CHECKERR: fish:
#CHECKERR: nonexistent-command-1234 1 2 3 4 #CHECKERR: nonexistent-command-1234 1 2 3 4
#CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^
$fish -C 'function fish_command_not_found; echo command-not-found $argv; end' -c "nonexistent-command-abcd foo bar baz" $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: command-not-found nonexistent-command-abcd foo bar baz
#CHECKERR: fish: #CHECKERR: fish:
#CHECKERR: nonexistent-command-abcd foo bar baz #CHECKERR: nonexistent-command-abcd foo bar baz
#CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^ #CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~^

View file

@ -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 # CHECK: bar

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
commandline --input "echo foo | bar" --is-valid commandline --input "echo foo | bar" --is-valid
and echo Valid and echo Valid

View file

@ -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; function fooc; true; end;

View file

@ -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 function complete_test_alpha1
echo $argv echo $argv
end end

View file

@ -1,4 +1,4 @@
#RUN: %fish --interactive %s #RUN: %fish --interactive %s | %filter-ctrlseqs
# ^ interactive so we can do `complete` # ^ interactive so we can do `complete`
mkdir -p __fish_complete_directories/ mkdir -p __fish_complete_directories/
cd __fish_complete_directories cd __fish_complete_directories

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
function commandline function commandline
if test $argv[1] = -ct if test $argv[1] = -ct
echo --long4\n-4 echo --long4\n-4

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Validate the behavior of the `count` command. # Validate the behavior of the `count` command.
# no args # no args

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
# Ensure we don't hang on deep command substitutions - see #6503. # Ensure we don't hang on deep command substitutions - see #6503.

View file

@ -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 if command -q getconf
# (no env -u, some systems don't support that) # (no env -u, some systems don't support that)

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
begin begin
end >. end >.
status -b; and echo "status -b returned true after bad redirect on a begin block" status -b; and echo "status -b returned true after bad redirect on a begin block"

View file

@ -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. # 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 # Here fish_test_helper is the process group leader; we attempt to disown

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
# See issue 5692 # See issue 5692

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
function getenvs function getenvs
env | string match FISH_ENV_TEST_\* env | string match FISH_ENV_TEST_\*

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
# Regression test for issue #4443 # Regression test for issue #4443
eval set -l previously_undefined foo eval set -l previously_undefined foo
echo $previously_undefined echo $previously_undefined

View file

@ -1,4 +1,4 @@
#RUN: %fish -C 'set -l fish %fish' %s #RUN: %fish -C 'set -l fish %fish' %s | %filter-ctrlseqs
exec cat <nosuchfile exec cat <nosuchfile
#CHECKERR: warning: An error occurred while redirecting file 'nosuchfile' #CHECKERR: warning: An error occurred while redirecting file 'nosuchfile'

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
argparse r-require= -- --require 2>/dev/null argparse r-require= -- --require 2>/dev/null
echo $status echo $status
# CHECK: 2 # CHECK: 2

View file

@ -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) # caret position (#5812)
printf '<%s>\n' ($fish -c ' $f[a]' 2>&1) printf '<%s>\n' ($fish -c ' $f[a]' 2>&1)

View file

@ -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. # Check that we don't leave stray FDs.

View file

@ -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 echo no&background
# CHECK: no&background # CHECK: no&background

View file

@ -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 # CHECK: nocaret: 0

View file

@ -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 # CHECK: nocaret: 0

View file

@ -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 # CHECK: careton:^/dev/null

View file

@ -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 # CHECK: caretoff: ^/dev/null

View file

@ -1,4 +1,4 @@
#RUN: %fish --features=remove-percent-self %s #RUN: %fish --features=remove-percent-self %s | %filter-ctrlseqs
echo %self echo %self
# CHECK: %self # CHECK: %self

View file

@ -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 # CHECK: remove-percent-self: 0

View file

@ -1,3 +1,3 @@
# Explicitly overriding HOME/XDG_CONFIG_HOME is only required if not invoking via `make test` # 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 #CHECK: qmarkon: 1

View file

@ -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 # CHECK: qmarkoff: 1

View file

@ -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 # CHECK: a\b\c

View file

@ -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 # CHECK: a\\b\\c

View file

@ -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. # This deals with $PATH manipulation. We need to be careful not to step on anything.

View file

@ -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. # fish_exit fires successfully.
echo 'function do_exit --on-event fish_exit; echo "fish_exiting $fish_pid"; end' > /tmp/test_exit.fish echo 'function do_exit --on-event fish_exit; echo "fish_exiting $fish_pid"; end' > /tmp/test_exit.fish

View file

@ -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. # This deals with $PATH manipulation. We need to be careful not to step on anything.

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
# A for-loop-variable is a local variable in the enclosing scope. # A for-loop-variable is a local variable in the enclosing scope.
set -g i global set -g i global

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
function stuff --argument a b c function stuff --argument a b c
# This is a comment # This is a comment

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
function t --argument-names a b c function t --argument-names a b c
echo t echo t
end end

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Test the `functions` builtin # Test the `functions` builtin
function f1 function f1

View file

@ -1,4 +1,4 @@
#RUN: %fish -i %s #RUN: %fish -i %s | %filter-ctrlseqs
# Note: ^ this is interactive so we test interactive behavior, # Note: ^ this is interactive so we test interactive behavior,
# e.g. the fish_git_prompt variable handlers test `status is-interactive`. # e.g. the fish_git_prompt variable handlers test `status is-interactive`.
#REQUIRES: command -v git #REQUIRES: command -v git
@ -198,4 +198,3 @@ end
$fish -c 'complete -C "git -C ./.gi"' $fish -c 'complete -C "git -C ./.gi"'
# CHECK: ./.git/ Directory # CHECK: ./.git/ Directory

View file

@ -1,4 +1,4 @@
# RUN: %fish %s # RUN: %fish %s | %filter-ctrlseqs
set -l oldpwd $PWD set -l oldpwd $PWD
cd (mktemp -d) cd (mktemp -d)

View file

@ -1,4 +1,4 @@
#RUN: %fish %s #RUN: %fish %s | %filter-ctrlseqs
# Verify that specifying unexpected options or arguments results in an error. # Verify that specifying unexpected options or arguments results in an error.
# First using the legacy, now deprecated, long options to specify a # First using the legacy, now deprecated, long options to specify a

View file

@ -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 # Test file for fish_indent
# Note that littlecheck ignores leading whitespace, so we have to use {{ }} to explicitly match it. # 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: begin
#CHECK: {{ }}echo hi #CHECK: {{ }}echo hi
#CHECK: #CHECK:
#CHECK: end | cat | cat | begin #CHECK: end | cat | cat | begin
#CHECK: {{ }}echo hi #CHECK: {{ }}echo hi
#CHECK: end | begin #CHECK: end | begin
@ -99,7 +99,7 @@ end
#CHECK: {{ }}{{ }}echo sup #CHECK: {{ }}{{ }}echo sup
#CHECK: {{ }}case beta gamma #CHECK: {{ }}case beta gamma
#CHECK: {{ }}{{ }}echo hi #CHECK: {{ }}{{ }}echo hi
#CHECK: #CHECK:
#CHECK: end #CHECK: end
echo -n ' echo -n '
@ -117,15 +117,15 @@ function hello_world
' | $fish_indent ' | $fish_indent
#CHECK: function hello_world #CHECK: function hello_world
#CHECK: #CHECK:
#CHECK: {{ }}begin #CHECK: {{ }}begin
#CHECK: {{ }}{{ }}echo hi #CHECK: {{ }}{{ }}echo hi
#CHECK: {{ }}end | cat #CHECK: {{ }}end | cat
#CHECK: #CHECK:
#CHECK: {{ }}echo sup #CHECK: {{ }}echo sup
#CHECK: {{ }}echo sup #CHECK: {{ }}echo sup
#CHECK: {{ }}echo hello #CHECK: {{ }}echo hello
#CHECK: #CHECK:
#CHECK: {{ }}echo hello #CHECK: {{ }}echo hello
#CHECK: end #CHECK: end
@ -149,13 +149,13 @@ qqq
end' | $fish_indent end' | $fish_indent
#CHECK: echo alpha #comment1 #CHECK: echo alpha #comment1
#CHECK: #comment2 #CHECK: #comment2
#CHECK: #CHECK:
#CHECK: #comment3 #CHECK: #comment3
#CHECK: for i in abc #comment1 #CHECK: for i in abc #comment1
#CHECK: {{ }}#comment2 #CHECK: {{ }}#comment2
#CHECK: {{ }}echo hi #CHECK: {{ }}echo hi
#CHECK: end #CHECK: end
#CHECK: #CHECK:
#CHECK: switch foo #abc #CHECK: switch foo #abc
#CHECK: {{ }}# bar #CHECK: {{ }}# bar
#CHECK: {{ }}case bar #CHECK: {{ }}case bar
@ -299,26 +299,26 @@ echo bye
#CHECK: {{ }}echo yes #CHECK: {{ }}echo yes
#CHECK: en\ #CHECK: en\
#CHECK: d #CHECK: d
#CHECK: #CHECK:
#CHECK: while true #CHECK: while true
#CHECK: {{ }}builtin yes #CHECK: {{ }}builtin yes
#CHECK: end #CHECK: end
#CHECK: #CHECK:
#CHECK: alpha | beta #CHECK: alpha | beta
#CHECK: #CHECK:
#CHECK: gamma | \ #CHECK: gamma | \
#CHECK: # comment3 #CHECK: # comment3
#CHECK: delta #CHECK: delta
#CHECK: #CHECK:
#CHECK: if true #CHECK: if true
#CHECK: {{ }}echo abc #CHECK: {{ }}echo abc
#CHECK: end #CHECK: end
#CHECK: #CHECK:
#CHECK: if false # comment4 #CHECK: if false # comment4
#CHECK: {{ }}and true && false #CHECK: {{ }}and true && false
#CHECK: {{ }}echo abc #CHECK: {{ }}echo abc
#CHECK: end #CHECK: end
#CHECK: #CHECK:
#CHECK: echo hi | #CHECK: echo hi |
#CHECK: {{ }}echo bye #CHECK: {{ }}echo bye

View file

@ -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: init-command
# CHECK: 2nd init-command # CHECK: 2nd init-command

View file

@ -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: init-command
# CHECK: command # CHECK: command

View file

@ -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: init-command
# CHECK: command # CHECK: command

View file

@ -1,2 +1,2 @@
#RUN: %fish -C 'echo init-command' #RUN: %fish -C 'echo init-command' | %filter-ctrlseqs
# CHECK: init-command # CHECK: init-command

View file

@ -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) # Test that fish doesn't crash if cwd is unreadable at the start (#6597)
set -l oldpwd $PWD set -l oldpwd $PWD

View file

@ -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." $fish -c "echo 1.2.3.4."
# CHECK: 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. # Regression test for a hang.
echo "set -L" | $fish > /dev/null echo "set -L" | $fish > /dev/null

Some files were not shown because too many files have changed in this diff Show more