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`). - 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`). - 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. - 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 Other improvements
------------------ ------------------

View File

@@ -10,7 +10,7 @@ Synopsis
fish_config [browse] fish_config [browse]
fish_config prompt (choose | list | save | show) 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 Description
----------- -----------
@@ -40,6 +40,9 @@ Available subcommands for the ``theme`` command:
- ``list`` lists the names of the available sample themes. - ``list`` lists the names of the available sample themes.
- ``save`` saves the given theme to :ref:`universal variables <variables-universal>`. - ``save`` saves the given theme to :ref:`universal variables <variables-universal>`.
- ``show`` shows what the given sample theme (or all) would look like. - ``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. 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** **-u** or **--underline**, or **-uSTYLE** or **--underline=STYLE**
Set the underline mode; supported styles are **single** (default) and **curly**. 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** **-h** or **--help**
Displays help about using this command. Displays help about using this command.

View File

@@ -33,6 +33,8 @@ Synopsis
status buildinfo status buildinfo
status get-file FILE status get-file FILE
status list-files [PATH] status list-files [PATH]
status xtgettcap TERMINFO-CAPABILITY
status xtversion
Description 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. 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. 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 Notes
----- -----

View File

@@ -177,7 +177,9 @@ Optional Commands
- Su - Su
- Reset underline color to the default (follow the foreground color). - Reset underline color to the default (follow the foreground color).
- kitty - kitty
* - ``\e[ Ps S`` * - .. _indn:
``\e[ Ps S``
- indn - indn
- Scroll forward Ps lines. - Scroll forward Ps lines.
- -
@@ -269,6 +271,12 @@ Optional Commands
- FinalTerm - FinalTerm
* - ``\eP+q Pt \e\\`` * - ``\eP+q Pt \e\\``
- -
- Request terminfo capability (XTGETTCAP). The parameter is the capability's hex-encoded terminfo code. - Request terminfo capability (XTGETTCAP).
Specifically, fish asks for the ``indn`` string capability. At the time of writing string capabilities are supported by kitty and foot. The parameter is the capability's hex-encoded terminfo code.
- XTerm, kitty, foot 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 complete fish_config -f
set -l prompt_commands choose save show list 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_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" \ 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' -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_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 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" \ 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' -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" \ 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' -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" \ 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' -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 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 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 -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 \ list-files \
print-stack-trace \ print-stack-trace \
stack-trace \ stack-trace \
test-feature test-feature \
xtgettcap \
xtversion
# These are the recognized flags. # These are the recognized flags.
complete -c status -s h -l help -d "Display help and exit" 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 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 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 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. # 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" 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 # Commands to run in interactive sessions can go here
end" >$__fish_config_dir/config.fish 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 set -Ue fish_color_keyword fish_color_option
end end
if test $__fish_initialized -lt 3800 && test "$fish_color_search_match[1]" = bryellow if test $__fish_initialized -lt 3800 && test "$fish_color_search_match[1]" = bryellow
set --universal fish_color_search_match[1] white set --universal fish_color_search_match[1] white
end end
fish_config theme update
# #
# Generate man page completions if not present. # 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-l __fish_list_current_token
bind --preset $argv alt-o __fish_preview_current_file bind --preset $argv alt-o __fish_preview_current_file
bind --preset $argv alt-w __fish_whatis_current_token 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-c clear-commandline
bind --preset $argv ctrl-u backward-kill-line bind --preset $argv ctrl-u backward-kill-line
bind --preset $argv ctrl-k 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" # Variables a theme is allowed to set
argparse h/help -- $argv 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 or return
if set -q _flag_help 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 -l cmd $argv[1]
set -e 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] set -q cmd[1]
or set cmd browse or set cmd browse
@@ -65,9 +80,6 @@ function fish_config --description "Launch fish's web based configuration"
return 1 return 1
end end
# Variables a theme is allowed to set
set -l theme_var_filter '^fish_(?:pager_)?color.*$'
switch $cmd switch $cmd
case prompt case prompt
# prompt - for prompt switching # prompt - for prompt switching
@@ -144,8 +156,8 @@ function fish_config --description "Launch fish's web based configuration"
functions --erase fish_right_prompt functions --erase fish_right_prompt
end end
case save case save
read -P"Overwrite prompt? [y/N]" -l yesno if set -q _flag_yes[1] ||
if string match -riq 'y(es)?' -- $yesno { read -P"Overwrite prompt? [y/N]" -l yesno; string match -riq 'y(es)?' -- $yesno }
echo Overwriting echo Overwriting
# Skip the cp if unnecessary, # Skip the cp if unnecessary,
# or we'd throw an error on a stock fish. # 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 set -l have_colors
if contains -- $cmd save if contains -- $cmd save
read -P"Overwrite your current theme? [y/N] " -l yesno if not set -q _flag_yes &&
if not string match -riq 'y(es)?' -- $yesno { read -P"Overwrite your current theme? [y/N] " -l yesno
not string match -riq 'y(es)?' -- $yesno
}
echo Not overwriting >&2 echo Not overwriting >&2
return 1 return 1
end 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. # 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`). # Otherwise, we'll persist the currently loaded/themed variables (in case of `theme save`).
if set -q argv[1] if set -q argv[1]
set -l files $dirs/$argv[1].theme __fish_config_theme_get $argv[1] | while read -lat toks
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
# The whitelist allows only color variables. # The whitelist allows only color variables.
# Not the specific list, but something named *like* a color variable. # Not the specific list, but something named *like* a color variable.
# This also takes care of empty lines and comment lines. # 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] if test x"$scope" = x-U; and set -qg $toks[1]
set -eg $toks[1] set -eg $toks[1]
end end
set $scope $toks set $scope $toks $_flag_track=$argv[1]
set -a have_colors $toks[1] set -a have_colors $toks[1]
end 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 # Erase conflicting global variables so we don't get a warning and
# so changes are observed immediately. # so changes are observed immediately.
set -eg $c set -eg $c
set $scope $c set $scope $c $_flag_track=$argv[1]
end end
else else
# We're persisting whatever current colors are loaded (maybe in the global scope) # 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 # 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. # state (if any). In all cases we haven't failed, so return 0.
return 0 return 0
case update
__fish_config_theme_update $argv
case dump case dump
# Write the current theme in .theme format, to stdout. # Write the current theme in .theme format, to stdout.
set -L | string match -r $theme_var_filter 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 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 green
fish_color_cwd_root red fish_color_cwd_root red
fish_color_end ba8baf fish_color_end ba8baf
fish_color_error ab4642 fish_color_error ab4642 --underline=curly
fish_color_escape 86c1b9 fish_color_escape 86c1b9
fish_color_history_current --bold fish_color_history_current --bold
fish_color_host normal fish_color_host normal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@
use crate::wutil::encoding::zero_mbstate; use crate::wutil::encoding::zero_mbstate;
use crate::wutil::perror; use crate::wutil::perror;
use libc::SEEK_CUR; use libc::SEEK_CUR;
use std::num::NonZeroUsize;
use std::os::fd::RawFd; use std::os::fd::RawFd;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@@ -219,12 +220,14 @@ fn read_interactive(
conf.complete_ok = shell; conf.complete_ok = shell;
conf.highlight_ok = shell; conf.highlight_ok = shell;
conf.syntax_check_ok = shell; conf.syntax_check_ok = shell;
conf.prompt_ok = true;
// No autosuggestions or abbreviations in builtin_read. // No autosuggestions or abbreviations in builtin_read.
conf.autosuggest_ok = false; conf.autosuggest_ok = false;
conf.expand_abbrev_ok = false; conf.expand_abbrev_ok = false;
conf.exit_on_interrupt = true; conf.exit_on_interrupt = true;
conf.in_builtin_read = true;
conf.in_silent_mode = silent; conf.in_silent_mode = silent;
conf.left_prompt_cmd = prompt.to_owned(); conf.left_prompt_cmd = prompt.to_owned();
@@ -244,7 +247,7 @@ fn read_interactive(
let mline = { let mline = {
let _interactive = parser.push_scope(|s| s.is_interactive = true); 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(); terminal_protocols_disable_ifn();
if let Some(line) = mline { 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. // In future we change both to actually print an error.
return Err(STATUS_INVALID_ARGS); 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)) => { Err(UnknownColor(arg)) => {
streams streams
.err .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.fg.unwrap_or(Color::None),
specified_face.bg.unwrap_or(Color::None), specified_face.bg.unwrap_or(Color::None),
specified_face.underline_color.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() { 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 super::prelude::*;
use crate::common::{get_executable_path, str2wcstring, PROGRAM_NAME}; use crate::common::{get_executable_path, str2wcstring, PROGRAM_NAME};
use crate::future_feature_flags::{self as features, feature_test}; use crate::future_feature_flags::{self as features, feature_test};
@@ -11,6 +9,9 @@
use libc::F_OK; use libc::F_OK;
use nix::errno::Errno; use nix::errno::Errno;
use nix::NixPath; use nix::NixPath;
use std::os::unix::ffi::OsStrExt;
mod query;
macro_rules! str_enum { macro_rules! str_enum {
($name:ident, $(($val:ident, $str:expr)),* $(,)?) => { ($name:ident, $(($val:ident, $str:expr)),* $(,)?) => {
@@ -37,7 +38,7 @@ fn to_wstr(self) -> &'static wstr {
use StatusCmd::*; use StatusCmd::*;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum StatusCmd { pub(in crate::builtins::status) enum StatusCmd {
STATUS_CURRENT_CMD = 1, STATUS_CURRENT_CMD = 1,
STATUS_BASENAME, STATUS_BASENAME,
STATUS_DIRNAME, STATUS_DIRNAME,
@@ -62,6 +63,8 @@ enum StatusCmd {
STATUS_BUILDINFO, STATUS_BUILDINFO,
STATUS_GET_FILE, STATUS_GET_FILE,
STATUS_LIST_FILES, STATUS_LIST_FILES,
STATUS_XTGETTCAP,
STATUS_XTVERSION,
} }
str_enum!( str_enum!(
@@ -96,6 +99,8 @@ enum StatusCmd {
(STATUS_STACK_TRACE, "print-stack-trace"), (STATUS_STACK_TRACE, "print-stack-trace"),
(STATUS_STACK_TRACE, "stack-trace"), (STATUS_STACK_TRACE, "stack-trace"),
(STATUS_TEST_FEATURE, "test-feature"), (STATUS_TEST_FEATURE, "test-feature"),
(STATUS_XTGETTCAP, "xtgettcap"),
(STATUS_XTVERSION, "xtversion"),
); );
/// Values that may be returned from the test-feature option to status. /// 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); 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 => { ref s => {
if !args.is_empty() { if !args.is_empty() {
streams.err.append(wgettext_fmt!( streams.err.append(wgettext_fmt!(
@@ -714,7 +725,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
| STATUS_FEATURES | STATUS_FEATURES
| STATUS_TEST_FEATURE | STATUS_TEST_FEATURE
| STATUS_GET_FILE | STATUS_GET_FILE
| STATUS_LIST_FILES => { | STATUS_LIST_FILES
| STATUS_XTGETTCAP
| STATUS_XTVERSION => {
unreachable!("") 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::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file};
use crate::terminal::Outputter; 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::threads::assert_is_background_thread;
use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir};
use crate::wchar::{wstr, WString, L}; use crate::wchar::{wstr, WString, L};
@@ -140,12 +140,16 @@ pub(crate) fn resolve_spec_uncached(
vars: &dyn Environment, vars: &dyn Environment,
) -> TextFace { ) -> TextFace {
let resolve_role = |role| { let resolve_role = |role| {
vars.get_unless_empty(get_highlight_var_name(role)) for role in [role, get_fallback(role), HighlightRole::normal] {
.or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role)))) if let Some(face) = vars
.or_else(|| vars.get_unless_empty(get_highlight_var_name(HighlightRole::normal))) .get_unless_empty(get_highlight_var_name(role))
.as_ref() .as_ref()
.map(parse_text_face_for_highlight) .and_then(parse_text_face_for_highlight)
.unwrap_or_else(TextFace::default) {
return face;
}
}
TextFace::default()
}; };
let mut face = resolve_role(highlight.foreground); let mut face = resolve_role(highlight.foreground);
@@ -162,7 +166,8 @@ pub(crate) fn resolve_spec_uncached(
if highlight.valid_path { if highlight.valid_path {
if let Some(valid_path_var) = vars.get(L!("fish_color_valid_path")) { if let Some(valid_path_var) = vars.get(L!("fish_color_valid_path")) {
// Historical behavior is to not apply background. // 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 // 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. // to only override foreground if the valid path color has an explicit foreground.
if !valid_path_face.fg.is_normal() { 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. /// 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 face = parse_text_face(var.as_list());
let default = TextFace::default(); (face != SpecifiedTextFace::default()).then(|| {
let fg = face.fg.unwrap_or(default.fg); let default = TextFace::default();
let bg = face.bg.unwrap_or(default.bg); let fg = face.fg.unwrap_or(default.fg);
let underline_color = face.underline_color.unwrap_or(default.underline_color); let bg = face.bg.unwrap_or(default.bg);
let style = face.style; let underline_color = face.underline_color.unwrap_or(default.underline_color);
TextFace { let style = face.style.unwrap_or_default();
fg, TextFace {
bg, fg,
underline_color, bg,
style, underline_color,
} style,
}
})
} }
fn command_is_valid( fn command_is_valid(

View File

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

View File

@@ -14,7 +14,7 @@
}; };
use crate::fds::{open_dir, BEST_O_SEARCH}; use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool; 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::io::IoChain;
use crate::job_group::MaybeJobId; use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT}; use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@@ -25,6 +25,7 @@
use crate::parse_execution::{EndExecutionReason, ExecutionContext}; use crate::parse_execution::{EndExecutionReason, ExecutionContext};
use crate::parse_tree::{parse_source, LineCounter, ParsedSourceRef}; use crate::parse_tree::{parse_source, LineCounter, ParsedSourceRef};
use crate::proc::{job_reap, JobGroupRef, JobList, JobRef, Pid, ProcStatus}; 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::signal::{signal_check_cancel, signal_clear_cancel, Signal};
use crate::threads::assert_is_main_thread; use crate::threads::assert_is_main_thread;
use crate::util::get_time; use crate::util::get_time;
@@ -37,6 +38,7 @@
#[cfg(not(target_has_atomic = "64"))] #[cfg(not(target_has_atomic = "64"))]
use portable_atomic::AtomicU64; use portable_atomic::AtomicU64;
use std::cell::{Ref, RefCell, RefMut}; use std::cell::{Ref, RefCell, RefMut};
use std::collections::VecDeque;
use std::ffi::{CStr, OsStr}; use std::ffi::{CStr, OsStr};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@@ -442,6 +444,10 @@ pub struct Parser {
pub global_event_blocks: AtomicU64, pub global_event_blocks: AtomicU64,
pub blocking_query: OnceCell<RefCell<Option<TerminalQuery>>>, pub blocking_query: OnceCell<RefCell<Option<TerminalQuery>>>,
pub pending_user_query: RefCell<Option<UserQuery>>,
pub pending_keys: RefCell<VecDeque<CharEvent>>,
} }
impl Parser { impl Parser {
@@ -460,6 +466,8 @@ pub fn new(variables: Rc<EnvStack>, cancel_behavior: CancelBehavior) -> Parser {
profile_items: RefCell::default(), profile_items: RefCell::default(),
global_event_blocks: AtomicU64::new(0), global_event_blocks: AtomicU64::new(0),
blocking_query: OnceCell::new(), 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) { 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::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionQuery; use crate::input_common::CursorPositionQuery;
use crate::input_common::ImplicitEvent; use crate::input_common::ImplicitEvent;
use crate::input_common::InputEventQueuer;
use crate::input_common::QueryResponseEvent; use crate::input_common::QueryResponseEvent;
use crate::input_common::TerminalQuery; use crate::input_common::TerminalQuery;
use crate::input_common::IN_DVTM; use crate::input_common::IN_DVTM;
@@ -134,9 +135,7 @@
QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute, QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute,
QueryXtgettcap, QueryXtversion, QueryXtgettcap, QueryXtversion,
}; };
use crate::terminal::{ use crate::terminal::{Capability, KITTY_KEYBOARD_SUPPORTED};
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update}; use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update};
use crate::text_face::parse_text_face; use crate::text_face::parse_text_face;
use crate::text_face::TextFace; use crate::text_face::TextFace;
@@ -238,25 +237,25 @@ fn redirect_tty_after_sighup() {
pub(crate) fn initial_query( pub(crate) fn initial_query(
blocking_query: &OnceCell<RefCell<Option<TerminalQuery>>>, blocking_query: &OnceCell<RefCell<Option<TerminalQuery>>>,
out: &mut impl Output, out: &mut impl Output,
vars: Option<&dyn Environment>,
) { ) {
blocking_query.get_or_init(|| { blocking_query.get_or_init(|| {
let query = if is_dumb() || IN_MIDNIGHT_COMMANDER.load() || IN_DVTM.load() { let query = querying_allowed().then(|| {
None
} else {
// Query for kitty keyboard protocol support. // Query for kitty keyboard protocol support.
out.write_command(QueryKittyKeyboardProgressiveEnhancements); out.write_command(QueryKittyKeyboardProgressiveEnhancements);
out.write_command(QueryXtversion); out.write_command(QueryXtversion);
if let Some(vars) = vars {
query_capabilities_via_dcs(out.by_ref(), vars);
}
out.write_command(QueryPrimaryDeviceAttribute); out.write_command(QueryPrimaryDeviceAttribute);
Some(TerminalQuery::PrimaryDeviceAttribute) TerminalQuery::PrimaryDeviceAttribute(None)
}; });
RefCell::new(query) 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. /// The stack of current interactive reading contexts.
fn reader_data_stack() -> &'static mut Vec<Pin<Box<ReaderData>>> { fn reader_data_stack() -> &'static mut Vec<Pin<Box<ReaderData>>> {
struct ReaderDataStack(UnsafeCell<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() reader_data_stack()
.iter() .iter()
.rev() .rev()
.any(|reader| reader.conf.exit_on_interrupt) .any(|reader| reader.conf.in_builtin_read)
} }
/// Access the top level reader data. /// Access the top level reader data.
@@ -338,6 +337,9 @@ pub struct ReaderConfig {
/// Whether to allow autosuggestions. /// Whether to allow autosuggestions.
pub autosuggest_ok: bool, pub autosuggest_ok: bool,
/// Whether to show a prompt.
pub prompt_ok: bool,
/// Whether to reexecute prompt function before final rendering. /// Whether to reexecute prompt function before final rendering.
pub transient_prompt: bool, pub transient_prompt: bool,
@@ -347,6 +349,9 @@ pub struct ReaderConfig {
/// Whether to exit on interrupt (^C). /// Whether to exit on interrupt (^C).
pub exit_on_interrupt: bool, pub exit_on_interrupt: bool,
/// Whether we are in builtin read.
pub in_builtin_read: bool,
/// If set, do not show what is typed. /// If set, do not show what is typed.
pub in_silent_mode: bool, pub in_silent_mode: bool,
@@ -691,6 +696,7 @@ fn read_i(parser: &Parser) {
conf.syntax_check_ok = true; conf.syntax_check_ok = true;
conf.expand_abbrev_ok = true; conf.expand_abbrev_ok = true;
conf.autosuggest_ok = check_bool_var(parser.vars(), L!("fish_autosuggestion_enabled"), 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); conf.transient_prompt = check_bool_var(parser.vars(), L!("fish_transient_prompt"), false);
if parser.is_breakpoint() && function::exists(DEBUG_PROMPT_FUNCTION_NAME, parser) { 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); term_donate(/*quiet=*/ true);
} }
} }
terminal_protocol_hacks();
} }
pub fn reader_deinit(in_signal_handler: bool, restore_foreground_pgroup: bool) { 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 /// 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 /// than nchars if a single keypress resulted in multiple characters being inserted into the
/// commandline. /// commandline.
pub fn reader_readline(parser: &Parser, nchars: usize) -> Option<WString> { pub fn reader_readline(parser: &Parser, nchars: Option<NonZeroUsize>) -> Option<WString> {
let nchars = NonZeroUsize::try_from(nchars).ok();
let data = current_data().unwrap(); let data = current_data().unwrap();
let mut reader = Reader { parser, data }; let mut reader = Reader { parser, data };
reader.readline(nchars) reader.readline(nchars)
@@ -2202,73 +2209,77 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
initial_query( initial_query(
&self.parser.blocking_query, &self.parser.blocking_query,
&mut BufferedOutputter::new(Outputter::stdoutput()), &mut BufferedOutputter::new(Outputter::stdoutput()),
Some(self.parser.vars()),
); );
// HACK: Don't abandon line for the first prompt, because if self.conf.prompt_ok {
// if we're started with the terminal it might not have settled, // HACK: Don't abandon line for the first prompt, because
// so the width is quite likely to be in flight. // 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 // This means that `printf %s foo; fish` will overwrite the `foo`,
// appear constantly. // 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 { // I can't see a good way around this.
self.screen if !self.first_prompt {
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap()); 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)) { while !self.rls().finished && !check_exit_loop_maybe_warning(Some(self)) {
if self.handle_char_event(None).is_break() { if self.handle_char_event(None).is_break() {
break; break;
} }
} }
if self.conf.transient_prompt { if self.conf.prompt_ok {
self.exec_prompt(true, true); 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 // Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the
// user presses enter. // user presses enter.
if self.is_repaint_needed(None) if self.is_repaint_needed(None)
|| self.screen.scrolled() || self.screen.scrolled()
|| self.conf.inputfd != STDIN_FILENO || self.conf.inputfd != STDIN_FILENO
{ {
self.layout_and_repaint_before_execution(); self.layout_and_repaint_before_execution();
} }
// Finish syntax highlighting (but do not wait forever). // Finish syntax highlighting (but do not wait forever).
if self.rls().finished { if self.rls().finished {
self.finish_highlighting_before_exec(); self.finish_highlighting_before_exec();
} }
// Emit a newline so that the output is on the line after the command. // 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. // 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() { if !self.screen.cursor_is_wrapped_to_own_line() {
let _ = write_to_fd(b"\n", STDOUT_FILENO); let _ = write_to_fd(b"\n", STDOUT_FILENO);
} }
// HACK: If stdin isn't the same terminal as stdout, we just moved the cursor. // 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. // For now, just reset it to the beginning of the line.
if self.conf.inputfd != STDIN_FILENO { if self.conf.inputfd != STDIN_FILENO {
let _ = write_loop(&STDOUT_FILENO, b"\r"); let _ = write_loop(&STDOUT_FILENO, b"\r");
} }
// Ensure we have no pager contents when we exit. // Ensure we have no pager contents when we exit.
if !self.pager.is_empty() { if !self.pager.is_empty() {
// Clear to end of screen to erase the pager contents. // Clear to end of screen to erase the pager contents.
screen_force_clear_to_end(); screen_force_clear_to_end();
self.clear_pager(); self.clear_pager();
}
} }
if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ { 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. // 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; self.parser.libdata_mut().exit_current_script = false;
if self.exit_loop_requested { if self.exit_loop_requested {
if !self.conf.prompt_ok {
return ControlFlow::Break(());
}
return ControlFlow::Continue(()); return ControlFlow::Continue(());
} }
@@ -2507,6 +2520,7 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
ImplicitEvent::Eof => { ImplicitEvent::Eof => {
reader_sighup(); reader_sighup();
} }
ImplicitEvent::Break => return ControlFlow::Break(()),
ImplicitEvent::CheckExit => (), ImplicitEvent::CheckExit => (),
ImplicitEvent::FocusIn => { ImplicitEvent::FocusIn => {
event::fire_generic(self.parser, L!("fish_focus_in").to_owned(), vec![]); 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) => { CharEvent::QueryResponse(query_response) => {
match query_response { match query_response {
QueryResponseEvent::PrimaryDeviceAttribute => { 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. // Rogue reply.
return ControlFlow::Continue(()); return ControlFlow::Continue(());
} };
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed) if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed)
== Capability::Unknown as _ == Capability::Unknown as _
{ {
KITTY_KEYBOARD_SUPPORTED KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release); .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) => { QueryResponseEvent::CursorPositionReport(cursor_pos) => {
let cursor_pos_query = match &*self.blocking_query() { 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) { if should_flog!(reader) {
let mut tmp = Vec::<u8>::new(); let mut tmp = Vec::<u8>::new();
tmp.write_command(QueryXtgettcap(cap)); tmp.write_command(command.clone());
FLOG!( FLOG!(
reader, 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 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 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(); let mut outp = Outputter::stdoutput().borrow_mut();
if let Some(fish_color_cancel) = self.vars().get(L!("fish_color_cancel")) { 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.write_wstr(L!("^C"));
outp.reset_text_face(); outp.reset_text_face();
@@ -3815,9 +3839,6 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
self.clear_screen_and_repaint(); self.clear_screen_and_repaint();
} }
rl::ScrollbackPush => { rl::ScrollbackPush => {
if !SCROLL_FORWARD_SUPPORTED.load() {
return;
}
let query = self.blocking_query(); let query = self.blocking_query();
let Some(query) = &*query else { let Some(query) = &*query else {
drop(query); drop(query);
@@ -3828,7 +3849,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
return; return;
}; };
match query { match query {
TerminalQuery::PrimaryDeviceAttribute => panic!(), TerminalQuery::PrimaryDeviceAttribute(_) => panic!(),
TerminalQuery::CursorPositionReport(_) => { TerminalQuery::CursorPositionReport(_) => {
// TODO: re-queue it I guess. // TODO: re-queue it I guess.
FLOG!( FLOG!(
@@ -4449,8 +4470,6 @@ fn reader_interactive_init(parser: &Parser) {
parser parser
.vars() .vars()
.set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned()); .set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned());
terminal_protocol_hacks();
} }
/// Destroy data for interactive use. /// Destroy data for interactive use.
@@ -4528,6 +4547,7 @@ pub fn reader_write_title(
impl<'a> Reader<'a> { impl<'a> Reader<'a> {
fn exec_prompt_cmd(&self, prompt_cmd: &wstr, final_prompt: bool) -> Vec<WString> { fn exec_prompt_cmd(&self, prompt_cmd: &wstr, final_prompt: bool) -> Vec<WString> {
assert!(self.conf.prompt_ok);
let mut output = vec![]; let mut output = vec![];
let prompt_cmd = if final_prompt && function::exists(prompt_cmd, self.parser) { let prompt_cmd = if final_prompt && function::exists(prompt_cmd, self.parser) {
Cow::Owned(prompt_cmd.to_owned() + L!(" --final-rendering")) 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. /// 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) { fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
assert!(self.conf.prompt_ok);
// Suppress fish_trace while in the prompt. // Suppress fish_trace while in the prompt.
let _suppress_trace = self.parser.push_scope(|s| s.suppress_fish_trace = true); 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::ToCString;
use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle}; use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle};
use crate::future_feature_flags::{self, FeatureFlag}; use crate::future_feature_flags::{self, FeatureFlag};
use crate::global_safety::RelaxedAtomicBool;
use crate::screen::{is_dumb, only_grayscale}; use crate::screen::{is_dumb, only_grayscale};
use crate::text_face::{TextFace, TextStyling, UnderlineStyle}; use crate::text_face::{TextFace, TextStyling, UnderlineStyle};
use crate::threads::MainThread; use crate::threads::MainThread;
use crate::wchar::prelude::*; use crate::wchar::prelude::*;
use crate::FLOGF; use crate::FLOGF;
use bitflags::bitflags; use bitflags::bitflags;
use once_cell::sync::OnceCell;
use std::cell::{RefCell, RefMut}; use std::cell::{RefCell, RefMut};
use std::env; use std::env;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
@@ -83,7 +83,7 @@ pub(crate) enum TerminalCommand<'a> {
// Commands related to querying (used for backwards-incompatible features). // Commands related to querying (used for backwards-incompatible features).
QueryPrimaryDeviceAttribute, QueryPrimaryDeviceAttribute,
QueryXtversion, QueryXtversion,
QueryXtgettcap(&'static str), QueryXtgettcap(&'a [u8]),
DecsetAlternateScreenBuffer, DecsetAlternateScreenBuffer,
DecrstAlternateScreenBuffer, DecrstAlternateScreenBuffer,
@@ -227,8 +227,7 @@ pub(crate) enum Capability {
pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _); 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 XTVERSION: OnceCell<WString> = OnceCell::new();
pub(crate) static SCROLL_FORWARD_TERMINFO_CODE: &str = "indn";
pub(crate) fn use_terminfo() -> bool { pub(crate) fn use_terminfo() -> bool {
!future_feature_flags::test(FeatureFlag::ignore_terminfo) && TERM.lock().unwrap().is_some() !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 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)); write_to_output!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap));
true true
} }
struct DisplayAsHex<'a>(&'a str); struct DisplayAsHex<'a>(&'a [u8]);
impl<'a> std::fmt::Display for DisplayAsHex<'a> { impl<'a> std::fmt::Display for DisplayAsHex<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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)?; write!(f, "{:x}", byte)?;
} }
Ok(()) 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 { fn scroll_forward(out: &mut impl Output, lines: usize) -> bool {
assert!(SCROLL_FORWARD_SUPPORTED.load());
write_to_output!(out, "\x1b[{}S", lines); write_to_output!(out, "\x1b[{}S", lines);
true true
} }

View File

@@ -111,6 +111,12 @@ pub(crate) struct TextFace {
pub(crate) style: TextStyling, pub(crate) style: TextStyling,
} }
impl Default for TextFace {
fn default() -> Self {
Self::default()
}
}
impl TextFace { impl TextFace {
pub const fn default() -> Self { pub const fn default() -> Self {
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) struct SpecifiedTextFace {
pub(crate) fg: Option<Color>, pub(crate) fg: Option<Color>,
pub(crate) bg: Option<Color>, pub(crate) bg: Option<Color>,
pub(crate) underline_color: 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 { pub(crate) fn parse_text_face(arguments: &[WString]) -> SpecifiedTextFace {
@@ -167,6 +173,7 @@ pub(crate) enum ParsedArgs<'argarray, 'args> {
pub(crate) enum ParseError<'args> { pub(crate) enum ParseError<'args> {
MissingOptArg, MissingOptArg,
MultipleTracking,
UnknownColor(&'args wstr), UnknownColor(&'args wstr),
UnknownUnderlineStyle(&'args wstr), UnknownUnderlineStyle(&'args wstr),
UnknownOption(usize), UnknownOption(usize),
@@ -189,6 +196,7 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
wopt(L!("reverse"), ArgType::NoArgument, 'r'), wopt(L!("reverse"), ArgType::NoArgument, 'r'),
wopt(L!("help"), ArgType::NoArgument, 'h'), wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("print-colors"), ArgType::NoArgument, 'c'), wopt(L!("print-colors"), ArgType::NoArgument, 'c'),
wopt(L!("track"), ArgType::RequiredArgument, '\x01'),
]; ];
let long_options = &long_options[..long_options.len() - builtin_extra_args]; 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 underline_colors = vec![];
let mut style = TextStyling::default(); let mut style = TextStyling::default();
let mut print_color_mode = false; let mut print_color_mode = false;
let mut tracking = false;
let mut w = WGetopter::new(short_options, long_options, argv); let mut w = WGetopter::new(short_options, long_options, argv);
while let Some(c) = w.next_opt() { 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); bg_colors.push(bg);
} }
} }
'\x01' => {
if is_builtin && tracking {
return Err(MultipleTracking);
}
tracking = true;
}
'\x02' => { '\x02' => {
if let Some(underline_color) = parse_color(w.woptarg.unwrap())? { if let Some(underline_color) = parse_color(w.woptarg.unwrap())? {
underline_colors.push(underline_color); underline_colors.push(underline_color);
@@ -297,6 +312,6 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
fg, fg,
bg, bg,
underline_color, 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.spawn.delaybeforesend = None
self.prompt_counter = 0 self.prompt_counter = 0
if env.get("TERM") != "dumb": 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): def time_since_first_message(self):
"""Return a delta in seconds since the first message, or 0 if this is the first.""" """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")