Move scrollback-push feature detection to fish script

A lot of terminals support CSI Ps S.  Currently we only allow them
to use scrollback-up if they advertise it via XTGETTCAP.  This seems
surprising; it's better to make visible in fish script  whether this
is supposed to be working.  The canonical place is in "bind ctrl-l"
output.

The downside here is that we need to expose something that's rarely
useful. But the namespace pollution is not so bad, and this gives
users a nice paper trail instead of having to look in the source code.
This commit is contained in:
Johannes Altmanninger
2025-09-26 20:12:49 +02:00
parent 06bbac8ed6
commit b964072c11
19 changed files with 137 additions and 55 deletions

View File

@@ -69,7 +69,7 @@ New or improved bindings
- Before clearing the screen and redrawing, :kbd:`ctrl-l` now pushes all text located above the prompt to the terminal's scrollback,
via a new special input function :ref:`scrollback-push <special-input-functions-scrollback-push>`.
For compatibility with terminals that do not implement ECMA-48's :ref:`SCROLL UP <term-compat-indn>` command,
this function is only enabled if the terminal advertises support for that via :ref:`XTGETTCAP <term-compat-xtgettcap>`.
this function is only used if the terminal advertises support for that via :ref:`XTGETTCAP <term-compat-xtgettcap>`.
- Vi mode has learned :kbd:`ctrl-a` (increment) and :kbd:`ctrl-x` (decrement) (:issue:`11570`).
Completions

View File

@@ -187,9 +187,7 @@ The following special input functions are available:
``scrollback-push``
pushes earlier output to the terminal scrollback, positioning the prompt at the top.
For compatibility with terminals that do not provide the ECMA-48 ``SCROLL UP`` command,
this command does nothing unless the terminal advertises support for that command via :ref:`XTGETTCAP <term-compat-xtgettcap>`.
This requires the terminal to implement the ECMA-48 :ref:`SCROLL UP <term-compat-indn>` command and :ref:`cursor position reporting <term-compat-cursor-position-report>`.
``complete``
guess the remainder of the current token

View File

@@ -34,6 +34,7 @@ Synopsis
status get-file FILE
status list-files [PATH]
status terminal
status test-terminal-feature FEATURE
Description
-----------
@@ -118,14 +119,25 @@ The following operations (subcommands) are available:
This lists the files embedded in the fish binary at compile time. Only files where the path starts with the optional *FILE* argument are shown.
Returns 0 if something was printed, 1 otherwise.
.. _status-terminal:
**terminal**
Prints the name and version of the terminal fish is running inside (for example as reported via :ref:`XTVERSION <term-compat-xtversion>`).
This is not available during early startup but only just before the first interactive prompt is shown (possibly via builtin :doc:`read <read>`),
that is, on the first ``fish_prompt`` or ``fish_read`` :ref:`event <event>`.
This is not available during early startup but only starting from when the first interactive prompt is shown, possibly via builtin :doc:`read <read>`,
so before the first ``fish_prompt`` or ``fish_read`` :ref:`event <event>`.
.. _status-test-terminal-features:
**test-terminal-feature** *FEATURE*
Returns 0 when the terminal was :ref:`detected <term-compat-xtgettcap>` to support the given feature.
Like :ref:`status terminal <status-terminal>`, this only works once the first interactive prompt is shown.
Currently the only available *FEATURE* is :ref:`scroll-content-up <term-compat-indn>`.
An error will be printed when passed an unrecognized feature.
Notes
-----
For backwards compatibility most subcommands can also be specified as a long or short option. For example, rather than ``status is-login`` you can type ``status --is-login``. The flag forms are deprecated and may be removed in a future release (but not before fish 4.0).
For backwards compatibility most subcommands can also be specified as a long or short option. For example, rather than ``status is-login`` you can type ``status --is-login``. The flag forms are deprecated and may be removed in a future release.
You can only specify one subcommand per invocation even if you use the flag form of the subcommand.

View File

@@ -198,8 +198,9 @@ Optional Commands
``\e[ Ps S``
- indn
- Scroll up the content (not the viewport) Ps lines (called ``SCROLL UP`` / ``SU`` by ECMA-48 and "scroll forward" by terminfo)
This enables the :ref:`scrollback-push <special-input-functions-scrollback-push>` special input function which is used by :kbd:`ctrl-l`.
- Scroll up the content (not the viewport) Ps lines (called ``SCROLL UP`` / ``SU`` by ECMA-48 and "scroll forward" by terminfo).
When fish detects support for this feature, :ref:`status test-terminal-features scroll-content-up <status-test-terminal-features>` will return 0,
which enables the :kbd:`ctrl-l` binding to use the :ref:`scrollback-push <special-input-functions-scrollback-push>` special input function.
- ECMA-48
* - ``\e[= Ps u``, ``\e[? Ps u``
- n/a
@@ -215,10 +216,8 @@ Optional Commands
and the second parameter is the column number.
Both start at 1.
This is used inside terminals that either
- implement the OSC 133 :ref:`click_events <term-compat-osc-133>` feature.
- advertise the :ref:`indn <term-compat-indn>` capability via :ref:`XTGETTCAP <term-compat-xtgettcap>`
This is used by the :ref:`scrollback-push <special-input-functions-scrollback-push>` special input function,
and inside terminals that implement the OSC 133 :ref:`click_events <term-compat-osc-133>` feature.
- VT100
* - ``\e[ \x20 q``
- Se

View File

@@ -265,10 +265,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr "%ls: -S erfordert eine nicht leere Zeichenkette\n"
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr ""
@@ -811,6 +807,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65152,6 +65153,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -263,10 +263,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr ""
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr ""
@@ -809,6 +805,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65148,6 +65149,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -364,10 +364,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr ""
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr "%ls : -l requiert une chaîne non-vide\n"
@@ -910,6 +906,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65249,6 +65250,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -259,10 +259,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr ""
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr ""
@@ -805,6 +801,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65144,6 +65145,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -264,10 +264,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr ""
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr ""
@@ -810,6 +806,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65171,6 +65172,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -260,10 +260,6 @@ msgstr ""
msgid "%ls: --set-cursor option requires --add\n"
msgstr ""
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr ""
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr ""
@@ -806,6 +802,11 @@ msgstr ""
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65147,6 +65148,9 @@ msgstr ""
msgid "Test if snap has yet to be given the subcommand"
msgstr ""
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr ""

View File

@@ -257,10 +257,6 @@ msgstr "%ls: --set-cursor 参数不能为空\n"
msgid "%ls: --set-cursor option requires --add\n"
msgstr "%ls: --set-cursor 选项需要 --add\n"
#, c-format
msgid "%ls: -S requires a non-empty string\n"
msgstr "%ls: -S 需要非空字符串\n"
#, c-format
msgid "%ls: -l requires a non-empty string\n"
msgstr "%ls: -l 需要一个非空字符串\n"
@@ -803,6 +799,11 @@ msgstr "%lu\n"
msgid "%s"
msgstr ""
#
#, c-format
msgid "%s %s: unrecognized feature '%ls'"
msgstr ""
#
#, c-format
msgid "%s and %s are mutually exclusive"
@@ -65147,6 +65148,9 @@ msgstr "测试Snap命令是否应该有文件作为可能的补全"
msgid "Test if snap has yet to be given the subcommand"
msgstr "测试是否尚未给定 snap 命令"
msgid "Test if the terminal suports the given feature"
msgstr ""
msgid "Test if there is a subcommand given"
msgstr "测试是否有一个子命令"

View File

@@ -28,7 +28,8 @@ set -l __fish_status_all_commands \
print-stack-trace \
stack-trace \
terminal \
test-feature
test-feature \
test-terminal-feature
# These are the recognized flags.
complete -c status -s h -l help -d "Display help and exit"
@@ -66,6 +67,8 @@ complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_com
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a list-files -d "List embedded files contained in the fish binary"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a fish-path -d "Print the path to the current instance of fish"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a terminal -d "Print name and version of the terminal fish is running in"
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a test-terminal-feature -d "Test if the terminal suports the given feature"
complete -f -c status -n "__fish_seen_subcommand_from test-terminal-feature" -a 'scroll-content-up\t"Command for scrolling up terminal contents"'
# The job-control command changes fish state.
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a job-control -d "Set which jobs are under job control"

View File

@@ -80,7 +80,9 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
bind --preset $argv alt-l __fish_list_current_token
bind --preset $argv alt-o __fish_preview_current_file
bind --preset $argv alt-w __fish_whatis_current_token
bind --preset $argv ctrl-l scrollback-push clear-screen
bind --preset $argv ctrl-l \
'status test-terminal-feature scroll-content-up && commandline -f scrollback-push' \
clear-screen
bind --preset $argv ctrl-c clear-commandline
bind --preset $argv ctrl-u backward-kill-line
bind --preset $argv ctrl-k kill-line

View File

@@ -7,7 +7,7 @@
get_job_control_mode, get_login, is_interactive_session, set_job_control_mode, JobControl,
};
use crate::reader::reader_in_interactive_read;
use crate::tty_handoff::xtversion;
use crate::tty_handoff::{get_scroll_content_up_capability, xtversion};
use crate::wutil::{waccess, wbasename, wdirname, wrealpath, Error};
use libc::F_OK;
use nix::errno::Errno;
@@ -65,6 +65,7 @@ enum StatusCmd {
STATUS_GET_FILE,
STATUS_LIST_FILES,
STATUS_TERMINAL,
STATUS_TEST_TERMINAL_FEATURE,
}
str_enum!(
@@ -101,6 +102,7 @@ enum StatusCmd {
(STATUS_STACK_TRACE, "stack-trace"),
(STATUS_TERMINAL, "terminal"),
(STATUS_TEST_FEATURE, "test-feature"),
(STATUS_TEST_TERMINAL_FEATURE, "test-terminal-feature"),
);
/// Values that may be returned from the test-feature option to status.
@@ -539,6 +541,33 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Err(STATUS_CMD_ERROR);
}
}
c @ STATUS_TEST_TERMINAL_FEATURE => {
if args.len() != 1 {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
1,
args.len()
));
return Err(STATUS_INVALID_ARGS);
}
if args[0] != "scroll-content-up" {
streams.err.appendln(wgettext_fmt!(
"%s %s: unrecognized feature '%ls'",
cmd,
c.to_wstr(),
args[0]
));
return Err(STATUS_INVALID_ARGS);
};
return if get_scroll_content_up_capability() == Some(true) {
Ok(SUCCESS)
} else {
Err(STATUS_CMD_ERROR)
};
}
ref s => {
if !args.is_empty() {
streams.err.append(wgettext_fmt!(
@@ -733,7 +762,8 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
| STATUS_FEATURES
| STATUS_TEST_FEATURE
| STATUS_GET_FILE
| STATUS_LIST_FILES => {
| STATUS_LIST_FILES
| STATUS_TEST_TERMINAL_FEATURE => {
unreachable!("")
}
}

View File

@@ -147,7 +147,6 @@
tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED,
TOK_SHOW_COMMENTS,
};
use crate::tty_handoff::get_scroll_content_up_capability;
use crate::tty_handoff::SCROLL_CONTENT_UP_TERMINFO_CODE;
use crate::tty_handoff::{
get_tty_protocols_active, initialize_tty_metadata, safe_deactivate_tty_protocols, TtyHandoff,
@@ -3988,9 +3987,6 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
self.clear_screen_and_repaint();
}
rl::ScrollbackPush => {
if !get_scroll_content_up_capability().unwrap() {
return;
}
let query = self.blocking_query();
let Some(query) = &*query else {
drop(query);

View File

@@ -6,7 +6,6 @@
use crate::screen::{is_dumb, only_grayscale};
use crate::text_face::{TextFace, TextStyling, UnderlineStyle};
use crate::threads::MainThread;
use crate::tty_handoff::get_scroll_content_up_capability;
use crate::wchar::prelude::*;
use crate::FLOGF;
use bitflags::bitflags;
@@ -410,7 +409,6 @@ fn osc_133_command_finished(out: &mut impl Output, exit_status: libc::c_int) ->
}
fn scroll_content_up(out: &mut impl Output, lines: usize) -> bool {
assert!(get_scroll_content_up_capability().unwrap());
write_to_output!(out, "\x1b[{}S", lines);
true
}

View File

@@ -134,3 +134,15 @@ printf "%s\n" (test-stack-trace-copy | string replace \t '<TAB>')[1..4]
# CHECK: <TAB>called on line {{\d+}} of file {{.*}}/status.fish
# CHECK: in function 'test-stack-trace-copy'
# CHECK: <TAB>called on line {{\d+}} of file {{.*}}/status.fish
status test-terminal-feature
and should have failed on missing arg
# CHECKERR: status: test-terminal-feature: expected 1 arguments; got 0
status test-terminal-feature 1 2
and should have failed on too many args
# CHECKERR: status: test-terminal-feature: expected 1 arguments; got 2
status test-terminal-feature unrecognized-feature
and should have failed on unrecognized feature
# CHECKERR: status test-terminal-feature: unrecognized feature 'unrecognized-feature'
status test-terminal-feature scroll-content-up
and should have failed when running without a TTY

View File

@@ -154,7 +154,7 @@ class SpawnedProc(object):
name="fish",
timeout=TIMEOUT_SECS,
env=os.environ.copy(),
scroll_up_content_supported: bool = False,
scroll_content_up_supported: bool = False,
**kwargs,
):
"""Construct from a name, timeout, and environment.
@@ -184,7 +184,7 @@ class SpawnedProc(object):
)
self.spawn.delaybeforesend = None
self.prompt_counter = 0
if scroll_up_content_supported:
if scroll_content_up_supported:
# XTGETTCAP
key = bytes.hex(b"indn")
value = bytes.hex(b"dont-care")

View File

@@ -5,7 +5,7 @@ import os
env = os.environ.copy()
env["TERM"] = "not-dumb"
sp = SpawnedProc(env=env, scroll_up_content_supported=True)
sp = SpawnedProc(env=env, scroll_content_up_supported=True)
sendline, expect_prompt = sp.sendline, sp.expect_prompt
expect_prompt()
@@ -14,5 +14,9 @@ expect_prompt()
sp.send(control("g"))
sp.send_cursor_position_report(y=10, x=5)
sp.send_primary_device_attribute()
sp.expect_str("\x1b[9S\x1b[9A")
sp.send(control("l"))
sp.send_cursor_position_report(y=15, x=5)
sp.send_primary_device_attribute()
sp.expect_str("\x1b[14S\x1b[14A")