scrollback-push to fall back to clear-screen if missing CPR feature

The new ctrl-l implementation relies on Cursor Position Reporting (CPR)
This may not work on exotic terminals that don't support CSI 6n yet

As a workaround, probe for this feature by sending a CSI 6n (CPR)
on startup.  Until the terminal responds, have scrollback-push fall
back to clear-screen.

The theoretical problem here is that we might handle scrollback-push
before we have handled the response to our feature probe. That seems
fairly unlikely; also e49dde87cc has the same characteristics.

This could query a capability instead (via XTGETTCAP or otherwise)
but I haven't found one; and this seems at least as reliable.

While at it, change the naming a bit.

See #11003
This commit is contained in:
Johannes Altmanninger 2025-01-04 01:21:52 +01:00
parent dda4371679
commit 75832b3c5d
3 changed files with 106 additions and 57 deletions

View file

@ -3,12 +3,14 @@ use crate::curses;
use crate::env::{Environment, CURSES_INITIALIZED};
use crate::event;
use crate::flog::FLOG;
use crate::input_common::CursorPositionBlockingWait::MouseLeft;
use crate::input_common::{
CharEvent, CharInputStyle, ImplicitEvent, InputData, InputEventQueuer, ReadlineCmd,
WaitingForCursorPosition, R_END_INPUT_FUNCTIONS,
CharEvent, CharInputStyle, CursorPositionWait, ImplicitEvent, InputData, InputEventQueuer,
ReadlineCmd, R_END_INPUT_FUNCTIONS,
};
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,
@ -456,19 +458,30 @@ impl<'a> InputEventQueuer for Reader<'a> {
)));
}
fn is_waiting_for_cursor_position(&self) -> bool {
self.waiting_for_cursor_position.is_some()
fn cursor_position_wait(&self) -> &CursorPositionWait {
&self.cursor_position_wait
}
fn cursor_position_wait_reason(&self) -> &Option<WaitingForCursorPosition> {
&self.waiting_for_cursor_position
fn is_blocked_waiting_for_cursor_position(&self) -> bool {
matches!(self.cursor_position_wait, CursorPositionWait::Blocking(_))
}
fn cursor_position_reporting_supported(&mut self) {
assert!(self.cursor_position_wait == CursorPositionWait::InitialFeatureProbe);
self.cursor_position_wait = CursorPositionWait::None;
}
fn stop_waiting_for_cursor_position(&mut self) -> bool {
self.waiting_for_cursor_position.take().is_some()
if !self.is_blocked_waiting_for_cursor_position() {
return false;
}
self.cursor_position_wait = CursorPositionWait::None;
true
}
fn on_mouse_left_click(&mut self, position: ViewportPosition) {
FLOG!(reader, "Mouse left click", position);
self.request_cursor_position(WaitingForCursorPosition::MouseLeft(position));
self.request_cursor_position(
&mut Outputter::stdoutput().borrow_mut(),
CursorPositionWait::Blocking(MouseLeft(position)),
);
}
}

View file

@ -596,17 +596,25 @@ impl InputData {
}
}
pub enum WaitingForCursorPosition {
#[derive(Eq, PartialEq)]
pub enum CursorPositionBlockingWait {
MouseLeft(ViewportPosition),
ScrollbackPush,
}
#[derive(Eq, PartialEq)]
pub enum CursorPositionWait {
None,
InitialFeatureProbe,
Blocking(CursorPositionBlockingWait),
}
/// A trait which knows how to produce a stream of input events.
/// Note this is conceptually a "base class" with override points.
pub trait InputEventQueuer {
/// Return the next event in the queue, or none if the queue is empty.
fn try_pop(&mut self) -> Option<CharEvent> {
if self.is_waiting_for_cursor_position() {
if self.is_blocked_waiting_for_cursor_position() {
match self.get_input_data().queue.front()? {
CharEvent::Key(_) | CharEvent::Readline(_) | CharEvent::Command(_) => {
return None; // No code execution while we're waiting for CPR.
@ -733,7 +741,7 @@ pub trait InputEventQueuer {
Some(seq.chars().skip(1).map(CharEvent::from_char)),
)
};
if self.is_waiting_for_cursor_position() {
if self.is_blocked_waiting_for_cursor_position() {
FLOG!(
reader,
"Still waiting for cursor position report from terminal, deferring key event",
@ -994,15 +1002,17 @@ pub trait InputEventQueuer {
if code != 0 || c != b'M' || modifiers.is_some() {
return None;
}
if self.is_waiting_for_cursor_position() {
// TODO: re-queue it I guess.
FLOG!(
reader,
"Received mouse left click while still waiting for Cursor Position Report"
);
return None;
match self.cursor_position_wait() {
CursorPositionWait::None => self.on_mouse_left_click(position),
CursorPositionWait::InitialFeatureProbe => (),
CursorPositionWait::Blocking(_) => {
// TODO: re-queue it I guess.
FLOG!(
reader,
"Ignoring mouse left click received while still waiting for Cursor Position Report"
);
}
}
self.on_mouse_left_click(position);
return None;
}
b't' => {
@ -1023,18 +1033,25 @@ pub trait InputEventQueuer {
b'P' => masked_key(function_key(1), None),
b'Q' => masked_key(function_key(2), None),
b'R' => {
let wait_reason = self.cursor_position_wait_reason().as_ref()?;
let y = usize::try_from(params[0][0] - 1).unwrap();
let x = usize::try_from(params[1][0] - 1).unwrap();
FLOG!(reader, "Received cursor position report y:", y, "x:", x);
let continuation = match wait_reason {
WaitingForCursorPosition::MouseLeft(click_position) => {
let blocking_wait = match self.cursor_position_wait() {
CursorPositionWait::None => return None,
CursorPositionWait::InitialFeatureProbe => {
self.cursor_position_reporting_supported();
return None;
}
CursorPositionWait::Blocking(blocking_wait) => blocking_wait,
};
let continuation = match blocking_wait {
CursorPositionBlockingWait::MouseLeft(click_position) => {
ImplicitEvent::MouseLeftClickContinuation(
ViewportPosition { x, y },
*click_position,
)
}
WaitingForCursorPosition::ScrollbackPush => {
CursorPositionBlockingWait::ScrollbackPush => {
ImplicitEvent::ScrollbackPushContinuation(y)
}
};
@ -1393,11 +1410,12 @@ pub trait InputEventQueuer {
}
}
fn is_waiting_for_cursor_position(&self) -> bool {
false
fn cursor_position_wait(&self) -> &CursorPositionWait {
&CursorPositionWait::InitialFeatureProbe
}
fn cursor_position_wait_reason(&self) -> &Option<WaitingForCursorPosition> {
&None
fn cursor_position_reporting_supported(&mut self) {}
fn is_blocked_waiting_for_cursor_position(&self) -> bool {
false
}
fn stop_waiting_for_cursor_position(&mut self) -> bool {
false

View file

@ -78,9 +78,10 @@ use crate::history::{
};
use crate::input::init_input;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionBlockingWait;
use crate::input_common::CursorPositionWait;
use crate::input_common::ImplicitEvent;
use crate::input_common::InputEventQueuer;
use crate::input_common::WaitingForCursorPosition;
use crate::input_common::IN_MIDNIGHT_COMMANDER_PRE_CSI_U;
use crate::input_common::KITTY_PROGRESSIVE_ENHANCEMENTS_QUERY;
use crate::input_common::{
@ -505,7 +506,7 @@ pub struct ReaderData {
/// The representation of the current screen contents.
screen: Screen,
pub waiting_for_cursor_position: Option<WaitingForCursorPosition>,
pub cursor_position_wait: CursorPositionWait,
/// Data associated with input events.
/// This is made public so that InputEventQueuer can be implemented on us.
@ -1158,7 +1159,7 @@ impl ReaderData {
first_prompt: true,
last_flash: Default::default(),
screen: Screen::new(),
waiting_for_cursor_position: None,
cursor_position_wait: CursorPositionWait::None,
input_data,
queued_repaint: false,
history,
@ -1376,11 +1377,12 @@ impl ReaderData {
pub fn request_cursor_position(
&mut self,
waiting_for_cursor_position: WaitingForCursorPosition,
out: &mut Outputter,
cursor_position_wait: CursorPositionWait,
) {
assert!(self.waiting_for_cursor_position.is_none());
self.waiting_for_cursor_position = Some(waiting_for_cursor_position);
self.screen.write_bytes(b"\x1b[6n");
assert!(self.cursor_position_wait == CursorPositionWait::None);
self.cursor_position_wait = cursor_position_wait;
let _ = out.write(b"\x1b[6n");
}
pub fn mouse_left_click(&mut self, cursor: ViewportPosition, click_position: ViewportPosition) {
@ -2087,8 +2089,13 @@ impl<'a> Reader<'a> {
static queried: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
if !queried.load() {
queried.store(true);
let mut out = Outputter::stdoutput().borrow_mut();
out.begin_buffering();
// Query for kitty keyboard protocol support.
let _ = write_loop(&STDOUT_FILENO, KITTY_PROGRESSIVE_ENHANCEMENTS_QUERY);
let _ = out.write(KITTY_PROGRESSIVE_ENHANCEMENTS_QUERY);
// Query for cursor position reporting support.
zelf.request_cursor_position(&mut out, CursorPositionWait::InitialFeatureProbe);
out.end_buffering();
}
// HACK: Don't abandon line for the first prompt, because
@ -3618,31 +3625,22 @@ impl<'a> Reader<'a> {
el.end_edit_group();
}
rl::ClearScreenAndRepaint => {
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"));
}
self.exec_prompt();
self.screen.reset_line(/*repaint_prompt=*/ true);
self.layout_and_repaint(L!("readline"));
self.force_exec_prompt_and_repaint = false;
self.parser.libdata_mut().is_repaint = false;
self.clear_screen_and_repaint();
}
rl::ScrollbackPush => {
if self.waiting_for_cursor_position.is_some() {
rl::ScrollbackPush => match self.cursor_position_wait() {
CursorPositionWait::None => self.request_cursor_position(
&mut Outputter::stdoutput().borrow_mut(),
CursorPositionWait::Blocking(CursorPositionBlockingWait::ScrollbackPush),
),
CursorPositionWait::InitialFeatureProbe => self.clear_screen_and_repaint(),
CursorPositionWait::Blocking(_) => {
// TODO: re-queue it I guess.
return;
FLOG!(
reader,
"Ignoring scrollback-push received while still waiting for Cursor Position Report"
);
}
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");
@ -3650,6 +3648,26 @@ impl<'a> Reader<'a> {
}
}
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"));
}
self.exec_prompt();
self.screen.reset_line(/*repaint_prompt=*/ true);
self.layout_and_repaint(L!("readline"));
self.force_exec_prompt_and_repaint = false;
self.parser.libdata_mut().is_repaint = false;
}
fn backward_token(&mut self) -> Option<usize> {
let (_elt, el) = self.active_edit_line();
let pos = el.position();