diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3bbff5bb2..785cd319b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ^^^^^^^^^^^ diff --git a/src/input.rs b/src/input.rs index 984d8c319..b5e49656e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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 { + &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. diff --git a/src/input_common.rs b/src/input_common.rs index 48854f029..3aa4525a4 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -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 { + 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 { + &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(); } } } diff --git a/src/key.rs b/src/key.rs index 27d6e2316..3a4a8e66b 100644 --- a/src/key.rs +++ b/src/key.rs @@ -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, diff --git a/src/pager.rs b/src/pager.rs index b5326e0de..95e9c9a5c 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -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. diff --git a/src/reader.rs b/src/reader.rs index 32dcd88d8..f471b9198 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -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, + /// 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(()) diff --git a/src/screen.rs b/src/screen.rs index 68f2bddca..dcc571666 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -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); }