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
10246 10251 10260 10267 10281 10347 10366 10368 10370 10371 10263 10270 10272 10276 10277
10278 10279 10291 10293 10305 10306 10309 10316 10317 10327 10328 10329 10330 10336 10340
10345 10346 10353 10354 10356 10372 10373 3299 10360
10345 10346 10353 10354 10356 10372 10373 3299 10360 10359
The entirety of fish's C++ code has been ported to Rust (:issue:`9512`).
This means a large change in dependencies and how to build fish.
@ -21,6 +21,24 @@ Packagers should see the :ref:`For Distributors <rust-packaging>` section at the
Notable backwards-incompatible changes
--------------------------------------
- Fish now decodes keyboard input into human-readable key names.
To make this for for a wide range of terminals, fish asks terminals to speak several keyboard protocols,
including CSI u, XTerm's ``modifyOtherKeys`` and some progressive enhancements from the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/).
Depending on terminal support, this allows to bind a lot more key combinations,
including arbitrary combinations of modifiers ``ctrl``, ``alt`` and ``shift``.
This comes with a new syntax for specifying keys to builtin ``bind``.
The new syntax introduces modifier names and names for some keys that don't have an obvious and printable Unicode code point.
The old syntax remains mostly supported but the new one is preferred.
- Existing bindings that use the new names have a different meaning now.
For example
- ``bind up 'do something'`` binds the up arrow key instead of a two-key sequence.
- ``bind ctrl-x,alt-c 'do something'`` binds a sequence of two keys.
Since ``,`` and ``-`` act as separators, there are some cases where they need to be written as ``comma`` and ``minus`` respectively.
- To minimize gratuitous breakage, the key argument to ``bind`` is parsed using the old syntax in two cases:
- If key starts with a raw escape character (``\e``) or a raw ASCII control character (``\c``).
- If key consists of exactly two characters, contains none of ``,`` or ``-`` and is not a named key.
- ``random`` now uses a different random number generator and so the values you get even with the same seed have changed.
Notably, it will now work much more sensibly with very small seeds.
The seed was never guaranteed to give the same result across systems,
@ -39,7 +57,7 @@ Notable backwards-incompatible changes
Notable improvements and fixes
------------------------------
- New function ``fish_should_add_to_history`` can be overridden to decide whether a command should be added to the history (:issue:`10302`).
- :kbd:`Control-C` during command input no longer prints ``^C`` and a new prompt but merely clears the command line. This restores the behavior from version 2.2. To revert to the old behavior use ``bind \cc __fish_cancel_commandline`` (:issue:`10213`).
- :kbd:`Control-C` during command input no longer prints ``^C`` and a new prompt but merely clears the command line. This restores the behavior from version 2.2. To revert to the old behavior use ``bind ctrl-c __fish_cancel_commandline`` (:issue:`10213`).
- The :kbd:`Control-R` history search now uses glob syntax (:issue:`10131`).
- The :kbd:`Control-R` history search now operates only on the line at cursor, making it easier to quickly compose a multi-line commandline by recalling previous commands.
@ -48,6 +66,12 @@ Deprecations and removed features
- ``commandline --tokenize`` (short option ``-o``) has been deprecated in favor of ``commandline --tokens-expanded`` (short option ``-x``) which expands variables and other shell expressions, removing the need to use "eval" in custom completions (:issue:`10212`).
- A new feature flag, ``remove-percent-self`` (see ``status features``) disables PID expansion of ``%self`` which has been supplanted by ``$fish_pid`` (:issue:`10262`).
- Specifying key names as terminfo name (``bind -k``) is deprecated and may be removed in a future version.
- Flow control -- which if enabled by ``stty ixon ixoff`` allows to pause terminal input with ``ctrl-s`` and resume it with ``ctrl-q`` -- now works only while fish is executing an external command.
- When a terminal pastes text into fish using bracketed paste, fish used to switch to a special ``paste`` bind mode.
This bind mode has been removed. The behavior on paste is currently not meant to be configurable.
- When fish is stopped or terminated by a signal that cannot be caught (SIGSTOP or SIGKILL), it may leave the terminal in a state where keypresses with modifiers are sent as CSI u sequences instead of traditional control characters or escape sequecnes (that are recognized by bash/readline). If this happens, you can use the ``reset`` command from ``ncurses`` to restore the terminal state.
- ``fish_key_reader --verbose`` is now ignored, so it no longer shows raw byte values or timing information. Since fish now decodes keys, this should no longer be necessary.
Scripting improvements
----------------------
@ -78,13 +102,14 @@ Interactive improvements
New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^
- Bindings can now mix special input functions and shell commands, so ``bind \cg expand-abbr "commandline -i \n"`` works as expected (:issue:`8186`).
- Bindings can now mix special input functions and shell commands, so ``bind ctrl-g expand-abbr "commandline -i \n"`` works as expected (:issue:`8186`).
- When the cursor is on a command that resolves to an executable script, :kbd:`Alt-O` will now open that script in your editor (:issue:`10266`).
- Two improvements to the :kbd:`Alt-E` binding which edits the commandline in an external editor:
- The editor's cursor position is copied back to fish. This is currently supported for Vim and Kakoune.
- Cursor position synchronization is only supported for a set of known editors. This has been extended by also resolving aliases. For example use ``complete --wraps my-vim vim`` to synchronize cursors when `EDITOR=my-vim`.
- ``backward-kill-path-component`` and friends now treat ``#`` as part of a path component (:issue:`10271`).
- The ``E`` binding in vi mode now correctly handles the last character of the word, by jumping to the next word (:issue:`9700`).
- If the terminal supports shifted key codes from the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), ``shift-enter`` now inserts a newline instead of executing the command line.
- Vi mode has seen some improvements but continues to suffer from the lack of people working on it.
- Insert-mode :kbd:`Control-N` accepts autosuggestions (:issue:`10339`).
- Outside insert mode, the cursor will no longer be placed beyond the last character on the commandline.
@ -103,6 +128,8 @@ Completions
Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
- Fish now sets the terminal window title unconditionally (:issue:`10037`).
- Focus reporting is enabled unconditionally, not just inside tmux.
To use it, define functions that handle events ``fish_focus_in`` and ``fish_focus_out``.
Other improvements
------------------

View file

@ -34,7 +34,12 @@ def get_prompt_re(counter):
"""Return a regular expression for matching a with a given prompt counter."""
return re.compile(
r"""(?:\r\n?|^) # beginning of line
(?:\x1b[\d\[KB(m]*)* # optional colors
(?:\x1b[\d[KB(m]*)* # optional colors
(?:\x1b[\?2004h) # Bracketed paste
(?:\x1b[>4;1m) # XTerm's modifyOtherKeys
(?:\x1b[>5u) # CSI u with kitty progressive enhancement
(?:\x1b=) # set application keypad mode, so the keypad keys send unique codes
(?:\x1b[\?1004h)? # enable focus notify
(?:\[.\]\ )? # optional vi mode prompt
"""
+ (r"prompt\ %d>" % counter) # prompt with counter

View file

@ -1,13 +1,6 @@
# This adds ctest support to the project
enable_testing()
# By default, ctest runs tests serially
if(NOT CTEST_PARALLEL_LEVEL)
include(ProcessorCount)
ProcessorCount(CORES)
set(CTEST_PARALLEL_LEVEL ${CORES})
endif()
# Put in a tests folder to reduce the top level targets in IDEs.
set(CMAKE_FOLDER tests)
@ -24,8 +17,6 @@ set(SKIP_RETURN_CODE 125)
# running `make test` does not require any of the binaries to be built before testing.
# * The only way to have a test depend on a binary is to add a fake test with a name like
# "build_fish" that executes CMake recursively to build the `fish` target.
# * It is not possible to set top-level CTest options/settings such as CTEST_PARALLEL_LEVEL from
# within the CMake configuration file.
# * Circling back to the point about individual tests not being actual Makefile targets, CMake does
# not offer any way to execute a named test via the `make`/`ninja`/whatever interface; the only
# way to manually invoke test `foo` is to to manually run `ctest` and specify a regex matching
@ -33,7 +24,7 @@ set(SKIP_RETURN_CODE 125)
# The top-level test target is "fish_run_tests".
add_custom_target(fish_run_tests
COMMAND env CTEST_PARALLEL_LEVEL=${CTEST_PARALLEL_LEVEL} FISH_FORCE_COLOR=1
COMMAND env FISH_FORCE_COLOR=1
FISH_SOURCE_DIR=${CMAKE_SOURCE_DIR}
${CMAKE_CTEST_COMMAND} --force-new-ctest-process # --verbose
--output-on-failure --progress

View file

@ -7,49 +7,67 @@ Synopsis
.. synopsis::
bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] [-k | --key] SEQUENCE COMMAND ...
bind [(-M | --mode) MODE] [-k | --key] [--preset] [--user] SEQUENCE
bind (-K | --key-names) [-a | --all] [--preset] [--user]
bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] KEYS COMMAND ...
bind [(-M | --mode) MODE] [--preset] [--user] [KEYS]
bind [-a | --all] [--preset] [--user]
bind (-f | --function-names)
bind (-L | --list-modes)
bind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | [-k | --key] SEQUENCE ...
bind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | KEYS ...
Description
-----------
``bind`` manages bindings.
``bind`` manages key bindings.
It can add bindings if given a SEQUENCE of characters to bind to. These should be written as :ref:`fish escape sequences <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.
.. note::
If a script changes the commandline, it should finish by calling the ``repaint`` special input function.
If no ``SEQUENCE`` is provided, all bindings (or just the bindings in the given ``MODE``) are printed. If ``SEQUENCE`` is provided but no ``COMMAND``, just the binding matching that sequence is printed.
If no ``KEYS`` argument is provided, all bindings (in the given ``MODE``) are printed. If ``KEYS`` is provided but no ``COMMAND``, just the binding matching that sequence is printed.
Key bindings may use "modes", which mimics vi's modal input behavior. The default mode is "default". Every key binding applies to a single mode; you can specify which one with ``-M MODE``. If the key binding should change the mode, you can specify the new mode with ``-m NEW_MODE``. The mode can be viewed and changed via the ``$fish_bind_mode`` variable. If you want to change the mode from inside a fish function, use ``set fish_bind_mode MODE``.
To bind a sequence of keys, separate them with ``,``.
To save custom key bindings, put the ``bind`` statements into :ref:`config.fish <configuration>`. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists.
Options
-------
The following options are available:
**-k** or **--key**
Specify a key name, such as 'left' or 'backspace' instead of a character sequence
**-K** or **--key-names**
Display a list of available key names. Specifying **-a** or **--all** includes keys that don't have a known mapping
**-f** or **--function-names**
Display a list of available input functions
@ -69,7 +87,7 @@ The following options are available:
Specifying **-a** or **--all** without **-M** or **--mode** erases all binds in all modes regardless of sequence.
**-a** or **--all**
See **--erase** and **--key-names**
See **--erase**
**--preset** and **--user**
Specify if bind should operate on user or preset bindings.
@ -349,7 +367,7 @@ Examples
Exit the shell when :kbd:`Control`\ +\ :kbd:`D` is pressed::
bind \cd 'exit'
bind ctrl-d 'exit'
Perform a history search when :kbd:`Page Up` is pressed::

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.
If the character sequence matches a special key name (see ``bind --key-names``), both ``bind CHARS ...`` and ``bind -k KEYNAME ...`` usage will be shown. In verbose mode (enabled by passing ``--verbose``), additional details about the characters received, such as the delay between chars, are written to standard error.
The following options are available:
**-c** or **--continuous**
Begins a session where multiple key sequences can be inspected. By default the program exits after capturing a single key sequence.
**-V** or **--verbose**
Tells fish_key_reader to output timing information and explain the sequence in more detail.
**-h** or **--help**
Displays help about using this command.
@ -34,8 +29,6 @@ The following options are available:
Usage Notes
-----------
In verbose mode, the delay in milliseconds since the previous character was received is included in the diagnostic information written to standard error. This information may be useful to determine the optimal ``fish_escape_delay_ms`` setting or learn the amount of lag introduced by tools like ``ssh``, ``mosh`` or ``tmux``.
``fish_key_reader`` intentionally disables handling of many signals. To terminate ``fish_key_reader`` in ``--continuous`` mode do:
- press :kbd:`Control`\ +\ :kbd:`C` twice, or
@ -51,12 +44,4 @@ Example
> fish_key_reader
Press a key:
# press up-arrow
bind \e\[A 'do something'
> fish_key_reader --verbose
Press a key:
# press alt+enter
hex: 1B char: \e
( 0.027 ms) hex: D char: \cM (or \r)
bind \e\r 'do something'
bind up 'do something'

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>`::
# 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``.
If you change your mind on a binding and want to go back to fish's default, you can simply erase it again::
bind --erase \cc
bind --erase ctrl-c
Fish remembers its preset bindings and so it will take effect again. This saves you from having to remember what it was before and add it again yourself.
If you use :ref:`vi bindings <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:
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 # pressing control-c
> fish_key_reader # Press Alt + right-arrow
Press a key:
Press [ctrl-C] again to exit
bind \cC 'do something'
> fish_key_reader # pressing the right-arrow
Press a key:
bind \e\[C 'do something'
Note that some key combinations are indistinguishable or unbindable. For instance control-i *is the same* as the tab key. This is a terminal limitation that fish can't do anything about. When ``fish_key_reader`` prints the same sequence for two different keys, then that is because your terminal sends the same sequence for them.
Also, :kbd:`Escape` is the same thing as :kbd:`Alt` in a terminal. To distinguish between pressing :kbd:`Escape` and then another key, and pressing :kbd:`Alt` and that key (or an escape sequence the key sends), fish waits for a certain time after seeing an escape character. This is configurable via the :envvar:`fish_escape_delay_ms` variable.
@ -576,10 +567,10 @@ If you want to be able to press :kbd:`Escape` and then a character and have it c
Similarly, to disambiguate *other* keypresses where you've bound a subsequence and a longer sequence, fish has :envvar:`fish_sequence_key_delay_ms`::
# This binds "jk" to switch to normal mode in vi mode.
# This binds the sequence j,k to switch to normal mode in vi mode.
# If you kept it like that, every time you press "j",
# fish would wait for a "k" or other key to disambiguate
bind -M insert -m default jk cancel repaint-mode
bind -M insert -m default j,k cancel repaint-mode
# After setting this, fish only waits 200ms for the "k",
# or decides to treat the "j" as a separate sequence, inserting it.

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 user -d 'Operate on user bindings'
complete -c bind -n __fish_bind_test1 -a '(bind --key-names)' -d 'Key name' -x
complete -c bind -n __fish_bind_test2 -a '(bind --function-names)' -d 'Function name' -x
function __fish_bind_complete
argparse M/mode= m/sets-mode= preset user s/silent \
a/all function-names list-modes e/erase -- (commandline -xpc)[2..] 2>/dev/null
or return 1
set -l token (commandline -ct)
if test (count $argv) = 0 && set -l prefix (string match -r -- '(.*,)?(ctrl-|alt-|shift-)*' $token)
printf '%sctrl-\tCtrl modifier…\n' $prefix
printf '%salt-\tAlt modifier…\n' $prefix
printf '%sshift-\tShift modifier…\n' $prefix
set -l key_names plus minus comma backspace delete escape \
enter up down left right pageup pagedown home end insert tab \
space F(seq 12)
printf '%s\tNamed key\n' $prefix$key_names
end
end
complete -c bind -k -a '(__fish_bind_complete)' -f

View file

@ -191,52 +191,6 @@ end" >$__fish_config_dir/config.fish
# Load key bindings
__fish_reload_key_bindings
# Enable bracketed paste exception when running unit tests so we don't have to add
# the sequences to bind.expect
if not set -q FISH_UNIT_TESTS_RUNNING
# Enable bracketed paste before every prompt (see __fish_shared_bindings for the bindings).
# We used to do this for read, but that would break non-interactive use and
# compound commandlines like `read; cat`, because
# it won't disable it after the read.
function __fish_enable_bracketed_paste --on-event fish_prompt
printf "\e[?2004h"
end
# Disable BP before every command because that might not support it.
function __fish_disable_bracketed_paste --on-event fish_preexec --on-event fish_exit
printf "\e[?2004l"
end
# Tell the terminal we support BP. Since we are in __f_c_i, the first fish_prompt
# has already fired.
# But only if we're interactive, in case we are in `read`
status is-interactive
and __fish_enable_bracketed_paste
end
# Similarly, enable TMUX's focus reporting when in tmux.
# This will be handled by
# - The keybindings (reading the sequence and triggering an event)
# - Any listeners (like the vi-cursor)
if set -q TMUX
and not set -q FISH_UNIT_TESTS_RUNNING
# Allow overriding these - we're called very late,
# and so it's otherwise awkward to disable focus reporting again.
not functions -q __fish_enable_focus
and function __fish_enable_focus --on-event fish_postexec
echo -n \e\[\?1004h
end
not functions -q __fish_disable_focus
and function __fish_disable_focus --on-event fish_preexec
echo -n \e\[\?1004l
end
# Note: Don't call this initially because, even though we're in a fish_prompt event,
# tmux reacts sooo quickly that we'll still get a sequence before we're prepared for it.
# So this means that we won't get focus events until you've run at least one command, but that's preferable
# to always seeing `^[[I` when starting fish.
# __fish_enable_focus
end
# Detect whether the terminal reflows on its own
# If it does we shouldn't do it.
# Allow $fish_handle_reflow to override it.

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)
or return 0 # We already printed a warning, so tell the caller to finish.
__fish_disable_bracketed_paste
$editor $command_path
__fish_enable_bracketed_paste
return 0
end

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"
set -l legacy_bind bind
# These are some bindings that are supposed to be shared between vi mode and default mode.
# They are supposed to be unrelated to text-editing (or movement).
# This takes $argv so the vi-bindings can pass the mode they are valid in.
@ -9,164 +10,128 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
return 1
end
bind --preset $argv \cy yank
bind --preset $argv ctrl-y yank
or return # protect against invalid $argv
bind --preset $argv \ey yank-pop
bind --preset $argv alt-y yank-pop
# Left/Right arrow
bind --preset $argv -k right forward-char
bind --preset $argv -k left backward-char
bind --preset $argv \e\[C forward-char
bind --preset $argv \e\[D backward-char
bind --preset $argv right forward-char
bind --preset $argv left backward-char
$legacy_bind --preset $argv -k right forward-char
$legacy_bind --preset $argv -k left backward-char
$legacy_bind --preset $argv \e\[C forward-char
$legacy_bind --preset $argv \e\[D backward-char
# Some terminals output these when they're in in keypad mode.
bind --preset $argv \eOC forward-char
bind --preset $argv \eOD backward-char
$legacy_bind --preset $argv \eOC forward-char
$legacy_bind --preset $argv \eOD backward-char
# Ctrl-left/right - these also work in vim.
bind --preset $argv \e\[1\;5C forward-word
bind --preset $argv \e\[1\;5D backward-word
bind --preset $argv ctrl-right forward-word
bind --preset $argv ctrl-left backward-word
$legacy_bind --preset $argv \e\[1\;5C forward-word
$legacy_bind --preset $argv \e\[1\;5D backward-word
bind --preset $argv -k ppage beginning-of-history
bind --preset $argv -k npage end-of-history
bind --preset $argv pageup beginning-of-history
bind --preset $argv pagedown end-of-history
$legacy_bind --preset $argv -k ppage beginning-of-history
$legacy_bind --preset $argv -k npage end-of-history
# Interaction with the system clipboard.
bind --preset $argv \cx fish_clipboard_copy
bind --preset $argv \cv fish_clipboard_paste
bind --preset $argv ctrl-x fish_clipboard_copy
bind --preset $argv ctrl-v fish_clipboard_paste
bind --preset $argv \e cancel
bind --preset $argv \t complete
bind --preset $argv \cs pager-toggle-search
bind --preset $argv escape cancel
bind --preset $argv ctrl-\[ cancel
bind --preset $argv tab complete
bind --preset $argv ctrl-i complete
bind --preset $argv ctrl-s pager-toggle-search
# shift-tab does a tab complete followed by a search.
bind --preset $argv --key btab complete-and-search
bind --preset $argv -k sdc history-pager-delete or backward-delete-char # shifted delete
bind --preset $argv shift-tab complete-and-search
$legacy_bind --preset $argv -k btab complete-and-search
bind --preset $argv shift-delete history-pager-delete or backward-delete-char
$legacy_bind --preset $argv -k sdc history-pager-delete or backward-delete-char
bind --preset $argv -k down down-or-search
bind --preset $argv -k up up-or-search
bind --preset $argv \e\[A up-or-search
bind --preset $argv \e\[B down-or-search
bind --preset $argv \eOA up-or-search
bind --preset $argv \eOB down-or-search
bind --preset $argv down down-or-search
$legacy_bind --preset $argv -k down down-or-search
bind --preset $argv up up-or-search
$legacy_bind --preset $argv -k up up-or-search
$legacy_bind --preset $argv \e\[A up-or-search
$legacy_bind --preset $argv \e\[B down-or-search
$legacy_bind --preset $argv \eOA up-or-search
$legacy_bind --preset $argv \eOB down-or-search
bind --preset $argv -k sright forward-bigword
bind --preset $argv -k sleft backward-bigword
bind --preset $argv shift-right forward-bigword
bind --preset $argv shift-left backward-bigword
$legacy_bind --preset $argv -k sright forward-bigword
$legacy_bind --preset $argv -k sleft backward-bigword
# Alt-left/Alt-right
bind --preset $argv \e\eOC nextd-or-forward-word
bind --preset $argv \e\eOD prevd-or-backward-word
bind --preset $argv \e\e\[C nextd-or-forward-word
bind --preset $argv \e\e\[D prevd-or-backward-word
bind --preset $argv \eO3C nextd-or-forward-word
bind --preset $argv \eO3D prevd-or-backward-word
bind --preset $argv \e\[3C nextd-or-forward-word
bind --preset $argv \e\[3D prevd-or-backward-word
bind --preset $argv \e\[1\;3C nextd-or-forward-word
bind --preset $argv \e\[1\;3D prevd-or-backward-word
bind --preset $argv \e\[1\;9C nextd-or-forward-word #iTerm2
bind --preset $argv \e\[1\;9D prevd-or-backward-word #iTerm2
bind --preset $argv alt-right nextd-or-forward-word
bind --preset $argv alt-left prevd-or-backward-word
$legacy_bind --preset $argv \e\eOC nextd-or-forward-word
$legacy_bind --preset $argv \e\eOD prevd-or-backward-word
$legacy_bind --preset $argv \e\e\[C nextd-or-forward-word
$legacy_bind --preset $argv \e\e\[D prevd-or-backward-word
$legacy_bind --preset $argv \eO3C nextd-or-forward-word
$legacy_bind --preset $argv \eO3D prevd-or-backward-word
$legacy_bind --preset $argv \e\[3C nextd-or-forward-word
$legacy_bind --preset $argv \e\[3D prevd-or-backward-word
$legacy_bind --preset $argv \e\[1\;3C nextd-or-forward-word
$legacy_bind --preset $argv \e\[1\;3D prevd-or-backward-word
$legacy_bind --preset $argv \e\[1\;9C nextd-or-forward-word #iTerm2
$legacy_bind --preset $argv \e\[1\;9D prevd-or-backward-word #iTerm2
# Alt-up/Alt-down
bind --preset $argv \e\eOA history-token-search-backward
bind --preset $argv \e\eOB history-token-search-forward
bind --preset $argv \e\e\[A history-token-search-backward
bind --preset $argv \e\e\[B history-token-search-forward
bind --preset $argv \eO3A history-token-search-backward
bind --preset $argv \eO3B history-token-search-forward
bind --preset $argv \e\[3A history-token-search-backward
bind --preset $argv \e\[3B history-token-search-forward
bind --preset $argv \e\[1\;3A history-token-search-backward
bind --preset $argv \e\[1\;3B history-token-search-forward
bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2
bind --preset $argv \e\[1\;9B history-token-search-forward # iTerm2
bind --preset $argv alt-up history-token-search-backward
bind --preset $argv alt-down history-token-search-forward
$legacy_bind --preset $argv \e\eOA history-token-search-backward
$legacy_bind --preset $argv \e\eOB history-token-search-forward
$legacy_bind --preset $argv \e\e\[A history-token-search-backward
$legacy_bind --preset $argv \e\e\[B history-token-search-forward
$legacy_bind --preset $argv \eO3A history-token-search-backward
$legacy_bind --preset $argv \eO3B history-token-search-forward
$legacy_bind --preset $argv \e\[3A history-token-search-backward
$legacy_bind --preset $argv \e\[3B history-token-search-forward
$legacy_bind --preset $argv \e\[1\;3A history-token-search-backward
$legacy_bind --preset $argv \e\[1\;3B history-token-search-forward
$legacy_bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2
$legacy_bind --preset $argv \e\[1\;9B history-token-search-forward # iTerm2
# Bash compatibility
# https://github.com/fish-shell/fish-shell/issues/89
bind --preset $argv \e. history-token-search-backward
bind --preset $argv alt-. history-token-search-backward
bind --preset $argv \el __fish_list_current_token
bind --preset $argv \eo __fish_preview_current_file
bind --preset $argv \ew __fish_whatis_current_token
bind --preset $argv \cl clear-screen
bind --preset $argv \cc cancel-commandline
bind --preset $argv \cu backward-kill-line
bind --preset $argv \cw backward-kill-path-component
bind --preset $argv \e\[F end-of-line
bind --preset $argv \e\[H beginning-of-line
bind --preset $argv alt-l __fish_list_current_token
bind --preset $argv alt-o __fish_preview_current_file
bind --preset $argv alt-w __fish_whatis_current_token
bind --preset $argv ctrl-l clear-screen
bind --preset $argv ctrl-c cancel-commandline
bind --preset $argv ctrl-u backward-kill-line
bind --preset $argv ctrl-w backward-kill-path-component
bind --preset $argv end end-of-line
$legacy_bind --preset $argv \e\[F end-of-line
bind --preset $argv home beginning-of-line
$legacy_bind --preset $argv \e\[H beginning-of-line
bind --preset $argv \ed 'set -l cmd (commandline); if test -z "$cmd"; echo; dirh; commandline -f repaint; else; commandline -f kill-word; end'
bind --preset $argv \cd delete-or-exit
bind --preset $argv alt-d 'set -l cmd (commandline); if test -z "$cmd"; echo; dirh; commandline -f repaint; else; commandline -f kill-word; end'
bind --preset $argv ctrl-d delete-or-exit
bind --preset $argv \es 'for cmd in sudo doas please; if command -q $cmd; fish_commandline_prepend $cmd; break; end; end'
bind --preset $argv alt-s 'for cmd in sudo doas please; if command -q $cmd; fish_commandline_prepend $cmd; break; end; end'
# Allow reading manpages by pressing F1 (many GUI applications) or Alt+h (like in zsh).
bind --preset $argv -k f1 __fish_man_page
bind --preset $argv \eh __fish_man_page
bind --preset $argv F1 __fish_man_page
$legacy_bind --preset $argv -k f1 __fish_man_page
bind --preset $argv alt-h __fish_man_page
# This will make sure the output of the current command is paged using the default pager when
# you press Meta-p.
# If none is set, less will be used.
bind --preset $argv \ep __fish_paginate
bind --preset $argv alt-p __fish_paginate
# Make it easy to turn an unexecuted command into a comment in the shell history. Also,
# remove the commenting chars so the command can be further edited then executed.
bind --preset $argv \e\# __fish_toggle_comment_commandline
bind --preset $argv alt-# __fish_toggle_comment_commandline
# The [meta-e] and [meta-v] keystrokes invoke an external editor on the command buffer.
bind --preset $argv \ee edit_command_buffer
bind --preset $argv \ev edit_command_buffer
# Tmux' focus events.
# Exclude paste mode because that should get _everything_ literally.
for mode in (bind --list-modes | string match -v paste)
# We only need the in-focus event currently (to redraw the vi-cursor).
bind --preset -M $mode \e\[I 'emit fish_focus_in'
bind --preset -M $mode \e\[O false
bind --preset -M $mode \e\[\?1004h false
end
# Support for "bracketed paste"
# The way it works is that we acknowledge our support by printing
# \e\[?2004h
# then the terminal will "bracket" every paste in
# \e\[200~ and \e\[201~
# Every character in between those two will be part of the paste and should not cause a binding to execute (like \n executing commands).
#
# We enable it after every command and disable it before (in __fish_config_interactive.fish)
#
# Support for this seems to be ubiquitous - emacs enables it unconditionally (!) since 25.1
# (though it only supports it since then, it seems to be the last term to gain support).
#
# NOTE: This is more of a "security" measure than a proper feature.
# The better way to paste remains the `fish_clipboard_paste` function (bound to \cv by default).
# We don't disable highlighting here, so it will be redone after every character (which can be slow),
# and it doesn't handle "paste-stop" sequences in the paste (which the terminal needs to strip).
#
# See http://thejh.net/misc/website-terminal-copy-paste.
# Bind the starting sequence in every bind mode, even user-defined ones.
# Exclude paste mode or there'll be an additional binding after switching between emacs and vi
for mode in (bind --list-modes | string match -v paste)
bind --preset -M $mode -m paste \e\[200~ "__fish_start_bracketed_paste $mode"
end
# This sequence ends paste-mode and returns to the previous mode we have saved before.
bind --preset -M paste \e\[201~ __fish_stop_bracketed_paste
# In paste-mode, everything self-inserts except for the sequence to get out of it
bind --preset -M paste "" self-insert
# Pass through formatting control characters else they may be dropped
# on some terminals.
bind --preset -M paste \b 'commandline -i \b'
bind --preset -M paste \t 'commandline -i \t'
bind --preset -M paste \v 'commandline -i \v'
# Without this, a \r will overwrite the other text, rendering it invisible - which makes the exercise kinda pointless.
bind --preset -M paste \r "commandline -i \n"
# We usually just pass the text through as-is to facilitate pasting code,
# but when the current token contains an unbalanced single-quote (`'`),
# we escape all single-quotes and backslashes, effectively turning the paste
# into one literal token, to facilitate pasting non-code (e.g. markdown or git commitishes)
bind --preset -M paste "'" "__fish_commandline_insert_escaped \' \$__fish_paste_quoted"
bind --preset -M paste \\ "__fish_commandline_insert_escaped \\\ \$__fish_paste_quoted"
# Only insert spaces if we're either quoted or not at the beginning of the commandline
# - this strips leading spaces if they would trigger histignore.
bind --preset -M paste " " self-insert-notfirst
# These keystrokes invoke an external editor on the command buffer.
bind --preset $argv alt-e edit_command_buffer
bind --preset $argv alt-v edit_command_buffer
# Bindings that are shared in text-insertion modes.
if not set -l index (contains --index -- -M $argv)
@ -183,48 +148,27 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
bind --preset $argv "&" self-insert expand-abbr
bind --preset $argv ">" self-insert expand-abbr
bind --preset $argv "<" self-insert expand-abbr
bind --preset $argv shift-enter expand-abbr "commandline -i \n"
# Shift+Return as sent with XTerm.vt100.formatOtherKeys: 0
bind --preset $argv \e\[27\;2\;13~ expand-abbr "commandline -i \n"
$legacy_bind --preset $argv \e\[27\;2\;13~ expand-abbr "commandline -i \n"
# Shift+Return CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1
bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n"
bind --preset $argv \e\n expand-abbr "commandline -i \n"
bind --preset $argv \e\r expand-abbr "commandline -i \n"
$legacy_bind --preset $argv \e\[13\;2u expand-abbr "commandline -i \n"
bind --preset $argv alt-enter expand-abbr "commandline -i \n"
# Closing a command substitution expands abbreviations
bind --preset $argv ")" self-insert expand-abbr
# Ctrl-space inserts space without expanding abbrs
bind --preset $argv ctrl-space 'test -n "$(commandline)" && commandline -i " "'
bind --preset $argv -k nul 'test -n "$(commandline)" && commandline -i " "'
# Shift-space (CSI u escape sequence) behaves like space because it's easy to mistype.
bind --preset $argv \e\[32\;2u 'commandline -i " "; commandline -f expand-abbr'
# Shift-space behaves like space because it's easy to mistype.
bind --preset $argv shift-space 'commandline -i " "; commandline -f expand-abbr'
$legacy_bind --preset $argv \e\[32\;2u 'commandline -i " "; commandline -f expand-abbr' # CSI u escape sequence
bind --preset $argv \n execute
bind --preset $argv \r execute
bind --preset $argv enter execute
bind --preset $argv ctrl-j execute
bind --preset $argv ctrl-m execute
# Make Control+Return behave like Return because it's easy to mistype after accepting an autosuggestion.
bind --preset $argv \e\[27\;5\;13~ execute # Sent with XTerm.vt100.formatOtherKeys: 0
bind --preset $argv \e\[13\;5u execute # CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1
bind --preset $argv ctrl-enter execute
$legacy_bind --preset $argv \e\[27\;5\;13~ execute # Sent with XTerm.vt100.formatOtherKeys: 0
$legacy_bind --preset $argv \e\[13\;5u execute # CSI u sequence, sent with XTerm.vt100.formatOtherKeys: 1
end
end
function __fish_commandline_insert_escaped --description 'Insert the first arg escaped if a second arg is given'
if set -q argv[2]
commandline -i \\$argv[1]
else
commandline -i $argv[1]
end
end
function __fish_start_bracketed_paste
# Save the last bind mode so we can restore it.
set -g __fish_last_bind_mode $argv[1]
# If the token is currently single-quoted,
# we escape single-quotes (and backslashes).
string match -q 'single*' (__fish_tokenizer_state -- (commandline -ct | string collect))
and set -g __fish_paste_quoted 1
commandline -f begin-undo-group
end
function __fish_stop_bracketed_paste
# Restore the last bind mode.
set fish_bind_mode $__fish_last_bind_mode
set -e __fish_paste_quoted
commandline -f end-undo-group
end

View file

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

View file

@ -23,49 +23,5 @@ function fish_clipboard_paste
return
end
# Also split on \r, otherwise it looks confusing
set data (string split \r -- $data | string split \n)
if commandline --search-field >/dev/null
commandline --search-field -i -- $data
return
end
# If the current token has an unmatched single-quote,
# escape all single-quotes (and backslashes) in the paste,
# in order to turn it into a single literal token.
#
# This eases pasting non-code (e.g. markdown or git commitishes).
set -l quote_state (__fish_tokenizer_state -- (commandline -ct | string collect))
if contains -- $quote_state single single-escaped
if status test-feature regex-easyesc
set data (string replace -ra "(['\\\])" '\\\\$1' -- $data)
else
set data (string replace -ra "(['\\\])" '\\\\\\\$1' -- $data)
end
else if not contains -- $quote_state double double-escaped
and set -q data[2]
# Leading whitespace in subsequent lines is unneded, since fish
# already indents. Also gets rid of tabs (issue #5274).
set -l tmp
for line in $data
switch $quote_state
case normal
set -a tmp (string trim -l -- $line)
case single single-escaped double double-escaped escaped
set -a tmp $line
end
set quote_state (__fish_tokenizer_state -i $quote_state -- $line)
end
set data $data[1] $tmp[2..]
end
if not string length -q -- (commandline -c)
# If we're at the beginning of the first line, trim whitespace from the start,
# so we don't trigger ignoring history.
set data[1] (string trim -l -- $data[1])
end
if test -n "$data"
commandline -i -- $data
end
__fish_paste $data
end

View file

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

View file

@ -1,4 +1,5 @@
function fish_vi_key_bindings --description 'vi-like key bindings for fish'
set -l legacy_bind bind
if contains -- -h $argv
or contains -- --help $argv
echo "Sorry but this function doesn't support -h or --help" >&2
@ -37,7 +38,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
set -l init_mode insert
# These are only the special vi-style keys
# not end/home, we share those.
set -l eol_keys \$ g\$
set -l eol_keys \$ g,\$
set -l bol_keys \^ 0 g\^
if contains -- $argv[1] insert default visual
@ -56,7 +57,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
# Add a way to switch from insert to normal (command) mode.
# Note if we are paging, we want to stay in insert mode
# See #2871
bind -s --preset -M insert \e '
set -l on_escape '
if commandline -P
commandline -f cancel
else
@ -67,23 +68,26 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
commandline -f repaint-mode
end
'
bind -s --preset -M insert escape $on_escape
bind -s --preset -M insert ctrl-\[ $on_escape
# Default (command) mode
bind -s --preset :q exit
bind -s --preset -m insert \cc cancel-commandline repaint-mode
bind -s --preset :,q exit
bind -s --preset -m insert ctrl-c cancel-commandline repaint-mode
bind -s --preset -M default h backward-char
bind -s --preset -M default l forward-char
bind -s --preset -m insert \n execute
bind -s --preset -m insert \r execute
bind -s --preset -m insert enter execute
bind -s --preset -m insert ctrl-j execute
bind -s --preset -m insert ctrl-m execute
bind -s --preset -m insert o 'set fish_cursor_end_mode exclusive' insert-line-under repaint-mode
bind -s --preset -m insert O 'set fish_cursor_end_modefish_cursor_end_modeexclusive' insert-line-over repaint-mode
bind -s --preset -m insert O 'set fish_cursor_end_mode exclusive' insert-line-over repaint-mode
bind -s --preset -m insert i repaint-mode
bind -s --preset -m insert I beginning-of-line repaint-mode
bind -s --preset -m insert a 'set fish_cursor_end_mode exclusive' forward-single-char repaint-mode
bind -s --preset -m insert A 'set fish_cursor_end_mode exclusive' end-of-line repaint-mode
bind -s --preset -m visual v begin-selection repaint-mode
bind -s --preset gg beginning-of-buffer
bind -s --preset g,g beginning-of-buffer
bind -s --preset G end-of-buffer
for key in $eol_keys
@ -94,7 +98,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
end
bind -s --preset u undo
bind -s --preset \cr redo
bind -s --preset ctrl-r redo
bind -s --preset [ history-token-search-backward
bind -s --preset ] history-token-search-forward
@ -104,14 +108,14 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset j down-or-search
bind -s --preset b backward-word
bind -s --preset B backward-bigword
bind -s --preset ge backward-word
bind -s --preset gE backward-bigword
bind -s --preset g,e backward-word
bind -s --preset g,E backward-bigword
bind -s --preset w forward-word forward-single-char
bind -s --preset W forward-bigword forward-single-char
bind -s --preset e 'set fish_cursor_end_mode exclusive' forward-single-char forward-word backward-char 'set fish_cursor_end_mode inclusive'
bind -s --preset E 'set fish_cursor_end_mode exclusive' forward-single-char forward-bigword backward-char 'set fish_cursor_end_mode inclusive'
bind -s --preset -M insert \cn accept-autosuggestion
bind -s --preset -M insert ctrl-n accept-autosuggestion
# Vi/Vim doesn't support these keys in insert mode but that seems silly so we do so anyway.
bind -s --preset -M insert -k home beginning-of-line
@ -128,104 +132,105 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M default -k dc delete-char 'set fish_cursor_end_mode exclusive' forward-single-char backward-char 'set fish_cursor_end_mode inclusive'
# Backspace deletes a char in insert mode, but not in normal/default mode.
bind -s --preset -M insert -k backspace backward-delete-char
bind -s --preset -M default -k backspace backward-char
bind -s --preset -M insert \ch backward-delete-char
bind -s --preset -M default \ch backward-char
bind -s --preset -M insert \x7f backward-delete-char
bind -s --preset -M default \x7f backward-char
bind -s --preset -M insert backspace backward-delete-char
bind -s --preset -M insert shift-backspace backward-delete-char
$legacy_bind -s --preset -M insert -k backspace backward-delete-char
bind -s --preset -M default backspace backward-char
$legacy_bind -s --preset -M default -k backspace backward-char
bind -s --preset -M insert ctrl-h backward-delete-char
bind -s --preset -M default ctrl-h backward-char
bind -s --preset dd kill-whole-line
bind -s --preset d,d kill-whole-line
bind -s --preset D kill-line
bind -s --preset d\$ kill-line
bind -s --preset d\^ backward-kill-line
bind -s --preset d0 backward-kill-line
bind -s --preset dw kill-word
bind -s --preset dW kill-bigword
bind -s --preset diw forward-single-char forward-single-char backward-word kill-word
bind -s --preset diW forward-single-char forward-single-char backward-bigword kill-bigword
bind -s --preset daw forward-single-char forward-single-char backward-word kill-word
bind -s --preset daW forward-single-char forward-single-char backward-bigword kill-bigword
bind -s --preset de kill-word
bind -s --preset dE kill-bigword
bind -s --preset db backward-kill-word
bind -s --preset dB backward-kill-bigword
bind -s --preset dge backward-kill-word
bind -s --preset dgE backward-kill-bigword
bind -s --preset df begin-selection forward-jump kill-selection end-selection
bind -s --preset dt begin-selection forward-jump backward-char kill-selection end-selection
bind -s --preset dF begin-selection backward-jump kill-selection end-selection
bind -s --preset dT begin-selection backward-jump forward-single-char kill-selection end-selection
bind -s --preset dh backward-char delete-char
bind -s --preset dl delete-char
bind -s --preset di backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection
bind -s --preset da backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection
bind -s --preset 'd;' begin-selection repeat-jump kill-selection end-selection
bind -s --preset 'd,' begin-selection repeat-jump-reverse kill-selection end-selection
bind -s --preset d,\$ kill-line
bind -s --preset d,\^ backward-kill-line
bind -s --preset d,0 backward-kill-line
bind -s --preset d,w kill-word
bind -s --preset d,W kill-bigword
bind -s --preset d,i,w forward-single-char forward-single-char backward-word kill-word
bind -s --preset d,i,W forward-single-char forward-single-char backward-bigword kill-bigword
bind -s --preset d,a,w forward-single-char forward-single-char backward-word kill-word
bind -s --preset d,a,W forward-single-char forward-single-char backward-bigword kill-bigword
bind -s --preset d,e kill-word
bind -s --preset d,E kill-bigword
bind -s --preset d,b backward-kill-word
bind -s --preset d,B backward-kill-bigword
bind -s --preset d,g,e backward-kill-word
bind -s --preset d,g,E backward-kill-bigword
bind -s --preset d,f begin-selection forward-jump kill-selection end-selection
bind -s --preset d,t begin-selection forward-jump backward-char kill-selection end-selection
bind -s --preset d,F begin-selection backward-jump kill-selection end-selection
bind -s --preset d,T begin-selection backward-jump forward-single-char kill-selection end-selection
bind -s --preset d,h backward-char delete-char
bind -s --preset d,l delete-char
bind -s --preset d,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection
bind -s --preset d,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection
bind -s --preset 'd,;' begin-selection repeat-jump kill-selection end-selection
bind -s --preset 'd,comma' begin-selection repeat-jump-reverse kill-selection end-selection
bind -s --preset -m insert s delete-char repaint-mode
bind -s --preset -m insert S kill-inner-line repaint-mode
bind -s --preset -m insert cc kill-inner-line repaint-mode
bind -s --preset -m insert c,c kill-inner-line repaint-mode
bind -s --preset -m insert C kill-line repaint-mode
bind -s --preset -m insert c\$ kill-line repaint-mode
bind -s --preset -m insert c\^ backward-kill-line repaint-mode
bind -s --preset -m insert c0 backward-kill-line repaint-mode
bind -s --preset -m insert cw kill-word repaint-mode
bind -s --preset -m insert cW kill-bigword repaint-mode
bind -s --preset -m insert ciw forward-single-char forward-single-char backward-word kill-word repaint-mode
bind -s --preset -m insert ciW forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
bind -s --preset -m insert caw forward-single-char forward-single-char backward-word kill-word repaint-mode
bind -s --preset -m insert caW forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
bind -s --preset -m insert ce kill-word repaint-mode
bind -s --preset -m insert cE kill-bigword repaint-mode
bind -s --preset -m insert cb backward-kill-word repaint-mode
bind -s --preset -m insert cB backward-kill-bigword repaint-mode
bind -s --preset -m insert cge backward-kill-word repaint-mode
bind -s --preset -m insert cgE backward-kill-bigword repaint-mode
bind -s --preset -m insert cf begin-selection forward-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert ct begin-selection forward-jump backward-char kill-selection end-selection repaint-mode
bind -s --preset -m insert cF begin-selection backward-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert cT begin-selection backward-jump forward-single-char kill-selection end-selection repaint-mode
bind -s --preset -m insert ch backward-char begin-selection kill-selection end-selection repaint-mode
bind -s --preset -m insert cl begin-selection kill-selection end-selection repaint-mode
bind -s --preset -m insert ci backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert ca backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert c,\$ kill-line repaint-mode
bind -s --preset -m insert c,\^ backward-kill-line repaint-mode
bind -s --preset -m insert c,0 backward-kill-line repaint-mode
bind -s --preset -m insert c,w kill-word repaint-mode
bind -s --preset -m insert c,W kill-bigword repaint-mode
bind -s --preset -m insert c,i,w forward-single-char forward-single-char backward-word kill-word repaint-mode
bind -s --preset -m insert c,i,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
bind -s --preset -m insert c,a,w forward-single-char forward-single-char backward-word kill-word repaint-mode
bind -s --preset -m insert c,a,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
bind -s --preset -m insert c,e kill-word repaint-mode
bind -s --preset -m insert c,E kill-bigword repaint-mode
bind -s --preset -m insert c,b backward-kill-word repaint-mode
bind -s --preset -m insert c,B backward-kill-bigword repaint-mode
bind -s --preset -m insert c,g,e backward-kill-word repaint-mode
bind -s --preset -m insert c,g,E backward-kill-bigword repaint-mode
bind -s --preset -m insert c,f begin-selection forward-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert c,t begin-selection forward-jump backward-char kill-selection end-selection repaint-mode
bind -s --preset -m insert c,F begin-selection backward-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert c,T begin-selection backward-jump forward-single-char kill-selection end-selection repaint-mode
bind -s --preset -m insert c,h backward-char begin-selection kill-selection end-selection repaint-mode
bind -s --preset -m insert c,l begin-selection kill-selection end-selection repaint-mode
bind -s --preset -m insert c,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode
bind -s --preset -m insert c,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode
bind -s --preset '~' togglecase-char forward-single-char
bind -s --preset gu downcase-word
bind -s --preset gU upcase-word
bind -s --preset g,u downcase-word
bind -s --preset g,U upcase-word
bind -s --preset J end-of-line delete-char
bind -s --preset K 'man (commandline -t) 2>/dev/null; or echo -n \a'
bind -s --preset yy kill-whole-line yank
for seq in '"*yy' '"*Y' '"+yy' '"+Y'
for seq in '",*,y,y' '",*,Y' '"+,y,y' '",+,Y'
bind -s --preset $seq fish_clipboard_copy
end
bind -s --preset Y kill-whole-line yank
bind -s --preset y\$ kill-line yank
bind -s --preset y\^ backward-kill-line yank
bind -s --preset y0 backward-kill-line yank
bind -s --preset yw kill-word yank
bind -s --preset yW kill-bigword yank
bind -s --preset yiw forward-single-char forward-single-char backward-word kill-word yank
bind -s --preset yiW forward-single-char forward-single-char backward-bigword kill-bigword yank
bind -s --preset yaw forward-single-char forward-single-char backward-word kill-word yank
bind -s --preset yaW forward-single-char forward-single-char backward-bigword kill-bigword yank
bind -s --preset ye kill-word yank
bind -s --preset yE kill-bigword yank
bind -s --preset yb backward-kill-word yank
bind -s --preset yB backward-kill-bigword yank
bind -s --preset yge backward-kill-word yank
bind -s --preset ygE backward-kill-bigword yank
bind -s --preset yf begin-selection forward-jump kill-selection yank end-selection
bind -s --preset yt begin-selection forward-jump-till kill-selection yank end-selection
bind -s --preset yF begin-selection backward-jump kill-selection yank end-selection
bind -s --preset yT begin-selection backward-jump-till kill-selection yank end-selection
bind -s --preset yh backward-char begin-selection kill-selection yank end-selection
bind -s --preset yl begin-selection kill-selection yank end-selection
bind -s --preset yi backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection
bind -s --preset ya backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection
bind -s --preset y,\$ kill-line yank
bind -s --preset y,\^ backward-kill-line yank
bind -s --preset y,0 backward-kill-line yank
bind -s --preset y,w kill-word yank
bind -s --preset y,W kill-bigword yank
bind -s --preset y,i,w forward-single-char forward-single-char backward-word kill-word yank
bind -s --preset y,i,W forward-single-char forward-single-char backward-bigword kill-bigword yank
bind -s --preset y,a,w forward-single-char forward-single-char backward-word kill-word yank
bind -s --preset y,a,W forward-single-char forward-single-char backward-bigword kill-bigword yank
bind -s --preset y,e kill-word yank
bind -s --preset y,E kill-bigword yank
bind -s --preset y,b backward-kill-word yank
bind -s --preset y,B backward-kill-bigword yank
bind -s --preset y,g,e backward-kill-word yank
bind -s --preset y,g,E backward-kill-bigword yank
bind -s --preset y,f begin-selection forward-jump kill-selection yank end-selection
bind -s --preset y,t begin-selection forward-jump-till kill-selection yank end-selection
bind -s --preset y,F begin-selection backward-jump kill-selection yank end-selection
bind -s --preset y,T begin-selection backward-jump-till kill-selection yank end-selection
bind -s --preset y,h backward-char begin-selection kill-selection yank end-selection
bind -s --preset y,l begin-selection kill-selection yank end-selection
bind -s --preset y,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection
bind -s --preset y,a backward-jump and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection
bind -s --preset f forward-jump
bind -s --preset F backward-jump
@ -240,32 +245,40 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
# \ so there's no need to go back a char, just paste it without moving
bind -s --preset p 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_modefish_cursor_end_modeinclusive' yank
bind -s --preset P yank
bind -s --preset gp yank-pop
bind -s --preset g,p yank-pop
# same vim 'pasting' note as upper
bind -s --preset '"*p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste
bind -s --preset '"*P' fish_clipboard_paste
bind -s --preset '"+p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste
bind -s --preset '"+P' fish_clipboard_paste
bind -s --preset '",*,p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste
bind -s --preset '",*,P' fish_clipboard_paste
bind -s --preset '",+,p' 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' fish_clipboard_paste
bind -s --preset '",+,P' fish_clipboard_paste
#
# Lowercase r, enters replace_one mode
#
bind -s --preset -m replace_one r repaint-mode
bind -s --preset -M replace_one -m default '' delete-char self-insert backward-char repaint-mode
bind -s --preset -M replace_one -m default \r 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default \e cancel repaint-mode
bind -s --preset -M replace_one -m default enter 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default ctrl-j 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default ctrl-m 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default escape cancel repaint-mode
bind -s --preset -M replace_one -m default ctrl-\[ cancel repaint-mode
#
# Uppercase R, enters replace mode
#
bind -s --preset -m replace R repaint-mode
bind -s --preset -M replace '' delete-char self-insert
bind -s --preset -M replace -m insert \r execute repaint-mode
bind -s --preset -M replace -m default \e cancel repaint-mode
bind -s --preset -M replace -m insert enter execute repaint-mode
bind -s --preset -M replace -m insert ctrl-j execute repaint-mode
bind -s --preset -M replace -m insert ctrl-m execute repaint-mode
bind -s --preset -M replace -m default escape cancel repaint-mode
bind -s --preset -M replace -m default ctrl-\[ cancel repaint-mode
# in vim (and maybe in vi), <BS> deletes the changes
# but this binding just move cursor backward, not delete the changes
bind -s --preset -M replace -k backspace backward-char
bind -s --preset -M replace backspace backward-char
bind -s --preset -M replace shift-backspace backward-char
$legacy_bind -s --preset -M replace -k backspace backward-char
#
# visual mode
@ -278,8 +291,8 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M visual b backward-word
bind -s --preset -M visual B backward-bigword
bind -s --preset -M visual ge backward-word
bind -s --preset -M visual gE backward-bigword
bind -s --preset -M visual g,e backward-word
bind -s --preset -M visual g,E backward-bigword
bind -s --preset -M visual w forward-word
bind -s --preset -M visual W forward-bigword
bind -s --preset -M visual e 'set fish_cursor_end_mode exclusive' forward-single-char forward-word backward-char 'set fish_cursor_end_mode inclusive'
@ -304,12 +317,13 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M visual -m default x kill-selection end-selection repaint-mode
bind -s --preset -M visual -m default X kill-whole-line end-selection repaint-mode
bind -s --preset -M visual -m default y kill-selection yank end-selection repaint-mode
bind -s --preset -M visual -m default '"*y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '"+y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '",*,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '",+,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '~' togglecase-selection end-selection repaint-mode
bind -s --preset -M visual -m default \cc end-selection repaint-mode
bind -s --preset -M visual -m default \e end-selection repaint-mode
bind -s --preset -M visual -m default ctrl-c end-selection repaint-mode
bind -s --preset -M visual -m default escape end-selection repaint-mode
bind -s --preset -M visual -m default ctrl-\[ end-selection repaint-mode
# Make it easy to turn an unexecuted command into a comment in the shell history. Also, remove
# the commenting chars so the command can be further edited then executed.

View file

@ -7,12 +7,7 @@
//!
//! Type "exit" or "quit" to terminate the program.
use core::panic;
use std::{
ops::ControlFlow,
os::unix::prelude::OsStrExt,
time::{Duration, Instant},
};
use std::{ops::ControlFlow, os::unix::prelude::OsStrExt};
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
@ -22,16 +17,15 @@ use fish::{
builtins::shared::BUILTIN_ERR_UNKNOWN,
common::{shell_modes, str2wcstring, PROGRAM_NAME},
env::env_init,
eprintf,
fallback::fish_wcwidth,
fprintf,
eprintf, fprintf,
input::input_terminfo_get_name,
input_common::{CharEvent, InputEventQueue, InputEventQueuer},
key::Key,
panic::panic_handler,
print_help::print_help,
printf,
proc::set_interactive_session,
reader::{check_exit_loop_maybe_warning, reader_init, reader_test_and_clear_interrupted},
reader::{check_exit_loop_maybe_warning, reader_init},
signal::signal_set_handlers,
threads,
topic_monitor::topic_monitor_init,
@ -40,15 +34,15 @@ use fish::{
};
/// 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 {
let c = if c < '\u{80}' { c as u8 } else { 0 };
recent_chars.push(c);
fn should_exit(recent_keys: &mut Vec<Key>, key: Key) -> bool {
recent_keys.push(key);
for evt in [VINTR, VEOF] {
let modes = shell_modes();
if c == modes.c_cc[evt] {
if recent_chars.iter().rev().nth(1) == Some(&modes.c_cc[evt]) {
let cc = Key::from_single_byte(modes.c_cc[evt]);
if key == cc {
if recent_keys.iter().rev().nth(1) == Some(&cc) {
return true;
}
eprintf!(
@ -59,7 +53,15 @@ fn should_exit(recent_chars: &mut Vec<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.
@ -80,116 +82,17 @@ fn sequence_name(recent_chars: &mut Vec<u8>, c: char) -> Option<WString> {
input_terminfo_get_name(&str2wcstring(recent_chars))
}
/// Return true if the character must be escaped when used in the sequence of chars to be bound in
/// a `bind` command.
fn must_escape(c: char) -> bool {
"[]()<>{}*\\?$#;&|'\"".contains(c)
}
fn ctrl_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
let ctrl_symbolic_names: [&wstr; 29] = {
std::array::from_fn(|i| match i {
8 => L!("\\b"),
9 => L!("\\t"),
10 => L!("\\n"),
13 => L!("\\r"),
27 => L!("\\e"),
28 => L!("\\x1c"),
_ => L!(""),
})
};
let c = u8::try_from(c).unwrap();
let cu = usize::from(c);
if !ctrl_symbolic_names[cu].is_empty() {
if bind_friendly {
sprintf!(=> buf, "%s", ctrl_symbolic_names[cu]);
} else {
sprintf!(=> buf, "\\c%c (or %ls)", char::from(c + 0x40), ctrl_symbolic_names[cu]);
}
} else {
sprintf!(=> buf, "\\c%c", char::from(c + 0x40));
}
}
fn space_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
if bind_friendly {
sprintf!(=> buf, "\\x%X", u32::from(c));
} else {
sprintf!(=> buf, "\\x%X (aka \"space\")", u32::from(c));
}
}
fn del_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
if bind_friendly {
sprintf!(=> buf, "\\x%X", u32::from(c));
} else {
sprintf!(=> buf, "\\x%X (aka \"del\")", u32::from(c));
}
}
fn ascii_printable_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
if bind_friendly && must_escape(c) {
sprintf!(=> buf, "\\%c", c);
} else {
sprintf!(=> buf, "%c", c);
}
}
/// Convert a wide-char to a symbol that can be used in our output.
fn char_to_symbol(c: char, bind_friendly: bool) -> WString {
let mut buff = WString::new();
let buf = &mut buff;
if c == '\x1b' {
// Escape - this is *technically* also \c[
buf.push_str("\\e");
} else if c < ' ' {
// ASCII control character
ctrl_to_symbol(buf, c, bind_friendly);
} else if c == ' ' {
// the "space" character
space_to_symbol(buf, c, bind_friendly);
} else if c == '\x7F' {
// the "del" character
del_to_symbol(buf, c, bind_friendly);
} else if c < '\u{80}' {
// ASCII characters that are not control characters
ascii_printable_to_symbol(buf, c, bind_friendly);
} else if fish_wcwidth(c) > 0 {
sprintf!(=> buf, "%lc", c);
} else if c <= '\u{FFFF}' {
// BMP Unicode chararacter
sprintf!(=> buf, "\\u%04X", u32::from(c));
} else {
sprintf!(=> buf, "\\U%06X", u32::from(c));
}
buff
}
fn add_char_to_bind_command(c: char, bind_chars: &mut Vec<char>) {
bind_chars.push(c);
}
fn output_bind_command(bind_chars: &mut Vec<char>) {
fn output_bind_command(bind_chars: &mut Vec<(Key, WString)>) {
if !bind_chars.is_empty() {
printf!("bind ");
for &bind_char in &*bind_chars {
printf!("%s", char_to_symbol(bind_char, true));
for (key, _seq) in &*bind_chars {
printf!("%s", key);
}
printf!(" 'do something'\n");
bind_chars.clear();
}
}
fn output_info_about_char(c: char) {
eprintf!(
"hex: %4X char: %ls\n",
u32::from(c),
char_to_symbol(c, false)
);
}
fn output_matching_key_name(recent_chars: &mut Vec<u8>, c: char) -> bool {
if let Some(name) = sequence_name(recent_chars, c) {
printf!("bind -k %ls 'do something'\n", name);
@ -198,72 +101,29 @@ fn output_matching_key_name(recent_chars: &mut Vec<u8>, c: char) -> bool {
false
}
fn output_elapsed_time(prev_timestamp: Instant, first_char_seen: bool, verbose: bool) -> Instant {
// How much time has passed since the previous char was received in microseconds.
let now = Instant::now();
let delta = now - prev_timestamp;
if verbose {
if delta >= Duration::from_millis(200) && first_char_seen {
eprintf!("\n");
}
if delta >= Duration::from_millis(1000) {
eprintf!(" ");
} else {
eprintf!(
"(%3lld.%03lld ms) ",
u64::try_from(delta.as_millis()).unwrap(),
u64::try_from(delta.as_micros() % 1000).unwrap()
);
}
}
now
}
/// Process the characters we receive as the user presses keys.
fn process_input(continuous_mode: bool, verbose: bool) -> i32 {
fn process_input(continuous_mode: bool) -> i32 {
let mut first_char_seen = false;
let mut prev_timestamp = Instant::now()
.checked_sub(Duration::from_millis(1000))
.unwrap_or(Instant::now());
let mut queue = InputEventQueue::new(STDIN_FILENO);
let mut bind_chars = vec![];
let mut recent_chars1 = vec![];
let mut recent_chars2 = vec![];
eprintf!("Press a key:\n");
while !check_exit_loop_maybe_warning(None) {
let evt = if reader_test_and_clear_interrupted() != 0 {
Some(CharEvent::from_char(char::from(shell_modes().c_cc[VINTR])))
} else {
queue.readch_timed_esc()
};
while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
let evt = queue.readch();
if evt.as_ref().is_none_or(|evt| !evt.is_char()) {
output_bind_command(&mut bind_chars);
if first_char_seen && !continuous_mode {
return 0;
}
let CharEvent::Key(kevt) = evt else {
continue;
}
let evt = evt.unwrap();
let c = evt.get_char().unwrap();
prev_timestamp = output_elapsed_time(prev_timestamp, first_char_seen, verbose);
// Hack for #3189. Do not suggest \c@ as the binding for nul, because a string containing
// nul cannot be passed to builtin_bind since it uses C strings. We'll output the name of
// this key (nul) elsewhere.
if c != '\0' {
add_char_to_bind_command(c, &mut bind_chars);
}
if verbose {
output_info_about_char(c);
}
};
let c = kevt.key.codepoint;
bind_chars.push((kevt.key, kevt.seq));
output_bind_command(&mut bind_chars);
if output_matching_key_name(&mut recent_chars1, c) {
output_bind_command(&mut bind_chars);
}
if continuous_mode && should_exit(&mut recent_chars2, c) {
if continuous_mode && should_exit(&mut recent_chars2, kevt.key) {
eprintf!("\nExiting at your request.\n");
break;
}
@ -274,7 +134,7 @@ fn process_input(continuous_mode: bool, verbose: bool) -> i32 {
}
/// Setup our environment (e.g., tty modes), process key strokes, then reset the environment.
fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> i32 {
fn setup_and_process_keys(continuous_mode: bool) -> i32 {
set_interactive_session(true);
topic_monitor_init();
threads::init();
@ -298,16 +158,16 @@ fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> i32 {
eprintf!("\n");
}
process_input(continuous_mode, verbose)
process_input(continuous_mode)
}
fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> ControlFlow<i32> {
fn parse_flags(continuous_mode: &mut bool) -> ControlFlow<i32> {
let short_opts: &wstr = L!("+chvV");
let long_opts: &[woption] = &[
wopt(L!("continuous"), woption_argument_t::no_argument, 'c'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("version"), woption_argument_t::no_argument, 'v'),
wopt(L!("verbose"), woption_argument_t::no_argument, 'V'),
wopt(L!("verbose"), woption_argument_t::no_argument, 'V'), // Removed
];
let args: Vec<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);
}
'V' => {
*verbose = true;
}
'V' => {}
'?' => {
printf!(
"%s",
@ -369,9 +227,8 @@ fn main() {
fn throwing_main() -> i32 {
let mut continuous_mode = false;
let mut verbose = false;
if let ControlFlow::Break(i) = parse_flags(&mut continuous_mode, &mut verbose) {
if let ControlFlow::Break(i) = parse_flags(&mut continuous_mode) {
return i;
}
@ -380,5 +237,5 @@ fn throwing_main() -> i32 {
return 1;
}
setup_and_process_keys(continuous_mode, verbose)
setup_and_process_keys(continuous_mode)
}

View file

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

View file

@ -1,6 +1,8 @@
//! Implementation of the fg builtin.
use crate::fds::make_fd_blocking;
use crate::input_common::terminal_protocols_disable_scoped;
use crate::proc::is_interactive_session;
use crate::reader::reader_write_title;
use crate::tokenizer::tok_command;
use crate::wutil::perror;
@ -155,6 +157,8 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Optio
}
}
}
let _terminal_protocols = (is_interactive_session() && job.group().wants_terminal())
.then(terminal_protocols_disable_scoped);
let mut transfer = TtyTransfer::new();
transfer.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume();

View file

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

View file

@ -24,6 +24,9 @@ use crate::fork_exec::postfork::{
#[cfg(FISH_USE_POSIX_SPAWN)]
use crate::fork_exec::spawn::PosixSpawner;
use crate::function::{self, FunctionProperties};
use crate::input_common::{
terminal_protocols_disable, terminal_protocols_disable_scoped, TERMINAL_PROTOCOLS,
};
use crate::io::{
BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe,
IoStreams, OutputStream, SeparatedBuffer, StringOutputStream,
@ -39,7 +42,7 @@ use crate::proc::{
print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType,
TtyTransfer, INVALID_PID,
};
use crate::reader::{reader_run_count, restore_term_mode};
use crate::reader::{reader_current_data, reader_run_count, restore_term_mode};
use crate::redirection::{dup2_list_resolve_chain, Dup2List};
use crate::threads::{iothread_perform_cant_wait, is_forked_child};
use crate::timer::push_timer;
@ -72,6 +75,15 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
return true;
}
let _terminal_protocols_disabled = (
// If interactive or inside noninteractive builtin read.
reader_current_data().is_some() &&
// If we try to start an external process.
job.group().wants_terminal()
&& TERMINAL_PROTOCOLS.get().borrow().is_some()
)
.then(terminal_protocols_disable_scoped);
// Handle an exec call.
if job.processes()[0].typ == ProcessType::exec {
// If we are interactive, perhaps disallow exec if there are background jobs.
@ -439,6 +451,9 @@ fn launch_process_nofork(vars: &EnvStack, p: &Process) -> ! {
// Ensure the terminal modes are what they were before we changed them.
restore_term_mode();
if reader_current_data().is_some() && TERMINAL_PROTOCOLS.get().borrow().is_some() {
terminal_protocols_disable();
}
// Bounce to launch_process. This never returns.
safe_launch_process(p, &actual_cmd, &argv, &*envp);
}

View file

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

View file

@ -6,6 +6,7 @@ use crate::flog::FLOG;
use crate::input_common::{
CharEvent, CharInputStyle, InputEventQueuer, ReadlineCmd, R_END_INPUT_FUNCTIONS,
};
use crate::key::{self, canonicalize_raw_escapes, ctrl, Key};
use crate::parser::Parser;
use crate::proc::job_reap;
use crate::reader::{
@ -33,7 +34,7 @@ pub const NUL_MAPPING_NAME: &wstr = L!("nul");
#[derive(Debug, Clone)]
pub struct InputMappingName {
pub seq: WString,
pub seq: Vec<Key>,
pub mode: WString,
}
@ -41,7 +42,7 @@ pub struct InputMappingName {
#[derive(Debug, Clone)]
struct InputMapping {
/// Character sequence which generates this event.
seq: WString,
seq: Vec<Key>,
/// Commands that should be evaluated by this mapping.
commands: Vec<WString>,
/// We wish to preserve the user-specified order. This is just an incrementing value.
@ -50,15 +51,18 @@ struct InputMapping {
mode: WString,
/// New mode that should be switched to after command evaluation, or None to leave the mode unchanged.
sets_mode: Option<WString>,
/// Whether this sequence was specified via its terminfo name.
terminfo_name: Option<WString>,
}
impl InputMapping {
/// Create a new mapping.
fn new(
seq: WString,
seq: Vec<Key>,
commands: Vec<WString>,
mode: WString,
sets_mode: Option<WString>,
terminfo_name: Option<WString>,
) -> InputMapping {
static LAST_INPUT_MAP_SPEC_ORDER: AtomicU32 = AtomicU32::new(0);
let specification_order = 1 + LAST_INPUT_MAP_SPEC_ORDER.fetch_add(1, Ordering::Relaxed);
@ -72,6 +76,7 @@ impl InputMapping {
specification_order,
mode,
sets_mode,
terminfo_name,
}
}
@ -115,6 +120,8 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
const INPUT_FUNCTION_METADATA: &[InputFunctionMetadata] = &[
// NULL makes it unusable - this is specially inserted when we detect mouse input
make_md(L!(""), ReadlineCmd::DisableMouseTracking),
make_md(L!(""), ReadlineCmd::FocusIn),
make_md(L!(""), ReadlineCmd::FocusOut),
make_md(L!("accept-autosuggestion"), ReadlineCmd::AcceptAutosuggestion),
make_md(L!("and"), ReadlineCmd::FuncAnd),
make_md(L!("backward-bigword"), ReadlineCmd::BackwardBigword),
@ -274,7 +281,8 @@ impl InputMappingSet {
/// Adds an input mapping.
pub fn add(
&mut self,
sequence: WString,
sequence: Vec<Key>,
terminfo_name: Option<WString>,
commands: Vec<WString>,
mode: WString,
sets_mode: Option<WString>,
@ -299,20 +307,28 @@ impl InputMappingSet {
}
// Add a new mapping, using the next order.
let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode);
let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode, terminfo_name);
input_mapping_insert_sorted(ml, new_mapping);
}
// Like add(), but takes a single command.
pub fn add1(
&mut self,
sequence: WString,
sequence: Vec<Key>,
terminfo_name: Option<WString>,
command: WString,
mode: WString,
sets_mode: Option<WString>,
user: bool,
) {
self.add(sequence, vec![command], mode, sets_mode, user);
self.add(
sequence,
terminfo_name,
vec![command],
mode,
sets_mode,
user,
);
}
}
@ -330,35 +346,44 @@ pub fn init_input() {
// If we have no keybindings, add a few simple defaults.
if input_mapping.preset_mapping_list.is_empty() {
// Helper for adding.
let mut add = |seq: &str, cmd: &str| {
let mut add = |key: Vec<Key>, cmd: &str| {
let mode = DEFAULT_BIND_MODE.to_owned();
let sets_mode = Some(DEFAULT_BIND_MODE.to_owned());
input_mapping.add1(seq.into(), cmd.into(), mode, sets_mode, false);
input_mapping.add1(key, None, cmd.into(), mode, sets_mode, false);
};
add("", "self-insert");
add("\n", "execute");
add("\r", "execute");
add("\t", "complete");
add("\x03", "cancel-commandline");
add("\x04", "exit");
add("\x05", "bind");
// ctrl-s
add("\x13", "pager-toggle-search");
// ctrl-u
add("\x15", "backward-kill-line");
// del/backspace
add("\x7f", "backward-delete-char");
add(vec![], "self-insert");
add(vec![Key::from_raw(key::Enter)], "execute");
add(vec![Key::from_raw(key::Tab)], "complete");
add(vec![ctrl('c')], "cancel-commandline");
add(vec![ctrl('d')], "exit");
add(vec![ctrl('e')], "bind");
add(vec![ctrl('s')], "pager-toggle-search");
add(vec![ctrl('u')], "backward-kill-line");
add(vec![Key::from_raw(key::Backspace)], "backward-delete-char");
// Arrows - can't have functions, so *-or-search isn't available.
add("\x1B[A", "up-line");
add("\x1B[B", "down-line");
add("\x1B[C", "forward-char");
add("\x1B[D", "backward-char");
// emacs-style ctrl-p/n/b/f
add("\x10", "up-line");
add("\x0e", "down-line");
add("\x02", "backward-char");
add("\x06", "forward-char");
add(vec![Key::from_raw(key::Up)], "up-line");
add(vec![Key::from_raw(key::Down)], "down-line");
add(vec![Key::from_raw(key::Right)], "forward-char");
add(vec![Key::from_raw(key::Left)], "backward-char");
// Emacs style
add(vec![ctrl('p')], "up-line");
add(vec![ctrl('n')], "down-line");
add(vec![ctrl('b')], "backward-char");
add(vec![ctrl('f')], "forward-char");
let mut add_legacy = |escape_sequence: &str, cmd: &str| {
add(
canonicalize_raw_escapes(
escape_sequence.chars().map(Key::from_single_char).collect(),
),
cmd,
);
};
add_legacy("\x1B[A", "up-line");
add_legacy("\x1B[B", "down-line");
add_legacy("\x1B[C", "forward-char");
add_legacy("\x1B[D", "backward-char");
}
}
@ -370,6 +395,7 @@ pub type CommandHandler<'a> = dyn FnMut(&[WString]) + 'a;
pub struct Inputter {
in_fd: RawFd,
queue: VecDeque<CharEvent>,
paste_buffer: Option<Vec<u8>>,
// We need a parser to evaluate bindings.
parser: Rc<Parser>,
input_function_args: Vec<char>,
@ -417,7 +443,7 @@ impl InputEventQueuer for Inputter {
if reader_reading_interrupted() != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_char(vintr.into()));
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
}
return;
}
@ -427,6 +453,23 @@ impl InputEventQueuer for Inputter {
fn uvar_change_notified(&mut self) {
self.parser.sync_uvars_and_fire(true /* always */);
}
fn paste_start_buffering(&mut self) {
self.paste_buffer = Some(vec![]);
}
fn paste_is_buffering(&self) -> bool {
self.paste_buffer.is_some()
}
fn paste_commit(&mut self) {
let buffer = self.paste_buffer.take().unwrap();
self.push_front(CharEvent::Command(sprintf!(
"__fish_paste %s",
escape(&str2wcstring(&buffer))
)));
}
fn paste_push_char(&mut self, b: u8) {
self.paste_buffer.as_mut().unwrap().push(b)
}
}
impl Inputter {
@ -435,6 +478,7 @@ impl Inputter {
Inputter {
in_fd,
queue: VecDeque::new(),
paste_buffer: None,
parser,
input_function_args: Vec::new(),
function_status: false,
@ -464,9 +508,12 @@ impl Inputter {
let arg: char;
loop {
let evt = self.readch();
if let Some(c) = evt.get_char() {
arg = c;
break;
if let Some(kevt) = evt.get_key() {
if let Some(c) = kevt.key.codepoint_text() {
// TODO forward the whole key
arg = c;
break;
}
}
skipped.push(evt);
}
@ -492,7 +539,16 @@ impl Inputter {
let evt = match input_function_get_code(cmd) {
Some(code) => {
self.function_push_args(code);
CharEvent::from_readline_seq(code, m.seq.clone())
// At this point, the sequence is only used for reinserting the keys into
// the event queue for self-insert. Modifiers make no sense here so drop them.
CharEvent::from_readline_seq(
code,
m.seq
.iter()
.filter(|key| key.modifiers.is_none())
.map(|key| key.codepoint)
.collect(),
)
}
None => CharEvent::Command(cmd.clone()),
};
@ -539,6 +595,8 @@ struct EventQueuePeeker<'q> {
/// The current index. This never exceeds peeked.len().
idx: usize,
/// The current index within a the raw characters within a single key event.
subidx: usize,
/// The queue from which to read more events.
event_queue: &'q mut Inputter,
@ -550,6 +608,7 @@ impl EventQueuePeeker<'_> {
peeked: Vec::new(),
had_timeout: false,
idx: 0,
subidx: 0,
event_queue,
}
}
@ -566,12 +625,13 @@ impl EventQueuePeeker<'_> {
}
let res = self.peeked[self.idx].clone();
self.idx += 1;
self.subidx = 0;
res
}
/// Check if the next event is the given character. This advances the index on success only.
/// If \p escaped is set, then return false if this (or any other) character had a timeout.
fn next_is_char(&mut self, c: char, escaped: bool) -> bool {
fn next_is_char(&mut self, key: Key, escaped: bool) -> bool {
assert!(
self.idx <= self.peeked.len(),
"Index must not be larger than dequeued event count"
@ -583,36 +643,74 @@ impl EventQueuePeeker<'_> {
// Grab a new event if we have exhausted what we have already peeked.
// Use either readch or readch_timed, per our param.
if self.idx == self.peeked.len() {
let newevt: CharEvent;
if !escaped {
if let Some(mevt) = self.event_queue.readch_timed_sequence_key() {
newevt = mevt;
} else {
self.had_timeout = true;
return false;
}
} else if let Some(mevt) = self.event_queue.readch_timed_esc() {
newevt = mevt;
let Some(newevt) = (if escaped {
self.event_queue.readch_timed_esc()
} else {
self.event_queue.readch_timed_sequence_key()
}) else {
self.had_timeout = true;
return false;
}
};
self.peeked.push(newevt);
}
// Now we have peeked far enough; check the event.
// If it matches the char, then increment the index.
if self.peeked[self.idx].get_char() == Some(c) {
let evt = &self.peeked[self.idx];
let Some(kevt) = evt.get_key() else {
return false;
};
if kevt.key == key {
self.idx += 1;
self.subidx = 0;
return true;
}
let actual_seq = kevt.seq.as_char_slice();
if !actual_seq.is_empty() {
let seq_char = actual_seq[self.subidx];
FLOG!(
reader,
"match mapping's",
key,
format!("against actual char {}", u32::from(seq_char)),
);
if Key::from_single_char(seq_char) == key {
self.subidx += 1;
if self.subidx == actual_seq.len() {
self.idx += 1;
self.subidx = 0;
}
FLOG!(reader, "matched legacy sequence");
return true;
}
if key.modifiers.alt
&& !key.modifiers.ctrl
&& !key.modifiers.shift
&& seq_char == '\x1b'
{
if self.subidx + 1 == actual_seq.len() {
self.idx += 1;
self.subidx = 0;
FLOG!(reader, "matched escape prefix of legacy alt sequence");
return self.next_is_char(Key::from_raw(key.codepoint), true);
} else if actual_seq
.get(self.subidx + 1)
.cloned()
.map(|c| Key::from_single_char(c).codepoint)
== Some(key.codepoint)
{
self.subidx += 2;
if self.subidx == actual_seq.len() {
self.idx += 1;
self.subidx = 0;
}
FLOG!(reader, "matched legacy alt sequence");
return true;
}
}
}
false
}
/// \return the current index.
fn len(&self) -> usize {
self.idx
}
/// Consume all events up to the current index.
/// Remaining events are returned to the queue.
fn consume(mut self) {
@ -620,6 +718,7 @@ impl EventQueuePeeker<'_> {
self.event_queue.insert_front(self.peeked.drain(self.idx..));
self.peeked.clear();
self.idx = 0;
self.subidx = 0;
}
/// Test if any of our peeked events are readline or check_exit.
@ -632,82 +731,42 @@ impl EventQueuePeeker<'_> {
/// Reset our index back to 0.
fn restart(&mut self) {
self.idx = 0;
self.subidx = 0;
}
}
impl Drop for EventQueuePeeker<'_> {
fn drop(&mut self) {
assert!(
self.idx == 0,
self.idx == 0 && self.subidx == 0,
"Events left on the queue - missing restart or consume?",
);
self.event_queue.insert_front(self.peeked.drain(self.idx..));
}
}
/// Try reading a mouse-tracking CSI sequence, using the given \p peeker.
/// Events are left on the peeker and the caller must restart or consume it.
/// \return true if matched, false if not.
fn have_mouse_tracking_csi(peeker: &mut EventQueuePeeker) -> bool {
// Maximum length of any CSI is NPAR (which is nominally 16), although this does not account for
// user input intermixed with pseudo input generated by the tty emulator.
// Check for the CSI first.
if !peeker.next_is_char('\x1b', false) || !peeker.next_is_char('[', true /* escaped */) {
return false;
}
let mut next = peeker.next().get_char();
let length;
if next == Some('M') {
// Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6 chars
// (although in mode 1005, the characters may be unicode and not necessarily just one byte
// long) reporting the button that was clicked and its location.
length = 6;
} else if next == Some('<') {
// Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters for button
// code, Px, and Py, ending with 'M' for button press or 'm' for button release.
loop {
next = peeker.next().get_char();
if next == Some('M') || next == Some('m') {
// However much we've read, we've consumed the CSI in its entirety.
length = peeker.len();
break;
}
if peeker.len() >= 16 {
// This is likely a malformed mouse-reporting CSI but we can't do anything about it.
return false;
}
}
} else if next == Some('t') {
// VT200 button released in mouse highlighting mode at valid text location. 5 chars.
length = 5;
} else if next == Some('T') {
// VT200 button released in mouse highlighting mode past end-of-line. 9 characters.
length = 9;
} else {
return false;
}
// Consume however many characters it takes to prevent the mouse tracking sequence from reaching
// the prompt, dependent on the class of mouse reporting as detected above.
while peeker.len() < length {
let _ = peeker.next();
}
true
}
/// \return true if a given \p peeker matches a given sequence of char events given by \p str.
fn try_peek_sequence(peeker: &mut EventQueuePeeker, str: &wstr) -> bool {
assert!(!str.is_empty(), "Empty string passed to try_peek_sequence");
let mut prev = '\0';
for c in str.chars() {
fn try_peek_sequence(peeker: &mut EventQueuePeeker, seq: &[Key]) -> bool {
assert!(
!seq.is_empty(),
"Empty sequence passed to try_peek_sequence"
);
let mut prev = Key::from_raw(key::Invalid);
for key in seq {
// If we just read an escape, we need to add a timeout for the next char,
// to distinguish between the actual escape key and an "alt"-modifier.
let escaped = prev == '\x1B';
if !peeker.next_is_char(c, escaped) {
let escaped = prev == Key::from_raw(key::Escape);
if !peeker.next_is_char(*key, escaped) {
return false;
}
prev = c;
prev = *key;
}
if peeker.subidx != 0 {
FLOG!(
reader,
"legacy binding matched prefix of key encoding but did not consume all of it"
);
return false;
}
true
}
@ -739,7 +798,7 @@ impl Inputter {
if try_peek_sequence(peeker, &m.seq) {
// A binding for just escape should also be deferred
// so escape sequences take precedence.
if m.seq == "\x1B" {
if m.seq == vec![Key::from_raw(key::Escape)] {
if escape.is_none() {
escape = Some(m);
}
@ -771,27 +830,12 @@ impl Inputter {
fn mapping_execute_matching_or_generic(&mut self) {
let vars = self.parser.vars_ref();
let mut peeker = EventQueuePeeker::new(self);
// Check for mouse-tracking CSI before mappings to prevent the generic mapping handler from
// taking over.
if have_mouse_tracking_csi(&mut peeker) {
// fish recognizes but does not actually support mouse reporting. We never turn it on, and
// it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn
// it off before exiting. We turn it off here to avoid wasting resources.
//
// Since this is only called when we detect an incoming mouse reporting payload, we know the
// terminal emulator supports mouse reporting, so no terminfo checks.
FLOG!(reader, "Disabling mouse tracking");
// We shouldn't directly manipulate stdout from here, so we ask the reader to do it.
// writembs(outputter_t::stdoutput(), "\x1B[?1000l");
peeker.consume();
self.push_front(CharEvent::from_readline(ReadlineCmd::DisableMouseTracking));
return;
}
peeker.restart();
// Check for ordinary mappings.
if let Some(mapping) = Self::find_mapping(&*vars, &mut peeker) {
FLOG!(
reader,
format!("Found mapping {:?} from {:?}", &mapping, &peeker.peeked)
);
peeker.consume();
self.mapping_execute(&mapping);
return;
@ -839,14 +883,7 @@ impl Inputter {
evt_to_return
}
/// Read a character from stdin. Try to convert some escape sequences into character constants,
/// but do not permanently block the escape character.
///
/// This is performed in the same way vim does it, i.e. if an escape character is read, wait for
/// more input for a short time (a few milliseconds). If more input is available, it is assumed
/// to be an escape sequence for a special character (such as an arrow key), and readch attempts
/// to parse it. If no more input follows after the escape key, it is assumed to be an actual
/// escape key press, and is returned as such.
/// Read a key from stdin.
pub fn read_char(&mut self) -> CharEvent {
// Clear the interrupted flag.
reader_reset_interrupted();
@ -867,8 +904,8 @@ impl Inputter {
// Hackish: mark the input style.
if readline_event.cmd == ReadlineCmd::SelfInsertNotFirst {
if let CharEvent::Char(cevt) = &mut res {
cevt.input_style = CharInputStyle::NotFirst;
if let CharEvent::Key(kevt) = &mut res {
kevt.input_style = CharInputStyle::NotFirst;
}
}
return res;
@ -897,7 +934,17 @@ impl Inputter {
// Allow the reader to check for exit conditions.
return evt;
}
CharEvent::Char(ref _cevt) => {
CharEvent::Key(ref kevt) => {
FLOG!(
reader,
"Read char",
kevt.key,
format!(
"-- {:?} -- {:?}",
kevt.key,
kevt.seq.chars().map(u32::from).collect::<Vec<_>>()
)
);
self.push_front(evt);
self.mapping_execute_matching_or_generic();
}
@ -941,7 +988,7 @@ impl InputMappingSet {
}
/// Erase binding for specified key sequence.
pub fn erase(&mut self, sequence: &wstr, mode: &wstr, user: bool) -> bool {
pub fn erase(&mut self, sequence: &[Key], mode: &wstr, user: bool) -> bool {
// Clear cached mappings.
self.all_mappings_cache = RefCell::new(None);
@ -965,11 +1012,12 @@ impl InputMappingSet {
/// it exists, false if not.
pub fn get<'a>(
&'a self,
sequence: &wstr,
sequence: &[Key],
mode: &wstr,
out_cmds: &mut &'a [WString],
user: bool,
out_sets_mode: &mut Option<&'a wstr>,
out_terminfo_name: &mut Option<WString>,
) -> bool {
let ml = if user {
&self.mapping_list
@ -980,6 +1028,7 @@ impl InputMappingSet {
if m.seq == sequence && m.mode == mode {
*out_cmds = &m.commands;
*out_sets_mode = m.sets_mode.as_deref();
*out_terminfo_name = m.terminfo_name.clone();
return true;
}
}

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::fd_readable_set::FdReadableSet;
use crate::flog::FLOG;
use crate::reader::reader_current_data;
use crate::threads::{iothread_port, iothread_service_main};
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, function_key, shift,
Key, Modifiers,
};
use crate::proc::is_interactive_session;
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::threads::{iothread_port, iothread_service_main, MainThread};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, zero_mbstate};
use crate::wutil::fish_wcstol;
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
use crate::wutil::{fish_wcstol, write_to_fd};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::os::fd::RawFd;
use std::ptr;
@ -111,6 +123,8 @@ pub enum ReadlineCmd {
EndUndoGroup,
RepeatJump,
DisableMouseTracking,
FocusIn,
FocusOut,
// ncurses uses the obvious name
ClearScreenAndRepaint,
// NOTE: This one has to be last.
@ -121,7 +135,7 @@ pub enum ReadlineCmd {
#[derive(Debug, Clone)]
pub enum CharEventType {
/// A character was entered.
Char(char),
Char(Key),
/// A readline event.
Readline(ReadlineCmd),
@ -147,9 +161,9 @@ pub struct ReadlineCmdEvent {
}
#[derive(Debug, Clone)]
pub struct PlainCharEvent {
pub struct KeyEvent {
// The key.
pub char: char,
pub key: Key,
// The style to use when inserting characters into the command line.
pub input_style: CharInputStyle,
/// The sequence of characters in the input mapping which generated this event.
@ -161,7 +175,7 @@ pub struct PlainCharEvent {
#[derive(Debug, Clone)]
pub enum CharEvent {
/// A character was entered.
Char(PlainCharEvent),
Key(KeyEvent),
/// A readline event.
Readline(ReadlineCmdEvent),
@ -179,7 +193,7 @@ pub enum CharEvent {
impl CharEvent {
pub fn is_char(&self) -> bool {
matches!(self, CharEvent::Char(_))
matches!(self, CharEvent::Key(_))
}
pub fn is_eof(&self) -> bool {
@ -198,6 +212,20 @@ impl CharEvent {
matches!(self, CharEvent::Readline(_) | CharEvent::Command(_))
}
pub fn get_char(&self) -> char {
let CharEvent::Key(kevt) = self else {
panic!("Not a char type");
};
kevt.key.codepoint
}
pub fn get_key(&self) -> Option<&KeyEvent> {
match self {
CharEvent::Key(kevt) => Some(kevt),
_ => None,
}
}
pub fn get_readline(&self) -> ReadlineCmd {
let CharEvent::Readline(c) = self else {
panic!("Not a readline type");
@ -213,19 +241,24 @@ impl 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> {
match self {
CharEvent::Char(cevt) => Some(cevt.char),
_ => None,
}
pub fn from_key(key: Key) -> CharEvent {
Self::from_key_seq(key, WString::new())
}
pub fn from_key_seq(key: Key, seq: WString) -> CharEvent {
CharEvent::Key(KeyEvent {
key,
input_style: CharInputStyle::Normal,
seq,
})
}
pub fn from_char_seq(c: char, seq: WString) -> CharEvent {
CharEvent::Char(PlainCharEvent {
char: c,
CharEvent::Key(KeyEvent {
key: Key::from_raw(c),
input_style: CharInputStyle::Normal,
seq,
})
@ -272,9 +305,11 @@ enum ReadbResult {
// Our ioport reported a change, so service main thread requests.
IOPortNotified,
NothingToRead,
}
fn readb(in_fd: RawFd) -> ReadbResult {
fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
assert!(in_fd >= 0, "Invalid in fd");
let mut fdset = FdReadableSet::new();
loop {
@ -293,7 +328,11 @@ fn readb(in_fd: RawFd) -> ReadbResult {
}
// Here's where we call select().
let select_res = fdset.check_readable(FdReadableSet::kNoTimeout);
let select_res = fdset.check_readable(if blocking {
FdReadableSet::kNoTimeout
} else {
0
});
if select_res < 0 {
let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN {
@ -305,12 +344,15 @@ fn readb(in_fd: RawFd) -> ReadbResult {
}
}
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) {
return ReadbResult::UvarNotified;
if blocking {
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)
{
return ReadbResult::UvarNotified;
}
}
}
@ -324,6 +366,9 @@ fn readb(in_fd: RawFd) -> ReadbResult {
// The common path is to return a u8.
return ReadbResult::Byte(arr[0]);
}
if !blocking {
return ReadbResult::NothingToRead;
}
// Check for iothread completions only if there is no data to be read from the stdin.
// This gives priority to the foreground.
@ -383,6 +428,111 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) {
}
}
pub static TERMINAL_PROTOCOLS: MainThread<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.
/// Note this is conceptually a "base class" with override points.
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
/// been read and then 'unread' using \c input_common_unreadch, that character is returned.
fn readch(&mut self) -> CharEvent {
let mut res: char = '\0';
let mut state = zero_mbstate();
let mut bytes = [0; 64 * 16];
let mut num_bytes = 0;
loop {
// Do we have something enqueued already?
// Note this may be initially true, or it may become true through calls to
@ -413,14 +560,13 @@ pub trait InputEventQueuer {
return mevt;
}
let rr = readb(self.get_in_fd());
let rr = readb(self.get_in_fd(), /*blocking=*/ true);
match rr {
ReadbResult::Eof => {
return CharEvent::Eof;
}
ReadbResult::Interrupted => {
// FIXME: here signals may break multibyte sequences.
self.select_interrupted();
}
@ -433,58 +579,405 @@ pub trait InputEventQueuer {
}
ReadbResult::Byte(read_byte) => {
if crate::libc::MB_CUR_MAX() == 1 {
// single-byte locale, all values are legal
// FIXME: this looks wrong, this falsely assumes that
// the single-byte locale is compatible with Unicode upper-ASCII.
res = read_byte.into();
return CharEvent::from_char(res);
let mut have_escape_prefix = false;
let mut buffer = vec![read_byte];
let key_with_escape = if read_byte == 0x1b {
self.parse_escape_sequence(&mut buffer, &mut have_escape_prefix)
} else {
canonicalize_control_char(read_byte)
};
if self.paste_is_buffering() {
if read_byte != 0x1b {
self.paste_push_char(read_byte);
}
continue;
}
let mut codepoint = u32::from(res);
let sz = unsafe {
mbrtowc(
std::ptr::addr_of_mut!(codepoint).cast(),
std::ptr::addr_of!(read_byte).cast(),
1,
let mut seq = WString::new();
let mut key = key_with_escape;
let mut consumed = 0;
for i in 0..buffer.len() {
self.parse_codepoint(
&mut state,
)
} as isize;
match sz {
-1 => {
FLOG!(reader, "Illegal input");
return CharEvent::from_check_exit();
}
-2 => {
// Sequence not yet complete.
bytes[num_bytes] = read_byte;
num_bytes += 1;
&mut key,
&mut seq,
&buffer,
i,
&mut consumed,
&mut have_escape_prefix,
);
}
return if let Some(key) = key {
CharEvent::from_key_seq(key, seq)
} else {
self.insert_front(seq.chars().skip(1).map(CharEvent::from_char));
let Some(c) = seq.chars().next() else {
continue;
}
0 => {
// Actual nul char.
return CharEvent::from_char('\0');
}
_ => (),
}
if let Some(res) = char::from_u32(codepoint) {
// Sequence complete.
if !fish_reserved_codepoint(res) {
return CharEvent::from_char(res);
}
}
bytes[num_bytes] = read_byte;
num_bytes += 1;
for &b in &bytes[1..num_bytes] {
let c = CharEvent::from_char(encode_byte_to_char(b));
self.push_back(c);
}
let res = CharEvent::from_char(encode_byte_to_char(bytes[0]));
return res;
};
CharEvent::from_key_seq(Key::from_raw(c), seq)
};
}
ReadbResult::NothingToRead => unreachable!(),
}
}
}
fn try_readb(&mut self, buffer: &mut Vec<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> {
self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed))
}
@ -556,6 +1049,24 @@ pub trait InputEventQueuer {
/// Return the fd corresponding to stdin.
fn get_in_fd(&self) -> RawFd;
// Support for "bracketed paste"
// The way it works is that we acknowledge our support by printing
// \e\[?2004h
// then the terminal will "bracket" every paste in
// \e\[200~ and \e\[201~
// Every character in between those two will be part of the paste and should not cause a binding to execute (like \n executing commands).
//
// We enable it after every command and disable it before, see the terminal protocols logic.
//
// Support for this seems to be ubiquitous - emacs enables it unconditionally (!) since 25.1
// (though it only supports it since then, it seems to be the last term to gain support).
//
// See http://thejh.net/misc/website-terminal-copy-paste.
fn paste_start_buffering(&mut self);
fn paste_is_buffering(&self) -> bool;
fn paste_push_char(&mut self, _b: u8) {}
fn paste_commit(&mut self);
/// Enqueue a character or a readline function to the queue of unread characters that
/// readch will return before actually reading from fd 0.
fn push_back(&mut self, ch: CharEvent) {
@ -622,8 +1133,8 @@ pub trait InputEventQueuer {
/// nothing.
fn prepare_to_select(&mut self) {}
/// Override point for when when select() is interrupted by a signal. The default does nothing.
fn select_interrupted(&mut self) {}
/// Called when select() is interrupted by a signal.
fn select_interrupted(&mut self);
/// Override point for when when select() is interrupted by the universal variable notifier.
/// The default does nothing.
@ -639,6 +1150,7 @@ pub trait InputEventQueuer {
pub struct InputEventQueue {
queue: VecDeque<CharEvent>,
in_fd: RawFd,
is_in_bracketed_paste: bool,
}
impl InputEventQueue {
@ -646,6 +1158,7 @@ impl InputEventQueue {
InputEventQueue {
queue: VecDeque::new(),
in_fd,
is_in_bracketed_paste: false,
}
}
}
@ -662,4 +1175,23 @@ impl InputEventQueuer for InputEventQueue {
fn get_in_fd(&self) -> RawFd {
self.in_fd
}
fn select_interrupted(&mut self) {
if reader_test_and_clear_interrupted() != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
}
}
}
fn paste_start_buffering(&mut self) {
self.is_in_bracketed_paste = true;
}
fn paste_is_buffering(&self) -> bool {
self.is_in_bracketed_paste
}
fn paste_commit(&mut self) {
self.is_in_bracketed_paste = false;
}
}

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 io;
pub mod job_group;
pub mod key;
pub mod kill;
#[allow(non_snake_case)]
pub mod libc;

View file

@ -11,6 +11,7 @@ use crate::env::Statuses;
use crate::event::{self, Event};
use crate::flog::{FLOG, FLOGF};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::terminal_protocols_enable;
use crate::io::IoChain;
use crate::job_group::{JobGroup, MaybeJobId};
use crate::parse_tree::ParsedSourceRef;
@ -1412,6 +1413,9 @@ fn process_mark_finished_children(parser: &Parser, block_ok: bool) {
let status = ProcStatus::from_waitpid(statusv);
handle_child_status(j, proc, &status);
if status.stopped() {
if is_interactive_session() && j.group().wants_terminal() {
terminal_protocols_enable();
}
j.group().set_is_foreground(false);
}
if status.continued() {

View file

@ -69,7 +69,9 @@ use crate::history::{
};
use crate::input::init_input;
use crate::input::Inputter;
use crate::input_common::{CharEvent, CharInputStyle, ReadlineCmd};
use crate::input_common::{
terminal_protocols_enable_scoped, CharEvent, CharInputStyle, ReadlineCmd,
};
use crate::io::IoChain;
use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate};
use crate::libc::MB_CUR_MAX;
@ -133,7 +135,7 @@ pub static SHELL_MODES: Lazy<Mutex<libc::termios>> =
Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() }));
/// 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() }));
/// Mode we use to execute programs.
@ -784,10 +786,15 @@ pub fn reader_init() -> impl ScopeGuarding<Target = ()> {
// Set up our fixed terminal modes once,
// so we don't get flow control just because we inherited it.
if is_interactive_session() && unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } {
term_donate(/*quiet=*/ true);
let mut terminal_protocols = None;
if is_interactive_session() {
terminal_protocols = Some(terminal_protocols_enable_scoped());
if unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } {
term_donate(/*quiet=*/ true);
}
}
ScopeGuard::new((), |()| {
ScopeGuard::new((), move |()| {
let _terminal_protocols = terminal_protocols;
restore_term_mode();
})
}
@ -1912,26 +1919,23 @@ impl ReaderData {
CharEvent::Command(command) => {
zelf.run_input_command_scripts(&command);
}
CharEvent::Char(cevt) => {
CharEvent::Key(kevt) => {
// Ordinary char.
let c = cevt.char;
if cevt.input_style == CharInputStyle::NotFirst
if kevt.input_style == CharInputStyle::NotFirst
&& zelf.active_edit_line().1.position() == 0
{
// This character is skipped.
} else if c.is_control() {
// This can happen if the user presses a control char we don't recognize. No
// reason to report this to the user unless they've enabled debugging output.
FLOG!(reader, wgettext_fmt!("Unknown key binding 0x%X", c));
} else {
// Regular character.
let (elt, _el) = zelf.active_edit_line();
zelf.insert_char(elt, c);
if let Some(c) = kevt.key.codepoint_text() {
zelf.insert_char(elt, c);
if elt == EditableLineTag::Commandline {
zelf.clear_pager();
// We end history search. We could instead update the search string.
zelf.history_search.reset();
if elt == EditableLineTag::Commandline {
zelf.clear_pager();
// We end history search. We could instead update the search string.
zelf.history_search.reset();
}
}
}
rls.last_cmd = None;
@ -2036,7 +2040,7 @@ impl ReaderData {
while accumulated_chars.len() < limit {
let evt = self.inputter.read_char();
let CharEvent::Char(cevt) = &evt else {
let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt);
break;
};
@ -2044,16 +2048,20 @@ impl ReaderData {
event_needing_handling = Some(evt);
break;
}
if cevt.input_style == CharInputStyle::NotFirst
if kevt.input_style == CharInputStyle::NotFirst
&& accumulated_chars.is_empty()
&& self.active_edit_line().1.position() == 0
{
// The cursor is at the beginning and nothing is accumulated, so skip this character.
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() {
last_exec_count = self.exec_count();
self.screen.save_status();
@ -3047,6 +3055,12 @@ impl ReaderData {
.borrow_mut()
.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 => {
self.parser().libdata_mut().pods.is_repaint = true;
let clear = screen_clear();

View file

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

View file

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

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 input;
mod input_common;
mod key;
mod pager;
mod parse_util;
mod parser;

View file

@ -277,6 +277,16 @@ pub trait WExt {
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.
/// The Suffix can be a char, a &str, a &wstr, or a &WString.
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.
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
function foo
echo ran foo

View file

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

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
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.

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

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
#

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

View file

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

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
set -l dir $PWD/(dirname (status -f))
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.
builtin -q string; and echo 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)
# CHECK: caller-exit foo1
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.
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)

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
# 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
# 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
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.

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
echo $(echo 1\n2)
# 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.

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

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

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
commandline --input "echo foo | bar" --is-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;

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

View file

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

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
function commandline
if test $argv[1] = -ct
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.
# 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.

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

View file

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

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.
# 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

View file

@ -1,4 +1,4 @@
# RUN: %fish %s
# RUN: %fish %s | %filter-ctrlseqs
function getenvs
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
eval set -l previously_undefined foo
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
#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
echo $status
# 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)
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.

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

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

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

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

View file

@ -1,4 +1,4 @@
#RUN: %fish --features=remove-percent-self %s
#RUN: %fish --features=remove-percent-self %s | %filter-ctrlseqs
echo %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

View file

@ -1,3 +1,3 @@
# Explicitly overriding HOME/XDG_CONFIG_HOME is only required if not invoking via `make test`
# RUN: %fish --features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"'
# RUN: %fish --features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"' | %filter-ctrlseqs
#CHECK: qmarkon: 1

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

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

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

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.

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

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.
set -g i global

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
function stuff --argument a b c
# 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
echo t
end

View file

@ -1,4 +1,4 @@
#RUN: %fish %s
#RUN: %fish %s | %filter-ctrlseqs
# Test the `functions` builtin
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,
# e.g. the fish_git_prompt variable handlers test `status is-interactive`.
#REQUIRES: command -v git
@ -198,4 +198,3 @@ end
$fish -c 'complete -C "git -C ./.gi"'
# CHECK: ./.git/ Directory

View file

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

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: 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: 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: command

View file

@ -1,2 +1,2 @@
#RUN: %fish -C 'echo init-command'
#RUN: %fish -C 'echo init-command' | %filter-ctrlseqs
# 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)
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."
# CHECK: 1.2.3.4.
@ -107,4 +107,3 @@ $fish --no-config -c 'echo notprinted | and true'
# Regression test for a hang.
echo "set -L" | $fish > /dev/null

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