Move cursor on mouse click via kitty's OSC 133 click_events=1

When the user clicks somewhere in the prompt, kitty asks the shell
to move the cursor there (since there is not much else to do).

This is currently implemented by sending an array of
forward-char-passive commands.  This has problems, for example it
is really slow on large command lines (probably because we repaint
everytime).

Implement kitty's `click_events=1` flag to set the
position directly.  To convert from terminal-coordinates
to fish-coordinates, query [CSI 6 n Report Cursor
Position](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
and use it to compute the left prompt's terminal-coordinates (which
are (0, 0) in fish-coordinates).

Unfortunately this doesn't yet work correctly while the terminal
is scrolled.  This is probably because the cursor position is wrong
if off-screen.  To fix that we could probably record the cursor
position while not scrolled, but it doesn't seem terribly important
(the existing implementation also doesn't get it right).

We still turn off mouse reporting.  If we turned it on, it
would be harder to select text in the terminal itself (not fish).
This would typically mean that mouse-drag will alter fish's
selection and shift+mouse-drag or alt+mouse-drag can be used.

To improve this, we could try to synchronize the selection: if parts
of the fish commandline are selected in the terminal's selection,
copy that to fish's selection and vice versa.

Or maybe there is an intuitive criteria, like: whenever we receive a
mouse event outside fish, turn off mouse reporting, and turn it back on
whenver we receive new keyboard input.  One problem is that we lose
one event (though we could send it back to the terminal). Another
problem is we would turn it back on too late in some scenarios.

Closes #10932
This commit is contained in:
Johannes Altmanninger 2024-12-21 19:41:41 +01:00
parent ca9c5f4cec
commit e1e963ae66
7 changed files with 310 additions and 44 deletions

View file

@ -16,6 +16,7 @@ Interactive improvements
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.
Completions
^^^^^^^^^^^

View file

@ -1,12 +1,13 @@
use crate::common::{escape, get_by_sorted_name, shell_modes, str2wcstring, Named};
use crate::common::{escape, get_by_sorted_name, str2wcstring, Named};
use crate::curses;
use crate::env::{Environment, CURSES_INITIALIZED};
use crate::event;
use crate::flog::FLOG;
use crate::input_common::{
CharEvent, CharInputStyle, ImplicitEvent, InputData, InputEventQueuer, ReadlineCmd,
R_END_INPUT_FUNCTIONS,
WaitingForCursorPosition, R_END_INPUT_FUNCTIONS,
};
use crate::key::ViewportPosition;
use crate::key::{self, canonicalize_raw_escapes, ctrl, Key, Modifiers};
use crate::proc::job_reap;
use crate::reader::{
@ -424,10 +425,7 @@ impl<'a> InputEventQueuer for Reader<'a> {
// Tell the reader an event occurred.
if reader_reading_interrupted(self) != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
}
self.enqueue_interrupt_key();
return;
}
self.push_front(CharEvent::from_check_exit());
@ -456,6 +454,21 @@ impl<'a> InputEventQueuer for Reader<'a> {
escape(&str2wcstring(&buffer))
)));
}
fn is_waiting_for_cursor_position(&self) -> bool {
self.waiting_for_cursor_position.is_some()
}
fn cursor_position_wait_reason(&self) -> &Option<WaitingForCursorPosition> {
&self.waiting_for_cursor_position
}
fn stop_waiting_for_cursor_position(&mut self) -> bool {
self.waiting_for_cursor_position.take().is_some()
}
fn on_mouse_left_click(&mut self, position: ViewportPosition) {
FLOG!(reader, "Mouse left click", position);
self.request_cursor_position(WaitingForCursorPosition::MouseLeft(position));
}
}
/// A struct which allows accumulating input events, or returns them to the queue.

View file

@ -6,12 +6,12 @@ use crate::common::{
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::FdReadableSet;
use crate::flog::FLOG;
use crate::flog::{FloggableDebug, FLOG};
use crate::fork_exec::flog_safe::FLOG_SAFE;
use crate::global_safety::RelaxedAtomicBool;
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, function_key, shift,
Key, Modifiers,
Key, Modifiers, ViewportPosition,
};
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::threads::{iothread_port, is_main_thread};
@ -189,6 +189,8 @@ pub enum ImplicitEvent {
FocusOut,
/// Request to disable mouse tracking.
DisableMouseTracking,
/// Handle mouse left click.
MouseLeftClickContinuation(ViewportPosition, ViewportPosition),
}
#[derive(Debug, Clone)]
@ -205,6 +207,7 @@ pub enum CharEvent {
/// Any event that has no user-visible representation.
Implicit(ImplicitEvent),
}
impl FloggableDebug for CharEvent {}
impl CharEvent {
pub fn is_char(&self) -> bool {
@ -585,11 +588,23 @@ impl InputData {
}
}
pub enum WaitingForCursorPosition {
MouseLeft(ViewportPosition),
}
/// 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() {
match self.get_input_data().queue.front()? {
CharEvent::Key(_) | CharEvent::Readline(_) | CharEvent::Command(_) => {
return None; // No code execution while we're waiting for CPR.
}
CharEvent::Implicit(_) => (),
}
}
self.get_input_data_mut().queue.pop_front()
}
@ -697,15 +712,42 @@ pub trait InputEventQueuer {
if !ok {
continue;
}
return if let Some(key) = key {
Some(CharEvent::from_key_seq(key, seq))
let (key_evt, extra) = if let Some(key) = key {
(CharEvent::from_key_seq(key, seq), None)
} else {
self.insert_front(seq.chars().skip(1).map(CharEvent::from_char));
let Some(c) = seq.chars().next() else {
continue;
};
Some(CharEvent::from_key_seq(Key::from_raw(c), seq))
(
CharEvent::from_key_seq(Key::from_raw(c), seq.clone()),
Some(seq.chars().skip(1).map(CharEvent::from_char)),
)
};
if self.is_waiting_for_cursor_position() {
FLOG!(
reader,
"Still waiting for cursor position report from terminal, deferring key event",
key_evt
);
self.push_back(key_evt);
extra.map(|extra| {
for evt in extra {
self.push_back(evt);
}
});
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 && key == Some(Key::from_single_byte(vintr)) {
FLOG!(
reader,
"Received interrupt key, giving up waiting for cursor position"
);
let ok = self.stop_waiting_for_cursor_position();
assert!(ok);
}
continue;
}
extra.map(|extra| self.insert_front(extra));
return Some(key_evt);
}
ReadbResult::NothingToRead => return None,
}
@ -902,22 +944,49 @@ pub trait InputEventQueuer {
b'H' => masked_key(key::Home, None), // PC/xterm style
b'M' | b'm' => {
self.disable_mouse_tracking();
// Generic X10 or modified VT200 sequence, or extended (SGR/1006) mouse
// reporting mode, with semicolon-separated parameters for button code, Px,
// and Py, ending with 'M' for button press or 'm' for button release.
let sgr = private_mode == Some(b'<');
if !sgr && c == b'm' {
return None;
}
// Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters
// for button code, Px, and Py, ending with 'M' for button press or 'm' for
// button release.
if sgr {
let button = if sgr {
params[0][0]
} else {
u32::from(next_char(self)) - 32
};
let x = usize::try_from(
if sgr {
params[1][0]
} else {
u32::from(next_char(self)) - 32
} - 1,
)
.unwrap();
let y = usize::try_from(
if sgr {
params[2][0]
} else {
u32::from(next_char(self)) - 32
} - 1,
)
.unwrap();
let position = ViewportPosition { x, y };
let modifiers = parse_mask((button >> 2) & 0x07);
let code = button & 0x43;
if code != 0 || c != b'M' || modifiers.is_some() {
return None;
}
// Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6
// chars (although in mode 1005, the characters may be unicode and not necessarily
// just one byte long) reporting the button that was clicked and its location.
let _ = next_char(self);
let _ = next_char(self);
let _ = next_char(self);
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;
}
self.on_mouse_left_click(position);
return None;
}
b't' => {
@ -937,6 +1006,22 @@ 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) => {
ImplicitEvent::MouseLeftClickContinuation(
ViewportPosition { x, y },
*click_position,
)
}
};
self.push_front(CharEvent::Implicit(continuation));
return None;
}
b'S' => masked_key(function_key(4), None),
b'~' => match params[0][0] {
1 => masked_key(key::Home, None), // VT220/tmux style
@ -1273,6 +1358,17 @@ pub trait InputEventQueuer {
}
}
fn is_waiting_for_cursor_position(&self) -> bool {
false
}
fn cursor_position_wait_reason(&self) -> &Option<WaitingForCursorPosition> {
&None
}
fn stop_waiting_for_cursor_position(&mut self) -> bool {
false
}
fn on_mouse_left_click(&mut self, _position: ViewportPosition) {}
/// Override point for when we are about to (potentially) block in select(). The default does
/// nothing.
fn prepare_to_select(&mut self) {}
@ -1280,6 +1376,22 @@ pub trait InputEventQueuer {
/// Called when select() is interrupted by a signal.
fn select_interrupted(&mut self) {}
fn enqueue_interrupt_key(&mut self) {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
let interrupt_evt = CharEvent::from_key(Key::from_single_byte(vintr));
if self.stop_waiting_for_cursor_position() {
FLOG!(
reader,
"Received interrupt, giving up on waiting for cursor position"
);
self.push_back(interrupt_evt);
} else {
self.push_front(interrupt_evt);
}
}
}
/// Override point for when when select() is interrupted by the universal variable notifier.
/// The default does nothing.
fn uvar_change_notified(&mut self) {}
@ -1323,10 +1435,7 @@ impl InputEventQueuer for InputEventQueue {
fn select_interrupted(&mut self) {
if reader_test_and_clear_interrupted() != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
}
self.enqueue_interrupt_key();
}
}
}

View file

@ -3,6 +3,7 @@ use libc::VERASE;
use crate::{
common::{escape_string, EscapeFlags, EscapeStringStyle},
fallback::fish_wcwidth,
flog::FloggableDebug,
reader::TERMINAL_MODE_ON_STARTUP,
wchar::{decode_byte_from_char, prelude::*},
wutil::{fish_is_pua, fish_wcstoi},
@ -77,6 +78,14 @@ impl Modifiers {
}
}
/// Position in terminal coordinates, i.e. not starting from the prompt
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ViewportPosition {
pub x: usize,
pub y: usize,
}
impl FloggableDebug for ViewportPosition {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Key {
pub modifiers: Modifiers,

View file

@ -252,7 +252,10 @@ impl Pager {
assert!(stop_row >= start_row);
assert!(stop_row <= row_count);
assert!(stop_row - start_row <= term_height);
// This always printed at the end of the command line.
let offset_in_cmdline = usize::MAX;
self.completion_print(
offset_in_cmdline,
cols,
&width_by_column,
start_row,
@ -297,6 +300,7 @@ impl Pager {
HighlightRole::pager_progress,
);
print_max(
offset_in_cmdline,
&progress_text,
spec,
term_width,
@ -325,6 +329,7 @@ impl Pager {
let mut search_field_remaining = term_width - 1;
search_field_remaining -= print_max(
offset_in_cmdline,
wgettext!(SEARCH_FIELD_PROMPT),
HighlightSpec::new(),
search_field_remaining,
@ -332,6 +337,7 @@ impl Pager {
search_field,
);
search_field_remaining -= print_max(
offset_in_cmdline,
&search_field_text,
underline,
search_field_remaining,
@ -401,6 +407,7 @@ impl Pager {
/// \param lst The list of completions to print
fn completion_print(
&self,
offset_in_cmdline: usize,
cols: usize,
width_by_column: &[usize; PAGER_MAX_COLS],
row_start: usize,
@ -429,12 +436,18 @@ impl Pager {
let is_selected = Some(idx) == effective_selected_idx;
// Print this completion on its own "line".
let mut line =
self.completion_print_item(prefix, el, col_width, row % 2 != 0, is_selected);
let mut line = self.completion_print_item(
offset_in_cmdline,
prefix,
el,
col_width,
row % 2 != 0,
is_selected,
);
// If there's more to come, append two spaces.
if col + 1 < cols {
line.append_str(PAGER_SPACER_STRING, HighlightSpec::new());
line.append_str(PAGER_SPACER_STRING, HighlightSpec::new(), offset_in_cmdline);
}
// Append this to the real line.
@ -449,6 +462,7 @@ impl Pager {
/// Print the specified item using at the specified amount of space.
fn completion_print_item(
&self,
offset_in_cmdline: usize,
prefix: &wstr,
c: &PagerComp,
width: usize,
@ -510,6 +524,7 @@ impl Pager {
for (i, comp) in c.comp.iter().enumerate() {
if i > 0 {
comp_remaining -= print_max(
offset_in_cmdline,
PAGER_SPACER_STRING,
bg,
comp_remaining,
@ -519,6 +534,7 @@ impl Pager {
}
comp_remaining -= print_max(
offset_in_cmdline,
prefix,
prefix_col,
comp_remaining,
@ -526,6 +542,7 @@ impl Pager {
&mut line_data,
);
comp_remaining -= print_max_impl(
offset_in_cmdline,
comp,
|i| {
if c.colors.is_empty() {
@ -546,12 +563,13 @@ impl Pager {
let mut desc_remaining = width - comp_width + comp_remaining;
if c.desc_width > 0 && desc_remaining > 4 {
// always have at least two spaces to separate completion and description
desc_remaining -= print_max(L!(" "), bg, 2, false, &mut line_data);
desc_remaining -= print_max(offset_in_cmdline, L!(" "), bg, 2, false, &mut line_data);
// right-justify the description by adding spaces
// the 2 here refers to the parenthesis below
while desc_remaining > c.desc_width + 2 {
desc_remaining -= print_max(L!(" "), bg, 1, false, &mut line_data);
desc_remaining -=
print_max(offset_in_cmdline, L!(" "), bg, 1, false, &mut line_data);
}
assert!(desc_remaining >= 2);
@ -563,14 +581,35 @@ impl Pager {
},
bg_role,
);
desc_remaining -= print_max(L!("("), paren_col, 1, false, &mut line_data);
desc_remaining -=
print_max(&c.desc, desc_col, desc_remaining - 1, false, &mut line_data);
desc_remaining -= print_max(L!(")"), paren_col, 1, false, &mut line_data);
desc_remaining -= print_max(
offset_in_cmdline,
L!("("),
paren_col,
1,
false,
&mut line_data,
);
desc_remaining -= print_max(
offset_in_cmdline,
&c.desc,
desc_col,
desc_remaining - 1,
false,
&mut line_data,
);
desc_remaining -= print_max(
offset_in_cmdline,
L!(")"),
paren_col,
1,
false,
&mut line_data,
);
let _ = desc_remaining;
} else {
// No description, or it won't fit. Just add spaces.
print_max(
offset_in_cmdline,
&WString::from_iter(std::iter::repeat(' ').take(desc_remaining)),
bg,
desc_remaining,
@ -1062,6 +1101,7 @@ fn divide_round_up(numer: usize, denom: usize) -> usize {
/// \param has_more if this flag is true, this is not the entire string, and the string should be
/// ellipsized even if the string fits but takes up the whole space.
fn print_max_impl(
offset_in_cmdline: usize,
s: &wstr,
color: impl Fn(usize) -> HighlightSpec,
max: usize,
@ -1082,13 +1122,13 @@ fn print_max_impl(
let ellipsis = get_ellipsis_char();
if (width_c == remaining) && (has_more || i + 1 < s.len()) {
line.append(ellipsis, color(i));
line.append(ellipsis, color(i), offset_in_cmdline);
let ellipsis_width = wcwidth_rendered(ellipsis);
remaining = remaining.saturating_sub(usize::try_from(ellipsis_width).unwrap());
break;
}
line.append(c, color(i));
line.append(c, color(i), offset_in_cmdline);
remaining = remaining.checked_sub(width_c).unwrap();
}
@ -1096,8 +1136,15 @@ fn print_max_impl(
max.checked_sub(remaining).unwrap()
}
fn print_max(s: &wstr, color: HighlightSpec, max: usize, has_more: bool, line: &mut Line) -> usize {
print_max_impl(s, |_| color, max, has_more, line)
fn print_max(
offset_in_cmdline: usize,
s: &wstr,
color: HighlightSpec,
max: usize,
has_more: bool,
line: &mut Line,
) -> usize {
print_max_impl(offset_in_cmdline, s, |_| color, max, has_more, line)
}
/// Trim leading and trailing whitespace, and compress other whitespace runs into a single space.

View file

@ -78,12 +78,15 @@ use crate::history::{
use crate::input::init_input;
use crate::input_common::terminal_protocols_disable_ifn;
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::{
terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, CharInputStyle, InputData,
ReadlineCmd,
};
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::isatty;
@ -495,6 +498,8 @@ pub struct ReaderData {
/// The representation of the current screen contents.
screen: Screen,
pub waiting_for_cursor_position: Option<WaitingForCursorPosition>,
/// Data associated with input events.
/// This is made public so that InputEventQueuer can be implemented on us.
pub input_data: InputData,
@ -1146,6 +1151,7 @@ impl ReaderData {
first_prompt: true,
last_flash: Default::default(),
screen: Screen::new(),
waiting_for_cursor_position: None,
input_data,
queued_repaint: false,
history,
@ -1349,6 +1355,30 @@ impl ReaderData {
}
true
}
pub fn request_cursor_position(
&mut self,
waiting_for_cursor_position: WaitingForCursorPosition,
) {
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");
}
pub fn mouse_left_click(&mut self, cursor: ViewportPosition, click_position: ViewportPosition) {
FLOG!(
reader,
"Cursor is at",
cursor,
"; received left mouse click at",
click_position
);
let new_pos = self
.screen
.offset_in_cmdline_given_cursor(click_position, cursor);
let (elt, _el) = self.active_edit_line();
self.update_buff_pos(elt, Some(new_pos));
}
}
/// Given a command line and an autosuggestion, return the string that gets shown to the user.
@ -2241,6 +2271,10 @@ impl<'a> Reader<'a> {
.borrow_mut()
.write_wstr(L!("\x1B[?1000l"));
}
ImplicitEvent::MouseLeftClickContinuation(cursor, click_position) => {
self.mouse_left_click(cursor, click_position);
self.stop_waiting_for_cursor_position();
}
},
}
ControlFlow::Continue(())

View file

@ -7,7 +7,9 @@
//! The current implementation is less smart than ncurses allows and can not for example move blocks
//! of text around to handle text insertion.
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};
@ -42,6 +44,8 @@ use crate::wutil::fstat;
pub struct HighlightedChar {
highlight: HighlightSpec,
character: char,
// Logical offset within the command line.
offset_in_cmdline: usize,
}
/// A class representing a single line of a screen.
@ -64,17 +68,18 @@ impl Line {
}
/// Append a single character `txt` to the line with color `c`.
pub fn append(&mut self, character: char, highlight: HighlightSpec) {
pub fn append(&mut self, character: char, highlight: HighlightSpec, offset_in_cmdline: usize) {
self.text.push(HighlightedChar {
highlight,
character: rendered_character(character),
offset_in_cmdline,
})
}
/// Append a nul-terminated string `txt` to the line, giving each character `color`.
pub fn append_str(&mut self, txt: &wstr, highlight: HighlightSpec) {
pub fn append_str(&mut self, txt: &wstr, highlight: HighlightSpec, offset_in_cmdline: usize) {
for c in txt.chars() {
self.append(c, highlight);
self.append(c, highlight, offset_in_cmdline);
}
}
@ -93,6 +98,11 @@ impl Line {
self.text[idx].highlight
}
/// Return the logical offset corresponding to this cell
pub fn offset_in_cmdline_at(&self, idx: usize) -> usize {
self.text[idx].offset_in_cmdline
}
/// Append the contents of `line` to this line.
pub fn append_line(&mut self, line: &Line) {
self.text.extend_from_slice(&line.text);
@ -320,6 +330,7 @@ impl Screen {
// Append spaces for the left prompt.
for _ in 0..layout.left_prompt_space {
let _ = self.desired_append_char(
/*offset_in_cmdline=*/ 0,
usize::MAX,
' ',
HighlightSpec::new(),
@ -363,6 +374,7 @@ impl Screen {
break scrolled_cursor.unwrap();
}
if !self.desired_append_char(
/*offset_in_cmdline=*/ i,
if is_final_rendering {
usize::MAX
} else {
@ -476,6 +488,43 @@ impl Screen {
self.r#move(0, self.actual.line_count());
}
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(|| {
FLOG!(
error,
"Reported cursor line index",
viewport_cursor_y,
"is above fish's cursor",
self.actual.cursor.y
);
0
})
}
pub fn offset_in_cmdline_given_cursor(
&mut self,
viewport_position: ViewportPosition,
viewport_cursor: ViewportPosition,
) -> usize {
let viewport_prompt_y = self.command_line_y_given_cursor_y(viewport_cursor.y);
let y = viewport_position.y - viewport_prompt_y;
let y = y.min(self.actual.line_count() - 1);
let viewport_prompt_x = viewport_cursor.x - self.actual.cursor.x;
let x = viewport_position.x - viewport_prompt_x;
let line = self.actual.line(y);
let x = x.max(line.indentation);
if x >= line.len() {
if self.actual.line_count() == 1 {
0
} else {
line.text.last().unwrap().offset_in_cmdline + 1
}
} else {
line.offset_in_cmdline_at(x)
}
}
/// Resets the screen buffer's internal knowledge about the contents of the screen,
/// abandoning the current line and going to the next line.
/// If clear_to_eos is set,
@ -597,6 +646,7 @@ impl Screen {
/// automatically handles linebreaks and lines longer than the screen width.
fn desired_append_char(
&mut self,
offset_in_cmdline: usize,
max_y: usize,
b: char,
c: HighlightSpec,
@ -622,6 +672,7 @@ impl Screen {
line.indentation = indentation;
for _ in 0..indentation {
if !self.desired_append_char(
offset_in_cmdline,
max_y,
' ',
HighlightSpec::default(),
@ -659,7 +710,9 @@ impl Screen {
self.desired.cursor.x = 0;
}
self.desired.line_mut(line_no).append(b, c);
self.desired
.line_mut(line_no)
.append(b, c, offset_in_cmdline);
self.desired.cursor.x += cw;
// Maybe wrap the cursor to the next line, even if the line itself did not wrap. This
@ -935,7 +988,7 @@ impl Screen {
zelf.r#move(0, 0);
let mut start = 0;
let osc_133_prompt_start =
|zelf: &mut Screen| zelf.write_bytes(b"\x1b]133;A;special_key=1\x07");
|zelf: &mut Screen| zelf.write_bytes(b"\x1b]133;A;click_events=1\x07");
if left_prompt_layout.line_breaks.is_empty() {
osc_133_prompt_start(&mut zelf);
}