From 83b0294fc9be9b61eb867ce920812963c1e77145 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 21 Dec 2024 19:41:41 +0100 Subject: [PATCH] ctrl-l to scroll content instead of erasing screen On ctrl-l we send `\e[2J` (Erase in Display). Some terminals interpret this to scroll the screen content instead of clearing it. This happens on VTE-based terminals like gnome-terminal for example. The traditional behavior of ctrl-l erasing the screen (but not the rest of the scrollback) is weird because: 1. `ctrl-l` is the easiest and most portable way to push the prompt to the top (and repaint after glitches I guess). But it's also a destructive action, truncating scrollback. I use it for scrolling and am frequently surprised when my scroll back is missing information. 2. the amount of lines erased depends on the window size. It would be more intuitive to erase by prompts, or erase the text in the terminal selection. Let's use scrolling behavior on all terminals. The new command could also be named "push-to-scrollback", for consistency with others. But if we anticipate a want to add other scrollback-related commands, "scrollback-push" is better. This causes tests/checks/tmux-history-search.fish to fail; that test seems pretty broken; M-d (alt-d) is supposed to delete the current search match but there is a rogue "echo" that is supposed to invalidate the search match. I'm not sure how that ever worked. Also, pexepect doesn't seem to support cursor position reporting, so work around that. Ref: https://codeberg.org/dnkl/foot/wiki#how-do-i-make-ctrl-l-scroll-the-content-instead-of-erasing-it as of wiki commit b57489e298f95d037fdf34da00ea60a5e8eafd6d Closes #10934 --- CHANGELOG.rst | 2 ++ doc_src/cmds/bind.rst | 5 ++++- .../functions/__fish_shared_key_bindings.fish | 2 +- src/curses.rs | 6 +++++ src/input.rs | 1 + src/input_common.rs | 7 ++++++ src/reader.rs | 7 ++++++ src/screen.rs | 22 +++++++++++++++++++ tests/checks/tmux-complete.fish | 2 +- tests/checks/tmux-history-search.fish | 3 +-- tests/pexpects/bind.py | 2 ++ 11 files changed, 54 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 785cd319b..baef63c5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,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` no longer clears the screen but only pushes to the terminal's scrollback all text above the prompt (via a new special input function ``scrollback-push``). + You can restore previous behavior with `bind ctrl-l clear-screen`. Completions ^^^^^^^^^^^ diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 9218123f7..02065308d 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -171,7 +171,10 @@ The following special input functions are available: make the current word begin with a capital letter ``clear-screen`` - clears the screen and redraws the prompt. if the terminal doesn't support clearing the screen it is the same as ``repaint``. + clears the screen and redraws the prompt. + +``scrollback-push`` + pushes earlier output to the terminal scrollback, positioning the prompt at the top. ``complete`` guess the remainder of the current token diff --git a/share/functions/__fish_shared_key_bindings.fish b/share/functions/__fish_shared_key_bindings.fish index 13faef8ca..c2d64e92a 100644 --- a/share/functions/__fish_shared_key_bindings.fish +++ b/share/functions/__fish_shared_key_bindings.fish @@ -66,7 +66,7 @@ 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 clear-screen + bind --preset $argv ctrl-l scrollback-push repaint bind --preset $argv ctrl-c cancel-commandline bind --preset $argv ctrl-u backward-kill-line bind --preset $argv ctrl-w backward-kill-path-component diff --git a/src/curses.rs b/src/curses.rs index ef08f86fb..1ecd361ce 100644 --- a/src/curses.rs +++ b/src/curses.rs @@ -66,8 +66,10 @@ pub struct Term { pub cursor_down: Option, pub cursor_left: Option, pub cursor_right: Option, + pub parm_cursor_up: Option, pub parm_left_cursor: Option, pub parm_right_cursor: Option, + pub parm_index: Option, pub clr_eol: Option, pub clr_eos: Option, @@ -215,8 +217,10 @@ fn new(db: terminfo::Database) -> Self { cursor_down: get_str_cap(&db, "do"), cursor_left: get_str_cap(&db, "le"), cursor_right: get_str_cap(&db, "nd"), + parm_cursor_up: get_str_cap(&db, "UP"), parm_left_cursor: get_str_cap(&db, "LE"), parm_right_cursor: get_str_cap(&db, "RI"), + parm_index: get_str_cap(&db, "SF"), clr_eol: get_str_cap(&db, "ce"), clr_eos: get_str_cap(&db, "cd"), @@ -425,8 +429,10 @@ pub fn setup_fallback_term() -> Arc { cursor_down: Some(CString::new("\n").unwrap()), cursor_left: Some(CString::new("\x08").unwrap()), cursor_right: Some(CString::new("\x1b[C").unwrap()), + parm_cursor_up: Some(CString::new("\x1b[%p1%dA").unwrap()), parm_left_cursor: Some(CString::new("\x1b[%p1%dD").unwrap()), parm_right_cursor: Some(CString::new("\x1b[%p1%dC").unwrap()), + parm_index: Some(CString::new("\x1b[%p1%dS").unwrap()), clr_eol: Some(CString::new("\x1b[K").unwrap()), clr_eos: Some(CString::new("\x1b[J").unwrap()), max_colors: Some(256), diff --git a/src/input.rs b/src/input.rs index b5e49656e..ce8117563 100644 --- a/src/input.rs +++ b/src/input.rs @@ -198,6 +198,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat make_md(L!("repaint-mode"), ReadlineCmd::RepaintMode), make_md(L!("repeat-jump"), ReadlineCmd::RepeatJump), make_md(L!("repeat-jump-reverse"), ReadlineCmd::ReverseRepeatJump), + make_md(L!("scrollback-push"), ReadlineCmd::ScrollbackPush), make_md(L!("self-insert"), ReadlineCmd::SelfInsert), make_md(L!("self-insert-notfirst"), ReadlineCmd::SelfInsertNotFirst), make_md(L!("suppress-autosuggestion"), ReadlineCmd::SuppressAutosuggestion), diff --git a/src/input_common.rs b/src/input_common.rs index abc3f8134..1996ca11e 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -131,6 +131,7 @@ pub enum ReadlineCmd { EndUndoGroup, RepeatJump, ClearScreenAndRepaint, + ScrollbackPush, // NOTE: This one has to be last. ReverseRepeatJump, } @@ -191,6 +192,8 @@ pub enum ImplicitEvent { DisableMouseTracking, /// Handle mouse left click. MouseLeftClickContinuation(ViewportPosition, ViewportPosition), + /// Push prompt to top. + ScrollbackPushContinuation(usize), } #[derive(Debug, Clone)] @@ -590,6 +593,7 @@ pub fn function_set_status(&mut self, status: bool) { pub enum WaitingForCursorPosition { MouseLeft(ViewportPosition), + ScrollbackPush, } /// A trait which knows how to produce a stream of input events. @@ -1025,6 +1029,9 @@ fn parse_csi(&mut self, buffer: &mut Vec) -> Option { *click_position, ) } + WaitingForCursorPosition::ScrollbackPush => { + ImplicitEvent::ScrollbackPushContinuation(y) + } }; self.push_front(CharEvent::Implicit(continuation)); return None; diff --git a/src/reader.rs b/src/reader.rs index f471b9198..d7a2676c6 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -2275,6 +2275,10 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo self.mouse_left_click(cursor, click_position); self.stop_waiting_for_cursor_position(); } + ImplicitEvent::ScrollbackPushContinuation(cursor_y) => { + self.screen.push_to_scrollback(cursor_y); + self.stop_waiting_for_cursor_position(); + } }, } ControlFlow::Continue(()) @@ -3499,6 +3503,9 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { self.force_exec_prompt_and_repaint = false; self.parser.libdata_mut().is_repaint = false; } + rl::ScrollbackPush => { + self.request_cursor_position(WaitingForCursorPosition::ScrollbackPush); + } rl::SelfInsert | rl::SelfInsertNotFirst | rl::FuncAnd | rl::FuncOr => { // This can be reached via `commandline -f and` etc // panic!("should have been handled by inputter_t::readch"); diff --git a/src/screen.rs b/src/screen.rs index dcc571666..66421a3f6 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -488,6 +488,28 @@ pub fn move_to_end(&mut self) { self.r#move(0, self.actual.line_count()); } + pub fn push_to_scrollback(&mut self, cursor_y: usize) { + let mut prompt_y = self.command_line_y_given_cursor_y(cursor_y); + prompt_y -= calc_prompt_lines(&self.actual_left_prompt) - 1; + if prompt_y == 0 { + return; + } + let zelf = self.scoped_buffer(); + let Some(term) = term() else { + return; + }; + let mut out = zelf.outp.borrow_mut(); + let prompt_y = i32::try_from(prompt_y).unwrap(); + // Scroll down. + if let Some(scroll) = term.parm_index.as_ref() { + out.tputs_if_some(&tparm1(scroll, prompt_y)); + } + // Reposition cursor. + if let Some(up) = term.parm_cursor_up.as_ref() { + out.tputs_if_some(&tparm1(up, prompt_y)); + } + } + fn command_line_y_given_cursor_y(&mut self, viewport_cursor_y: usize) -> usize { let prompt_y = viewport_cursor_y.checked_sub(self.actual.cursor.y); prompt_y.unwrap_or_else(|| { diff --git a/tests/checks/tmux-complete.fish b/tests/checks/tmux-complete.fish index 9137a05b1..820af9619 100644 --- a/tests/checks/tmux-complete.fish +++ b/tests/checks/tmux-complete.fish @@ -43,7 +43,7 @@ isolated-tmux capture-pane -p | sed -n '1p;$p' # Also ensure that the pager is actually fully disclosed. # CHECK: rows 1 to {{\d+}} of {{\d+}} -# Canceling the pager removes the inserted completion, no mater what happens in the search field. +# Canceling the pager removes the inserted completion, no matter what happens in the search field. # The common prefix remains because it is inserted before the pager is shown. isolated-tmux send-keys C-c tmux-sleep diff --git a/tests/checks/tmux-history-search.fish b/tests/checks/tmux-history-search.fish index 07a887276..b9ae96900 100644 --- a/tests/checks/tmux-history-search.fish +++ b/tests/checks/tmux-history-search.fish @@ -47,8 +47,7 @@ isolated-tmux capture-pane -p | grep 'prompt 2>' isolated-tmux send-keys C-c isolated-tmux send-keys 'echo 1' Enter 'echo 2' Enter 'echo 3' Enter -isolated-tmux send-keys C-l echo Up -isolated-tmux send-keys echo M-d +isolated-tmux send-keys C-l echo Up M-d tmux-sleep isolated-tmux capture-pane -p #CHECK: prompt 5> echo 2 diff --git a/tests/pexpects/bind.py b/tests/pexpects/bind.py index 991325e33..198bd1556 100644 --- a/tests/pexpects/bind.py +++ b/tests/pexpects/bind.py @@ -22,6 +22,8 @@ send, sendline, sleep, expect_prompt, expect_re, expect_str = ( ) expect_prompt() +sendline("bind ctrl-l repaint") +expect_prompt() # Clear twice (regression test for #7280). send("\f") expect_prompt(increment=False)