Compare commits

...

3 Commits

Author SHA1 Message Date
Johannes Altmanninger
ba2ad33a81 Use curly underlines for errors in default themes
Given that this curly underline will also be red, it should be widely
understood as error.

Since fish always renders immediately (and synchronously), typing "echo"
will briefly show an intermediate curly line.  Maybe fish should redraw
after a timer elapses? This is probably unrelated to this patch.

As mentioned in cc9849c279 (Curly underlines in set_color and fish_color_*,
2025-04-13), there are still some terminals that interpret "\e[4:3m" as
something other than curly underline.

Some of them interpret it as background/foreground color, and some terminal
multiplexers downgrade it to straight underlines (which often happens due
to a false positive).  I want to change this in multiplexers where possible
(see https://github.com/orgs/tmux/discussions/4477) but for now, disable
this feature in multiplexers (there are just a handful).

In a few years, those terminals will maybe agree with XTerm.  Until then,
use XTGETTCAP as a temporary stepping stone. We could also read the terminfo
database but that will give only very few true positives, and lots of false
negatives.  Better implement XTGETTCAP in the relevant terminals.

Note that if the universal variables use the "--track" flag (from the
grandparent commit), then

	rm -rf /tmp/newhome
	foot -e env $HOME=/tmp/newhome fish
	xterm -e env $HOME=/tmp/newhome fish

will "magically work". For foot, $fish_color_error will have --underline=curly.
For xterm, it will not, due to the call to "fish_config theme update".
But of course since it's a universal variable, running fish in xterm
would also downgrade the fish running in other terminals.

Add a "fish_config theme save --yes" flag because "status xtgettcap"
requires stdin to be a terminal.  I'd probably drop this requirement, and
make "status xtgettcap" always use fish's stdin and not its own.  That'd be
cheating because an external command can't do that but I don't think this
change would be hurting anyone.
2025-04-29 13:59:27 +02:00
Johannes Altmanninger
3453565a41 Builtin for querying terminal capabilities/name/version
The next commit wants to add a conditional default for styled underlines.
Due to various incompatibilities in terminals, our best option seems to ask
the terminal.

Today we can make XTGETTCAP queries using something like

	printf '\eP+q5373\e\\' # Su
	printf '\e[0c'
	while read -n 1
	    ...
	end

This doesn't seem safe because builtin read might consume other data written
by the terminal such as keyboard input.

Avoid this problem by providing it as a builtin that can enqueue unrelated
input and process it after the query has been answered.

For the same reason, add a builtin to query for XTVERSION; this allows us
to add workarounds for specific terminals (example in the next commit).

TODO:
- Naming -- maybe add a level of nesting:

	status query-terminal xtgettcap
	status query-terminal xtversion
	# Possible future additions:
	status query-terminal os-name
	status query-terminal cursor-position

or "status query-xtgettcap".
We could also hide the exact protocol by saying
"status query terminfo-capability".

Note that xtgettcap, xtversion and os-name are expected to always give the
same results throughout the lifetime of the fish process.  Keep caching
XTVERSION as before, though that's probably not needed.

Future work:

XTGETTCAP potentially supports all of terminfo: boolean, numeric and string
capabilities. Today we have no use beyond checking for presence/absence of
a capabilty. If we ever need more, we can use stdout.
2025-04-29 13:59:27 +02:00
Johannes Altmanninger
cb2b1c6621 Apply theme updates to implicitly-set universal color variables
On first run we copy the default theme to universal variables for colors
(since these defaults are not included in the binary).

Similarly, we have "fish_config theme save", which copies the given theme
to the same universal variables.

Sometimes we make changes to a default theme.  Existing users won't see
those updates by default because previous defaults have been turned into
universal variables, which are indistinguishable from user-set variables.

This also means that installing fish on two systems might result in different
colors if the initially installed versions differ.  This is surprising.

It feels difficult to get rid of universal variables here without breaking
features such as instant propagation on "set fish_color_normal blue"
or tab-completion on "set fish_color" showing the current value even when
that's a default.

Assuming that we don't want to stop setting these universal variables,
try a different solution: keep track of whether a variable wants to follow
future updates/removals.
This is opt-in via "--track" (e.g. "fish_config theme save --track THEME")
but crucially, enabled for the default theme when that one is saved on first launch.
(Of course this means that this enhancement only works on new installations.)

This allows customizing individual color variables (which likely means
removing --track marker) while still getting updates for other color variables.

Note that whenever we add a new color, we still need to write a migration
(see "__fish_initialized") that initializes this color in the universal
scope with the "--track" marker.

TODO:
- add a "follow future updates to this theme" checkbox to webconfig
- changelog
2025-04-29 13:59:27 +02:00
55 changed files with 720 additions and 225 deletions

View File

@@ -69,7 +69,12 @@ Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
- Support for curly underlines in `fish_color_*` variables and :doc:`set_color <cmds/set_color>` (:issue:`10957`).
- Underlines can now be colored independent of text (:issue:`7619`).
- Errors are now highlighted with curly underlines in the default themes.
For compatibility with terminals that interpret the curly-underline escape sequence in an unexpected way,
the default themes enable this only if the terminal advertises support for the ``Su`` capability via XTGETTCAP.
- New documentation page `Terminal Compatibility <terminal-compatibility.html>`_ (also accessible via ``man fish-terminal-compatibility``) lists required and optional terminal control sequences used by fish.
- :doc:`status <cmds/status>` learned the ``xtgettcap`` subcommand, to query terminfo capabilities via XTGETTCAP commands.
- :doc:`status <cmds/status>` learned the ``xtversion`` subcommand, to show the terminal's name and version (as reported by via the XTVERSION command).
Other improvements
------------------

View File

@@ -10,7 +10,7 @@ Synopsis
fish_config [browse]
fish_config prompt (choose | list | save | show)
fish_config theme (choose | demo | dump | list | save | show)
fish_config theme (choose | demo | dump | list | save | show | update)
Description
-----------
@@ -40,6 +40,9 @@ Available subcommands for the ``theme`` command:
- ``list`` lists the names of the available sample themes.
- ``save`` saves the given theme to :ref:`universal variables <variables-universal>`.
- ``show`` shows what the given sample theme (or all) would look like.
- ``update`` updates any ``fish_color_*`` and ``fish_pager_color_*`` variables whose value contains
"--track=THEME". They are set to the latest version of that theme, and the tracking
option is preserved. Note that ``fish_config theme update`` is run at fish startup.
The **-h** or **--help** option displays help about using this command.

View File

@@ -59,6 +59,12 @@ The following options are available:
**-u** or **--underline**, or **-uSTYLE** or **--underline=STYLE**
Set the underline mode; supported styles are **single** (default) and **curly**.
**--track=THEME**
Ignored. Included by default in universally-scoped color variables to tell fish to update
them as the associated theme changes.
This flag can be set for all variables when loading a theme with the `--track`` option, that is
:doc:`fish_config theme save THEME --track <fish_config>`.
**-h** or **--help**
Displays help about using this command.

View File

@@ -33,6 +33,8 @@ Synopsis
status buildinfo
status get-file FILE
status list-files [PATH]
status xtgettcap TERMINFO-CAPABILITY
status xtversion
Description
-----------
@@ -117,6 +119,13 @@ The following operations (subcommands) are available:
This lists the files embedded in the fish binary at compile time. Only files where the path starts with the optional *FILE* argument are shown.
Returns 0 if something was printed, 1 otherwise.
**xtgettcap** *TERMINFO-CAPABILITY*
Query the terminal for a terminfo capability via XTGETTCAP.
Returns 0 if the capability is present and 1 otherwise.
**xtversion**
Show the terminal name and version (XTVERSION).
Notes
-----

View File

@@ -177,7 +177,9 @@ Optional Commands
- Su
- Reset underline color to the default (follow the foreground color).
- kitty
* - ``\e[ Ps S``
* - .. _indn:
``\e[ Ps S``
- indn
- Scroll forward Ps lines.
-
@@ -269,6 +271,12 @@ Optional Commands
- FinalTerm
* - ``\eP+q Pt \e\\``
-
- Request terminfo capability (XTGETTCAP). The parameter is the capability's hex-encoded terminfo code.
Specifically, fish asks for the ``indn`` string capability. At the time of writing string capabilities are supported by kitty and foot.
- XTerm, kitty, foot
- Request terminfo capability (XTGETTCAP).
The parameter is the capability's hex-encoded terminfo code.
To advertise a capability, the response must of the form
``\eP1+q Pt \e\\`` or ``\eP1+q Pt = Pt \e\\``.
In either variant the first parameter must be the hex-encoded terminfo code.
In the second variant, fish ignores the part after the equals sign.
At startup, fish checks for the presence of the `indn <#indn>`_ string capability.
- XTerm, kitty, foot, wezterm, contour

0
reader Normal file
View File

View File

@@ -1,6 +1,6 @@
complete fish_config -f
set -l prompt_commands choose save show list
set -l theme_commands choose demo dump save show list
set -l theme_commands choose demo dump save show list update
complete fish_config -n __fish_use_subcommand -a prompt -d 'View and pick from the sample prompts'
complete fish_config -n "__fish_seen_subcommand_from prompt; and not __fish_seen_subcommand_from $prompt_commands" \
-a choose -d 'View and pick from the sample prompts'
@@ -16,6 +16,7 @@ complete fish_config -n __fish_use_subcommand -a browse -d 'Open the web-based U
complete fish_config -n __fish_use_subcommand -a theme -d 'View and pick from the sample themes'
complete fish_config -n '__fish_seen_subcommand_from theme; and __fish_seen_subcommand_from choose save show' -a '(fish_config theme list)'
complete fish_config -n '__fish_seen_subcommand_from theme; and __fish_seen_subcommand_from save' -l track -d 'Add --track to color variables to apply future theme updates'
complete fish_config -n "__fish_seen_subcommand_from theme; and not __fish_seen_subcommand_from $theme_commands" \
-a choose -d 'View and pick from the sample themes'
complete fish_config -n "__fish_seen_subcommand_from theme; and not __fish_seen_subcommand_from $theme_commands" \
@@ -28,3 +29,5 @@ complete fish_config -n "__fish_seen_subcommand_from theme; and not __fish_seen_
-a demo -d 'Show example in the current theme'
complete fish_config -n "__fish_seen_subcommand_from theme; and not __fish_seen_subcommand_from $theme_commands" \
-a dump -d 'Print the current theme in .theme format'
complete fish_config -n "__fish_seen_subcommand_from theme; and not __fish_seen_subcommand_from $theme_commands" \
-a update -d "Update universal colors that have the tracking flag set"

View File

@@ -8,3 +8,4 @@ complete -c set_color -s r -l reverse -d 'Reverse color text'
complete -c set_color -s u -l underline -d 'Underline style' -a 'single curly'
complete -c set_color -s h -l help -d 'Display help and exit'
complete -c set_color -s c -l print-colors -d 'Print a list of all accepted color names'
complete -c set_color -l track -xa '(fish_config theme list)' -d 'Ignored. Used in color variables to follow theme changes'

View File

@@ -27,7 +27,9 @@ set -l __fish_status_all_commands \
list-files \
print-stack-trace \
stack-trace \
test-feature
test-feature \
xtgettcap \
xtversion
# These are the recognized flags.
complete -c status -s h -l help -d "Display help and exit"
@@ -64,6 +66,8 @@ complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_com
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a get-file -d "Print an embedded file from the fish binary"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a list-files -d "List embedded files contained in the fish binary"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a fish-path -d "Print the path to the current instance of fish"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a xtgettcap -d "Query the terminal for a terminfo capability"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a xtversion -d "Show terminal name and version"
# The job-control command changes fish state.
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a job-control -d "Set which jobs are under job control"

View File

@@ -30,12 +30,13 @@ if status is-interactive
# Commands to run in interactive sessions can go here
end" >$__fish_config_dir/config.fish
echo yes | fish_config theme save "fish default"
fish_config theme save "fish default" --yes --track
set -Ue fish_color_keyword fish_color_option
end
if test $__fish_initialized -lt 3800 && test "$fish_color_search_match[1]" = bryellow
set --universal fish_color_search_match[1] white
end
fish_config theme update
#
# Generate man page completions if not present.

View File

@@ -0,0 +1,3 @@
function __fish_in_gnu_screen
test -n "$STY" || contains -- $TERM screen screen-256color
end

View File

@@ -0,0 +1,9 @@
function __fish_in_terminal_multiplexer
set -l terminal_name "(status xtversion | string match -r '^\S*')"
string match -q -- tmux $terminal_name ||
__fish_in_gnu_screen ||
# Emacs does probably not support multiplexing between multiple parent
# terminals, but it is affected by the same issues. Same for Vim's
# :terminal TODO detect that before they implement XTGETTCAP.
contains -- $TERM dvtm-256color eterm eterm-color
end

View File

@@ -77,7 +77,11 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
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 scrollback-push clear-screen
bind --preset $argv ctrl-l (
if not __fish_in_gnu_screen && and __fish_xtgettcap indn
echo scrollback-push
end
) clear-screen
bind --preset $argv ctrl-c clear-commandline
bind --preset $argv ctrl-u backward-kill-line
bind --preset $argv ctrl-k kill-line

View File

@@ -0,0 +1,12 @@
function __fish_xtgettcap --argument-names terminfo_code
if test $FISH_UNIT_TESTS_RUNNING = 1
return 1
end
set -l varname __fish_tcap_$terminfo_code
if not set -q $varname\[0\]
set --global $varname (
status xtgettcap $terminfo_code && echo true || echo false
)
end
test "$$varname" = true
end

View File

@@ -1,5 +1,11 @@
function fish_config --description "Launch fish's web based configuration"
argparse h/help -- $argv
# Variables a theme is allowed to set
set -l theme_var_filter '^fish_(?:pager_)?color.*$'
function fish_config --description "Launch fish's web based configuration" \
--inherit-variable theme_var_filter
set -l _flag_track
argparse h/help track yes -- $argv
or return
if set -q _flag_help
@@ -10,6 +16,15 @@ function fish_config --description "Launch fish's web based configuration"
set -l cmd $argv[1]
set -e argv[1]
if set -q _flag_track[1] && not { test "$cmd" = theme && test "$argv[1]" = save }
echo >&2 fish_config: --track: unknown option
return 1
end
if set -q _flag_yes[1] && not { contains "$cmd" prompt theme && test "$argv[1]" = save }
echo >&2 fish_config: --yes: unknown option
return 1
end
set -q cmd[1]
or set cmd browse
@@ -65,9 +80,6 @@ function fish_config --description "Launch fish's web based configuration"
return 1
end
# Variables a theme is allowed to set
set -l theme_var_filter '^fish_(?:pager_)?color.*$'
switch $cmd
case prompt
# prompt - for prompt switching
@@ -144,8 +156,8 @@ function fish_config --description "Launch fish's web based configuration"
functions --erase fish_right_prompt
end
case save
read -P"Overwrite prompt? [y/N]" -l yesno
if string match -riq 'y(es)?' -- $yesno
if set -q _flag_yes[1] ||
{ read -P"Overwrite prompt? [y/N]" -l yesno; string match -riq 'y(es)?' -- $yesno }
echo Overwriting
# Skip the cp if unnecessary,
# or we'd throw an error on a stock fish.
@@ -274,8 +286,10 @@ function fish_config --description "Launch fish's web based configuration"
set -l have_colors
if contains -- $cmd save
read -P"Overwrite your current theme? [y/N] " -l yesno
if not string match -riq 'y(es)?' -- $yesno
if not set -q _flag_yes &&
{ read -P"Overwrite your current theme? [y/N] " -l yesno
not string match -riq 'y(es)?' -- $yesno
}
echo Not overwriting >&2
return 1
end
@@ -292,34 +306,7 @@ function fish_config --description "Launch fish's web based configuration"
# If we are choosing a theme or saving from a named theme, load the theme now.
# Otherwise, we'll persist the currently loaded/themed variables (in case of `theme save`).
if set -q argv[1]
set -l files $dirs/$argv[1].theme
set -l file
for f in $files
if test -e "$f"
set file $f
break
end
end
if not set -q file[1]
if status list-files tools/web_config/themes/$argv[1].theme &>/dev/null
set file tools/web_config/themes/$argv[1].theme
else
echo "No such theme: $argv[1]" >&2
echo "Searched directories: $dirs" >&2
return 1
end
end
set -l content
if string match -qr '^tools/' -- $file
set content (status get-file $file)
else
read -z content < $file
end
printf %s\n $content | while read -lat toks
__fish_config_theme_get $argv[1] | while read -lat toks
# The whitelist allows only color variables.
# Not the specific list, but something named *like* a color variable.
# This also takes care of empty lines and comment lines.
@@ -331,7 +318,7 @@ function fish_config --description "Launch fish's web based configuration"
if test x"$scope" = x-U; and set -qg $toks[1]
set -eg $toks[1]
end
set $scope $toks
set $scope $toks $_flag_track=$argv[1]
set -a have_colors $toks[1]
end
@@ -343,7 +330,7 @@ function fish_config --description "Launch fish's web based configuration"
# Erase conflicting global variables so we don't get a warning and
# so changes are observed immediately.
set -eg $c
set $scope $c
set $scope $c $_flag_track=$argv[1]
end
else
# We're persisting whatever current colors are loaded (maybe in the global scope)
@@ -365,6 +352,8 @@ function fish_config --description "Launch fish's web based configuration"
# If we've made it this far, we've either found a theme file or persisted the current
# state (if any). In all cases we haven't failed, so return 0.
return 0
case update
__fish_config_theme_update $argv
case dump
# Write the current theme in .theme format, to stdout.
set -L | string match -r $theme_var_filter
@@ -374,3 +363,100 @@ function fish_config --description "Launch fish's web based configuration"
end
end
end
function __fish_config_theme_get
set -l dirs $__fish_config_dir/themes $__fish_data_dir/tools/web_config/themes
set -l files $dirs/$argv[1].theme
set -l file
set -l is_default_theme false
for f in $files
if test -e "$f"
set file $f
if test $f = $__fish_data_dir/tools/web_config/themes/$argv[1].theme
set is_default_theme true
end
break
end
end
if not set -q file[1]
if status list-files tools/web_config/themes/$argv[1].theme &>/dev/null
set file tools/web_config/themes/$argv[1].theme
set is_default_theme true
else
echo "No such theme: $argv[1]" >&2
echo "Searched directories: $dirs" >&2
return 1
end
end
set -l content (
if string match -qr '^tools/' -- $file
status get-file $file
else
string join \n <$file
end
)
if $is_default_theme && not __fish_in_gnu_screen && not __fish_xtgettcap Su && not __fish_in_terminal_multiplexer
set content (string replace -- --underline=curly "" $content)
end
string join \n $content
end
function __fish_config_show_tracked_color_vars
set -l color_var $argv[1]
set -l _flag_track
argparse --ignore-unknown track= -- _set_color $argv[2..]
or return $status
if not set -q _flag_track[1]
return
end
if set -q _flag_track[2]
echo >&2 "fish_config: $color_var: --track option can only be specified once"
exit 1
end
if test (printf %s $_flag_track | count) -ne 0
echo >&2 "fish_config: $color_var: error: tracking theme name must not contain newlines"
exit 1
end
printf %s\n $color_var $_flag_track
end
function __fish_config_theme_update --inherit-variable theme_var_filter
if set -q argv[1]
echo "fish_config: too many arguments" >&2
return 1
end
set -l themes
set -l tracking_variables (
set --universal --long |
string match -r '^fish_(?:pager_)?color.*$' |
string replace -r '.*' '__fish_config_show_tracked_color_vars $0' |
source
)
or return $status
string join \n $tracking_variables |
while read --line _colorvar theme
if not contains -- $theme $themes
set -a themes $theme
end
end
for theme in $themes
set -l colorvars
string join \n $tracking_variables |
while read --line color_var t
if test $t = $theme
set -a colorvars $color_var
end
end
set -l theme_escaped (string escape -- $theme)
__fish_config_theme_get $theme |
string match -r -- "^(?:$(string join '|' $colorvars))\b .*" |
string replace -r '.*' "set -U \$0 --track=$theme_escaped" |
source
end
end

View File

@@ -10,7 +10,7 @@ fish_color_comment f7ca88
fish_color_cwd green
fish_color_cwd_root red
fish_color_end ba8baf
fish_color_error ab4642
fish_color_error ab4642 --underline=curly
fish_color_escape 86c1b9
fish_color_history_current --bold
fish_color_host normal

View File

@@ -10,7 +10,7 @@ fish_color_comment f7ca88
fish_color_cwd green
fish_color_cwd_root red
fish_color_end ba8baf
fish_color_error ab4642
fish_color_error ab4642 --underline=curly
fish_color_escape 86c1b9
fish_color_history_current --bold
fish_color_host normal

View File

@@ -10,7 +10,7 @@ fish_color_comment ffcc66
fish_color_cwd green
fish_color_cwd_root red
fish_color_end cc99cc
fish_color_error f2777a
fish_color_error f2777a --underline=curly
fish_color_escape 66cccc
fish_color_history_current --bold
fish_color_host normal

View File

@@ -9,7 +9,7 @@ fish_color_comment FF9640
fish_color_cwd green
fish_color_cwd_root red
fish_color_end FFB273
fish_color_error FF7400
fish_color_error FF7400 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -21,7 +21,7 @@ fish_color_comment 6272a4
fish_color_cwd 50fa7b
fish_color_cwd_root red
fish_color_end ffb86c
fish_color_error ff5555
fish_color_error ff5555 --underline=curly
fish_color_escape ff79c6
fish_color_history_current --bold
fish_color_host bd93f9

View File

@@ -9,7 +9,7 @@ fish_color_comment FFE100
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 8D003B
fish_color_error EC3B86
fish_color_error EC3B86 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -9,7 +9,7 @@ fish_color_comment B0B0B0
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 969696
fish_color_error FFA779
fish_color_error FFA779 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -6,7 +6,7 @@ fish_color_command FF9400
fish_color_quote BF9C30
fish_color_redirection BF5B30
fish_color_end FF4C00
fish_color_error FFDD73
fish_color_error FFDD73 --underline=curly
fish_color_param FFC000
fish_color_comment A63100
fish_color_selection white --background=brblack --bold

View File

@@ -9,7 +9,7 @@ fish_color_comment 4e4e4e
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 767676
fish_color_error b2b2b2
fish_color_error b2b2b2 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -6,7 +6,7 @@ fish_color_command ffffff
fish_color_quote a8a8a8
fish_color_redirection 808080
fish_color_end 949494
fish_color_error 585858
fish_color_error 585858 --underline=curly
fish_color_param d7d7d7
fish_color_comment bcbcbc
fish_color_selection white --background=brblack --bold

View File

@@ -9,7 +9,7 @@ fish_color_comment
fish_color_cwd normal
fish_color_cwd_root normal
fish_color_end
fish_color_error
fish_color_error --underline=curly
fish_color_escape
fish_color_history_current
fish_color_host normal

View File

@@ -11,7 +11,7 @@ fish_color_comment 4c566a --italics
fish_color_cwd 5e81ac
fish_color_cwd_root bf616a
fish_color_end 81a1c1
fish_color_error bf616a
fish_color_error bf616a --underline=curly
fish_color_escape ebcb8b
fish_color_history_current e5e9f0 --bold
fish_color_host a3be8c

View File

@@ -9,7 +9,7 @@ fish_color_comment 30BE30
fish_color_cwd green
fish_color_cwd_root red
fish_color_end FF7B7B
fish_color_error A40000
fish_color_error A40000 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -9,7 +9,7 @@ fish_color_comment 5C9900
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 8EEB00
fish_color_error 60B9CE
fish_color_error 60B9CE --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -6,7 +6,7 @@ fish_color_command 164CC9
fish_color_quote 4C3499
fish_color_redirection 248E8E
fish_color_end 02BDBD
fish_color_error 9177E5
fish_color_error 9177E5 --underline=curly
fish_color_param 4319CC
fish_color_comment 007B7B
fish_color_selection white --background=brblack --bold

View File

@@ -10,7 +10,7 @@ fish_color_comment 586e75
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 268bd2
fish_color_error dc322f
fish_color_error dc322f --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -7,7 +7,7 @@ fish_color_command 586e75
fish_color_quote 839496
fish_color_redirection 6c71c4
fish_color_end 268bd2
fish_color_error dc322f
fish_color_error dc322f --underline=curly
fish_color_param 657b83
fish_color_comment 93a1a1
fish_color_selection white --background=brblack --bold

View File

@@ -10,7 +10,7 @@ fish_color_comment e7c547
fish_color_cwd green
fish_color_cwd_root red
fish_color_end c397d8
fish_color_error d54e53
fish_color_error d54e53 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -7,7 +7,7 @@ fish_color_command b294bb
fish_color_quote b5bd68
fish_color_redirection 8abeb7
fish_color_end b294bb
fish_color_error cc6666
fish_color_error cc6666 --underline=curly
fish_color_param 81a2be
fish_color_comment f0c674
fish_color_selection white --background=brblack --bold

View File

@@ -10,7 +10,7 @@ fish_color_comment eab700
fish_color_cwd green
fish_color_cwd_root red
fish_color_end 8959a8
fish_color_error c82829
fish_color_error c82829 --underline=curly
fish_color_escape 00a6b2
fish_color_history_current --bold
fish_color_host normal

View File

@@ -7,7 +7,7 @@ fish_color_command 39BAE6
fish_color_quote C2D94C
fish_color_redirection FFEE99
fish_color_end F29668
fish_color_error FF3333
fish_color_error FF3333 --underline=curly
fish_color_param B3B1AD
fish_color_comment 626A73
fish_color_selection --background=E6B450 --bold

View File

@@ -10,7 +10,7 @@ fish_color_comment ABB0B6
fish_color_cwd 399EE6
fish_color_cwd_root red
fish_color_end ED9366
fish_color_error F51818
fish_color_error F51818 --underline=curly
fish_color_escape 4CBF99
fish_color_history_current --bold
fish_color_host normal

View File

@@ -10,7 +10,7 @@ fish_color_comment 5C6773
fish_color_cwd 73D0FF
fish_color_cwd_root red
fish_color_end F29E74
fish_color_error FF3333
fish_color_error FF3333 --underline=curly
fish_color_escape 95E6CB
fish_color_history_current --bold
fish_color_host normal

View File

@@ -9,7 +9,7 @@ fish_color_comment '888' '--italics'
fish_color_cwd 0A0
fish_color_cwd_root A00
fish_color_end 009900
fish_color_error F22
fish_color_error F22 --underline=curly
fish_color_escape 0AA
fish_color_history_current 0AA
fish_color_host normal

View File

@@ -11,7 +11,7 @@ fish_color_comment red
fish_color_cwd green
fish_color_cwd_root red
fish_color_end green
fish_color_error brred
fish_color_error brred --underline=curly
fish_color_escape brcyan
fish_color_history_current --bold
fish_color_host normal

View File

@@ -153,7 +153,7 @@ fn setup_and_process_keys(
unsafe { libc::tcsetattr(0, TCSANOW, &*shell_modes()) };
terminal_protocol_hacks();
let blocking_query: OnceCell<RefCell<Option<TerminalQuery>>> = OnceCell::new();
initial_query(&blocking_query, streams.out, None);
initial_query(&blocking_query, streams.out);
if continuous_mode {
streams.err.append(L!("\n"));

View File

@@ -28,6 +28,7 @@
use crate::wutil::encoding::zero_mbstate;
use crate::wutil::perror;
use libc::SEEK_CUR;
use std::num::NonZeroUsize;
use std::os::fd::RawFd;
use std::sync::atomic::Ordering;
@@ -219,12 +220,14 @@ fn read_interactive(
conf.complete_ok = shell;
conf.highlight_ok = shell;
conf.syntax_check_ok = shell;
conf.prompt_ok = true;
// No autosuggestions or abbreviations in builtin_read.
conf.autosuggest_ok = false;
conf.expand_abbrev_ok = false;
conf.exit_on_interrupt = true;
conf.in_builtin_read = true;
conf.in_silent_mode = silent;
conf.left_prompt_cmd = prompt.to_owned();
@@ -244,7 +247,7 @@ fn read_interactive(
let mline = {
let _interactive = parser.push_scope(|s| s.is_interactive = true);
reader_readline(parser, nchars)
reader_readline(parser, NonZeroUsize::try_from(nchars).ok())
};
terminal_protocols_disable_ifn();
if let Some(line) = mline {

View File

@@ -86,6 +86,13 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
// In future we change both to actually print an error.
return Err(STATUS_INVALID_ARGS);
}
Err(MultipleTracking) => {
streams.err.append(wgettext_fmt!(
"%ls: --track option can only be specified once\n",
argv[0]
));
return Err(STATUS_INVALID_ARGS);
}
Err(UnknownColor(arg)) => {
streams
.err
@@ -124,7 +131,7 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
specified_face.fg.unwrap_or(Color::None),
specified_face.bg.unwrap_or(Color::None),
specified_face.underline_color.unwrap_or(Color::None),
specified_face.style,
specified_face.style.unwrap_or_default(),
));
if specified_face.fg.is_some() && outp.contents().is_empty() {

View File

@@ -1,5 +1,3 @@
use std::os::unix::prelude::*;
use super::prelude::*;
use crate::common::{get_executable_path, str2wcstring, PROGRAM_NAME};
use crate::future_feature_flags::{self as features, feature_test};
@@ -11,6 +9,9 @@
use libc::F_OK;
use nix::errno::Errno;
use nix::NixPath;
use std::os::unix::ffi::OsStrExt;
mod query;
macro_rules! str_enum {
($name:ident, $(($val:ident, $str:expr)),* $(,)?) => {
@@ -37,7 +38,7 @@ fn to_wstr(self) -> &'static wstr {
use StatusCmd::*;
#[derive(Clone, Copy)]
enum StatusCmd {
pub(in crate::builtins::status) enum StatusCmd {
STATUS_CURRENT_CMD = 1,
STATUS_BASENAME,
STATUS_DIRNAME,
@@ -62,6 +63,8 @@ enum StatusCmd {
STATUS_BUILDINFO,
STATUS_GET_FILE,
STATUS_LIST_FILES,
STATUS_XTGETTCAP,
STATUS_XTVERSION,
}
str_enum!(
@@ -96,6 +99,8 @@ enum StatusCmd {
(STATUS_STACK_TRACE, "print-stack-trace"),
(STATUS_STACK_TRACE, "stack-trace"),
(STATUS_TEST_FEATURE, "test-feature"),
(STATUS_XTGETTCAP, "xtgettcap"),
(STATUS_XTVERSION, "xtversion"),
);
/// Values that may be returned from the test-feature option to status.
@@ -527,6 +532,12 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Err(STATUS_CMD_ERROR);
}
}
STATUS_XTGETTCAP => {
return query::status_xtgettcap(parser, streams, cmd, args);
}
STATUS_XTVERSION => {
return query::status_xtversion(parser, streams, cmd, args);
}
ref s => {
if !args.is_empty() {
streams.err.append(wgettext_fmt!(
@@ -714,7 +725,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
| STATUS_FEATURES
| STATUS_TEST_FEATURE
| STATUS_GET_FILE
| STATUS_LIST_FILES => {
| STATUS_LIST_FILES
| STATUS_XTGETTCAP
| STATUS_XTVERSION => {
unreachable!("")
}
}

View File

@@ -0,0 +1,166 @@
use std::ops::ControlFlow;
use std::rc::Rc;
use crate::builtins::prelude::*;
use crate::common::wcs2string;
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::{
terminal_protocols_disable_ifn, InputEventQueuer, TerminalQuery, XtgettcapQuery,
};
use crate::nix::isatty;
use crate::reader::{
query_xtgettcap, querying_allowed, reader_pop, reader_push, reader_readline, ReaderConfig,
UserQuery,
};
use crate::terminal::TerminalCommand::QueryPrimaryDeviceAttribute;
use crate::terminal::{Output, Outputter, XTVERSION};
use libc::STDOUT_FILENO;
use super::StatusCmd;
pub(crate) fn status_xtversion(
parser: &Parser,
streams: &mut IoStreams,
cmd: &wstr,
args: &[&wstr],
) -> BuiltinResult {
use super::StatusCmd::STATUS_XTVERSION;
if !args.is_empty() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
STATUS_XTVERSION.to_wstr(),
0,
args.len()
));
return Err(STATUS_INVALID_ARGS);
}
let run_query = { move |_query: &mut Option<TerminalQuery>| ControlFlow::Break(()) };
synchronous_query(parser, streams, cmd, &STATUS_XTVERSION, Box::new(run_query))?;
let Some(xtversion) = XTVERSION.get() else {
return Err(STATUS_CMD_ERROR);
};
streams.out.appendln(xtversion);
Ok(SUCCESS)
}
pub(crate) fn status_xtgettcap(
parser: &Parser,
streams: &mut IoStreams,
cmd: &wstr,
args: &[&wstr],
) -> BuiltinResult {
use super::StatusCmd::STATUS_XTGETTCAP;
if !querying_allowed() {
return Err(STATUS_CMD_ERROR);
}
if args.len() != 1 {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
STATUS_XTGETTCAP.to_wstr(),
1,
args.len()
));
return Err(STATUS_INVALID_ARGS);
}
let result = Rc::new(RelaxedAtomicBool::new(false));
let run_query = {
let terminfo_code = wcs2string(args[0]);
let result = Rc::clone(&result);
move |query: &mut Option<TerminalQuery>| {
assert!(matches!(
*query,
None | Some(TerminalQuery::PrimaryDeviceAttribute(None))
));
let mut output = Outputter::stdoutput().borrow_mut();
output.begin_buffering();
query_xtgettcap(output.by_ref(), &terminfo_code);
output.write_command(QueryPrimaryDeviceAttribute);
output.end_buffering();
*query = Some(TerminalQuery::PrimaryDeviceAttribute(Some(
XtgettcapQuery {
terminfo_code,
result,
},
)));
ControlFlow::Continue(())
}
};
synchronous_query(parser, streams, cmd, &STATUS_XTGETTCAP, Box::new(run_query))?;
if !result.load() {
return Err(STATUS_CMD_ERROR);
}
Ok(SUCCESS)
}
fn synchronous_query(
parser: &Parser,
streams: &mut IoStreams,
cmd: &wstr,
subcmd: &StatusCmd,
run_query: UserQuery,
) -> Result<(), ErrorCode> {
if !isatty(streams.stdin_fd) {
streams.err.append(wgettext_fmt!(
"%s %s: stdin is not a TTY",
cmd,
subcmd.to_wstr(),
));
return Err(STATUS_INVALID_ARGS);
};
let out_fd = STDOUT_FILENO;
if !isatty(out_fd) {
streams.err.append(wgettext_fmt!(
"%s %s: stdout is not a TTY",
cmd,
subcmd.to_wstr(),
));
return Err(STATUS_INVALID_ARGS);
};
if let Some(query_state) = parser.blocking_query.get() {
if (run_query)(&mut query_state.borrow_mut()).is_break() {
return Ok(());
}
} else {
// We are the first reader.
let empty_spot = parser.pending_user_query.replace(Some(run_query));
assert!(empty_spot.is_none());
}
let mut conf = ReaderConfig::default();
conf.inputfd = streams.stdin_fd;
conf.prompt_ok = false;
conf.exit_on_interrupt = true;
let pending_keys = {
let mut reader = reader_push(parser, L!(""), conf);
{
let _interactive = parser.push_scope(|s| s.is_interactive = true);
let no_line = reader_readline(parser, None);
assert!(no_line.is_none());
}
terminal_protocols_disable_ifn();
let input_data = reader.get_input_data_mut();
let pending_keys = std::mem::take(&mut input_data.queue);
// We blocked code and mapping execution so input function args must be empty.
assert!(input_data.input_function_args.is_empty());
if input_data.paste_buffer.is_some() {
FLOG!(
reader,
"Bracketed paste was interrupted; dropping uncommitted paste buffer"
)
}
reader_pop();
pending_keys
};
FLOGF!(reader, "Adding %lu pending keys", pending_keys.len());
parser.pending_keys.borrow_mut().extend(pending_keys);
Ok(())
}

View File

@@ -28,7 +28,7 @@
};
use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file};
use crate::terminal::Outputter;
use crate::text_face::{parse_text_face, TextFace, UnderlineStyle};
use crate::text_face::{parse_text_face, SpecifiedTextFace, TextFace, UnderlineStyle};
use crate::threads::assert_is_background_thread;
use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir};
use crate::wchar::{wstr, WString, L};
@@ -140,12 +140,16 @@ pub(crate) fn resolve_spec_uncached(
vars: &dyn Environment,
) -> TextFace {
let resolve_role = |role| {
vars.get_unless_empty(get_highlight_var_name(role))
.or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role))))
.or_else(|| vars.get_unless_empty(get_highlight_var_name(HighlightRole::normal)))
.as_ref()
.map(parse_text_face_for_highlight)
.unwrap_or_else(TextFace::default)
for role in [role, get_fallback(role), HighlightRole::normal] {
if let Some(face) = vars
.get_unless_empty(get_highlight_var_name(role))
.as_ref()
.and_then(parse_text_face_for_highlight)
{
return face;
}
}
TextFace::default()
};
let mut face = resolve_role(highlight.foreground);
@@ -162,7 +166,8 @@ pub(crate) fn resolve_spec_uncached(
if highlight.valid_path {
if let Some(valid_path_var) = vars.get(L!("fish_color_valid_path")) {
// Historical behavior is to not apply background.
let valid_path_face = parse_text_face_for_highlight(&valid_path_var);
let valid_path_face =
parse_text_face_for_highlight(&valid_path_var).unwrap_or_default();
// Apply the foreground, except if it's normal. The intention here is likely
// to only override foreground if the valid path color has an explicit foreground.
if !valid_path_face.fg.is_normal() {
@@ -181,19 +186,21 @@ pub(crate) fn resolve_spec_uncached(
}
/// Return the internal color code representing the specified color.
pub(crate) fn parse_text_face_for_highlight(var: &EnvVar) -> TextFace {
pub(crate) fn parse_text_face_for_highlight(var: &EnvVar) -> Option<TextFace> {
let face = parse_text_face(var.as_list());
let default = TextFace::default();
let fg = face.fg.unwrap_or(default.fg);
let bg = face.bg.unwrap_or(default.bg);
let underline_color = face.underline_color.unwrap_or(default.underline_color);
let style = face.style;
TextFace {
fg,
bg,
underline_color,
style,
}
(face != SpecifiedTextFace::default()).then(|| {
let default = TextFace::default();
let fg = face.fg.unwrap_or(default.fg);
let bg = face.bg.unwrap_or(default.bg);
let underline_color = face.underline_color.unwrap_or(default.underline_color);
let style = face.style.unwrap_or_default();
TextFace {
fg,
bg,
underline_color,
style,
}
})
}
fn command_is_valid(

View File

@@ -18,10 +18,7 @@
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{
Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED,
SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::terminal::{Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED, XTVERSION};
use crate::threads::{iothread_port, is_main_thread};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
@@ -32,6 +29,7 @@
use std::os::fd::RawFd;
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
@@ -327,6 +325,8 @@ pub struct KeyInputEvent {
pub enum ImplicitEvent {
/// end-of-file was reached.
Eof,
/// Done
Break,
/// An event was handled internally, or an interrupt was received. Check to see if the reader
/// loop should exit.
CheckExit,
@@ -758,15 +758,21 @@ pub fn function_set_status(&mut self, status: bool) {
}
}
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CursorPositionQuery {
MouseLeft(ViewportPosition),
ScrollbackPush,
}
#[derive(Eq, PartialEq)]
#[derive(Debug)]
pub struct XtgettcapQuery {
pub terminfo_code: Vec<u8>,
pub result: Rc<RelaxedAtomicBool>,
}
#[derive(Debug)]
pub enum TerminalQuery {
PrimaryDeviceAttribute,
PrimaryDeviceAttribute(Option<XtgettcapQuery>),
CursorPositionReport(CursorPositionQuery),
}
@@ -932,6 +938,8 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
);
let ok = stop_query(self.blocking_query());
assert!(ok);
// TODO only if cancellation
self.push_front(CharEvent::Implicit(ImplicitEvent::Break));
}
continue;
}
@@ -959,10 +967,7 @@ fn parse_escape_sequence(
assert!(buffer.len() <= 2);
let recursive_invocation = buffer.len() == 2;
let Some(next) = self.try_readb(buffer) else {
if !self.paste_is_buffering() {
return Some(KeyEvent::from_raw(key::Escape));
}
return None;
return Some(KeyEvent::from_raw(key::Escape));
};
let invalid = KeyEvent::from_raw(key::Invalid);
if recursive_invocation && next == b'\x1b' {
@@ -1223,6 +1228,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
_ => return None,
},
b'c' if private_mode == Some(b'?') => {
FLOG!(reader, "Received primary device attribute response");
self.push_front(CharEvent::QueryResponse(
QueryResponseEvent::PrimaryDeviceAttribute,
));
@@ -1369,13 +1375,12 @@ fn parse_xtversion(&mut self, buffer: &mut Vec<u8>) -> Option<()> {
if buffer.get(3)? != &b'|' {
return None;
}
let xtversion = str2wcstring(&buffer[4..buffer.len()]);
FLOG!(
reader,
format!(
"Received XTVERSION response: {}",
str2wcstring(&buffer[4..buffer.len()])
)
format!("Received XTVERSION response: {}", xtversion)
);
XTVERSION.get_or_init(|| xtversion);
None
}
@@ -1432,9 +1437,17 @@ fn parse_dcs(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
format!("Received XTGETTCAP response: {}", str2wcstring(&key))
);
}
if key == SCROLL_FORWARD_TERMINFO_CODE.as_bytes() {
SCROLL_FORWARD_SUPPORTED.store(true);
FLOG!(reader, "Scroll forward is supported");
if let Some(TerminalQuery::PrimaryDeviceAttribute(Some(tcap_query))) =
&*self.blocking_query()
{
if tcap_query.terminfo_code == key {
if tcap_query.result.swap(true) {
FLOG!(
reader,
"Error: received multiple XTGETTCAP responses for user query"
);
}
}
}
return None;
}

View File

@@ -772,6 +772,10 @@ pub fn new(fd: RawFd) -> Self {
}
}
pub fn as_raw_fd(&self) -> RawFd {
self.fd
}
fn append(&mut self, s: &wstr) -> bool {
if self.errored {
return false;

View File

@@ -14,7 +14,7 @@
};
use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::{terminal_protocols_disable_ifn, TerminalQuery};
use crate::input_common::{terminal_protocols_disable_ifn, CharEvent, TerminalQuery};
use crate::io::IoChain;
use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@@ -25,6 +25,7 @@
use crate::parse_execution::{EndExecutionReason, ExecutionContext};
use crate::parse_tree::{parse_source, LineCounter, ParsedSourceRef};
use crate::proc::{job_reap, JobGroupRef, JobList, JobRef, Pid, ProcStatus};
use crate::reader::UserQuery;
use crate::signal::{signal_check_cancel, signal_clear_cancel, Signal};
use crate::threads::assert_is_main_thread;
use crate::util::get_time;
@@ -37,6 +38,7 @@
#[cfg(not(target_has_atomic = "64"))]
use portable_atomic::AtomicU64;
use std::cell::{Ref, RefCell, RefMut};
use std::collections::VecDeque;
use std::ffi::{CStr, OsStr};
use std::fs::File;
use std::io::Write;
@@ -442,6 +444,10 @@ pub struct Parser {
pub global_event_blocks: AtomicU64,
pub blocking_query: OnceCell<RefCell<Option<TerminalQuery>>>,
pub pending_user_query: RefCell<Option<UserQuery>>,
pub pending_keys: RefCell<VecDeque<CharEvent>>,
}
impl Parser {
@@ -460,6 +466,8 @@ pub fn new(variables: Rc<EnvStack>, cancel_behavior: CancelBehavior) -> Parser {
profile_items: RefCell::default(),
global_event_blocks: AtomicU64::new(0),
blocking_query: OnceCell::new(),
pending_user_query: RefCell::new(None),
pending_keys: RefCell::new(VecDeque::new()),
};
match open_dir(CStr::from_bytes_with_nul(b".\0").unwrap(), BEST_O_SEARCH) {

View File

@@ -85,6 +85,7 @@
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionQuery;
use crate::input_common::ImplicitEvent;
use crate::input_common::InputEventQueuer;
use crate::input_common::QueryResponseEvent;
use crate::input_common::TerminalQuery;
use crate::input_common::IN_DVTM;
@@ -134,9 +135,7 @@
QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute,
QueryXtgettcap, QueryXtversion,
};
use crate::terminal::{
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::terminal::{Capability, KITTY_KEYBOARD_SUPPORTED};
use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update};
use crate::text_face::parse_text_face;
use crate::text_face::TextFace;
@@ -238,25 +237,25 @@ fn redirect_tty_after_sighup() {
pub(crate) fn initial_query(
blocking_query: &OnceCell<RefCell<Option<TerminalQuery>>>,
out: &mut impl Output,
vars: Option<&dyn Environment>,
) {
blocking_query.get_or_init(|| {
let query = if is_dumb() || IN_MIDNIGHT_COMMANDER.load() || IN_DVTM.load() {
None
} else {
let query = querying_allowed().then(|| {
// Query for kitty keyboard protocol support.
out.write_command(QueryKittyKeyboardProgressiveEnhancements);
out.write_command(QueryXtversion);
if let Some(vars) = vars {
query_capabilities_via_dcs(out.by_ref(), vars);
}
out.write_command(QueryPrimaryDeviceAttribute);
Some(TerminalQuery::PrimaryDeviceAttribute)
};
TerminalQuery::PrimaryDeviceAttribute(None)
});
RefCell::new(query)
});
}
pub(crate) type UserQuery = Box<dyn FnOnce(&mut Option<TerminalQuery>) -> ControlFlow<()>>;
pub(crate) fn querying_allowed() -> bool {
!(is_dumb() || IN_MIDNIGHT_COMMANDER.load() || IN_DVTM.load())
}
/// The stack of current interactive reading contexts.
fn reader_data_stack() -> &'static mut Vec<Pin<Box<ReaderData>>> {
struct ReaderDataStack(UnsafeCell<Vec<Pin<Box<ReaderData>>>>);
@@ -273,7 +272,7 @@ pub fn reader_in_interactive_read() -> bool {
reader_data_stack()
.iter()
.rev()
.any(|reader| reader.conf.exit_on_interrupt)
.any(|reader| reader.conf.in_builtin_read)
}
/// Access the top level reader data.
@@ -338,6 +337,9 @@ pub struct ReaderConfig {
/// Whether to allow autosuggestions.
pub autosuggest_ok: bool,
/// Whether to show a prompt.
pub prompt_ok: bool,
/// Whether to reexecute prompt function before final rendering.
pub transient_prompt: bool,
@@ -347,6 +349,9 @@ pub struct ReaderConfig {
/// Whether to exit on interrupt (^C).
pub exit_on_interrupt: bool,
/// Whether we are in builtin read.
pub in_builtin_read: bool,
/// If set, do not show what is typed.
pub in_silent_mode: bool,
@@ -691,6 +696,7 @@ fn read_i(parser: &Parser) {
conf.syntax_check_ok = true;
conf.expand_abbrev_ok = true;
conf.autosuggest_ok = check_bool_var(parser.vars(), L!("fish_autosuggestion_enabled"), true);
conf.prompt_ok = true;
conf.transient_prompt = check_bool_var(parser.vars(), L!("fish_transient_prompt"), false);
if parser.is_breakpoint() && function::exists(DEBUG_PROMPT_FUNCTION_NAME, parser) {
@@ -882,6 +888,8 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
term_donate(/*quiet=*/ true);
}
}
terminal_protocol_hacks();
}
pub fn reader_deinit(in_signal_handler: bool, restore_foreground_pgroup: bool) {
@@ -1061,8 +1069,7 @@ pub fn reader_reading_interrupted(data: &mut ReaderData) -> i32 {
/// characters even if a full line has not yet been read. Note: the returned value may be longer
/// than nchars if a single keypress resulted in multiple characters being inserted into the
/// commandline.
pub fn reader_readline(parser: &Parser, nchars: usize) -> Option<WString> {
let nchars = NonZeroUsize::try_from(nchars).ok();
pub fn reader_readline(parser: &Parser, nchars: Option<NonZeroUsize>) -> Option<WString> {
let data = current_data().unwrap();
let mut reader = Reader { parser, data };
reader.readline(nchars)
@@ -2202,73 +2209,77 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
initial_query(
&self.parser.blocking_query,
&mut BufferedOutputter::new(Outputter::stdoutput()),
Some(self.parser.vars()),
);
// HACK: Don't abandon line for the first prompt, because
// if we're started with the terminal it might not have settled,
// so the width is quite likely to be in flight.
//
// This means that `printf %s foo; fish` will overwrite the `foo`,
// but that's a smaller problem than having the omitted newline char
// appear constantly.
//
// I can't see a good way around this.
if !self.first_prompt {
self.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
if self.conf.prompt_ok {
// HACK: Don't abandon line for the first prompt, because
// if we're started with the terminal it might not have settled,
// so the width is quite likely to be in flight.
//
// This means that `printf %s foo; fish` will overwrite the `foo`,
// but that's a smaller problem than having the omitted newline char
// appear constantly.
//
// I can't see a good way around this.
if !self.first_prompt {
self.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
}
self.first_prompt = false;
if !self.conf.event.is_empty() {
event::fire_generic(self.parser, self.conf.event.to_owned(), vec![]);
}
self.exec_prompt(true, false);
// Start out as initially dirty.
self.force_exec_prompt_and_repaint = true;
}
self.first_prompt = false;
if !self.conf.event.is_empty() {
event::fire_generic(self.parser, self.conf.event.to_owned(), vec![]);
}
self.exec_prompt(true, false);
// Start out as initially dirty.
self.force_exec_prompt_and_repaint = true;
self.insert_front(self.parser.pending_keys.take());
while !self.rls().finished && !check_exit_loop_maybe_warning(Some(self)) {
if self.handle_char_event(None).is_break() {
break;
}
}
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
if self.conf.prompt_ok {
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
// Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the
// user presses enter.
if self.is_repaint_needed(None)
|| self.screen.scrolled()
|| self.conf.inputfd != STDIN_FILENO
{
self.layout_and_repaint_before_execution();
}
// Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the
// user presses enter.
if self.is_repaint_needed(None)
|| self.screen.scrolled()
|| self.conf.inputfd != STDIN_FILENO
{
self.layout_and_repaint_before_execution();
}
// Finish syntax highlighting (but do not wait forever).
if self.rls().finished {
self.finish_highlighting_before_exec();
}
// Finish syntax highlighting (but do not wait forever).
if self.rls().finished {
self.finish_highlighting_before_exec();
}
// Emit a newline so that the output is on the line after the command.
// But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826.
if !self.screen.cursor_is_wrapped_to_own_line() {
let _ = write_to_fd(b"\n", STDOUT_FILENO);
}
// Emit a newline so that the output is on the line after the command.
// But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826.
if !self.screen.cursor_is_wrapped_to_own_line() {
let _ = write_to_fd(b"\n", STDOUT_FILENO);
}
// HACK: If stdin isn't the same terminal as stdout, we just moved the cursor.
// For now, just reset it to the beginning of the line.
if self.conf.inputfd != STDIN_FILENO {
let _ = write_loop(&STDOUT_FILENO, b"\r");
}
// HACK: If stdin isn't the same terminal as stdout, we just moved the cursor.
// For now, just reset it to the beginning of the line.
if self.conf.inputfd != STDIN_FILENO {
let _ = write_loop(&STDOUT_FILENO, b"\r");
}
// Ensure we have no pager contents when we exit.
if !self.pager.is_empty() {
// Clear to end of screen to erase the pager contents.
screen_force_clear_to_end();
self.clear_pager();
// Ensure we have no pager contents when we exit.
if !self.pager.is_empty() {
// Clear to end of screen to erase the pager contents.
screen_force_clear_to_end();
self.clear_pager();
}
}
if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ {
@@ -2430,10 +2441,12 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
});
// If we ran `exit` anywhere, exit.
self.exit_loop_requested =
self.exit_loop_requested || self.parser.libdata().exit_current_script;
self.exit_loop_requested |= self.parser.libdata().exit_current_script;
self.parser.libdata_mut().exit_current_script = false;
if self.exit_loop_requested {
if !self.conf.prompt_ok {
return ControlFlow::Break(());
}
return ControlFlow::Continue(());
}
@@ -2507,6 +2520,7 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
ImplicitEvent::Eof => {
reader_sighup();
}
ImplicitEvent::Break => return ControlFlow::Break(()),
ImplicitEvent::CheckExit => (),
ImplicitEvent::FocusIn => {
event::fire_generic(self.parser, L!("fish_focus_in").to_owned(), vec![]);
@@ -2531,16 +2545,31 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
CharEvent::QueryResponse(query_response) => {
match query_response {
QueryResponseEvent::PrimaryDeviceAttribute => {
if *self.blocking_query() != Some(TerminalQuery::PrimaryDeviceAttribute) {
let mut query = self.blocking_query();
let Some(TerminalQuery::PrimaryDeviceAttribute(tcap_query)) = &*query
else {
// Rogue reply.
return ControlFlow::Continue(());
}
};
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed)
== Capability::Unknown as _
{
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
}
assert!(
tcap_query.is_none()
|| self.parser.pending_user_query.borrow().is_none()
);
if tcap_query.is_some() {
stop_query(query);
// TODO hack
return ControlFlow::Break(());
}
if let Some(queued_query) = self.parser.pending_user_query.take() {
// TODO hack
return (queued_query)(&mut query);
}
}
QueryResponseEvent::CursorPositionReport(cursor_pos) => {
let cursor_pos_query = match &*self.blocking_query() {
@@ -2567,29 +2596,22 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
}
}
fn send_xtgettcap_query(out: &mut impl Output, cap: &'static str) {
pub(crate) fn query_xtgettcap(out: &mut impl Output, cap: &[u8]) {
let command = QueryXtgettcap(cap);
if should_flog!(reader) {
let mut tmp = Vec::<u8>::new();
tmp.write_command(QueryXtgettcap(cap));
tmp.write_command(command.clone());
FLOG!(
reader,
format!("Sending XTGETTCAP request for {}: {:?}", cap, tmp)
sprintf!(
"Sending XTGETTCAP request for %s: %s",
str2wcstring(cap),
escape(&str2wcstring(&tmp))
)
);
}
out.write_command(QueryXtgettcap(cap));
}
fn query_capabilities_via_dcs(out: &mut impl Output, vars: &dyn Environment) {
if vars.get_unless_empty(L!("STY")).is_some()
|| vars.get_unless_empty(L!("TERM")).is_some_and(|term| {
let term = &term.as_list()[0];
term == "screen" || term == "screen-256color"
})
{
return;
}
out.write_command(DecsetAlternateScreenBuffer); // enable alternative screen buffer
send_xtgettcap_query(out, SCROLL_FORWARD_TERMINFO_CODE);
out.write_command(command);
out.write_command(DecrstAlternateScreenBuffer); // disable alternative screen buffer
}
@@ -2688,7 +2710,9 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
let mut outp = Outputter::stdoutput().borrow_mut();
if let Some(fish_color_cancel) = self.vars().get(L!("fish_color_cancel")) {
outp.set_text_face(parse_text_face_for_highlight(&fish_color_cancel));
outp.set_text_face(
parse_text_face_for_highlight(&fish_color_cancel).unwrap_or_default(),
);
}
outp.write_wstr(L!("^C"));
outp.reset_text_face();
@@ -3815,9 +3839,6 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
self.clear_screen_and_repaint();
}
rl::ScrollbackPush => {
if !SCROLL_FORWARD_SUPPORTED.load() {
return;
}
let query = self.blocking_query();
let Some(query) = &*query else {
drop(query);
@@ -3828,7 +3849,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
return;
};
match query {
TerminalQuery::PrimaryDeviceAttribute => panic!(),
TerminalQuery::PrimaryDeviceAttribute(_) => panic!(),
TerminalQuery::CursorPositionReport(_) => {
// TODO: re-queue it I guess.
FLOG!(
@@ -4449,8 +4470,6 @@ fn reader_interactive_init(parser: &Parser) {
parser
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned());
terminal_protocol_hacks();
}
/// Destroy data for interactive use.
@@ -4528,6 +4547,7 @@ pub fn reader_write_title(
impl<'a> Reader<'a> {
fn exec_prompt_cmd(&self, prompt_cmd: &wstr, final_prompt: bool) -> Vec<WString> {
assert!(self.conf.prompt_ok);
let mut output = vec![];
let prompt_cmd = if final_prompt && function::exists(prompt_cmd, self.parser) {
Cow::Owned(prompt_cmd.to_owned() + L!(" --final-rendering"))
@@ -4540,6 +4560,7 @@ fn exec_prompt_cmd(&self, prompt_cmd: &wstr, final_prompt: bool) -> Vec<WString>
/// Execute prompt commands based on the provided arguments. The output is inserted into prompt_buff.
fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
assert!(self.conf.prompt_ok);
// Suppress fish_trace while in the prompt.
let _suppress_trace = self.parser.push_scope(|s| s.suppress_fish_trace = true);

View File

@@ -3,13 +3,13 @@
use crate::common::ToCString;
use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle};
use crate::future_feature_flags::{self, FeatureFlag};
use crate::global_safety::RelaxedAtomicBool;
use crate::screen::{is_dumb, only_grayscale};
use crate::text_face::{TextFace, TextStyling, UnderlineStyle};
use crate::threads::MainThread;
use crate::wchar::prelude::*;
use crate::FLOGF;
use bitflags::bitflags;
use once_cell::sync::OnceCell;
use std::cell::{RefCell, RefMut};
use std::env;
use std::ffi::{CStr, CString};
@@ -83,7 +83,7 @@ pub(crate) enum TerminalCommand<'a> {
// Commands related to querying (used for backwards-incompatible features).
QueryPrimaryDeviceAttribute,
QueryXtversion,
QueryXtgettcap(&'static str),
QueryXtgettcap(&'a [u8]),
DecsetAlternateScreenBuffer,
DecrstAlternateScreenBuffer,
@@ -227,8 +227,7 @@ pub(crate) enum Capability {
pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static SCROLL_FORWARD_TERMINFO_CODE: &str = "indn";
pub(crate) static XTVERSION: OnceCell<WString> = OnceCell::new();
pub(crate) fn use_terminfo() -> bool {
!future_feature_flags::test(FeatureFlag::ignore_terminfo) && TERM.lock().unwrap().is_some()
@@ -349,16 +348,16 @@ fn cursor_move(out: &mut impl Output, direction: CardinalDirection, steps: usize
true
}
fn query_xtgettcap(out: &mut impl Output, cap: &str) -> bool {
fn query_xtgettcap(out: &mut impl Output, cap: &[u8]) -> bool {
write_to_output!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap));
true
}
struct DisplayAsHex<'a>(&'a str);
struct DisplayAsHex<'a>(&'a [u8]);
impl<'a> std::fmt::Display for DisplayAsHex<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in self.0.bytes() {
for byte in self.0 {
write!(f, "{:x}", byte)?;
}
Ok(())
@@ -399,7 +398,6 @@ fn osc_133_command_finished(out: &mut impl Output, exit_status: libc::c_int) ->
}
fn scroll_forward(out: &mut impl Output, lines: usize) -> bool {
assert!(SCROLL_FORWARD_SUPPORTED.load());
write_to_output!(out, "\x1b[{}S", lines);
true
}

View File

@@ -111,6 +111,12 @@ pub(crate) struct TextFace {
pub(crate) style: TextStyling,
}
impl Default for TextFace {
fn default() -> Self {
Self::default()
}
}
impl TextFace {
pub const fn default() -> Self {
Self {
@@ -131,12 +137,12 @@ pub fn new(fg: Color, bg: Color, underline_color: Color, style: TextStyling) ->
}
}
#[derive(Default)]
#[derive(Default, Eq, PartialEq)]
pub(crate) struct SpecifiedTextFace {
pub(crate) fg: Option<Color>,
pub(crate) bg: Option<Color>,
pub(crate) underline_color: Option<Color>,
pub(crate) style: TextStyling,
pub(crate) style: Option<TextStyling>,
}
pub(crate) fn parse_text_face(arguments: &[WString]) -> SpecifiedTextFace {
@@ -167,6 +173,7 @@ pub(crate) enum ParsedArgs<'argarray, 'args> {
pub(crate) enum ParseError<'args> {
MissingOptArg,
MultipleTracking,
UnknownColor(&'args wstr),
UnknownUnderlineStyle(&'args wstr),
UnknownOption(usize),
@@ -189,6 +196,7 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
wopt(L!("reverse"), ArgType::NoArgument, 'r'),
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("print-colors"), ArgType::NoArgument, 'c'),
wopt(L!("track"), ArgType::RequiredArgument, '\x01'),
];
let long_options = &long_options[..long_options.len() - builtin_extra_args];
@@ -210,6 +218,7 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
let mut underline_colors = vec![];
let mut style = TextStyling::default();
let mut print_color_mode = false;
let mut tracking = false;
let mut w = WGetopter::new(short_options, long_options, argv);
while let Some(c) = w.next_opt() {
@@ -219,6 +228,12 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
bg_colors.push(bg);
}
}
'\x01' => {
if is_builtin && tracking {
return Err(MultipleTracking);
}
tracking = true;
}
'\x02' => {
if let Some(underline_color) = parse_color(w.woptarg.unwrap())? {
underline_colors.push(underline_color);
@@ -297,6 +312,6 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
fg,
bg,
underline_color,
style,
style: (style != TextStyling::default()).then_some(style),
}))
}

View File

@@ -0,0 +1,51 @@
#RUN: %fish %s
mkdir $__fish_config_dir/themes
echo >$__fish_config_dir/themes/foo.theme '
fish_color_normal cyan
fish_color_error brred --underline=curly
'
set -g fish_pager_color_secondary_background custom-value
fish_config theme choose foo
set -S fish_color_normal
# CHECK: $fish_color_normal: set in global scope, unexported, with 1 elements
# CHECK: $fish_color_normal[1]: |cyan|
set -S fish_pager_color_secondary_background
# CHECK: $fish_pager_color_secondary_background: set in global scope, unexported, with 0 elements
# Not a default theme, so we allow --underline=curly even if we don't run inside a terminal that
# advertises support via XTGETTCAP.
set -S fish_color_error
# CHECK: $fish_color_error: set in global scope, unexported, with 2 elements
# CHECK: $fish_color_error[1]: |brred|
# CHECK: $fish_color_error[2]: |--underline=curly|
function change-theme
echo >$__fish_config_dir/themes/fake-default.theme 'fish_color_command' $argv
end
change-theme 'green'
echo yes | fish_config theme save --track fake-default
echo $fish_color_command
# CHECK: green --track=fake-default
change-theme 'green --bold'
fish_config theme update
echo $fish_color_command
# CHECK: green --bold --track=fake-default
# Test that we silently update when there is a shadowing global.
change-theme 'green --italics'
set -g fish_color_command normal
fish_config theme update
set -S fish_color_command
# CHECK: $fish_color_command: set in global scope, unexported, with 1 elements
# CHECK: $fish_color_command[1]: |normal|
# CHECK: $fish_color_command: set in universal scope, unexported, with 3 elements
# CHECK: $fish_color_command[1]: |green|
# CHECK: $fish_color_command[2]: |--italics|
# CHECK: $fish_color_command[3]: |--track=fake-default|

View File

@@ -180,7 +180,10 @@ class SpawnedProc(object):
self.spawn.delaybeforesend = None
self.prompt_counter = 0
if env.get("TERM") != "dumb":
self.spawn.send("\x1b[?123c") # Primary Device Attribute
self.send_primary_device_attribute()
def send_primary_device_attribute(self):
self.spawn.send("\x1b[?123c")
def time_since_first_message(self):
"""Return a delta in seconds since the first message, or 0 if this is the first."""

32
tests/pexpects/query.py Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
import os
env = os.environ.copy()
env["TERM"] = "foot"
sp = SpawnedProc(env=env)
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
sp.send,
sp.sendline,
sp.sleep,
sp.expect_prompt,
sp.expect_re,
sp.expect_str,
)
expect_prompt()
sendline("function check; and echo true; or echo false; end")
sendline("status xtgettcap am; check")
expect_str("\x1bP+q616d\x1b\\") # 616d is "am" in hex
send("\x1bP1+r616d\x1b\\") # success
sp.send_primary_device_attribute()
expect_str("true")
sendline("status xtgettcap an; check")
expect_str("\x1bP+q616e\x1b\\") # 616e is "an" in hex
send("\x1bP0+r616e\x1b\\") # failure
sp.send_primary_device_attribute()
expect_str("false")