From 17b4b39c8b461e30d612e61c86762f1226cbb04a Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 20 Mar 2025 22:02:38 +0100 Subject: [PATCH] Stop reading terminfo database Our use of the terminfo database in /usr/share/terminfo/$TERM is both 1. a way for users to configure app behavior in their terminal (by setting TERM, copying around and modifying terminfo files) 2. a way for terminal emulator developers to advertise support for backwards-incompatible features that are not otherwise easily observable. To 1: this is not ideal (it's very easy to break things). There's not many things that realistically need configuration; let's use shell variables instead. To 2: in practice, feature-probing via terminfo is often wrong. There's not many backwards-incompatible features that need this; for the ones that do we can still use terminfo capabilities but query the terminal via XTGETTCAP directly, skipping the file (which may not exist on the same system as the terminal). --- Get rid of terminfo. If anyone finds a $TERM where we need different behavior, we can hardcode that into fish. * Allow to override this with `fish_features=no-ignore-terminfo fish` Not sure if we should document this, since it's supposed to be removed soon, and if someone needs this (which we don't expect), we'd like to know. * This is supported on a best-effort basis; it doesn't match the previous behavior exactly. For simplicity of implementation, it will not change the fact that we now: * use parm_left_cursor (CSI Ps D) instead of cursor_left (CSI D) if terminfo claims the former is supported * no longer support eat_newline_glitch, which seems no longer present on today's ConEmu and ConHost * Tested as described in https://github.com/fish-shell/fish-shell/pull/11345#discussion_r2030121580 * add `man fish-terminal-compatibility` to state our assumptions. This could help terminal emulator developers. * assume `parm_up_cursor` is supported if the terminal supports XTGETTCAP * Extract all control sequences to src/terminal_command.rs. * Remove the "\x1b(B" prefix from EXIT_ATTRIBUTE_MODE. I doubt it's really needed. * assume it's generally okay to output 256 colors Things have improved since commit 3669805627 (Improve compatibility with 0-16 color terminals., 2016-07-21). Apparently almost every actively developed terminal supports it, including Terminal.app and GNU screen. * That is, we default `fish_term256` to true and keep it only as a way to opt out of the the full 256 palette (e.g. switching to the 16-color palette). * `TERM=xterm-16color` has the same opt-out effect. * `TERM` is generally ignored but add back basic compatiblity by turning off color for "ansi-m", "linux-m" and "xterm-mono"; these are probably not set accidentally. * Since `TERM` is (mostly) ignored, we don't need the magic "xterm" in tests. Unset it instead. * Note that our pexpect tests used a dumb terminal because: 1. it makes fish do a full redraw of the commandline everytime, making it easier to write assertions. 2. it disables all control sequences for colors, etc, which we usually don't want to test explicitly. I don't think TERM=dumb has any other use, so it would be better to print escape sequences unconditionally, and strip them in the test driver (leaving this for later, since it's a bit more involved). Closes #11344 Closes #11345 --- .github/ISSUE_TEMPLATE/bug.md | 2 +- CHANGELOG.rst | 8 +- README.rst | 2 - doc_src/cmds/fish_indent.rst | 2 +- doc_src/cmds/set_color.rst | 11 +- doc_src/conf.py | 1 + doc_src/faq.rst | 8 + doc_src/index.rst | 1 + doc_src/language.rst | 11 +- doc_src/terminal-compatibility.rst | 253 +++++ share/completions/help.fish | 1 + .../functions/__fish_config_interactive.fish | 3 + share/functions/__fish_print_commands.fish | 2 +- share/functions/fish_clipboard_copy.fish | 2 +- share/functions/help.fish | 2 + share/tools/web_config/webconfig.py | 56 +- src/bin/fish.rs | 1 + src/builtins/fish_key_reader.rs | 19 +- src/builtins/set_color.rs | 65 +- src/color.rs | 10 + src/common.rs | 2 +- src/env/environment.rs | 2 +- src/env_dispatch.rs | 155 +-- src/future_feature_flags.rs | 12 + src/highlight/highlight.rs | 2 +- src/input.rs | 2 +- src/input_common.rs | 68 +- src/io.rs | 8 + src/lib.rs | 1 - src/output.rs | 614 ------------ src/reader.rs | 158 ++- src/screen.rs | 264 ++--- src/terminal.rs | 907 ++++++++++++++++-- src/tests/env.rs | 2 +- tests/checks/read.fish | 2 - tests/checks/status.fish | 1 + tests/checks/string.fish | 4 +- tests/test_driver.py | 3 +- tests/test_functions/isolated-tmux-start.fish | 8 +- 39 files changed, 1483 insertions(+), 1192 deletions(-) create mode 100644 doc_src/terminal-compatibility.rst delete mode 100644 src/output.rs diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 3ee7798ab..bf0f3f8a4 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -12,7 +12,7 @@ Please tell us which fish version you are using by executing the following: fish --version echo $version -Please tell us which operating system and terminal you are using. The output of `uname -a` and `echo $TERM` may be helpful in this regard although other commands might be relevant in your specific situation. +Please tell us which operating system (output of `uname`) and terminal you are using. Please tell us if you tried fish without third-party customizations by executing this command and whether it affected the behavior you are reporting: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ca4ee5956..00b815bb2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Deprecations and removed features --------------------------------- - Tokens like ``{ echo, echo }`` in command position are no longer interpreted as brace expansion but as compound command. - Terminfo-style key names (``bind -k``) are no longer supported. They had been superseded by the native notation. +- fish no longer reads the terminfo database, so its behavior is no longer affected by the :envvar:`TERM` environment variable (:issue:`11344`). Scripting improvements ---------------------- @@ -19,7 +20,7 @@ Interactive improvements - Autosuggestions are now also provided in multi-line command lines. Like `ctrl-r`, autosuggestions operate only on the current line. - Autosuggestions used to not suggest multi-line commandlines from history; now autosuggestions include individual lines from multi-line command lines. - The history search now preserves ordering between :kbd:`ctrl-s` forward and :kbd:`ctrl-r` backward searches. -- Left mouse click now can select pager items. +- Left mouse click (as requested by `click_events `__) can now select pager items. - Instead of flashing all the text to the left of the cursor, fish now flashes the matched token during history token search, the completed token during completion (:issue:`11050`), the autosuggestion when deleting it, and the full command line in all other cases. - Pasted commands are now stripped of any ``$`` prefix. @@ -31,8 +32,8 @@ New or improved bindings - :kbd:`ctrl-z` (undo) after executing a command will restore the previous cursor position instead of placing the cursor at the end of the command line. - The OSC 133 prompt marking feature has learned about kitty's ``click_events=1`` flag, which allows moving fish's cursor by clicking. - :kbd:`ctrl-l` now pushes all text located above the prompt to the terminal's scrollback, before clearing and redrawing the screen (via a new special input function ``scrollback-push``). - This feature depends on the terminal advertising via XTGETTCAP support for the ``indn`` and ``cuu`` terminfo capabilities, - and on the terminal supporting Synchronized Output (which is used by fish to detect features). + This feature depends on the terminal advertising via XTGETTCAP support for the ``indn`` terminfo capability, + and on the terminal supporting Synchronized Output (which is currently used by fish to detect features). If any is missing, the binding falls back to ``clear-screen``. - Bindings using shift with non-ASCII letters (such as :kbd:`ctrl-shift-ä`) are now supported. If there is any modifier other than shift, this is the recommended notation (as opposed to :kbd:`ctrl-Ä`). @@ -42,6 +43,7 @@ Completions Improved terminal support ^^^^^^^^^^^^^^^^^^^^^^^^^ +- New documentation page `Terminal Compatibility `_ (also accessible via ``man fish-terminal-compatibility``) lists required and optional terminal control sequences used by fish. Other improvements ------------------ diff --git a/README.rst b/README.rst index d43f3f430..84b95c7d7 100644 --- a/README.rst +++ b/README.rst @@ -87,8 +87,6 @@ Dependencies Running fish requires: -- A terminfo database, typically from curses or ncurses (preinstalled on most \*nix systems) - this needs to be the directory tree format, not the "hashed" database. - If this is unavailable, fish uses an included xterm-256color definition. - some common \*nix system utilities (currently ``mktemp``), in addition to the basic POSIX utilities (``cat``, ``cut``, ``dirname``, ``file``, ``ls``, ``mkdir``, ``mkfifo``, ``rm``, ``sort``, ``tee``, ``tr``, diff --git a/doc_src/cmds/fish_indent.rst b/doc_src/cmds/fish_indent.rst index 77329214a..85f9bb772 100644 --- a/doc_src/cmds/fish_indent.rst +++ b/doc_src/cmds/fish_indent.rst @@ -38,7 +38,7 @@ The following options are available: Displays the current :program:`fish` version and then exits. **--ansi** - Colorizes the output using ANSI escape sequences, appropriate for the current :envvar:`TERM`, using the colors defined in the environment (such as :envvar:`fish_color_command`). + Colorizes the output using ANSI escape sequences using the colors defined in the environment (such as :envvar:`fish_color_command`). **--html** Outputs HTML, which supports syntax highlighting if the appropriate CSS is defined. The CSS class names are the same as the variable names, such as ``fish_color_command``. diff --git a/doc_src/cmds/set_color.rst b/doc_src/cmds/set_color.rst index 621df6cbf..ccda9b158 100644 --- a/doc_src/cmds/set_color.rst +++ b/doc_src/cmds/set_color.rst @@ -22,7 +22,7 @@ Valid colors include: The *br*- (as in 'bright') forms are full-brightness variants of the 8 standard-brightness colors on many terminals. **brblack** has higher brightness than **black** - towards gray. -An RGB value with three or six hex digits, such as A0FF33 or f2f can be used. Fish will choose the closest supported color. A three digit value is equivalent to specifying each digit twice; e.g., ``set_color 2BC`` is the same as ``set_color 22BBCC``. Hexadecimal RGB values can be in lower or uppercase. Depending on the capabilities of your terminal (and the level of support ``set_color`` has for it) the actual color may be approximated by a nearby matching reserved color name or ``set_color`` may not have an effect on color. +An RGB value with three or six hex digits, such as A0FF33 or f2f can be used. Fish will choose the closest supported color. A three digit value is equivalent to specifying each digit twice; e.g., ``set_color 2BC`` is the same as ``set_color 22BBCC``. Hexadecimal RGB values can be in lower or uppercase. Depending on the capabilities of your terminal (and the level of support ``set_color`` has for it) the actual color may be approximated by a nearby matching reserved color. A second color may be given as a desired fallback color. e.g. ``set_color 124212 brblue`` will instruct set_color to use *brblue* if a terminal is not capable of the exact shade of grey desired. This is very useful when an 8 or 16 color terminal might otherwise not use a color. @@ -81,15 +81,8 @@ Fish uses some heuristics to determine what colors a terminal supports to avoid In particular it will: -- Enable 256 colors if :envvar:`TERM` contains "xterm", except for known exceptions (like MacOS 10.6 Terminal.app) - Enable 24-bit ("true-color") even if the $TERM entry only reports 256 colors. This includes modern xterm, VTE-based terminals like Gnome Terminal, Konsole and iTerm2. -- Detect support for italics, dim, reverse and other modes. - -If terminfo reports 256 color support for a terminal, 256 color support will always be enabled. To force true-color support on or off, set :envvar:`fish_term24bit` to "1" for on and 0 for off - ``set -g fish_term24bit 1``. -To debug color palette problems, ``tput colors`` may be useful to see the number of colors in terminfo for a terminal. Fish launched as ``fish -d term_support`` will include diagnostic messages that indicate the color support mode in use. - -The ``set_color`` command uses the terminfo database to look up how to change terminal colors on whatever terminal is in use. Some systems have old and incomplete terminfo databases, and lack color information for terminals that support it. Fish assumes that all terminals can use the `ANSI X3.64 `_ escape sequences if the terminfo definition indicates a color below 16 is not supported. - +Fish launched as ``fish -d term_support`` will include diagnostic messages that indicate the color support mode in use. diff --git a/doc_src/conf.py b/doc_src/conf.py index d63f5300c..3bd1f577a 100644 --- a/doc_src/conf.py +++ b/doc_src/conf.py @@ -195,6 +195,7 @@ man_pages = [ ("language", "fish-language", "", [author], 1), ("interactive", "fish-interactive", "", [author], 1), ("relnotes", "fish-releasenotes", "", [author], 1), + ("terminal-compatibility", "fish-terminal-compatibility", "", [author], 1), ("completions", "fish-completions", "", [author], 1), ("prompt", "fish-prompt-tutorial", "", [author], 1), ( diff --git a/doc_src/faq.rst b/doc_src/faq.rst index 74bf65bd5..5109251c7 100644 --- a/doc_src/faq.rst +++ b/doc_src/faq.rst @@ -340,6 +340,14 @@ This also means that a few things are unsupportable: - Non-monospace fonts - there is *no way* for fish to figure out what width a specific character has as it has no influence on the terminal's font rendering. - Different widths for multiple ambiguous width characters - there is no way for fish to know which width you assign to each character. +.. _faq-terminal-compatibility: + +fish does not work in a specific terminal +----------------------------------------- + +The terminal might not meet all of :doc:`fish's requirements `. +Please report this to your terminal's and to fish's issue tracker. + .. _faq-uninstalling: Uninstalling fish diff --git a/doc_src/index.rst b/doc_src/index.rst index 4bf5ec17c..2209d8887 100644 --- a/doc_src/index.rst +++ b/doc_src/index.rst @@ -174,5 +174,6 @@ Other help pages prompt design relnotes + terminal-compatibility contributing license diff --git a/doc_src/language.rst b/doc_src/language.rst index 13dc3a93e..8694619c0 100644 --- a/doc_src/language.rst +++ b/doc_src/language.rst @@ -1571,8 +1571,7 @@ You can change the settings of fish by changing the values of certain variables. .. envvar:: fish_term256 - If this is set to 1, fish will assume the terminal understands 256 colors, and won't translate matching colors down to the 16 color palette. - This is usually autodetected. + If this is set to 0, fish will not output 256 colors, but translate colors down to the 16 color palette. .. envvar:: fish_ambiguous_width @@ -1725,12 +1724,6 @@ Fish also provides additional information through the values of certain environm the "generation" count of ``$status``. This will be incremented only when the previous command produced an explicit status. (For example, background jobs will not increment this). -.. ENVVAR:: TERM - - the type of the current terminal. When fish tries to determine how the terminal works - how many colors it supports, what sequences it sends for keys and other things - it looks at this variable and the corresponding information in the terminfo database (see ``man terminfo``). - - Note: Typically this should not be changed as the terminal sets it to the correct value. - .. ENVVAR:: USER the current username. This variable can be changed. @@ -2024,6 +2017,7 @@ You can see the current list of features via ``status features``:: ampersand-nobg-in-token on 3.4 & only backgrounds if followed by a separating character remove-percent-self off 4.0 %self is no longer expanded (use $fish_pid) test-require-arg off 4.0 builtin test requires an argument + ignore-terminfo on 4.1 do not look up $TERM in terminfo database Here is what they mean: @@ -2033,6 +2027,7 @@ Here is what they mean: - ``ampersand-nobg-in-token`` was introduced in fish 3.4 (and made the default in 3.5). It makes it so a ``&`` i no longer interpreted as the backgrounding operator in the middle of a token, so dealing with URLs becomes easier. Either put spaces or a semicolon after the ``&``. This is recommended formatting anyway, and ``fish_indent`` will have done it for you already. - ``remove-percent-self`` turns off the special ``%self`` expansion. It was introduced in 4.0. To get fish's pid, you can use the :envvar:`fish_pid` variable. - ``test-require-arg`` removes :doc:`builtin test `'s one-argument form (``test "string"``. It was introduced in 4.0. To test if a string is non-empty, use ``test -n "string"``. If disabled, any call to ``test`` that would change sends a :ref:`debug message ` of category "deprecated-test", so starting fish with ``fish --debug=deprecated-test`` can be used to find offending calls. +- ``ignore-terminfo`` disables lookup of $TERM in the terminfo database. Use ``no-ignore-terminfo`` to turn it back on. These changes are introduced off by default. They can be enabled on a per session basis:: diff --git a/doc_src/terminal-compatibility.rst b/doc_src/terminal-compatibility.rst new file mode 100644 index 000000000..140377f91 --- /dev/null +++ b/doc_src/terminal-compatibility.rst @@ -0,0 +1,253 @@ +Terminal Compatibility +====================== + +fish writes various control sequences to the terminal. +Some must be implemented to enable basic functionality, +while others enable optional features and may be ignored by the terminal. + +The terminal must be able to parse Control Sequence Introducer (CSI) commands, Operating System Commands (OSC) and optionally Device Control Strings (DCS). +These are defined by ECMA-48. +If a valid CSI, OSC or DCS sequence does not represent a command implemented by the terminal, the terminal must ignore it. + +Control sequences are denoted in a fish-like syntax. +Special characters other than ``\`` are not escaped. +Spaces are only added for readability and are not part of the sequence. +Placeholders are written as ```` for a number or ```` for an arbitrary printable string. + +**NOTE:** fish does not rely on your system's terminfo database. +In this document, terminfo (TI) codes are included for reference only. + +Required Commands +----------------- + +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Sequence + - TI + - Description + - Origin + * - ``\r`` + - n/a + - Move cursor to the beginning of the line + - + * - ``\n`` + - cud1 + - Move cursor down one line. + - + * - ``\e[ Ps A`` + - cuu + - Move cursor up Ps columns, or one column if no parameter. + - VT100 + * - ``\e[ Ps C`` + - cuf + - Move cursor to the right Ps columns, or one column if no parameter. + - VT100 + * - ``\x08`` + - cub1 + - Move cursor one column to the left. + - VT100 + * - ``\e[ Ps D`` + - cub + - Move cursor to the left Ps times. + - VT100 + * - ``\e[H`` + - cup + - Set cursor position (no parameters means: move to row 1, column 1). + - VT100 + * - ``\e[K`` + - el + - Clear to end of line. + - VT100 + * - ``\e[J`` + - ed + - Clear to the end of screen. + - VT100 + * - ``\e[2J`` + - clear + - Clear the screen. + - VT100 + * - ``\e[0c`` + - + - Request primary device attribute. + The terminal must respond with a CSI command that starts with the ``?`` parameter byte (so a sequence starting with ``\e[?``) and has ``c`` as final byte. + - VT100 + * - n/a + - am + - Soft wrap text at screen width. + - + * - n/a + - xenl + - Printing to the last column does not move the cursor to the next line. + Verify this by running ``printf %0"$COLUMNS"d 0; sleep 3`` + - + +Optional Commands +----------------- + +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Sequence + - TI + - Description + - Origin + * - ``\t`` + - it + - Move the cursor to the next tab stop (à 8 columns). + This is mainly relevant if your prompt includes tabs. + - + + * - ``\e[m`` + - sgr0 + - Turn off bold/dim/italic/underline/reverse attribute modes. + - + * - ``\e[1m`` + - bold + - Enter bold mode. + - + * - ``\e[2m`` + - dim + - Enter dim mode. + - + * - ``\e[3m`` + - sitm + - Enter italic mode. + - + * - ``\e[4m`` + - smul + - Enter underline mode. + - + * - ``\e[7m`` + - rev + - Enter reverse video mode (swap foreground and background colors). + - + * - ``\e[23m`` + - ritm + - Exit italic mode. + - + * - ``\e[24m`` + - rmul + - Exit underline mode. + - + * - ``\e[38;5; Ps m`` + - setaf + - Select foreground color Ps from the 256-color-palette. + - + * - ``\e[48;5; Ps m`` + - setab + - Select background color Ps from the 256-color-palette. + - + * - ``\e[ Ps m`` + - setaf + setab + - Select foreground/background color. This uses a color in the aforementioned 256-color-palette, based on the range that contains the parameter: + 30-37 maps to foreground 0-7, + 40-47 maps to background 0-7, + 90-97 maps to foreground 8-15 and + 100-107 maps to background 8-15. + - + * - ``\e[38;2; Ps ; Ps ; Ps m`` + - + - Select foreground color from 24-bit RGB colors. + - + * - ``\e[48;2; Ps ; Ps ; Ps m`` + - + - Select background color from 24-bit RGB colors. + - + * - ``\e[ Ps S`` + - indn + - Scroll forward Ps lines. + - + * - ``\e[= Ps u``, ``\e[? Ps u`` + - n/a + - Enable the kitty keyboard protocol. + - kitty + * - ``\e[6n`` + - n/a + - Request cursor position report. + - VT100 + * - ``\e[ Ps q`` + - n/a + - Request terminal name and version (XTVERSION). + - XTerm + * - ``\e[?25h`` + - cvvis + - Enable cursor visibility (DECTCEM). + - VT220 + * - ``\e[?1000l`` + - n/a + - Disable mouse reporting. + - XTerm + * - ``\e[?1004h`` + - f/a + - Enable focus reporting. + - + * - ``\e[?1004l`` + - n/a + - Disable focus reporting. + - + * - ``\e[?1049h`` + - n/a + - Enable alternate screen buffer. + - XTerm + * - ``\e[?1049l`` + - n/a + - Disable alternate screen buffer. + - XTerm + * - ``\e[?2004h`` + - + - Enable bracketed paste. + - XTerm + * - ``\e[?2004l`` + - + - Disable bracketed paste. + - XTerm + * - ``\e[?2026h`` + - + - Begin synchronized output. + - contour + * - ``\e[?2026l`` + - + - End synchronized output. + - contour + * - ``\e]0; Pt \x07`` + - ts + - Set window title (OSC 0). + - XTerm + * - ``\e]7;file:// Pt / Pt \x07`` + - + - Report working directory (OSC 7). + Since the terminal may be running on a different system than a (remote) shell, + the hostname (first parameter) will *not* be ``localhost``. + - iTerm2 + * - ``\e]8;; Pt \e\\`` + - + - Create a `hyperlink (OSC 8) `_. + This is used in fish's man pages. + - GNOME Terminal + * - ``\e]52;c; Pt \x07`` + - + - Copy to clipboard (OSC 52). + - XTerm + * - .. _click-events: + + ``\e]133;A; click_events=1\x07`` + - + - Mark prompt start (OSC 133), with kitty's ``click_events`` extension. + - FinalTerm, kitty + * - ``\e]133;C; cmdline_url= Pt \x07`` + - + - Mark command start (OSC 133), with kitty's ``cmdline_url`` extension whose parameter is the URL-encoded command line. + - FinalTerm, kitty + * - ``\e]133;D; Ps \x07`` + - + - Mark command end (OSC 133); Ps is the exit status. + - 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 diff --git a/share/completions/help.fish b/share/completions/help.fish index 9e937cf27..c5ecddf3c 100644 --- a/share/completions/help.fish +++ b/share/completions/help.fish @@ -101,6 +101,7 @@ complete -c help -x -a tut-why_fish -d 'Why fish?' complete -c help -x -a tut-wildcards -d Wildcards # Other pages +complete -c help -x -a terminal-compatibility -d "Terminal features used by fish" complete -c help -x -a releasenotes -d "Fish's release notes" complete -c help -x -a completions -d "How to write completions" complete -c help -x -a faq -d "Frequently Asked Questions" diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index f3117b426..cc97fe927 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -216,6 +216,9 @@ end" >$__fish_config_dir/config.fish if set -q KONSOLE_VERSION set host '' end + if [ "$TERM" = dumb ] + return + end printf \e\]7\;file://%s%s\a $host (string escape --style=url -- $PWD) end __fish_update_cwd_osc # Run once because we might have already inherited a PWD from an old tab diff --git a/share/functions/__fish_print_commands.fish b/share/functions/__fish_print_commands.fish index 19a393571..c4325cf89 100644 --- a/share/functions/__fish_print_commands.fish +++ b/share/functions/__fish_print_commands.fish @@ -3,7 +3,7 @@ function __fish_print_commands --description "Print a list of documented fish co for file in $__fish_data_dir/man/man1/**.1* string replace -r '.*/' '' -- $file | string replace -r '.1(.gz)?$' '' | - string match -rv '^fish-(?:changelog|completions|doc|tutorial|faq|for-bash-users|interactive|language|releasenotes)$' + string match -rv '^fish-(?:changelog|completions|doc|tutorial|faq|for-bash-users|interactive|language|releasenotes|terminal-compatibility)$' end end end diff --git a/share/functions/fish_clipboard_copy.fish b/share/functions/fish_clipboard_copy.fish index 50964d384..74b1cd74c 100644 --- a/share/functions/fish_clipboard_copy.fish +++ b/share/functions/fish_clipboard_copy.fish @@ -27,7 +27,7 @@ function fish_clipboard_copy # Copy with OSC 52; useful if we are running in an SSH session or in # a container. - if type -q base64 + if type -q base64 && [ "$TERM" != dumb ] if not isatty stdout echo "fish_clipboard_copy: stdout is not a terminal" >&2 return 1 diff --git a/share/functions/help.fish b/share/functions/help.fish index 27a8aa350..11647355a 100644 --- a/share/functions/help.fish +++ b/share/functions/help.fish @@ -147,6 +147,8 @@ function help --description 'Show help for the fish shell' set fish_help_page "tutorial.html" case releasenotes set fish_help_page relnotes.html + case terminal-compatiblity + set fish_help_page terminal-compatibility.html case completions set fish_help_page completions.html case commands diff --git a/share/tools/web_config/webconfig.py b/share/tools/web_config/webconfig.py index 366a030c3..f9f264231 100755 --- a/share/tools/web_config/webconfig.py +++ b/share/tools/web_config/webconfig.py @@ -44,6 +44,10 @@ except ImportError: import simplejson as json +ENTER_BOLD_MODE = "\x1b[1m" +EXIT_ATTRIBUTE_MODE = "\x1b[m" +ENTER_UNDERLINE_MODE = "\x1b[4m" + # Disable CLI web browsers term = os.environ.pop("TERM", None) # This import must be done with an empty $TERM, otherwise a command-line browser may be started @@ -567,48 +571,6 @@ def html_color_for_ansi_color_index(val): return arr[val] -# Function to return special ANSI escapes like exit_attribute_mode -g_special_escapes_dict = None - - -def get_special_ansi_escapes(): - global g_special_escapes_dict - if g_special_escapes_dict is None: - try: - import curses - - g_special_escapes_dict = {} - curses.setupterm() - - # Helper function to get a value for a tparm - def get_tparm(key): - val = None - key = curses.tigetstr(key) - if key: - val = curses.tparm(key) - if val: - val = val.decode("utf-8") - # Use an empty string instead of None. - return "" if val is None else val - - # Just a few for now - g_special_escapes_dict["exit_attribute_mode"] = get_tparm("sgr0") - g_special_escapes_dict["bold"] = get_tparm("bold") - g_special_escapes_dict["underline"] = get_tparm("smul") - except ImportError: - print("WARNING: The python curses module is missing.") - print("WARNING: Falling back to xterm-256color settings.") - print("WARNING: Rebuild python with curses headers!") - - g_special_escapes_dict = { - "exit_attribute_mode": "\x1b(B\x1b[m", - "bold": "\x1b[1m", - "underline": "\x1b[4m", - } - - return g_special_escapes_dict - - # Given a known ANSI escape sequence, convert it to HTML and append to the list # Returns whether we have an open @@ -653,8 +615,7 @@ def append_html_for_ansi_escape(full_val, result, span_open): return True # span now open # Try special escapes - special_escapes = get_special_ansi_escapes() - if full_val == special_escapes["exit_attribute_mode"]: + if full_val == EXIT_ATTRIBUTE_MODE: close_span() return False @@ -1533,16 +1494,15 @@ fileurl = "file://" + f.name if is_windows(): fileurl = fileurl.replace("\\", "/") -esc = get_special_ansi_escapes() print( "Web config started at %s%s%s" - % (esc["underline"], fileurl, esc["exit_attribute_mode"]) + % (ENTER_UNDERLINE_MODE, fileurl, EXIT_ATTRIBUTE_MODE) ) print( "If that doesn't work, try opening %s%s%s" - % (esc["underline"], url, esc["exit_attribute_mode"]) + % (ENTER_UNDERLINE_MODE, url, EXIT_ATTRIBUTE_MODE) ) -print("%sHit ENTER to stop.%s" % (esc["bold"], esc["exit_attribute_mode"])) +print("%sHit ENTER to stop.%s" % (ENTER_BOLD_MODE, EXIT_ATTRIBUTE_MODE)) def runThing(): diff --git a/src/bin/fish.rs b/src/bin/fish.rs index bca4e9958..76c7a3075 100644 --- a/src/bin/fish.rs +++ b/src/bin/fish.rs @@ -708,6 +708,7 @@ fn throwing_main() -> i32 { } } features::set_from_string(opts.features.as_utfstr()); + fish::env_dispatch::read_terminfo_database(EnvStack::globals()); proc_init(); fish::env::misc_init(); reader_init(true); diff --git a/src/builtins/fish_key_reader.rs b/src/builtins/fish_key_reader.rs index 7b55185a6..ffc0f251e 100644 --- a/src/builtins/fish_key_reader.rs +++ b/src/builtins/fish_key_reader.rs @@ -18,17 +18,21 @@ common::{shell_modes, str2wcstring, PROGRAM_NAME}, env::env_init, input_common::{ - kitty_progressive_enhancements_query, terminal_protocol_hacks, - terminal_protocols_enable_ifn, Capability, CharEvent, ImplicitEvent, InputEventQueue, - InputEventQueuer, KeyEvent, KITTY_KEYBOARD_SUPPORTED, + terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, ImplicitEvent, + InputEventQueue, InputEventQueuer, KeyEvent, }, key::{char_to_symbol, Key, Modifiers}, nix::isatty, panic::panic_handler, print_help::print_help, proc::set_interactive_session, - reader::{check_exit_loop_maybe_warning, reader_init, QUERY_PRIMARY_DEVICE_ATTRIBUTE}, + reader::{check_exit_loop_maybe_warning, reader_init}, signal::signal_set_handlers, + terminal::{ + Capability, Output, + TerminalCommand::{QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute}, + KITTY_KEYBOARD_SUPPORTED, + }, threads, topic_monitor::topic_monitor_init, wchar::prelude::*, @@ -151,12 +155,11 @@ fn setup_and_process_keys( // in fish-proper this is done once a command is run. unsafe { libc::tcsetattr(0, TCSANOW, &*shell_modes()) }; terminal_protocol_hacks(); + streams .out - .append(str2wcstring(kitty_progressive_enhancements_query())); - streams - .out - .append(str2wcstring(QUERY_PRIMARY_DEVICE_ATTRIBUTE)); + .write_command(QueryKittyKeyboardProgressiveEnhancements); + streams.out.write_command(QueryPrimaryDeviceAttribute); if continuous_mode { streams.err.append(L!("\n")); diff --git a/src/builtins/set_color.rs b/src/builtins/set_color.rs index 5d232c87f..4def1fab8 100644 --- a/src/builtins/set_color.rs +++ b/src/builtins/set_color.rs @@ -3,13 +3,15 @@ use super::prelude::*; use crate::color::RgbColor; use crate::common::str2wcstring; -use crate::output::{self, Outputter}; -use crate::terminal::{self, term, Term}; +use crate::terminal::TerminalCommand::{ + EnterBoldMode, EnterDimMode, EnterItalicsMode, EnterReverseMode, EnterStandoutMode, + EnterUnderlineMode, ExitAttributeMode, +}; +use crate::terminal::{best_color, get_color_support, Output, Outputter}; #[allow(clippy::too_many_arguments)] fn print_modifiers( outp: &mut Outputter, - term: &Term, bold: bool, underline: bool, italics: bool, @@ -17,40 +19,30 @@ fn print_modifiers( reverse: bool, bg: RgbColor, ) { - let Term { - enter_bold_mode, - enter_underline_mode, - enter_italics_mode, - enter_dim_mode, - enter_reverse_mode, - enter_standout_mode, - exit_attribute_mode, - .. - } = term; if bold { - outp.tputs_if_some(enter_bold_mode); + outp.write_command(EnterBoldMode); } if underline { - outp.tputs_if_some(enter_underline_mode); + outp.write_command(EnterUnderlineMode); } if italics { - outp.tputs_if_some(enter_italics_mode); + outp.write_command(EnterItalicsMode); } if dim { - outp.tputs_if_some(enter_dim_mode); + outp.write_command(EnterDimMode); } #[allow(clippy::collapsible_if)] if reverse { - if !outp.tputs_if_some(enter_reverse_mode) { - outp.tputs_if_some(enter_standout_mode); + if !outp.write_command(EnterReverseMode) { + outp.write_command(EnterStandoutMode); } } if !bg.is_none() && bg.is_normal() { - outp.tputs_if_some(exit_attribute_mode); + outp.write_command(ExitAttributeMode); } } @@ -65,7 +57,7 @@ fn print_colors( reverse: bool, bg: RgbColor, ) { - let outp = &mut output::Outputter::new_buffering(); + let outp = &mut Outputter::new_buffering(); // Rebind args to named_colors if there are no args. let named_colors; @@ -76,19 +68,9 @@ fn print_colors( &named_colors }; - let term = terminal::term(); for color_name in args { if streams.out_is_terminal() { - print_modifiers( - outp, - term.as_ref(), - bold, - underline, - italics, - dim, - reverse, - bg, - ); + print_modifiers(outp, bold, underline, italics, dim, reverse, bg); let color = RgbColor::from_wstr(color_name).unwrap_or(RgbColor::NONE); outp.set_color(color, RgbColor::NONE); if !bg.is_none() { @@ -99,7 +81,7 @@ fn print_colors( if !bg.is_none() { // If we have a background, stop it after the color // or it goes to the end of the line and looks ugly. - outp.tputs_if_some(&term.exit_attribute_mode); + outp.write_command(ExitAttributeMode); } outp.writech('\n'); } // conveniently, 'normal' is always the last color so we don't need to reset here @@ -217,25 +199,18 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - // #1323: We may have multiple foreground colors. Choose the best one. If we had no foreground // color, we'll get none(); if we have at least one we expect not-none. - let fg = output::best_color(&fgcolors, output::get_color_support()); + let fg = best_color(&fgcolors, get_color_support()); assert!(fgcolors.is_empty() || !fg.is_none()); - // Test if we have at least basic support for setting fonts, colors and related bits - otherwise - // just give up... - let term = term(); - let Some(exit_attribute_mode) = &term.exit_attribute_mode else { - return Err(STATUS_CMD_ERROR); - }; - - let outp = &mut output::Outputter::new_buffering(); - print_modifiers(outp, &term, bold, underline, italics, dim, reverse, bg); + let outp = &mut Outputter::new_buffering(); + print_modifiers(outp, bold, underline, italics, dim, reverse, bg); if bgcolor.is_some() && bg.is_normal() { - outp.tputs(exit_attribute_mode); + outp.write_command(ExitAttributeMode); } if !fg.is_none() { if fg.is_normal() || fg.is_reset() { - outp.tputs(exit_attribute_mode); + outp.write_command(ExitAttributeMode); } else if !outp.write_color(fg, true /* is_fg */) { // We need to do *something* or the lack of any output messes up // when the cartesian product here would make "foo" disappear: diff --git a/src/color.rs b/src/color.rs index 3012cc82b..b685296cb 100644 --- a/src/color.rs +++ b/src/color.rs @@ -177,6 +177,16 @@ pub fn set_reverse(&mut self, reverse: bool) { self.flags.set(Flags::REVERSE, reverse) } + pub fn is_grayscale(&self) -> bool { + match self.typ { + Type::None => true, + Type::Named { idx } => [0, 7, 8, 15, 16].contains(&idx) || (232..=255).contains(&idx), + Type::Rgb(rgb) => rgb.r == rgb.g && rgb.r == rgb.b, + Type::Normal => true, + Type::Reset => true, + } + } + /// Returns the name index for the given color. Requires that the color be named or RGB. pub fn to_name_index(self) -> u8 { // TODO: This should look for the nearest color. diff --git a/src/common.rs b/src/common.rs index 55cfbdb51..c1001c7ef 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,8 +10,8 @@ use crate::global_safety::RelaxedAtomicBool; use crate::key; use crate::libc::MB_CUR_MAX; -use crate::output::Output; use crate::parse_util::parse_util_escape_string_with_quote; +use crate::terminal::Output; use crate::termsize::Termsize; use crate::wchar::{decode_byte_from_char, encode_byte_to_char, prelude::*}; use crate::wcstringutil::wcs2string_callback; diff --git a/src/env/environment.rs b/src/env/environment.rs index 9f9fd469a..7b9a7ddd9 100644 --- a/src/env/environment.rs +++ b/src/env/environment.rs @@ -318,7 +318,7 @@ pub fn pop(&self) { assert!(self.can_push_pop, "push/pop not allowed on global stack"); let popped = self.lock().pop(); if self.dispatches_var_changes { - // TODO: we would like to coalesce locale / curses changes, so that we only re-initialize + // TODO: we would like to coalesce locale changes, so that we only re-initialize // once. for key in popped { env_dispatch_var_change(&key, self); diff --git a/src/env_dispatch.rs b/src/env_dispatch.rs index c8caac556..230c28950 100644 --- a/src/env_dispatch.rs +++ b/src/env_dispatch.rs @@ -1,21 +1,20 @@ -use crate::common::ToCString; use crate::complete::complete_invalidate_path; use crate::env::{setenv_lock, unsetenv_lock, EnvMode, EnvStack, Environment}; use crate::env::{DEFAULT_READ_BYTE_LIMIT, READ_BYTE_LIMIT}; use crate::flog::FLOG; -use crate::function; use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms}; -use crate::output::ColorSupport; -use crate::proc::is_interactive_session; use crate::reader::{ reader_change_cursor_end_mode, reader_change_cursor_selection_mode, reader_change_history, reader_schedule_prompt_repaint, reader_set_autosuggestion_enabled, }; -use crate::screen::screen_set_midnight_commander_hack; -use crate::screen::LAYOUT_CACHE_SHARED; -use crate::terminal::{self, Term}; +use crate::screen::{ + screen_set_midnight_commander_hack, IS_DUMB, LAYOUT_CACHE_SHARED, ONLY_GRAYSCALE, +}; +use crate::terminal::use_terminfo; +use crate::terminal::ColorSupport; use crate::wchar::prelude::*; use crate::wutil::fish_wcstoi; +use crate::{function, terminal}; use std::borrow::Cow; use std::collections::HashMap; use std::ffi::{CStr, CString}; @@ -305,6 +304,7 @@ fn handle_locale_change(vars: &EnvStack) { fn handle_term_change(vars: &EnvStack) { guess_emoji_width(vars); init_terminal(vars); + read_terminfo_database(vars); reader_schedule_prompt_repaint(); } @@ -379,43 +379,22 @@ fn update_fish_color_support(vars: &EnvStack) { // Detect or infer term256 support. If fish_term256 is set, we respect it. Otherwise, infer it // from $TERM or use terminfo. - let term = vars - .get(L!("TERM")) - .map(|v| v.as_string()) - .unwrap_or_else(WString::new); - let max_colors = terminal::term().max_colors; - let mut supports_256color = false; - let mut supports_24bit = false; + let term = vars.get_unless_empty(L!("TERM")); + let term = term.as_ref().map_or(L!(""), |term| &term.as_list()[0]); - if let Some(fish_term256) = vars.get(L!("fish_term256")).map(|v| v.as_string()) { - // $fish_term256 - supports_256color = crate::wcstringutil::bool_from_string(&fish_term256); + let supports_256color = if let Some(fish_term256) = vars.get(L!("fish_term256")) { + let ok = crate::wcstringutil::bool_from_string(&fish_term256.as_string()); FLOG!( term_support, "256-color support determined by $fish_term256:", - supports_256color + ok ); - } else if term.find(L!("256color")).is_some() { - // TERM contains "256color": 256 colors explicitly supported. - supports_256color = true; - FLOG!(term_support, "256-color support enabled for TERM", term); - } else if term.find(L!("xterm")).is_some() { - // Assume that all "xterm" terminals can handle 256 - supports_256color = true; - FLOG!(term_support, "256-color support enabled for TERM", term); - } - // See if terminfo happens to identify 256 colors - else if let Some(max_colors) = max_colors { - supports_256color = max_colors >= 256; - FLOG!( - term_support, - "256-color support:", - max_colors, - "per termcap/terminfo entry for", - term - ); - } + ok + } else { + term != "xterm-16color" + }; + let mut supports_24bit = false; if let Some(fish_term24bit) = vars.get(L!("fish_term24bit")).map(|v| v.as_string()) { // $fish_term24bit supports_24bit = crate::wcstringutil::bool_from_string(&fish_term24bit); @@ -436,18 +415,6 @@ fn update_fish_color_support(vars: &EnvStack) { term_support, "True-color support: disabled for eterm/screen" ); - } else if max_colors.unwrap_or(0) > 32767 { - // $TERM wins, xterm-direct reports 32767 colors and we assume that's the minimum as xterm - // is weird when it comes to color. - supports_24bit = true; - FLOG!( - term_support, - "True-color support: enabled per termcap/terminfo for", - term, - "with", - max_colors.unwrap(), - "colors" - ); } else if let Some(ct) = vars.get(L!("COLORTERM")).map(|v| v.as_string()) { // If someone sets $COLORTERM, that's the sort of color they want. if ct == "truecolor" || ct == "24bit" { @@ -495,52 +462,33 @@ fn update_fish_color_support(vars: &EnvStack) { let mut color_support = ColorSupport::default(); color_support.set(ColorSupport::TERM_256COLOR, supports_256color); color_support.set(ColorSupport::TERM_24BIT, supports_24bit); - crate::output::set_color_support(color_support); -} - -/// Apply any platform- or environment-specific hacks to our terminfo [`Term`] instance. -fn apply_term_hacks(vars: &EnvStack, term: &mut Term) { - if cfg!(apple) { - // Hack in missing italics and dim capabilities omitted from macOS xterm-256color terminfo. - // Improves the user experience under Terminal.app and iTerm. - let term_prog = vars - .get(L!("TERM_PROGRAM")) - .map(|v| v.as_string()) - .unwrap_or(WString::new()); - if term_prog == "Apple_Terminal" || term_prog == "iTerm.app" { - if let Some(term_val) = vars.get(L!("TERM")).map(|v| v.as_string()) { - if term_val == "xterm-256color" { - const SITM_ESC: &[u8] = b"\x1B[3m"; - const RITM_ESC: &[u8] = b"\x1B[23m"; - const DIM_ESC: &[u8] = b"\x1B[2m"; - - if term.enter_italics_mode.is_none() { - term.enter_italics_mode = Some(SITM_ESC.to_cstring()); - } - if term.exit_italics_mode.is_none() { - term.exit_italics_mode = Some(RITM_ESC.to_cstring()); - } - if term.enter_dim_mode.is_none() { - term.enter_dim_mode = Some(DIM_ESC.to_cstring()); - } - } - } - } - } -} - -/// Apply any platform- or environment-specific hacks that don't involve a `Term` instance. -fn apply_non_term_hacks(vars: &EnvStack) { - // Midnight Commander tries to extract the last line of the prompt, and does so in a way that is - // broken if you do '\r' after it like we normally do. - // See https://midnight-commander.org/ticket/4258. - if vars.get(L!("MC_SID")).is_some() { - screen_set_midnight_commander_hack(); - } + crate::terminal::set_color_support(color_support); } // Initialize the terminal subsystem fn init_terminal(vars: &EnvStack) { + let term = vars.get(L!("TERM")); + let term = term + .as_ref() + .and_then(|v| v.as_list().get(0)) + .map(|v| v.as_utfstr()) + .unwrap_or(L!("")); + + IS_DUMB.store(term == "dumb"); + ONLY_GRAYSCALE.store(term == "ansi-m" || term == "linux-m" || term == "xterm-mono"); + + if vars.get(L!("MC_SID")).is_some() { + screen_set_midnight_commander_hack(); + } + + update_fish_color_support(vars); +} + +pub fn read_terminfo_database(vars: &EnvStack) { + if !use_terminfo() { + return; + } + // The current process' environment needs to be modified because the terminfo crate will // read these variables for var_name in CURSES_VARIABLES { @@ -556,29 +504,8 @@ fn init_terminal(vars: &EnvStack) { } } - if terminal::setup(|term| apply_term_hacks(vars, term)).is_none() { - if is_interactive_session() { - let term = vars.get_unless_empty(L!("TERM")).map(|v| v.as_string()); - if let Some(term) = term { - FLOG!( - term_support, - wgettext_fmt!("Could not set up terminal for $TERM '%ls'. Falling back to hardcoded xterm-256color values", term) - ); - } else { - FLOG!( - term_support, - wgettext!("Could not set up terminal because $TERM is unset. Falling back to hardcoded xterm-256color values") - ); - } - } + terminal::setup(); - terminal::setup_fallback_term(); - } - - // Configure hacks that apply regardless of whether we successfully init - apply_non_term_hacks(vars); - - update_fish_color_support(vars); // Invalidate the cached escape sequences since they may no longer be valid. LAYOUT_CACHE_SHARED.lock().unwrap().clear(); } diff --git a/src/future_feature_flags.rs b/src/future_feature_flags.rs index 503da62bd..4804c7a55 100644 --- a/src/future_feature_flags.rs +++ b/src/future_feature_flags.rs @@ -27,6 +27,9 @@ pub enum FeatureFlag { /// Remove `test`'s one and zero arg mode (make `test -n` return false etc) test_require_arg, + + /// Do not look up $TERM in terminfo database. + ignore_terminfo, } struct Features { @@ -107,6 +110,14 @@ pub struct FeatureMetadata { default_value: false, read_only: false, }, + FeatureMetadata { + flag: FeatureFlag::ignore_terminfo, + name: L!("ignore-terminfo"), + groups: L!("4.1"), + description: L!("do not look up $TERM in terminfo database"), + default_value: true, + read_only: false, + }, ]; thread_local!( @@ -168,6 +179,7 @@ const fn new() -> Self { AtomicBool::new(METADATA[3].default_value), AtomicBool::new(METADATA[4].default_value), AtomicBool::new(METADATA[5].default_value), + AtomicBool::new(METADATA[6].default_value), ], } } diff --git a/src/highlight/highlight.rs b/src/highlight/highlight.rs index c7e04ceee..33092a4ae 100644 --- a/src/highlight/highlight.rs +++ b/src/highlight/highlight.rs @@ -20,7 +20,6 @@ use crate::highlight::file_tester::FileTester; use crate::history::{all_paths_are_valid, HistoryItem}; use crate::operation_context::OperationContext; -use crate::output::{parse_color, Outputter}; use crate::parse_constants::{ ParseKeyword, ParseTokenType, ParseTreeFlags, SourceRange, StatementDecoration, }; @@ -28,6 +27,7 @@ parse_util_locate_cmdsubst_range, parse_util_slice_length, MaybeParentheses, }; use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file}; +use crate::terminal::{parse_color, Outputter}; use crate::threads::assert_is_background_thread; use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; use crate::wchar::{wstr, WString, L}; diff --git a/src/input.rs b/src/input.rs index a66d7ed15..024ee0b8c 100644 --- a/src/input.rs +++ b/src/input.rs @@ -12,12 +12,12 @@ }; use crate::key::ViewportPosition; use crate::key::{self, canonicalize_raw_escapes, ctrl, Key, Modifiers}; -use crate::output::Outputter; use crate::proc::job_reap; use crate::reader::{ reader_reading_interrupted, reader_reset_interrupted, reader_schedule_prompt_repaint, Reader, }; use crate::signal::signal_clear_cancel; +use crate::terminal::Outputter; use crate::threads::{assert_is_main_thread, iothread_service_main}; use crate::wchar::prelude::*; use once_cell::sync::Lazy; diff --git a/src/input_common.rs b/src/input_common.rs index ed28bc7fa..4162b8312 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -1,8 +1,6 @@ -use libc::STDOUT_FILENO; - use crate::common::{ fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, - str2wcstring, write_loop, ScopeGuard, WSL, + str2wcstring, ScopeGuard, WSL, }; use crate::env::{EnvStack, Environment}; use crate::fd_readable_set::{FdReadableSet, Timeout}; @@ -14,6 +12,16 @@ function_key, shift, Key, Modifiers, ViewportPosition, }; use crate::reader::{reader_current_data, reader_test_and_clear_interrupted}; +use crate::terminal::TerminalCommand::{ + ApplicationKeypadModeDisable, ApplicationKeypadModeEnable, DecrstBracketedPaste, + DecrstFocusReporting, DecsetBracketedPaste, DecsetFocusReporting, + KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable, + ModifyOtherKeysDisable, ModifyOtherKeysEnable, +}; +use crate::terminal::{ + Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, + SCROLL_FORWARD_TERMINFO_CODE, SYNCHRONIZED_OUTPUT_SUPPORTED, +}; use crate::threads::{iothread_port, is_main_thread}; use crate::universal_notifier::default_notifier; use crate::wchar::{encode_byte_to_char, prelude::*}; @@ -24,7 +32,7 @@ use std::os::fd::RawFd; use std::os::unix::ffi::OsStrExt; use std::ptr; -use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Mutex, MutexGuard}; use std::time::Duration; @@ -579,26 +587,6 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) { static TERMINAL_PROTOCOLS: AtomicBool = AtomicBool::new(false); static BRACKETED_PASTE: AtomicBool = AtomicBool::new(false); -pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); -pub(crate) static CURSOR_UP_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); - -#[repr(u8)] -pub(crate) enum Capability { - Unknown, - Supported, - NotSupported, -} -pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _); - -pub(crate) static SYNCHRONIZED_OUTPUT_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); - -pub fn kitty_progressive_enhancements_query() -> &'static [u8] { - if std::env::var_os("TERM").is_some_and(|term| term.as_os_str().as_bytes() == b"st-256color") { - return b""; - } - b"\x1b[?u" -} - static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false); pub(crate) static IN_MIDNIGHT_COMMANDER: RelaxedAtomicBool = RelaxedAtomicBool::new(false); @@ -649,11 +637,12 @@ pub fn terminal_protocols_enable_ifn() { reader_current_data().map(|data| data.save_screen_state()); } }); + let mut out = Outputter::stdoutput().borrow_mut(); if !BRACKETED_PASTE.load(Ordering::Relaxed) { BRACKETED_PASTE.store(true, Ordering::Release); - let _ = write_loop(&STDOUT_FILENO, b"\x1b[?2004h"); + out.write_command(DecsetBracketedPaste); if IS_TMUX.load() { - let _ = write_loop(&STDOUT_FILENO, "\x1b[?1004h".as_bytes()); // focus reporting + out.write_command(DecsetFocusReporting); } did_write.store(true); } @@ -667,10 +656,10 @@ pub fn terminal_protocols_enable_ifn() { TERMINAL_PROTOCOLS.store(true, Ordering::Release); FLOG!(term_protocols, "Enabling extended keys"); if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() { - let _ = write_loop(&STDOUT_FILENO, b"\x1b[>4;1m"); // XTerm's modifyOtherKeys - let _ = write_loop(&STDOUT_FILENO, b"\x1b="); // set application keypad mode, so the keypad keys send unique codes + out.write_command(ModifyOtherKeysEnable); // XTerm's modifyOtherKeys + out.write_command(ApplicationKeypadModeEnable); // set application keypad mode, so the keypad keys send unique codes } else { - let _ = write_loop(&STDOUT_FILENO, b"\x1b[=5u"); // kitty progressive enhancements + out.write_command(KittyKeyboardProgressiveEnhancementsEnable); } did_write.store(true); } @@ -684,10 +673,11 @@ pub(crate) fn terminal_protocols_disable_ifn() { } }) }); + let mut out = Outputter::stdoutput().borrow_mut(); if BRACKETED_PASTE.load(Ordering::Acquire) { - let _ = write_loop(&STDOUT_FILENO, b"\x1b[?2004l"); + out.write_command(DecrstBracketedPaste); if IS_TMUX.load() { - let _ = write_loop(&STDOUT_FILENO, "\x1b[?1004l".as_bytes()); + out.write_command(DecrstFocusReporting); } BRACKETED_PASTE.store(false, Ordering::Release); did_write.store(true); @@ -699,10 +689,10 @@ pub(crate) fn terminal_protocols_disable_ifn() { let kitty_keyboard_supported = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Acquire); assert_ne!(kitty_keyboard_supported, Capability::Unknown as _); if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() { - let _ = write_loop(&STDOUT_FILENO, b"\x1b[>4;0m"); // XTerm's modifyOtherKeys - let _ = write_loop(&STDOUT_FILENO, b"\x1b>"); // application keypad mode + out.write_command(ModifyOtherKeysDisable); + out.write_command(ApplicationKeypadModeDisable); } else { - let _ = write_loop(&STDOUT_FILENO, b"\x1b[=0u"); // kitty progressive enhancements + out.write_command(KittyKeyboardProgressiveEnhancementsDisable); } TERMINAL_PROTOCOLS.store(false, Ordering::Release); did_write.store(true); @@ -1401,13 +1391,9 @@ fn disable_mouse_tracking(&mut self) { // fish recognizes but does not actually support mouse reporting. We never turn it on, and // it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn // it off before exiting. We turn it off here to avoid wasting resources. - // - // Since this is only called when we detect an incoming mouse reporting payload, we know the - // terminal emulator supports mouse reporting, so no terminfo checks. FLOG!(reader, "Disabling mouse tracking"); // We shouldn't directly manipulate stdout from here, so we ask the reader to do it. - // writembs(outputter_t::stdoutput(), "\x1B[?1000l"); self.push_front(CharEvent::Implicit(ImplicitEvent::DisableMouseTracking)); } @@ -1534,14 +1520,10 @@ fn parse_dcs(&mut self, buffer: &mut Vec) -> Option { str2wcstring(&value) ) ); - if key == b"indn" { + if key == SCROLL_FORWARD_TERMINFO_CODE.as_bytes() { SCROLL_FORWARD_SUPPORTED.store(true); FLOG!(reader, "Scroll forward is supported"); } - if key == b"cuu" { - CURSOR_UP_SUPPORTED.store(true); - FLOG!(reader, "Cursor up is supported"); - } return None; } diff --git a/src/io.rs b/src/io.rs index 8acc47f8c..dedd8fc56 100644 --- a/src/io.rs +++ b/src/io.rs @@ -10,6 +10,7 @@ use crate::proc::JobGroupRef; use crate::redirection::{RedirectionMode, RedirectionSpecList}; use crate::signal::SigChecker; +use crate::terminal::Output; use crate::topic_monitor::Topic; use crate::wchar::prelude::*; use crate::wutil::{perror, perror_io, wdirname, wstat, wwrite_to_fd}; @@ -741,6 +742,13 @@ pub fn append_narrow_buffer(&mut self, buffer: &SeparatedBuffer) -> bool { } } +impl Output for OutputStream { + fn write_bytes(&mut self, command_part: &[u8]) { + // TODO Retry on interrupt. + self.append(str2wcstring(command_part)); + } +} + /// An output stream for builtins which outputs to an fd. /// Note the fd may be something like stdout; there is no ownership implied here. pub struct FdOutputStream { diff --git a/src/lib.rs b/src/lib.rs index 3c5c791ae..f101f9590 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,6 @@ pub mod nix; pub mod null_terminated_array; pub mod operation_context; -pub mod output; pub mod pager; pub mod panic; pub mod parse_constants; diff --git a/src/output.rs b/src/output.rs deleted file mode 100644 index e2080d8d3..000000000 --- a/src/output.rs +++ /dev/null @@ -1,614 +0,0 @@ -// Generic output functions. -use crate::color::{self, RgbColor}; -use crate::common::{self, wcs2string_appending}; -use crate::env::EnvVar; -use crate::terminal::{tparm1, Term}; -use crate::threads::MainThread; -use crate::wchar::prelude::*; -use bitflags::bitflags; -use std::cell::{RefCell, RefMut}; -use std::ffi::CStr; -use std::io::{Result, Write}; -use std::ops::{Deref, DerefMut}; -use std::os::fd::RawFd; -use std::sync::atomic::{AtomicU8, Ordering}; - -bitflags! { - #[derive(Copy, Clone, Default)] - pub struct ColorSupport: u8 { - const TERM_256COLOR = 1<<0; - const TERM_24BIT = 1<<1; - } -} - -/// Whether term256 and term24bit are supported. -static COLOR_SUPPORT: AtomicU8 = AtomicU8::new(0); - -/// Returns true if we think tparm can handle outputting a color index. -fn term_supports_color_natively(term: &Term, c: u8) -> bool { - #[allow(clippy::int_plus_one)] - if let Some(max_colors) = term.max_colors { - max_colors >= usize::from(c) + 1 - } else { - false - } -} - -pub fn get_color_support() -> ColorSupport { - let val = COLOR_SUPPORT.load(Ordering::Relaxed); - ColorSupport::from_bits_truncate(val) -} - -pub fn set_color_support(val: ColorSupport) { - COLOR_SUPPORT.store(val.bits(), Ordering::Relaxed); -} - -pub(crate) trait Output { - fn write_bytes(&mut self, buf: &[u8]); -} - -impl Output for Vec { - fn write_bytes(&mut self, buf: &[u8]) { - self.extend_from_slice(buf); - } -} - -fn index_for_color(c: RgbColor) -> u8 { - if c.is_named() || !(get_color_support().contains(ColorSupport::TERM_256COLOR)) { - return c.to_name_index(); - } - c.to_term256_index() -} - -fn write_color_escape(outp: &mut Outputter, term: &Term, todo: &CStr, mut idx: u8, is_fg: bool) { - if term_supports_color_natively(term, idx) { - // Use tparm to emit color escape. - outp.tputs_if_some(&tparm1(todo, idx.into())); - } else { - // We are attempting to bypass the term here. Generate the ANSI escape sequence ourself. - if idx < 16 { - // this allows the non-bright color to happen instead of no color working at all when a - // bright is attempted when only colors 0-7 are supported. - // - // TODO: enter bold mode in builtin_set_color in the same circumstance- doing that combined - // with what we do here, will make the brights actually work for virtual consoles/ancient - // emulators. - if term.max_colors == Some(8) && idx > 8 { - idx -= 8; - } - write_to_output!( - outp, - "\x1B[{}m", - (if idx > 7 { 82 } else { 30 }) + i32::from(idx) + ((i32::from(!is_fg)) * 10) - ); - } else { - write_to_output!(outp, "\x1B[{};5;{}m", if is_fg { 38 } else { 48 }, idx); - } - } -} - -fn write_foreground_color(outp: &mut Outputter, idx: u8, term: &Term) -> bool { - if let Some(cap) = &term.set_a_foreground { - write_color_escape(outp, term, cap, idx, true); - true - } else if let Some(cap) = &term.set_foreground { - write_color_escape(outp, term, cap, idx, true); - true - } else { - false - } -} - -fn write_background_color(outp: &mut Outputter, idx: u8, term: &Term) -> bool { - if let Some(cap) = &term.set_a_background { - write_color_escape(outp, term, cap, idx, false); - true - } else if let Some(cap) = &term.set_background { - write_color_escape(outp, term, cap, idx, false); - true - } else { - false - } -} - -pub struct Outputter { - /// Storage for buffered contents. - contents: Vec, - - /// Count of how many outstanding begin_buffering() calls there are. - buffer_count: u32, - - /// fd to output to, or -1 for none. - fd: RawFd, - - /// Foreground. - last_color: RgbColor, - - /// Background. - last_color2: RgbColor, - - was_bold: bool, - was_underline: bool, - was_italics: bool, - was_dim: bool, - was_reverse: bool, -} - -impl Outputter { - /// Construct an outputter which outputs to a given fd. - /// If the fd is negative, the outputter will buffer its output. - const fn new_from_fd(fd: RawFd) -> Self { - Self { - contents: Vec::new(), - buffer_count: 0, - fd, - last_color: RgbColor::NORMAL, - last_color2: RgbColor::NORMAL, - was_bold: false, - was_underline: false, - was_italics: false, - was_dim: false, - was_reverse: false, - } - } - - /// Construct an outputter which outputs to its string buffer. - pub fn new_buffering() -> Self { - Self::new_from_fd(-1) - } - - fn reset_modes(&mut self) { - self.was_bold = false; - self.was_underline = false; - self.was_italics = false; - self.was_dim = false; - self.was_reverse = false; - } - - fn maybe_flush(&mut self) { - if self.fd >= 0 && self.buffer_count == 0 { - self.flush_to(self.fd); - } - } - - /// Unconditionally write the color string to the output. - /// Exported for builtin_set_color's usage only. - pub fn write_color(&mut self, color: RgbColor, is_fg: bool) -> bool { - let term = crate::terminal::term(); - let supports_term24bit = get_color_support().contains(ColorSupport::TERM_24BIT); - if !supports_term24bit || !color.is_rgb() { - // Indexed or non-24 bit color. - let idx = index_for_color(color); - if is_fg { - return write_foreground_color(self, idx, &term); - } else { - return write_background_color(self, idx, &term); - }; - } - - // 24 bit! No tparm here, just ANSI escape sequences. - // Foreground: ^[38;2;;;m - // Background: ^[48;2;;;m - let rgb = color.to_color24(); - write_to_output!( - self, - "\x1B[{};2;{};{};{}m", - if is_fg { 38 } else { 48 }, - rgb.r, - rgb.g, - rgb.b - ); - true - } - - /// Sets the fg and bg color. May be called as often as you like, since if the new color is the same - /// as the previous, nothing will be written. Negative values for set_color will also be ignored. - /// Since the terminfo string this function emits can potentially cause the screen to flicker, the - /// function takes care to write as little as possible. - /// - /// Possible values for colors are RgbColor colors or special values like RgbColor::NORMAL - /// - /// In order to set the color to normal, three terminfo strings may have to be written. - /// - /// - First a string to set the color, such as set_a_foreground. This is needed because otherwise - /// the previous strings colors might be removed as well. - /// - /// - After that we write the exit_attribute_mode string to reset all color attributes. - /// - /// - Lastly we may need to write set_a_background or set_a_foreground to set the other half of the - /// color pair to what it should be. - #[allow(clippy::if_same_then_else)] - pub fn set_color(&mut self, mut fg: RgbColor, mut bg: RgbColor) { - // Test if we have at least basic support for setting fonts, colors and related bits - otherwise - // just give up... - let term = crate::terminal::term(); - let Term { - enter_bold_mode, - enter_underline_mode, - exit_underline_mode, - enter_italics_mode, - exit_italics_mode, - enter_dim_mode, - enter_reverse_mode, - enter_standout_mode, - exit_attribute_mode, - .. - } = &*term; - let Some(exit_attribute_mode) = exit_attribute_mode else { - return; - }; - - const normal: RgbColor = RgbColor::NORMAL; - let mut bg_set = false; - let mut last_bg_set = false; - let is_bold = fg.is_bold() || bg.is_bold(); - let is_underline = fg.is_underline() || bg.is_underline(); - let is_italics = fg.is_italics() || bg.is_italics(); - let is_dim = fg.is_dim() || bg.is_dim(); - let is_reverse = fg.is_reverse() || bg.is_reverse(); - - if fg.is_reset() || bg.is_reset() { - #[allow(unused_assignments)] - { - fg = normal; - bg = normal; - } - self.reset_modes(); - // If we exit attribute mode, we must first set a color, or previously colored text might - // lose its color. Terminals are weird... - write_foreground_color(self, 0, &term); - self.tputs(exit_attribute_mode); - return; - } - if (self.was_bold && !is_bold) - || (self.was_dim && !is_dim) - || (self.was_reverse && !is_reverse) - { - // Only way to exit bold/dim/reverse mode is a reset of all attributes. - self.tputs(exit_attribute_mode); - self.last_color = normal; - self.last_color2 = normal; - self.reset_modes(); - } - if !self.last_color2.is_special() { - // Background was set. - // "Special" here refers to the special "normal", "reset" and "none" colors, - // that really just disable the background. - last_bg_set = true; - } - if !bg.is_special() { - // Background is set. - bg_set = true; - if fg == bg { - fg = if bg == RgbColor::WHITE { - RgbColor::BLACK - } else { - RgbColor::WHITE - }; - } - } - - if let Some(enter_bold_mode) = &enter_bold_mode { - if bg_set && !last_bg_set { - // Background color changed and is set, so we enter bold mode to make reading easier. - // This means bold mode is _always_ on when the background color is set. - self.tputs(enter_bold_mode); - } - if !bg_set && last_bg_set { - // Background color changed and is no longer set, so we exit bold mode. - self.tputs(exit_attribute_mode); - self.reset_modes(); - // We don't know if exit_attribute_mode resets colors, so we set it to something known. - if write_foreground_color(self, 0, &term) { - self.last_color = RgbColor::BLACK; - } - } - } - - if self.last_color != fg { - if fg.is_normal() { - write_foreground_color(self, 0, &term); - self.tputs(exit_attribute_mode); - - self.last_color2 = RgbColor::NORMAL; - self.reset_modes(); - } else if !fg.is_special() { - self.write_color(fg, true /* foreground */); - } - } - self.last_color = fg; - - if self.last_color2 != bg { - if bg.is_normal() { - write_background_color(self, 0, &term); - - self.tputs(exit_attribute_mode); - if !self.last_color.is_normal() { - self.write_color(self.last_color, true /* foreground */); - } - self.reset_modes(); - self.last_color2 = bg; - } else if !bg.is_special() { - self.write_color(bg, false /* not foreground */); - self.last_color2 = bg; - } - } - - // Lastly, we set bold, underline, italics, dim, and reverse modes correctly. - if is_bold && !self.was_bold && !bg_set && self.tputs_if_some(enter_bold_mode) { - self.was_bold = is_bold; - } - - if !self.was_underline && is_underline && self.tputs_if_some(enter_underline_mode) { - self.was_underline = is_underline; - } else if self.was_underline && !is_underline && self.tputs_if_some(exit_underline_mode) { - self.was_underline = is_underline; - } - - if self.was_italics && !is_italics && self.tputs_if_some(exit_italics_mode) { - self.was_italics = is_italics; - } else if !self.was_italics && is_italics && self.tputs_if_some(enter_italics_mode) { - self.was_italics = is_italics; - } - - if is_dim && !self.was_dim && self.tputs_if_some(enter_dim_mode) { - self.was_dim = is_dim; - } - // N.B. there is no exit_dim_mode in terminfo, it's handled by exit_attribute_mode above. - - if is_reverse && !self.was_reverse { - // Some terms do not have a reverse mode set, so standout mode is a fallback. - if self.tputs_if_some(enter_reverse_mode) { - self.was_reverse = is_reverse; - } else if self.tputs_if_some(enter_standout_mode) { - self.was_reverse = is_reverse; - } - } - } - - /// Write a wide character to the receiver. - pub fn writech(&mut self, ch: char) { - self.write_wstr(wstr::from_char_slice(&[ch])); - } - - /// Write a narrow character to the receiver. - pub fn push(&mut self, ch: u8) { - self.contents.push(ch); - self.maybe_flush(); - } - - /// Write a wide string. - pub fn write_wstr(&mut self, str: &wstr) { - wcs2string_appending(&mut self.contents, str); - self.maybe_flush(); - } - - /// Return the "output" contents. - pub fn contents(&self) -> &[u8] { - &self.contents - } - - /// Output any buffered data to the given `fd`. - fn flush_to(&mut self, fd: RawFd) { - if fd >= 0 && !self.contents.is_empty() { - let _ = common::write_loop(&fd, &self.contents); - self.contents.clear(); - } - } - - /// Begins buffering. Output will not be automatically flushed until a corresponding - /// end_buffering() call. - pub fn begin_buffering(&mut self) { - self.buffer_count += 1; - assert!(self.buffer_count > 0, "buffer_count overflow"); - } - - /// Balance a begin_buffering() call. - pub fn end_buffering(&mut self) { - assert!(self.buffer_count > 0, "buffer_count underflow"); - self.buffer_count -= 1; - self.maybe_flush(); - } -} - -/// Outputter implements Write, so it may be used as the receiver of the write! macro. -/// Only ASCII data should be written this way: do NOT assume that the receiver is UTF-8. -impl Write for Outputter { - fn write(&mut self, buf: &[u8]) -> Result { - self.contents.extend_from_slice(buf); - self.maybe_flush(); - Ok(buf.len()) - } - - fn flush(&mut self) -> Result<()> { - self.flush_to(self.fd); - Ok(()) - } -} - -impl Output for Outputter { - fn write_bytes(&mut self, buf: &[u8]) { - self.contents.extend_from_slice(buf); - self.maybe_flush(); - } -} - -impl Outputter { - /// Emit a terminfo string, like tputs. - /// affcnt (number of lines affected) is assumed to be 1, i.e. not applicable. - pub fn tputs(&mut self, str: &CStr) { - self.tputs_bytes(str.to_bytes()); - } - - pub fn tputs_bytes(&mut self, str: &[u8]) { - self.begin_buffering(); - let _ = self.write_all(str); - self.end_buffering(); - } - - /// Convenience cover over tputs, in recognition of the fact that our Term has Optional fields. - /// If `str` is Some, write it with tputs and return true. Otherwise, return false. - pub fn tputs_if_some(&mut self, str: &Option>) -> bool { - if let Some(str) = str { - self.tputs(str.as_ref()); - true - } else { - false - } - } - - /// Access the outputter for stdout. - /// This should only be used from the main thread. - pub fn stdoutput() -> &'static RefCell { - static STDOUTPUT: MainThread> = - MainThread::new(RefCell::new(Outputter::new_from_fd(libc::STDOUT_FILENO))); - STDOUTPUT.get() - } -} - -pub struct BufferedOutputter<'a>(RefMut<'a, Outputter>); - -impl<'a> BufferedOutputter<'a> { - pub fn new(outputter: &'a RefCell) -> Self { - let mut outputter = outputter.borrow_mut(); - outputter.begin_buffering(); - Self(outputter) - } -} - -impl<'a> Drop for BufferedOutputter<'a> { - fn drop(&mut self) { - self.0.end_buffering(); - } -} - -impl Deref for BufferedOutputter<'_> { - type Target = Outputter; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for BufferedOutputter<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl<'a> Output for BufferedOutputter<'a> { - fn write_bytes(&mut self, buf: &[u8]) { - self.0.write_bytes(buf); - } -} - -/// Given a list of RgbColor, pick the "best" one, as determined by the color support. Returns -/// RgbColor::NONE if empty. -pub fn best_color(candidates: &[RgbColor], support: ColorSupport) -> RgbColor { - if candidates.is_empty() { - return RgbColor::NONE; - } - - let mut first_rgb = RgbColor::NONE; - let mut first_named = RgbColor::NONE; - for color in candidates { - if first_rgb.is_none() && color.is_rgb() { - first_rgb = *color; - } - if first_named.is_none() && color.is_named() { - first_named = *color; - } - } - - // If we have both RGB and named colors, then prefer rgb if term256 is supported. - let mut result; - let has_term256 = support.contains(ColorSupport::TERM_256COLOR); - if (!first_rgb.is_none() && has_term256) || first_named.is_none() { - result = first_rgb; - } else { - result = first_named; - } - if result.is_none() { - result = candidates[0]; - } - result -} - -/// Return the internal color code representing the specified color. -/// TODO: This code should be refactored to enable sharing with builtin_set_color. -/// In particular, the argument parsing still isn't fully capable. -pub fn parse_color(var: &EnvVar, is_background: bool) -> RgbColor { - let mut result = parse_color_maybe_none(var, is_background); - if result.is_none() { - result.typ = color::Type::Normal; - } - result -} - -pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> RgbColor { - let mut is_bold = false; - let mut is_underline = false; - let mut is_italics = false; - let mut is_dim = false; - let mut is_reverse = false; - - let mut candidates: Vec = Vec::new(); - - let prefix = L!("--background="); - - let mut next_is_background = false; - let mut color_name = WString::new(); - for next in var.as_list() { - color_name.clear(); - #[allow(clippy::collapsible_else_if)] - if is_background { - if color_name.is_empty() && next_is_background { - color_name = next.to_owned(); - next_is_background = false; - } else if next.starts_with(prefix) { - // Look for something like "--background=red". - color_name = next.slice_from(prefix.char_count()).to_owned(); - } else if next == "--background" || next == "-b" { - // Without argument attached the next token is the color - // - if it's another option it's an error. - next_is_background = true; - } else if next == "--reverse" || next == "-r" { - // Reverse should be meaningful in either context - is_reverse = true; - } else if next.starts_with("-b") { - // Look for something like "-bred". - // Yes, that length is hardcoded. - color_name = next.slice_from(2).to_owned(); - } - } else { - if next == "--bold" || next == "-o" { - is_bold = true; - } else if next == "--underline" || next == "-u" { - is_underline = true; - } else if next == "--italics" || next == "-i" { - is_italics = true; - } else if next == "--dim" || next == "-d" { - is_dim = true; - } else if next == "--reverse" || next == "-r" { - is_reverse = true; - } else { - color_name = next.clone(); - } - } - - if !color_name.is_empty() { - let color: Option = RgbColor::from_wstr(&color_name); - if let Some(color) = color { - candidates.push(color); - } - } - } - - let mut result = best_color(&candidates, get_color_support()); - result.set_bold(is_bold); - result.set_underline(is_underline); - result.set_italics(is_italics); - result.set_dim(is_dim); - result.set_reverse(is_reverse); - result -} diff --git a/src/reader.rs b/src/reader.rs index 4733a5090..d8f8aeff7 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -26,7 +26,6 @@ use std::cell::UnsafeCell; use std::cmp; use std::io::BufReader; -use std::io::Write; use std::num::NonZeroUsize; use std::ops::ControlFlow; use std::ops::Range; @@ -51,8 +50,8 @@ use crate::common::restore_term_foreground_process_group_for_exit; use crate::common::{ escape, escape_string, exit_without_destructors, get_ellipsis_char, get_obfuscation_read_char, - redirect_tty_output, shell_modes, str2wcstring, wcs2string, write_loop, EscapeFlags, - EscapeStringStyle, ScopeGuard, PROGRAM_NAME, UTF8_BOM_WCHAR, + redirect_tty_output, shell_modes, str2wcstring, write_loop, EscapeFlags, EscapeStringStyle, + ScopeGuard, PROGRAM_NAME, UTF8_BOM_WCHAR, }; use crate::complete::{ complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList, @@ -79,35 +78,25 @@ SearchType, }; use crate::input::init_input; -use crate::input_common::kitty_progressive_enhancements_query; use crate::input_common::terminal_protocols_disable_ifn; use crate::input_common::unblock_input; use crate::input_common::BlockingWait; -use crate::input_common::Capability; use crate::input_common::CursorPositionWait; use crate::input_common::ImplicitEvent; use crate::input_common::InputEventQueuer; use crate::input_common::Queried; use crate::input_common::IN_DVTM; use crate::input_common::IN_MIDNIGHT_COMMANDER; -use crate::input_common::KITTY_KEYBOARD_SUPPORTED; -use crate::input_common::SYNCHRONIZED_OUTPUT_SUPPORTED; use crate::input_common::{ terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, CharInputStyle, InputData, ReadlineCmd, }; -use crate::input_common::{CURSOR_UP_SUPPORTED, SCROLL_FORWARD_SUPPORTED}; use crate::io::IoChain; use crate::key::ViewportPosition; use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; use crate::libc::MB_CUR_MAX; use crate::nix::{getpgrp, getpid, isatty}; use crate::operation_context::{get_bg_context, OperationContext}; -use crate::output::parse_color; -use crate::output::parse_color_maybe_none; -use crate::output::BufferedOutputter; -use crate::output::Output; -use crate::output::Outputter; use crate::pager::{PageRendering, Pager, SelectionMotion}; use crate::panic::AT_EXIT; use crate::parse_constants::SourceRange; @@ -130,11 +119,32 @@ }; use crate::reader_history_search::{smartcase_flags, ReaderHistorySearch, SearchMode}; use crate::screen::is_dumb; -use crate::screen::{screen_clear, screen_force_clear_to_end, CharOffset, Screen}; +use crate::screen::{screen_force_clear_to_end, CharOffset, Screen}; +use crate::should_flog; use crate::signal::{ signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers, signal_set_handlers_once, }; +use crate::terminal::parse_color; +use crate::terminal::parse_color_maybe_none; +use crate::terminal::BufferedOutputter; +use crate::terminal::Output; +use crate::terminal::Outputter; +use crate::terminal::TerminalCommand::DecrstAlternateScreenBuffer; +use crate::terminal::TerminalCommand::DecrstMouseTracking; +use crate::terminal::TerminalCommand::DecrstSynchronizedUpdate; +use crate::terminal::TerminalCommand::DecsetAlternateScreenBuffer; +use crate::terminal::TerminalCommand::DecsetShowCursor; +use crate::terminal::TerminalCommand::DecsetSynchronizedUpdate; +use crate::terminal::TerminalCommand::QueryCursorPosition; +use crate::terminal::TerminalCommand::{ + ClearScreen, Osc0WindowTitle, Osc133CommandStart, QueryKittyKeyboardProgressiveEnhancements, + QueryPrimaryDeviceAttribute, QuerySynchronizedOutput, QueryXtgettcap, QueryXtversion, +}; +use crate::terminal::{ + Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE, + SYNCHRONIZED_OUTPUT_SUPPORTED, +}; use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update}; use crate::threads::{ assert_is_background_thread, assert_is_main_thread, iothread_service_main_with_timeout, @@ -689,12 +699,7 @@ fn read_i(parser: &Parser) { data.clear(EditableLineTag::Commandline); data.update_buff_pos(EditableLineTag::Commandline, None); - // OSC 133 "Command start" - write_to_output!( - &mut BufferedOutputter::new(Outputter::stdoutput()), - "\x1b]133;C;cmdline_url={}\x07", - escape_string(&command, EscapeStringStyle::Url), - ); + BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandStart(&command)); event::fire_generic(parser, L!("fish_preexec").to_owned(), vec![command.clone()]); let eval_res = reader_run_command(parser, &command); signal_clear_cancel(); @@ -706,18 +711,15 @@ fn read_i(parser: &Parser) { data.exit_loop_requested |= parser.libdata().exit_current_script; parser.libdata_mut().exit_current_script = false; - // OSC 133 "Command finished" - write_to_output!( - &mut BufferedOutputter::new(Outputter::stdoutput()), - "\x1b]133;D;{}\x07", - parser.get_last_status() + BufferedOutputter::new(Outputter::stdoutput()).write_command( + crate::terminal::TerminalCommand::Osc133CommandFinished(parser.get_last_status()), ); event::fire_generic(parser, L!("fish_postexec").to_owned(), vec![command]); // Allow any pending history items to be returned in the history array. data.history.resolve_pending(); // Make cursor visible. Every even vaguely used terminal agrees on this sequence. - data.screen.write_bytes(b"\x1b[?25h"); + data.screen.write_command(DecsetShowCursor); let already_warned = data.did_warn_for_bg_jobs; if check_exit_loop_maybe_warning(Some(&mut data)) { @@ -1450,7 +1452,7 @@ pub fn request_cursor_position( assert!(wait_guard.is_none()); *wait_guard = Some(BlockingWait::CursorPosition(cursor_position_wait)); } - out.write_bytes(b"\x1b[6n"); + out.write_command(QueryCursorPosition); self.save_screen_state(); } @@ -2149,8 +2151,6 @@ fn jump( } } -pub const QUERY_PRIMARY_DEVICE_ATTRIBUTE: &[u8] = b"\x1b[0c"; - impl<'a> Reader<'a> { /// Read a command to execute, respecting input bindings. /// Return the command, or none if we were asked to cancel (e.g. SIGHUP). @@ -2205,13 +2205,13 @@ fn readline(&mut self, nchars: Option) -> Option { let mut out = Outputter::stdoutput().borrow_mut(); out.begin_buffering(); // Query for kitty keyboard protocol support. - let _ = out.write_all(kitty_progressive_enhancements_query()); + out.write_command(QueryKittyKeyboardProgressiveEnhancements); // Query for cursor position reporting support. self.request_cursor_position(&mut out, None); // Query for synchronized output support. - let _ = out.write_all(b"\x1b[?2026$p"); - let _ = out.write_all(b"\x1b[>0q"); // XTVERSION - let _ = out.write_all(QUERY_PRIMARY_DEVICE_ATTRIBUTE); + out.write_command(QuerySynchronizedOutput); + out.write_command(QueryXtversion); + out.write_command(QueryPrimaryDeviceAttribute); out.end_buffering(); } } @@ -2274,7 +2274,6 @@ fn readline(&mut self, nchars: Option) -> Option { // Ensure we have no pager contents when we exit. if !self.pager.is_empty() { // Clear to end of screen to erase the pager contents. - // TODO: this may fail if eos doesn't exist, in which case we should emit newlines. screen_force_clear_to_end(); self.clear_pager(); } @@ -2527,7 +2526,7 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo ImplicitEvent::DisableMouseTracking => { Outputter::stdoutput() .borrow_mut() - .write_wstr(L!("\x1B[?1000l")); + .write_command(DecrstMouseTracking); self.save_screen_state(); } ImplicitEvent::PrimaryDeviceAttribute => { @@ -2553,7 +2552,7 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo let mut out = Outputter::stdoutput().borrow_mut(); out.begin_buffering(); query_capabilities_via_dcs(out.by_ref()); - let _ = out.write_all(QUERY_PRIMARY_DEVICE_ATTRIBUTE); + out.write_command(QueryPrimaryDeviceAttribute); out.end_buffering(); *wait_guard = Some(BlockingWait::Startup(Queried::Twice)); drop(wait_guard); @@ -2579,25 +2578,24 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo } } -fn xtgettcap(out: &mut impl Output, cap: &str) { - FLOG!( - reader, - format!( - "Sending XTGETTCAP request for {}: {:?}", - cap, - format!("\x1bP+q{}\x1b\\", DisplayAsHex(cap)) - ) - ); - write_to_output!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap)); +fn send_xtgettcap_query(out: &mut impl Output, cap: &'static str) { + if should_flog!(reader) { + let mut tmp = Vec::::new(); + tmp.write_command(QueryXtgettcap(cap)); + FLOG!( + reader, + format!("Sending XTGETTCAP request for {}: {:?}", cap, tmp) + ); + } + out.write_command(QueryXtgettcap(cap)); } fn query_capabilities_via_dcs(out: &mut impl Output) { - out.write_bytes(b"\x1b[?2026h"); // begin synchronized update - out.write_bytes(b"\x1b[?1049h"); // enable alternative screen buffer - xtgettcap(out, "indn"); - xtgettcap(out, "cuu"); - out.write_bytes(b"\x1b[?1049l"); // disable alternative screen buffer - out.write_bytes(b"\x1b[?2026l"); // end synchronized update + out.write_command(DecsetSynchronizedUpdate); // begin synchronized update + out.write_command(DecsetAlternateScreenBuffer); // enable alternative screen buffer + send_xtgettcap_query(out, SCROLL_FORWARD_TERMINFO_CODE); + out.write_command(DecrstAlternateScreenBuffer); // disable alternative screen buffer + out.write_command(DecrstSynchronizedUpdate); // end synchronized update } impl<'a> Reader<'a> { @@ -3825,7 +3823,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { self.clear_screen_and_repaint(); } rl::ScrollbackPush => { - if !SCROLL_FORWARD_SUPPORTED.load() || !CURSOR_UP_SUPPORTED.load() { + if !SCROLL_FORWARD_SUPPORTED.load() { return; } let wait_guard = self.blocking_wait(); @@ -3857,17 +3855,29 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { fn clear_screen_and_repaint(&mut self) { self.parser.libdata_mut().is_repaint = true; - let clear = screen_clear(); - if !clear.is_empty() { - // Clear the screen if we can. - // This is subtle: We first clear, draw the old prompt, - // and *then* reexecute the prompt and overdraw it. - // This removes the flicker, - // while keeping the prompt up-to-date. - Outputter::stdoutput().borrow_mut().write_wstr(&clear); - self.screen.reset_line(/*repaint_prompt=*/ true); - self.layout_and_repaint(L!("readline")); - } + + // Clear the screen. + // This is subtle: We first clear, draw the old prompt, + // and *then* reexecute the prompt and overdraw it. + // This removes the flicker, + // while keeping the prompt up-to-date. + Outputter::stdoutput() + .borrow_mut() + .write_command(ClearScreen); + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("readline")); + + // Clear the screen. + // This is subtle: We first clear, draw the old prompt, + // and *then* reexecute the prompt and overdraw it. + // This removes the flicker, + // while keeping the prompt up-to-date. + Outputter::stdoutput() + .borrow_mut() + .write_command(ClearScreen); + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("readline")); + self.exec_prompt(); self.screen.reset_line(/*repaint_prompt=*/ true); self.layout_and_repaint(L!("readline")); @@ -4462,17 +4472,6 @@ fn reader_interactive_init(parser: &Parser) { terminal_protocol_hacks(); } -struct DisplayAsHex<'a>(&'a str); - -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() { - write!(f, "{:x}", byte)?; - } - Ok(()) - } -} - /// Destroy data for interactive use. fn reader_interactive_destroy() { Outputter::stdoutput() @@ -4535,15 +4534,10 @@ pub fn reader_write_title( Some(&mut lst), /*apply_exit_status=*/ false, ); + let mut out = BufferedOutputter::new(Outputter::stdoutput()); if !lst.is_empty() { - let mut title_line = L!("\x1B]0;").to_owned(); - for val in &lst { - title_line.push_utfstr(val); - } - title_line.push_str("\x07"); // BEL - let narrow = wcs2string(&title_line); - out.write_bytes(&narrow); + out.write_command(Osc0WindowTitle(&lst)); } out.set_color(RgbColor::RESET, RgbColor::RESET); diff --git a/src/screen.rs b/src/screen.rs index c811f66c0..c7f9c04f7 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -8,13 +8,11 @@ //! of text around to handle text insertion. use crate::editable_line::line_at_cursor; -use crate::input_common::{CURSOR_UP_SUPPORTED, SCROLL_FORWARD_SUPPORTED}; use crate::key::ViewportPosition; use crate::pager::{PageRendering, Pager, PAGER_MIN_HEIGHT}; use crate::FLOG; use std::cell::RefCell; use std::collections::LinkedList; -use std::ffi::{CStr, CString}; use std::io::Write; use std::ops::Range; use std::sync::atomic::AtomicU32; @@ -34,8 +32,11 @@ use crate::future::IsSomeAnd; use crate::global_safety::RelaxedAtomicBool; use crate::highlight::{HighlightColorResolver, HighlightSpec}; -use crate::output::{BufferedOutputter, Output, Outputter}; -use crate::terminal::{term, tparm1}; +use crate::terminal::TerminalCommand::{ + self, ClearToEndOfLine, ClearToEndOfScreen, CursorDown, CursorLeft, CursorMove, CursorRight, + CursorUp, EnterDimMode, ExitAttributeMode, Osc133PromptStart, ScrollForward, +}; +use crate::terminal::{use_terminfo, BufferedOutputter, CardinalDirection, Output, Outputter}; use crate::termsize::{termsize_last, Termsize}; use crate::wchar::prelude::*; use crate::wcstringutil::{fish_wcwidth_visible, string_prefixes_string}; @@ -584,13 +585,10 @@ pub fn push_to_scrollback(&mut self, cursor_y: usize) { return; } let mut out = BufferedOutputter::new(self.outp); - let lines_to_scroll = i32::try_from(lines_to_scroll).unwrap(); // Scroll down. - assert!(SCROLL_FORWARD_SUPPORTED.load()); - out.write_bytes(format!("\x1b[{}S", lines_to_scroll).as_bytes()); - assert!(CURSOR_UP_SUPPORTED.load()); + out.write_command(ScrollForward(lines_to_scroll)); // Reposition cursor. - out.write_bytes(format!("\x1b[{}A", lines_to_scroll).as_bytes()); + out.write_command(CursorMove(CardinalDirection::Up, lines_to_scroll)); } fn command_line_y_given_cursor_y(&mut self, viewport_cursor_y: usize) -> usize { @@ -666,47 +664,55 @@ pub fn reset_abandoning_line(&mut self, screen_width: usize) { // Don't need to check for fish_wcwidth errors; this is done when setting up // omitted_newline_char in common.rs. let non_space_width = get_omitted_newline_width(); - let term = term(); // We do `>` rather than `>=` because the code below might require one extra space. if screen_width > non_space_width { - let mut justgrey = true; - let add = |abandon_line_string: &mut WString, s: Option| { - let Some(s) = s else { - return false; + if use_terminfo() { + use crate::terminal::tparm1; + use std::ffi::CString; + let term = crate::terminal::term(); + let mut justgrey = true; + let add = |abandon_line_string: &mut WString, s: Option| { + let Some(s) = s else { + return false; + }; + abandon_line_string.push_utfstr(&str2wcstring(s.as_bytes())); + true }; - abandon_line_string.push_utfstr(&str2wcstring(s.as_bytes())); - true - }; - if let Some(enter_dim_mode) = term.enter_dim_mode.as_ref() { - if add(&mut abandon_line_string, Some(enter_dim_mode.clone())) { - // Use dim if they have it, so the color will be based on their actual normal - // color and the background of the terminal. - justgrey = false; - } - } - if let (true, Some(set_a_foreground)) = (justgrey, term.set_a_foreground.as_ref()) { - let max_colors = term.max_colors.unwrap_or_default(); - if max_colors >= 238 { - // draw the string in a particular grey - add(&mut abandon_line_string, tparm1(set_a_foreground, 237)); - } else if max_colors >= 9 { - // bright black (the ninth color, looks grey) - add(&mut abandon_line_string, tparm1(set_a_foreground, 8)); - } else if max_colors >= 2 { - if let Some(enter_bold_mode) = term.enter_bold_mode.as_ref() { - // we might still get that color by setting black and going bold for bright - add(&mut abandon_line_string, Some(enter_bold_mode.clone())); - add(&mut abandon_line_string, tparm1(set_a_foreground, 0)); + if let Some(enter_dim_mode) = term.enter_dim_mode.as_ref() { + if add(&mut abandon_line_string, Some(enter_dim_mode.clone())) { + // Use dim if they have it, so the color will be based on their actual normal + // color and the background of the terminal. + justgrey = false; } } + if let (true, Some(set_a_foreground)) = (justgrey, term.set_a_foreground.as_ref()) { + let max_colors = term.max_colors.unwrap_or_default(); + if max_colors >= 238 { + // draw the string in a particular grey + add(&mut abandon_line_string, tparm1(set_a_foreground, 237)); + } else if max_colors >= 9 { + // bright black (the ninth color, looks grey) + add(&mut abandon_line_string, tparm1(set_a_foreground, 8)); + } else if max_colors >= 2 { + if let Some(enter_bold_mode) = term.enter_bold_mode.as_ref() { + // we might still get that color by setting black and going bold for bright + add(&mut abandon_line_string, Some(enter_bold_mode.clone())); + add(&mut abandon_line_string, tparm1(set_a_foreground, 0)); + } + } + } + } else { + let mut tmp = Vec::::new(); + tmp.write_command(EnterDimMode); + abandon_line_string.push_utfstr(&str2wcstring(&tmp)); } abandon_line_string.push_utfstr(&get_omitted_newline_str()); - if let Some(exit_attribute_mode) = term.exit_attribute_mode.as_ref() { - // normal text ANSI escape sequence - add(&mut abandon_line_string, Some(exit_attribute_mode.clone())); - } + // normal text ANSI escape sequence + let mut tmp = Vec::::new(); + tmp.write_command(ExitAttributeMode); + abandon_line_string.push_utfstr(&str2wcstring(&tmp)); for _ in 0..screen_width - non_space_width { abandon_line_string.push(' '); @@ -727,9 +733,9 @@ pub fn reset_abandoning_line(&mut self, screen_width: usize) { // line above your prompt. This doesn't make a difference in normal usage, but copying and // pasting your terminal log becomes a pain. This commit clears that line, making it an // actual empty line. - if let Some(clr_eol) = term.clr_eol.as_ref() { - abandon_line_string.push_utfstr(&str2wcstring(clr_eol.as_bytes())); - } + let mut tmp = Vec::::new(); + tmp.write_command(ClearToEndOfLine); + abandon_line_string.push_utfstr(&str2wcstring(&tmp)); let narrow_abandon_line_string = wcs2string(&abandon_line_string); let _ = write_loop(&STDOUT_FILENO, &narrow_abandon_line_string); @@ -896,29 +902,31 @@ fn do_move(&mut self, new_x: usize, new_y: usize) { let y_steps = isize::try_from(new_y).unwrap() - isize::try_from(self.actual.cursor.y).unwrap(); - let term = term(); - let s = if y_steps < 0 { - term.cursor_up.as_ref() + Some(CursorUp) } else if y_steps > 0 { - let s = term.cursor_down.as_ref(); - if (shell_modes().c_oflag & ONLCR) != 0 && s.is_some_and(|s| s.as_bytes() == b"\n") { + if (shell_modes().c_oflag & ONLCR) != 0 + && (!use_terminfo() + || crate::terminal::term() + .cursor_down + .as_ref() + .is_some_and(|cud| cud.as_bytes() == b"\n")) + { // See GitHub issue #4505. // Most consoles use a simple newline as the cursor down escape. // If ONLCR is enabled (which it normally is) this will of course // also move the cursor to the beginning of the line. - // We could do: - // if (std::strcmp(cursor_up, "\x1B[A") == 0) str = "\x1B[B"; - // else ... but that doesn't work for unknown reasons. self.actual.cursor.x = 0; } - s + Some(CursorDown) } else { None }; - for _ in 0..y_steps.abs_diff(0) { - self.outp.borrow_mut().tputs_if_some(&s); + if let Some(s) = s { + for _ in 0..y_steps.abs_diff(0) { + self.outp.borrow_mut().write_command(s.clone()); + } } let mut x_steps = @@ -928,25 +936,27 @@ fn do_move(&mut self, new_x: usize, new_y: usize) { x_steps = 0; } - let (s, multi_str) = if x_steps < 0 { - (term.cursor_left.as_ref(), term.parm_left_cursor.as_ref()) - } else { - (term.cursor_right.as_ref(), term.parm_right_cursor.as_ref()) - }; - - // Use the bulk ('multi') output for cursor movement if it is supported. - // Note that this is required to avoid some visual glitches in iTerm (issue #1448). - if multi_str.is_some() && x_steps.abs_diff(0) > 1 { - let multi_param = tparm1( - multi_str.as_ref().unwrap(), - i32::try_from(x_steps.abs_diff(0)).unwrap(), - ); - self.outp.borrow_mut().tputs_if_some(&multi_param); - } else { - for _ in 0..x_steps.abs_diff(0) { - self.outp.borrow_mut().tputs_if_some(&s); + // Note that bulk output is a way to avoid some visual glitches in iTerm (issue #1448). + let mut outp = BufferedOutputter::new(self.outp); + match x_steps { + 0 => (), + -1 => { + outp.write_command(CursorLeft); } - } + 1 => { + outp.write_command(CursorRight); + } + _ => { + outp.write_command(CursorMove( + if x_steps < 0 { + CardinalDirection::Left + } else { + CardinalDirection::Right + }, + x_steps.abs_diff(0), + )); + } + }; self.actual.cursor.x = new_x; self.actual.cursor.y = new_y; @@ -956,7 +966,7 @@ fn do_move(&mut self, new_x: usize, new_y: usize) { fn write_char(&mut self, c: char, width: usize) { self.actual.cursor.x = self.actual.cursor.x.wrapping_add(width); self.outp.borrow_mut().writech(c); - if Some(self.actual.cursor.x) == self.actual.screen_width && allow_soft_wrap() { + if Some(self.actual.cursor.x) == self.actual.screen_width { self.soft_wrap_location = Some(Cursor { x: 0, y: self.actual.cursor.y + 1, @@ -970,17 +980,8 @@ fn write_char(&mut self, c: char, width: usize) { } } - /// Send the specified string through tputs and append the output to the screen's outputter. - fn write_mbs(&mut self, s: &CStr) { - self.outp.borrow_mut().tputs(s); - } - - fn write_mbs_if_some(&mut self, s: &Option>) -> bool { - self.outp.borrow_mut().tputs_if_some(s) - } - - pub(crate) fn write_bytes(&mut self, s: &[u8]) { - self.outp.borrow_mut().tputs_bytes(s); + pub(crate) fn write_command(&mut self, command: TerminalCommand) { + self.outp.borrow_mut().write_command(command); } /// Convert a wide string to a multibyte string and append it to the buffer. @@ -1081,12 +1082,10 @@ fn update( need_clear_screen = true; } - let term = term(); - // Output the left prompt if it has changed. if self.scrolled() && !is_final_rendering { self.r#move(0, 0); - self.write_mbs_if_some(&term.clr_eol.as_ref()); + self.write_command(ClearToEndOfLine); self.actual_left_prompt = None; self.actual.cursor.x = 0; } else if self @@ -1098,10 +1097,9 @@ fn update( { self.r#move(0, 0); let mut start = 0; - let osc_133_prompt_start = - |zelf: &mut Screen| zelf.write_bytes(b"\x1b]133;A;click_events=1\x07"); + let mark_prompt_start = |zelf: &mut Screen| zelf.write_command(Osc133PromptStart); if left_prompt_layout.line_breaks.is_empty() { - osc_133_prompt_start(self); + mark_prompt_start(self); } if self .actual_left_prompt @@ -1110,9 +1108,9 @@ fn update( || (self.scrolled() && is_final_rendering) { for (i, &line_break) in left_prompt_layout.line_breaks.iter().enumerate() { - self.write_mbs_if_some(&term.clr_eol); + self.write_command(ClearToEndOfLine); if i == 0 { - osc_133_prompt_start(self); + mark_prompt_start(self); } self.write_str(&left_prompt[start..=line_break]); start = line_break + 1; @@ -1147,7 +1145,6 @@ fn s_line(zelf: &Screen, i: usize) -> &Line { // Don't issue clr_eos if we think the cursor will end up in the last column - see #6951. let should_clear_screen_this_line = need_clear_screen && i + 1 == self.desired.line_count() - && term.clr_eos.is_some() && !(self.desired.cursor.x == 0 && self.desired.cursor.y == self.desired.line_count()); @@ -1162,17 +1159,14 @@ fn s_line(zelf: &Screen, i: usize) -> &Line { }; let mut skip_prefix = shared_prefix; if shared_prefix < o_line(self, i).indentation { - if o_line(self, i).indentation > s_line(self, i).indentation - && !has_cleared_screen - && term.clr_eol.is_some() - && term.clr_eos.is_some() + if o_line(self, i).indentation > s_line(self, i).indentation && !has_cleared_screen { set_color(self, HighlightSpec::new()); self.r#move(0, i); - self.write_mbs_if_some(if should_clear_screen_this_line { - &term.clr_eos + self.write_command(if should_clear_screen_this_line { + ClearToEndOfScreen } else { - &term.clr_eol + ClearToEndOfLine }); has_cleared_screen = should_clear_screen_this_line; has_cleared_line = true; @@ -1228,7 +1222,7 @@ fn s_line(zelf: &Screen, i: usize) -> &Line { { set_color(self, HighlightSpec::new()); self.r#move(current_width, i); - self.write_mbs_if_some(&term.clr_eos.as_ref()); + self.write_command(ClearToEndOfScreen); has_cleared_screen = true; } if done { @@ -1271,9 +1265,9 @@ fn s_line(zelf: &Screen, i: usize) -> &Line { // This means that we switch background correctly on the next, // including our weird implicit bolding. set_color(self, HighlightSpec::new()); - if let (true, Some(clr_eol)) = (clear_remainder, term.clr_eol.as_ref()) { + if clear_remainder { self.r#move(current_width, i); - self.write_mbs(clr_eol); + self.write_command(ClearToEndOfLine); } // Output any rprompt if this is the first line. @@ -1310,13 +1304,11 @@ fn s_line(zelf: &Screen, i: usize) -> &Line { } // Clear remaining lines (if any) if we haven't cleared the screen. - if let (false, true, Some(clr_eol)) = - (has_cleared_screen, need_clear_screen, term.clr_eol.as_ref()) - { + if !has_cleared_screen && need_clear_screen { set_color(self, HighlightSpec::new()); for i in self.desired.line_count()..lines_with_stuff { self.r#move(0, i); - self.write_mbs(clr_eol); + self.write_command(ClearToEndOfLine); } } @@ -1342,7 +1334,7 @@ pub fn mtime_stdout_stderr() -> (Option, Option) { pub fn screen_force_clear_to_end() { Outputter::stdoutput() .borrow_mut() - .tputs_if_some(&term().clr_eos); + .write_command(ClearToEndOfScreen); } /// Information about the layout of a prompt. @@ -1382,6 +1374,7 @@ pub struct LayoutCache { // Singleton of the cached escape sequences seen in prompts and similar strings. // Note this is deliberately exported so that init_terminal can clear it. +// TODO un-export this once we remove the terminfo option. pub static LAYOUT_CACHE_SHARED: Mutex = Mutex::new(LayoutCache::new()); impl LayoutCache { @@ -1547,13 +1540,13 @@ pub fn find_prompt_layout( /// Returns the number of characters in the escape code starting at 'code'. We only handle sequences /// that begin with \x1B. If it doesn't we return zero. We also return zero if we don't recognize -/// the escape sequence based on querying terminfo and other heuristics. +/// the escape sequence. pub fn escape_code_length(code: &wstr) -> Option { if code.char_at(0) != '\x1B' { return None; } - is_visual_escape_seq(code) + is_terminfo_escape_seq(code) .or_else(|| is_screen_name_escape_seq(code)) .or_else(|| is_osc_escape_seq(code)) .or_else(|| is_three_byte_escape_seq(code)) @@ -1561,16 +1554,11 @@ pub fn escape_code_length(code: &wstr) -> Option { .or_else(|| is_two_byte_escape_seq(code)) } -pub fn screen_clear() -> WString { - term() - .clear_screen - .as_ref() - .map(|clear_screen| str2wcstring(clear_screen.as_bytes())) - .unwrap_or_default() -} - static MIDNIGHT_COMMANDER_HACK: RelaxedAtomicBool = RelaxedAtomicBool::new(false); +/// Midnight Commander tries to extract the last line of the prompt, and does so in a way that is +/// broken if you do '\r' after it like we normally do. +/// See https://midnight-commander.org/ticket/4258. pub fn screen_set_midnight_commander_hack() { MIDNIGHT_COMMANDER_HACK.store(true) } @@ -1595,16 +1583,19 @@ fn try_sequence(seq: &[u8], s: &wstr) -> usize { /// Returns the number of columns left until the next tab stop, given the current cursor position. fn next_tab_stop(current_line_width: usize) -> usize { - // Assume tab stops every 8 characters if undefined. - let tab_width = term().init_tabs.unwrap_or(8); + // Assume tab stops every 8 characters. + let tab_width = if use_terminfo() { + crate::terminal::term().init_tabs.unwrap_or(8) + } else { + 8 + }; ((current_line_width / tab_width) + 1) * tab_width } /// Whether we permit soft wrapping. If so, in some cases we don't explicitly move to the second /// physical line on a wrapped logical line; instead we just output it. fn allow_soft_wrap() -> bool { - // Should we be looking at eat_newline_glitch as well? - term().auto_right_margin + !use_terminfo() || crate::terminal::term().auto_right_margin } /// Does this look like the escape sequence for setting a screen name? @@ -1721,8 +1712,11 @@ fn is_csi_style_escape_seq(code: &wstr) -> Option { /// Detect whether the escape sequence sets one of the terminal attributes that affects how text is /// displayed other than the color. -fn is_visual_escape_seq(code: &wstr) -> Option { - let term = term(); +fn is_terminfo_escape_seq(code: &wstr) -> Option { + if !use_terminfo() { + return None; + } + let term = crate::terminal::term(); let esc2 = [ &term.enter_bold_mode, &term.exit_attribute_mode, @@ -1895,13 +1889,23 @@ fn line_shared_prefix(a: &Line, b: &Line) -> usize { idx } +pub(crate) static IS_DUMB: RelaxedAtomicBool = RelaxedAtomicBool::new(false); +pub(crate) static ONLY_GRAYSCALE: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + /// Returns true if we are using a dumb terminal. pub(crate) fn is_dumb() -> bool { - let term = term(); - term.cursor_up.is_none() - || term.cursor_down.is_none() - || term.cursor_left.is_none() - || term.cursor_right.is_none() + if use_terminfo() { + let term = crate::terminal::term(); + return term.cursor_up.is_none() + || term.cursor_down.is_none() + || term.cursor_left.is_none() + || term.cursor_right.is_none(); + } + IS_DUMB.load() +} + +pub(crate) fn only_grayscale() -> bool { + ONLY_GRAYSCALE.load() } // Exposed for testing. diff --git a/src/terminal.rs b/src/terminal.rs index 8d105d9c9..6e20fd088 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,16 +1,852 @@ -//! A wrapper around the terminfo library to expose the functionality that fish needs. -//! Note that this is, on the whole, extremely little, and in practice terminfo -//! barely matters anymore. Even the few terminals in use that don't use "xterm-256color" -//! do not differ much. - +// Generic output functions. +use crate::color::{self, Color24, RgbColor}; use crate::common::ToCString; +use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle}; +use crate::env::EnvVar; +use crate::future_feature_flags::{self, FeatureFlag}; +use crate::global_safety::RelaxedAtomicBool; +use crate::screen::{is_dumb, only_grayscale}; +use crate::threads::MainThread; +use crate::wchar::prelude::*; use crate::FLOGF; +use bitflags::bitflags; +use std::cell::{RefCell, RefMut}; use std::env; use std::ffi::{CStr, CString}; +use std::ops::{Deref, DerefMut}; +use std::os::fd::RawFd; +use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; +use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; use std::sync::Mutex; +bitflags! { + #[derive(Copy, Clone, Default)] + pub struct ColorSupport: u8 { + const TERM_256COLOR = 1<<0; + const TERM_24BIT = 1<<1; + } +} + +/// Whether term256 and term24bit are supported. +static COLOR_SUPPORT: AtomicU8 = AtomicU8::new(0); + +pub fn get_color_support() -> ColorSupport { + let val = COLOR_SUPPORT.load(Ordering::Relaxed); + ColorSupport::from_bits_truncate(val) +} + +pub fn set_color_support(val: ColorSupport) { + COLOR_SUPPORT.store(val.bits(), Ordering::Relaxed); +} + +#[derive(Clone)] +pub(crate) enum TerminalCommand<'a> { + // Text attributes + ExitAttributeMode, + EnterBoldMode, + EnterDimMode, + EnterItalicsMode, + EnterUnderlineMode, + EnterReverseMode, + EnterStandoutMode, + ExitItalicsMode, + ExitUnderlineMode, + + // Screen clearing + ClearScreen, + ClearToEndOfLine, + ClearToEndOfScreen, + + // Colors + SelectPaletteColor(/*is_foreground=*/ bool, u8), + SelectRgbColor(/*is_foreground=*/ bool, u8, u8, u8), + + // Cursor Movement + CursorUp, + CursorDown, + CursorLeft, + CursorRight, + CursorMove(CardinalDirection, usize), + + // Commands related to querying (used for backwards-incompatible features). + QueryPrimaryDeviceAttribute, + QueryXtversion, + QueryXtgettcap(&'static str), + + DecsetAlternateScreenBuffer, + DecrstAlternateScreenBuffer, + DecsetSynchronizedUpdate, + DecrstSynchronizedUpdate, + + QuerySynchronizedOutput, + + // Keyboard protocols + KittyKeyboardProgressiveEnhancementsEnable, + KittyKeyboardProgressiveEnhancementsDisable, + QueryKittyKeyboardProgressiveEnhancements, + + ModifyOtherKeysEnable, + ModifyOtherKeysDisable, + + ApplicationKeypadModeEnable, + ApplicationKeypadModeDisable, + + // OSC sequences + // + // Note that OSC 7 and OSC 52 are written from fish script, and OSC 8 is written in our + // man pages (via "man_show_urls"). + Osc0WindowTitle(&'a [WString]), + Osc133CommandStart(&'a wstr), + Osc133PromptStart, + Osc133CommandFinished(libc::c_int), + + // Other terminal features + QueryCursorPosition, + ScrollForward(usize), + + DecsetShowCursor, + DecrstMouseTracking, + DecsetFocusReporting, + DecrstFocusReporting, + DecsetBracketedPaste, + DecrstBracketedPaste, +} + +pub(crate) trait Output { + fn write_bytes(&mut self, buf: &[u8]); + + fn by_ref(&mut self) -> &mut Self + where + Self: Sized, + { + self + } + + fn write_command(&mut self, cmd: TerminalCommand<'_>) -> bool + where + Self: Sized, + { + use TerminalCommand::*; + if is_dumb() { + assert!(!matches!(cmd, CursorDown)); + return false; + } + let ti = maybe_terminfo; + fn write(out: &mut impl Output, sequence: &'static [u8]) -> bool { + out.write_bytes(sequence); + true + } + match cmd { + ExitAttributeMode => ti(self, b"\x1b[m", |t| &t.exit_attribute_mode), + EnterBoldMode => ti(self, b"\x1b[1m", |t| &t.enter_bold_mode), + EnterDimMode => ti(self, b"\x1b[2m", |t| &t.enter_dim_mode), + EnterItalicsMode => ti(self, b"\x1b[3m", |t| &t.enter_italics_mode), + EnterUnderlineMode => ti(self, b"\x1b[4m", |t| &t.enter_underline_mode), + EnterReverseMode => ti(self, b"\x1b[7m", |t| &t.enter_reverse_mode), + EnterStandoutMode => ti(self, b"\x1b[23m", |t| &t.enter_standout_mode), + ExitItalicsMode => ti(self, b"\x1b[24m", |t| &t.exit_italics_mode), + ExitUnderlineMode => ti(self, b"\x1b[m", |t| &t.exit_underline_mode), + ClearScreen => ti(self, b"\x1b[H\x1b[2J", |term| &term.clear_screen), + ClearToEndOfLine => ti(self, b"\x1b[K", |term| &term.clr_eol), + ClearToEndOfScreen => ti(self, b"\x1b[J", |term| &term.clr_eos), + SelectPaletteColor(is_foreground, idx) => palette_color(self, is_foreground, idx), + SelectRgbColor(is_foreground, r, g, b) => rgb_color(self, is_foreground, r, g, b), + CursorUp => ti(self, b"\x1b[A", |term| &term.cursor_up), + CursorDown => ti(self, b"\n", |term| &term.cursor_down), + CursorLeft => ti(self, b"\x08", |term| &term.cursor_left), + CursorRight => ti(self, b"\x1b[C", |term| &term.cursor_right), + CursorMove(direction, steps) => cursor_move(self, direction, steps), + QueryPrimaryDeviceAttribute => write(self, b"\x1b[0c"), + QueryXtversion => write(self, b"\x1b[>0q"), + QueryXtgettcap(cap) => query_xtgettcap(self, cap), + DecsetAlternateScreenBuffer => write(self, b"\x1b[?1049h"), + DecrstAlternateScreenBuffer => write(self, b"\x1b[?1049l"), + DecsetSynchronizedUpdate => write(self, b"\x1b[?2026h"), + DecrstSynchronizedUpdate => write(self, b"\x1b[?2026l"), + QuerySynchronizedOutput => write(self, b"\x1b[?2026$p"), + KittyKeyboardProgressiveEnhancementsEnable => write(self, b"\x1b[=5u"), + KittyKeyboardProgressiveEnhancementsDisable => write(self, b"\x1b[=0u"), + QueryKittyKeyboardProgressiveEnhancements => query_kitty_progressive_enhancements(self), + ModifyOtherKeysEnable => write(self, b"\x1b[>4;1m"), + ModifyOtherKeysDisable => write(self, b"\x1b[>4;0m"), + ApplicationKeypadModeEnable => write(self, b"\x1b="), + ApplicationKeypadModeDisable => write(self, b"\x1b>"), + Osc0WindowTitle(title) => osc_0_window_title(self, title), + Osc133PromptStart => write(self, OSC_133_PROMPT_START), + Osc133CommandStart(command) => osc_133_command_start(self, command), + Osc133CommandFinished(s) => osc_133_command_finished(self, s), + QueryCursorPosition => write(self, b"\x1b[6n"), + ScrollForward(lines) => scroll_forward(self, lines), + DecsetShowCursor => write(self, b"\x1b[?25h"), + DecrstMouseTracking => write(self, b"\x1b[?1000l"), + DecsetFocusReporting => write(self, b"\x1b[?1004h"), + DecrstFocusReporting => write(self, b"\x1b[?1004l"), + DecsetBracketedPaste => write(self, b"\x1b[?2004h"), + DecrstBracketedPaste => write(self, b"\x1b[?2004l"), + } + } +} + +impl Output for Vec { + fn write_bytes(&mut self, buf: &[u8]) { + self.extend_from_slice(buf); + } +} + +fn maybe_terminfo( + out: &mut impl Output, + sequence: &'static [u8], + terminfo: fn(&Term) -> &Option, +) -> bool { + if use_terminfo() { + let term = crate::terminal::term(); + let Some(sequence) = (terminfo)(&term) else { + return false; + }; + out.write_bytes(sequence.to_bytes()); + } else { + out.write_bytes(sequence); + } + true +} + +#[repr(u8)] +pub(crate) enum Capability { + Unknown, + Supported, + NotSupported, +} + +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 SYNCHRONIZED_OUTPUT_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +pub(crate) fn use_terminfo() -> bool { + !future_feature_flags::test(FeatureFlag::ignore_terminfo) && TERM.lock().unwrap().is_some() +} + +fn palette_color(out: &mut impl Output, foreground: bool, mut idx: u8) -> bool { + if only_grayscale() + && !(RgbColor { + typ: color::Type::Named { idx }, + flags: color::Flags::DEFAULT, + }) + .is_grayscale() + { + return false; + } + if use_terminfo() { + let term = crate::terminal::term(); + let Some(command) = (if foreground { + term.set_a_foreground + .as_ref() + .or(term.set_foreground.as_ref()) + } else { + term.set_a_background + .as_ref() + .or(term.set_background.as_ref()) + }) else { + return false; + }; + if term_supports_color_natively(&term, idx) { + let Some(sequence) = tparm1(command, idx.into()) else { + return false; + }; + out.write_bytes(sequence.as_bytes()); + return true; + } + if term.max_colors == Some(8) && idx > 8 { + idx -= 8; + } + } + let bg = if foreground { 0 } else { 10 }; + match idx { + 0..=7 => write_to_output!(out, "\x1b[{}m", 30 + bg + idx), + 8..=15 => write_to_output!(out, "\x1b[{}m", 90 + bg + (idx - 8)), + _ => write_to_output!(out, "\x1b[{};5;{}m", 38 + bg, idx), + }; + true +} + +/// Returns true if we think tparm can handle outputting a color index. +fn term_supports_color_natively(term: &Term, c: u8) -> bool { + #[allow(clippy::int_plus_one)] + if let Some(max_colors) = term.max_colors { + max_colors >= usize::from(c) + 1 + } else { + false + } +} + +fn rgb_color(out: &mut impl Output, foreground: bool, r: u8, g: u8, b: u8) -> bool { + // Foreground: ^[38;2;;;m + // Background: ^[48;2;;;m + write_to_output!( + out, + "\x1b[{};2;{};{};{}m", + if foreground { 38 } else { 48 }, + r, + g, + b + ); + true +} + +#[derive(Clone)] +pub(crate) enum CardinalDirection { + Up, + Left, + Right, +} + +fn cursor_move(out: &mut impl Output, direction: CardinalDirection, steps: usize) -> bool { + if use_terminfo() { + let term = crate::terminal::term(); + if let Some(command) = match direction { + CardinalDirection::Up => None, // Historical + CardinalDirection::Left => term.parm_left_cursor.as_ref(), + CardinalDirection::Right => term.parm_right_cursor.as_ref(), + } { + if let Some(sequence) = tparm1(command, i32::try_from(steps).unwrap()) { + out.write_bytes(sequence.as_bytes()); + return true; + } + } else if let Some(command) = match direction { + CardinalDirection::Up => term.cursor_up.as_ref(), + CardinalDirection::Left => term.cursor_left.as_ref(), + CardinalDirection::Right => term.cursor_right.as_ref(), + } { + for _i in 0..steps { + out.write_bytes(command.as_bytes()); + } + return true; + } + return false; + } + write_to_output!( + out, + "\x1b[{steps}{}", + match direction { + CardinalDirection::Up => 'A', + CardinalDirection::Left => 'D', + CardinalDirection::Right => 'C', + } + ); + true +} + +fn query_xtgettcap(out: &mut impl Output, cap: &str) -> bool { + write_to_output!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap)); + true +} + +struct DisplayAsHex<'a>(&'a str); + +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() { + write!(f, "{:x}", byte)?; + } + Ok(()) + } +} + +fn query_kitty_progressive_enhancements(out: &mut impl Output) -> bool { + if std::env::var_os("TERM").is_some_and(|term| term.as_os_str().as_bytes() == b"st-256color") { + return false; + } + out.write_bytes(b"\x1b[?u"); + true +} + +fn osc_0_window_title(out: &mut impl Output, title: &[WString]) -> bool { + out.write_bytes(b"\x1b]0;"); + for title_line in title { + out.write_bytes(&wcs2string(title_line)); + } + out.write_bytes(b"\x07"); // BEL + true +} + +const OSC_133_PROMPT_START: &[u8] = b"\x1b]133;A;click_events=1\x07"; + +fn osc_133_command_start(out: &mut impl Output, command: &wstr) -> bool { + write_to_output!( + out, + "\x1b]133;C;cmdline_url={}\x07", + escape_string(command, EscapeStringStyle::Url), + ); + true +} + +fn osc_133_command_finished(out: &mut impl Output, exit_status: libc::c_int) -> bool { + write_to_output!(out, "\x1b]133;D;{}\x07", exit_status); + true +} + +fn scroll_forward(out: &mut impl Output, lines: usize) -> bool { + assert!(SCROLL_FORWARD_SUPPORTED.load()); + write_to_output!(out, "\x1b[{}S", lines); + true +} + +fn index_for_color(c: RgbColor) -> u8 { + if c.is_named() || !(get_color_support().contains(ColorSupport::TERM_256COLOR)) { + return c.to_name_index(); + } + c.to_term256_index() +} + +fn write_foreground_color(outp: &mut Outputter, idx: u8) -> bool { + outp.write_command(TerminalCommand::SelectPaletteColor(true, idx)) +} + +fn write_background_color(outp: &mut Outputter, idx: u8) -> bool { + outp.write_command(TerminalCommand::SelectPaletteColor(false, idx)) +} + +pub struct Outputter { + /// Storage for buffered contents. + contents: Vec, + + /// Count of how many outstanding begin_buffering() calls there are. + buffer_count: u32, + + /// fd to output to, or -1 for none. + fd: RawFd, + + /// Foreground. + last_color: RgbColor, + + /// Background. + last_color2: RgbColor, + + was_bold: bool, + was_underline: bool, + was_italics: bool, + was_dim: bool, + was_reverse: bool, +} + +impl Outputter { + /// Construct an outputter which outputs to a given fd. + /// If the fd is negative, the outputter will buffer its output. + const fn new_from_fd(fd: RawFd) -> Self { + Self { + contents: Vec::new(), + buffer_count: 0, + fd, + last_color: RgbColor::NORMAL, + last_color2: RgbColor::NORMAL, + was_bold: false, + was_underline: false, + was_italics: false, + was_dim: false, + was_reverse: false, + } + } + + /// Construct an outputter which outputs to its string buffer. + pub fn new_buffering() -> Self { + Self::new_from_fd(-1) + } + + fn reset_modes(&mut self) { + self.was_bold = false; + self.was_underline = false; + self.was_italics = false; + self.was_dim = false; + self.was_reverse = false; + } + + fn maybe_flush(&mut self) { + if self.fd >= 0 && self.buffer_count == 0 { + self.flush_to(self.fd); + } + } + + /// Unconditionally write the color string to the output. + /// Exported for builtin_set_color's usage only. + pub fn write_color(&mut self, color: RgbColor, is_fg: bool) -> bool { + let supports_term24bit = get_color_support().contains(ColorSupport::TERM_24BIT); + if !supports_term24bit || !color.is_rgb() { + // Indexed or non-24 bit color. + let idx = index_for_color(color); + if is_fg { + return write_foreground_color(self, idx); + } else { + return write_background_color(self, idx); + }; + } + + if only_grayscale() && color.is_grayscale() { + return false; + } + + // 24 bit! + let Color24 { r, g, b } = color.to_color24(); + self.write_command(TerminalCommand::SelectRgbColor(is_fg, r, g, b)) + } + + /// Sets the fg and bg color. May be called as often as you like, since if the new color is the same + /// as the previous, nothing will be written. Negative values for set_color will also be ignored. + /// Since the command this function emits can potentially cause the screen to flicker, the + /// function takes care to write as little as possible. + /// + /// Possible values for colors are RgbColor colors or special values like RgbColor::NORMAL + /// + /// In order to set the color to normal, three commands may have to be written. + /// + /// - First a command to set the color, such as set_a_foreground. This is needed because otherwise + /// the previous strings colors might be removed as well. + /// + /// - After that we write the exit_attribute_mode command to reset all color attributes. + /// + /// - Lastly we may need to write set_a_background or set_a_foreground to set the other half of the + /// color pair to what it should be. + #[allow(clippy::if_same_then_else)] + pub fn set_color(&mut self, mut fg: RgbColor, mut bg: RgbColor) { + const normal: RgbColor = RgbColor::NORMAL; + let mut bg_set = false; + let mut last_bg_set = false; + let is_bold = fg.is_bold() || bg.is_bold(); + let is_underline = fg.is_underline() || bg.is_underline(); + let is_italics = fg.is_italics() || bg.is_italics(); + let is_dim = fg.is_dim() || bg.is_dim(); + let is_reverse = fg.is_reverse() || bg.is_reverse(); + + if fg.is_reset() || bg.is_reset() { + #[allow(unused_assignments)] + { + fg = normal; + bg = normal; + } + self.reset_modes(); + // If we exit attribute mode, we must first set a color, or previously colored text might + // lose its color. Terminals are weird... + write_foreground_color(self, 0); + self.write_command(ExitAttributeMode); + return; + } + use TerminalCommand::{ + EnterBoldMode, EnterDimMode, EnterItalicsMode, EnterReverseMode, EnterStandoutMode, + EnterUnderlineMode, ExitAttributeMode, ExitItalicsMode, ExitUnderlineMode, + }; + if (self.was_bold && !is_bold) + || (self.was_dim && !is_dim) + || (self.was_reverse && !is_reverse) + { + // Only way to exit bold/dim/reverse mode is a reset of all attributes. + self.write_command(ExitAttributeMode); + self.last_color = normal; + self.last_color2 = normal; + self.reset_modes(); + } + if !self.last_color2.is_special() { + // Background was set. + // "Special" here refers to the special "normal", "reset" and "none" colors, + // that really just disable the background. + last_bg_set = true; + } + if !bg.is_special() { + // Background is set. + bg_set = true; + if fg == bg { + fg = if bg == RgbColor::WHITE { + RgbColor::BLACK + } else { + RgbColor::WHITE + }; + } + } + + if bg_set && !last_bg_set { + // Background color changed and is set, so we enter bold mode to make reading easier. + // This means bold mode is _always_ on when the background color is set. + self.write_command(EnterBoldMode); + } + if !bg_set && last_bg_set { + // Background color changed and is no longer set, so we exit bold mode. + self.write_command(ExitAttributeMode); + self.reset_modes(); + // We don't know if exit_attribute_mode resets colors, so we set it to something known. + if write_foreground_color(self, 0) { + self.last_color = RgbColor::BLACK; + } + } + + if self.last_color != fg { + if fg.is_normal() { + write_foreground_color(self, 0); + self.write_command(ExitAttributeMode); + + self.last_color2 = RgbColor::NORMAL; + self.reset_modes(); + } else if !fg.is_special() { + self.write_color(fg, true /* foreground */); + } + } + self.last_color = fg; + + if self.last_color2 != bg { + if bg.is_normal() { + write_background_color(self, 0); + + self.write_command(ExitAttributeMode); + if !self.last_color.is_normal() { + self.write_color(self.last_color, true /* foreground */); + } + self.reset_modes(); + self.last_color2 = bg; + } else if !bg.is_special() { + self.write_color(bg, false /* not foreground */); + self.last_color2 = bg; + } + } + + // Lastly, we set bold, underline, italics, dim, and reverse modes correctly. + if is_bold && !self.was_bold && !bg_set && self.write_command(EnterBoldMode) { + self.was_bold = is_bold; + } + + if !self.was_underline && is_underline && self.write_command(EnterUnderlineMode) { + self.was_underline = is_underline; + } else if self.was_underline && !is_underline && self.write_command(ExitUnderlineMode) { + self.was_underline = is_underline; + } + + if self.was_italics && !is_italics && self.write_command(ExitItalicsMode) { + self.was_italics = is_italics; + } else if !self.was_italics && is_italics && self.write_command(EnterItalicsMode) { + self.was_italics = is_italics; + } + + if is_dim && !self.was_dim && self.write_command(EnterDimMode) { + self.was_dim = is_dim; + } + // N.B. there is no exit_dim_mode, it's handled by exit_attribute_mode above. + + if is_reverse && !self.was_reverse { + if self.write_command(EnterReverseMode) || self.write_command(EnterStandoutMode) { + self.was_reverse = is_reverse; + } + } + } + + /// Write a wide character to the receiver. + pub fn writech(&mut self, ch: char) { + self.write_wstr(wstr::from_char_slice(&[ch])); + } + + /// Write a narrow character to the receiver. + pub fn push(&mut self, ch: u8) { + self.contents.push(ch); + self.maybe_flush(); + } + + /// Write a wide string. + pub fn write_wstr(&mut self, str: &wstr) { + wcs2string_appending(&mut self.contents, str); + self.maybe_flush(); + } + + /// Return the "output" contents. + pub fn contents(&self) -> &[u8] { + &self.contents + } + + /// Output any buffered data to the given `fd`. + fn flush_to(&mut self, fd: RawFd) { + if fd >= 0 && !self.contents.is_empty() { + let _ = common::write_loop(&fd, &self.contents); + self.contents.clear(); + } + } + + /// Begins buffering. Output will not be automatically flushed until a corresponding + /// end_buffering() call. + pub fn begin_buffering(&mut self) { + self.buffer_count += 1; + assert!(self.buffer_count > 0, "buffer_count overflow"); + } + + /// Balance a begin_buffering() call. + pub fn end_buffering(&mut self) { + assert!(self.buffer_count > 0, "buffer_count underflow"); + self.buffer_count -= 1; + self.maybe_flush(); + } +} + +impl Output for Outputter { + fn write_bytes(&mut self, buf: &[u8]) { + self.contents.extend_from_slice(buf); + self.maybe_flush(); + } +} + +impl Outputter { + /// Access the outputter for stdout. + /// This should only be used from the main thread. + pub fn stdoutput() -> &'static RefCell { + static STDOUTPUT: MainThread> = + MainThread::new(RefCell::new(Outputter::new_from_fd(libc::STDOUT_FILENO))); + STDOUTPUT.get() + } +} + +pub struct BufferedOutputter<'a>(RefMut<'a, Outputter>); + +impl<'a> BufferedOutputter<'a> { + pub fn new(outputter: &'a RefCell) -> Self { + let mut outputter = outputter.borrow_mut(); + outputter.begin_buffering(); + Self(outputter) + } +} + +impl<'a> Drop for BufferedOutputter<'a> { + fn drop(&mut self) { + self.0.end_buffering(); + } +} + +impl Deref for BufferedOutputter<'_> { + type Target = Outputter; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BufferedOutputter<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> Output for BufferedOutputter<'a> { + fn write_bytes(&mut self, buf: &[u8]) { + self.0.write_bytes(buf); + } +} + +/// Given a list of RgbColor, pick the "best" one, as determined by the color support. Returns +/// RgbColor::NONE if empty. +pub fn best_color(candidates: &[RgbColor], support: ColorSupport) -> RgbColor { + if candidates.is_empty() { + return RgbColor::NONE; + } + + let mut first_rgb = RgbColor::NONE; + let mut first_named = RgbColor::NONE; + for color in candidates { + if first_rgb.is_none() && color.is_rgb() { + first_rgb = *color; + } + if first_named.is_none() && color.is_named() { + first_named = *color; + } + } + + // If we have both RGB and named colors, then prefer rgb if term256 is supported. + let mut result; + let has_term256 = support.contains(ColorSupport::TERM_256COLOR); + if (!first_rgb.is_none() && has_term256) || first_named.is_none() { + result = first_rgb; + } else { + result = first_named; + } + if result.is_none() { + result = candidates[0]; + } + result +} + +/// Return the internal color code representing the specified color. +/// TODO: This code should be refactored to enable sharing with builtin_set_color. +/// In particular, the argument parsing still isn't fully capable. +pub fn parse_color(var: &EnvVar, is_background: bool) -> RgbColor { + let mut result = parse_color_maybe_none(var, is_background); + if result.is_none() { + result.typ = color::Type::Normal; + } + result +} + +pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> RgbColor { + let mut is_bold = false; + let mut is_underline = false; + let mut is_italics = false; + let mut is_dim = false; + let mut is_reverse = false; + + let mut candidates: Vec = Vec::new(); + + let prefix = L!("--background="); + + let mut next_is_background = false; + let mut color_name = WString::new(); + for next in var.as_list() { + color_name.clear(); + #[allow(clippy::collapsible_else_if)] + if is_background { + if color_name.is_empty() && next_is_background { + color_name = next.to_owned(); + next_is_background = false; + } else if next.starts_with(prefix) { + // Look for something like "--background=red". + color_name = next.slice_from(prefix.char_count()).to_owned(); + } else if next == "--background" || next == "-b" { + // Without argument attached the next token is the color + // - if it's another option it's an error. + next_is_background = true; + } else if next == "--reverse" || next == "-r" { + // Reverse should be meaningful in either context + is_reverse = true; + } else if next.starts_with("-b") { + // Look for something like "-bred". + // Yes, that length is hardcoded. + color_name = next.slice_from(2).to_owned(); + } + } else { + if next == "--bold" || next == "-o" { + is_bold = true; + } else if next == "--underline" || next == "-u" { + is_underline = true; + } else if next == "--italics" || next == "-i" { + is_italics = true; + } else if next == "--dim" || next == "-d" { + is_dim = true; + } else if next == "--reverse" || next == "-r" { + is_reverse = true; + } else { + color_name = next.clone(); + } + } + + if !color_name.is_empty() { + let color: Option = RgbColor::from_wstr(&color_name); + if let Some(color) = color { + candidates.push(color); + } + } + } + + let mut result = best_color(&candidates, get_color_support()); + result.set_bold(is_bold); + result.set_underline(is_underline); + result.set_italics(is_italics); + result.set_dim(is_dim); + result.set_reverse(is_reverse); + result +} + /// The [`Term`] singleton. Initialized via a call to [`setup()`] and surfaced to the outside world via [`term()`]. /// /// We can't just use an [`AtomicPtr>`](std::sync::atomic::AtomicPtr) here because there's a race condition when the old Arc @@ -44,7 +880,6 @@ pub struct Term { pub set_a_background: Option, pub set_background: Option, pub exit_attribute_mode: Option, - pub set_title: Option, pub clear_screen: Option, pub cursor_up: Option, pub cursor_down: Option, @@ -83,7 +918,6 @@ fn new(db: terminfo::Database) -> Self { set_a_background: get_str_cap(&db, "AB"), set_background: get_str_cap(&db, "Sb"), exit_attribute_mode: get_str_cap(&db, "me"), - set_title: get_str_cap(&db, "ts"), clear_screen: get_str_cap(&db, "cl"), cursor_up: get_str_cap(&db, "up"), cursor_down: get_str_cap(&db, "do"), @@ -108,14 +942,8 @@ fn new(db: terminfo::Database) -> Self { /// Initializes our database from $TERM. /// Returns a reference to the newly initialized [`Term`] singleton on success or `None` if this failed. /// -/// The `configure` parameter may be set to a callback that takes an `&mut Term` reference to -/// override any capabilities before the `Term` is permanently made immutable. -/// /// Any existing references from `terminal::term()` will be invalidated by this call! -pub fn setup(configure: F) -> Option> -where - F: Fn(&mut Term), -{ +pub fn setup() { let mut global_term = TERM.lock().expect("Mutex poisoned!"); let res = terminfo::Database::from_env().or_else(|x| { @@ -144,61 +972,14 @@ pub fn setup(configure: F) -> Option> // Safely store the new Term instance or replace the old one. We have the lock so it's safe to // drop the old TERM value and have its refcount decremented - no one will be cloning it. if let Ok(result) = res { - // Create a new `Term` instance, prepopulate the capabilities we care about, and allow the - // caller to override any as needed. - let mut term = Term::new(result); - (configure)(&mut term); - - let term = Arc::new(term); + // Create a new `Term` instance, prepopulate the capabilities we care about. + let term = Arc::new(Term::new(result)); *global_term = Some(term.clone()); - Some(term) } else { *global_term = None; - None } } -pub fn setup_fallback_term() -> Arc { - let mut global_term = TERM.lock().expect("Mutex poisoned!"); - // These values extracted from xterm-256color from ncurses 6.4 - let term = Term { - enter_bold_mode: Some(CString::new("\x1b[1m").unwrap()), - enter_italics_mode: Some(CString::new("\x1b[3m").unwrap()), - exit_italics_mode: Some(CString::new("\x1b[23m").unwrap()), - enter_dim_mode: Some(CString::new("\x1b[2m").unwrap()), - enter_underline_mode: Some(CString::new("\x1b[4m").unwrap()), - exit_underline_mode: Some(CString::new("\x1b[24m").unwrap()), - enter_reverse_mode: Some(CString::new("\x1b[7m").unwrap()), - enter_standout_mode: Some(CString::new("\x1b[7m").unwrap()), - set_a_foreground: Some( - CString::new("\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m") - .unwrap(), - ), - set_a_background: Some( - CString::new("\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m") - .unwrap(), - ), - exit_attribute_mode: Some(CString::new("\x1b(B\x1b[m").unwrap()), - clear_screen: Some(CString::new("\x1b[H\x1b[2J").unwrap()), - cursor_up: Some(CString::new("\x1b[A").unwrap()), - cursor_down: Some(CString::new("\n").unwrap()), - cursor_left: Some(CString::new("\x08").unwrap()), - cursor_right: Some(CString::new("\x1b[C").unwrap()), - parm_left_cursor: Some(CString::new("\x1b[%p1%dD").unwrap()), - parm_right_cursor: Some(CString::new("\x1b[%p1%dC").unwrap()), - clr_eol: Some(CString::new("\x1b[K").unwrap()), - clr_eos: Some(CString::new("\x1b[J").unwrap()), - max_colors: Some(256), - init_tabs: Some(8), - eat_newline_glitch: true, - auto_right_margin: true, - ..Default::default() - }; - let term = Arc::new(term); - *global_term = Some(term.clone()); - term -} - /// Return a nonempty String capability from termcap, or None if missing or empty. /// Panics if the given code string does not contain exactly two bytes. fn get_str_cap(db: &terminfo::Database, code: &str) -> Option { diff --git a/src/tests/env.rs b/src/tests/env.rs index 3bee4f0c5..5b3b461e4 100644 --- a/src/tests/env.rs +++ b/src/tests/env.rs @@ -92,7 +92,7 @@ fn test_timezone_env_vars() { fn test_env_vars() { let _cleanup = test_init(); test_timezone_env_vars(); - // TODO: Add tests for the locale and ncurses vars. + // TODO: Add tests for the locale vars. let v1 = EnvVar::new(L!("abc").to_owned(), EnvVarFlags::EXPORT); let v2 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::EXPORT); diff --git a/tests/checks/read.fish b/tests/checks/read.fish index b1324b5b5..f1ea2f898 100644 --- a/tests/checks/read.fish +++ b/tests/checks/read.fish @@ -1,6 +1,4 @@ # RUN: fish=%fish %fish %s -# Set term again explicitly to ensure behavior. -set -gx TERM xterm # Read with no vars is not an error read diff --git a/tests/checks/status.fish b/tests/checks/status.fish index 3b2973268..b452261a1 100644 --- a/tests/checks/status.fish +++ b/tests/checks/status.fish @@ -59,6 +59,7 @@ status features #CHECK: ampersand-nobg-in-token on 3.4 & only backgrounds if followed by a separator #CHECK: remove-percent-self off 4.0 %self is no longer expanded (use $fish_pid) #CHECK: test-require-arg off 4.0 builtin test requires an argument +#CHECK: ignore-terminfo on 4.1 do not look up $TERM in terminfo database status test-feature stderr-nocaret echo $status #CHECK: 0 diff --git a/tests/checks/string.fish b/tests/checks/string.fish index bc6f25fb8..cb91753ff 100644 --- a/tests/checks/string.fish +++ b/tests/checks/string.fish @@ -1045,12 +1045,12 @@ end string shorten -m6 (set_color blue)s(set_color red)t(set_color --bold brwhite)rin(set_color red)g(set_color yellow)-shorten | string escape # Renders like "strin…" in colors # Note that red sequence that we still pass on because it's width 0. -# CHECK: \e\[34ms\e\[31mt\e\[1m\e\[37mrin\e\[31m… +# CHECK: \e\[34ms\e\[31mt\e\[1m\e\[97mrin\e\[31m… # See that colors aren't counted in ellipsis string shorten -c (set_color blue)s(set_color red)t(set_color --bold brwhite)rin(set_color red)g -m 8 abcdefghijklmno | string escape # Renders like "abstring" in colors -# CHECK: ab\e\[34ms\e\[31mt\e\[1m\e\[37mrin\e\[31mg +# CHECK: ab\e\[34ms\e\[31mt\e\[1m\e\[97mrin\e\[31mg set -l str (set_color blue)s(set_color red)t(set_color --bold brwhite)rin(set_color red)g(set_color yellow)-shorten for i in (seq 1 (string length -V -- $str)) diff --git a/tests/test_driver.py b/tests/test_driver.py index 80c856ed8..8dc806d4d 100755 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -72,8 +72,8 @@ def makeenv(script_path, home, test_helper_path): "XDG_DATA_DIRS", "LANGUAGE", "COLORTERM", - "KONSOLE_PROFILE_NAME", "KONSOLE_VERSION", + "TERM", # Erase this since we still respect TERM=dumb etc. "TERM_PROGRAM", "TERM_PROGRAM_VERSION", "VTE_VERSION", @@ -95,7 +95,6 @@ def makeenv(script_path, home, test_helper_path): "XDG_RUNTIME_DIR": xdg_runtime, "XDG_CACHE_HOME": xdg_cache, "fish_test_helper": home + "/fish_test_helper", - "TERM": "xterm", "LANG": "C", "LC_CTYPE": "en_US.UTF-8", } diff --git a/tests/test_functions/isolated-tmux-start.fish b/tests/test_functions/isolated-tmux-start.fish index 21cce5922..c1db91393 100644 --- a/tests/test_functions/isolated-tmux-start.fish +++ b/tests/test_functions/isolated-tmux-start.fish @@ -2,13 +2,7 @@ function isolated-tmux-start set -l tmpdir (mktemp -d) cd $tmpdir - begin - echo 'set -g mode-keys emacs' - # macOS lacks the tmux-256color terminfo, use screen-256color instead. - if test (uname) = Darwin - echo 'set -g default-terminal "screen-256color"' - end - end >./.tmux.conf + echo 'set -g mode-keys emacs' >.tmux.conf function isolated-tmux --inherit-variable tmpdir # tmux can't handle session sockets in paths that are too long, and macOS has a very long