diff --git a/CMakeLists.txt b/CMakeLists.txt index bab8275f2..9ca960d17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,7 +126,6 @@ set(FISH_SRCS src/path.cpp src/reader.cpp src/rustffi.cpp - src/screen.cpp src/wcstringutil.cpp src/wgetopt.cpp src/wutil.cpp diff --git a/fish-rust/build.rs b/fish-rust/build.rs index 386f5ef4f..c407dd2b5 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -98,6 +98,7 @@ fn main() { "fish-rust/src/proc.rs", "fish-rust/src/reader.rs", "fish-rust/src/redirection.rs", + "fish-rust/src/screen.rs", "fish-rust/src/signal.rs", "fish-rust/src/smoke.rs", "fish-rust/src/termsize.rs", diff --git a/fish-rust/src/builtins/string.rs b/fish-rust/src/builtins/string.rs index 3f25141ff..7cd42e4fb 100644 --- a/fish-rust/src/builtins/string.rs +++ b/fish-rust/src/builtins/string.rs @@ -1,4 +1,4 @@ -use crate::wcstringutil::fish_wcwidth_visible; +use crate::{screen::escape_code_length, wcstringutil::fish_wcwidth_visible}; // Forward some imports to make subcmd implementations easier use super::prelude::*; @@ -267,16 +267,6 @@ fn width_without_escapes(ins: &wstr, start_pos: usize) -> usize { return width as usize; } -fn escape_code_length(code: &wstr) -> Option { - use crate::ffi::escape_code_length_ffi; - use crate::wchar_ffi::wstr_to_u32string; - - match escape_code_length_ffi(wstr_to_u32string(code).as_ptr()).into() { - -1 => None, - n => Some(n as usize), - } -} - /// Empirically determined. /// This is probably down to some pipe buffer or some such, /// but too small means we need to call `read(2)` and str2wcstring a lot. diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index 1bca3c10b..e77890444 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -1008,8 +1008,15 @@ pub fn exit_without_destructors(code: libc::c_int) -> ! { unsafe { libc::_exit(code) }; } -/// Save the shell mode on startup so we can restore them on exit. -static SHELL_MODES: Lazy> = Lazy::new(|| Mutex::new(unsafe { mem::zeroed() })); +pub fn shell_modes() -> &'static libc::termios { + let modes = crate::ffi::shell_modes_ffi() as *const libc::termios; + unsafe { &*modes } +} + +pub fn shell_modes_mut() -> &'static mut libc::termios { + let modes = crate::ffi::shell_modes_ffi() as *mut libc::termios; + unsafe { &mut *modes } +} /// The character to use where the text has been truncated. Is an ellipsis on unicode system and a $ /// on other systems. diff --git a/fish-rust/src/curses.rs b/fish-rust/src/curses.rs index e7d3b2f45..bcccbe512 100644 --- a/fish-rust/src/curses.rs +++ b/fish-rust/src/curses.rs @@ -113,18 +113,37 @@ pub struct Term { pub exit_underline_mode: Option, pub enter_reverse_mode: Option, pub enter_standout_mode: Option, + pub exit_standout_mode: Option, + pub enter_blink_mode: Option, + pub enter_protected_mode: Option, + pub enter_shadow_mode: Option, + pub exit_shadow_mode: Option, + pub enter_secure_mode: Option, + pub enter_alt_charset_mode: Option, + pub exit_alt_charset_mode: Option, pub set_a_foreground: Option, pub set_foreground: Option, pub set_a_background: Option, pub set_background: Option, pub exit_attribute_mode: Option, pub set_title: Option, + pub clear_screen: Option, + pub cursor_up: Option, + pub cursor_down: Option, + pub cursor_left: Option, + pub cursor_right: Option, + pub parm_left_cursor: Option, + pub parm_right_cursor: Option, + pub clr_eol: Option, + pub clr_eos: Option, // Number capabilities pub max_colors: Option, + pub init_tabs: Option, // Flag/boolean capabilities pub eat_newline_glitch: bool, + pub auto_right_margin: bool, } impl Term { @@ -141,18 +160,37 @@ impl Term { exit_underline_mode: get_str_cap("ue"), enter_reverse_mode: get_str_cap("mr"), enter_standout_mode: get_str_cap("so"), + exit_standout_mode: get_str_cap("se"), + enter_blink_mode: get_str_cap("mb"), + enter_protected_mode: get_str_cap("mp"), + enter_shadow_mode: get_str_cap("ZM"), + exit_shadow_mode: get_str_cap("ZU"), + enter_secure_mode: get_str_cap("mk"), + enter_alt_charset_mode: get_str_cap("as"), + exit_alt_charset_mode: get_str_cap("ae"), set_a_foreground: get_str_cap("AF"), set_foreground: get_str_cap("Sf"), set_a_background: get_str_cap("AB"), set_background: get_str_cap("Sb"), exit_attribute_mode: get_str_cap("me"), set_title: get_str_cap("ts"), + clear_screen: get_str_cap("cl"), + cursor_up: get_str_cap("up"), + cursor_down: get_str_cap("do"), + cursor_left: get_str_cap("le"), + cursor_right: get_str_cap("nd"), + parm_left_cursor: get_str_cap("LE"), + parm_right_cursor: get_str_cap("RI"), + clr_eol: get_str_cap("ce"), + clr_eos: get_str_cap("cd"), // Number capabilities max_colors: get_num_cap("Co"), + init_tabs: get_num_cap("it"), // Flag/boolean capabilities eat_newline_glitch: get_flag_cap("xn"), + auto_right_margin: get_flag_cap("am"), } } } @@ -267,6 +305,16 @@ const fn to_cstr_code(code: &str) -> [libc::c_char; 3] { [code[0] as c_char, code[1] as c_char, b'\0' as c_char] } +/// Covers over tparm(). +pub fn tparm0(cap: &CStr) -> Option { + // Take the lock because tparm races with del_curterm, etc. + let _term: std::sync::MutexGuard>> = TERM.lock().unwrap(); + assert!(!cap.to_bytes().is_empty()); + let cap_ptr = cap.as_ptr() as *mut libc::c_char; + // Safety: we're trusting tparm here. + unsafe { try_ptr_to_cstr(tparm(cap_ptr)) } +} + /// Covers over tparm(). pub fn tparm1(cap: &CStr, param1: i32) -> Option { // Take the lock because tparm races with del_curterm, etc. diff --git a/fish-rust/src/env_dispatch.rs b/fish-rust/src/env_dispatch.rs index a31ce2088..bedc433ce 100644 --- a/fish-rust/src/env_dispatch.rs +++ b/fish-rust/src/env_dispatch.rs @@ -8,6 +8,8 @@ use crate::function; use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms}; use crate::output::ColorSupport; use crate::proc::is_interactive_session; +use crate::screen::screen_set_midnight_commander_hack; +use crate::screen::LAYOUT_CACHE_SHARED; use crate::wchar::prelude::*; use crate::wchar_ffi::WCharToFFI; use crate::wutil::fish_wcstoi; @@ -578,7 +580,7 @@ fn apply_non_term_hacks(vars: &EnvStack) { // broken if you do '\r' after it like we normally do. // See https://midnight-commander.org/ticket/4258. if vars.get(L!("MC_SID")).is_some() { - crate::ffi::screen_set_midnight_commander_hack(); + screen_set_midnight_commander_hack(); } } @@ -686,7 +688,7 @@ fn init_curses(vars: &EnvStack) { update_fish_color_support(vars); // Invalidate the cached escape sequences since they may no longer be valid. - crate::ffi::screen_clear_layout_cache_ffi(); + unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() }.clear(); CURSES_INITIALIZED.store(true, Ordering::Relaxed); } diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index 4856ea83e..c4cb7ab25 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -36,6 +36,7 @@ include_cpp! { #include "parser.h" #include "parse_util.h" #include "path.h" + #include "pager.h" #include "proc.h" #include "reader.h" #include "screen.h" @@ -77,6 +78,7 @@ include_cpp! { generate_pod!("pipes_ffi_t") + generate!("shell_modes_ffi") generate!("make_pipes_ffi") generate!("log_extra_to_flog_file") @@ -103,14 +105,16 @@ include_cpp! { generate!("commandline_get_state_text_ffi") generate!("completion_apply_to_command_line") + generate!("pager_t") + generate!("page_rendering_t") + generate!("pager_set_term_size_ffi") + generate!("pager_update_rendering_ffi") + generate!("get_history_variable_text_ffi") generate_pod!("escape_string_style_t") - generate!("screen_set_midnight_commander_hack") - generate!("screen_clear_layout_cache_ffi") - generate!("escape_code_length_ffi") generate!("reader_schedule_prompt_repaint") generate!("reader_change_history") generate!("reader_change_cursor_selection_mode") @@ -169,6 +173,8 @@ impl Repin for IoStreams<'_> {} impl Repin for wcstring_list_ffi_t {} impl Repin for rgb_color_t {} impl Repin for OutputStreamFfi<'_> {} +impl Repin for pager_t {} +impl Repin for page_rendering_t {} pub use autocxx::c_int; pub use ffi::*; diff --git a/fish-rust/src/future.rs b/fish-rust/src/future.rs new file mode 100644 index 000000000..2fcc3f3bc --- /dev/null +++ b/fish-rust/src/future.rs @@ -0,0 +1,25 @@ +//! stdlib backports + +pub trait IsSomeAnd { + type Type; + #[allow(clippy::wrong_self_convention)] + fn is_some_and(self, s: impl FnOnce(Self::Type) -> bool) -> bool; + #[allow(clippy::wrong_self_convention)] + fn is_none_or(self, s: impl FnOnce(Self::Type) -> bool) -> bool; +} + +impl IsSomeAnd for Option { + type Type = T; + fn is_some_and(self, f: impl FnOnce(T) -> bool) -> bool { + match self { + Some(v) => f(v), + None => false, + } + } + fn is_none_or(self, f: impl FnOnce(T) -> bool) -> bool { + match self { + Some(v) => f(v), + None => true, + } + } +} diff --git a/fish-rust/src/highlight.rs b/fish-rust/src/highlight.rs index edc9de95a..e40010166 100644 --- a/fish-rust/src/highlight.rs +++ b/fish-rust/src/highlight.rs @@ -130,11 +130,11 @@ pub struct HighlightColorResolver { /// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be /// one screen redraw. impl HighlightColorResolver { - fn new() -> Self { + pub fn new() -> Self { Default::default() } /// \return an RGB color for a given highlight spec. - fn resolve_spec( + pub fn resolve_spec( &mut self, highlight: &HighlightSpec, is_background: bool, @@ -1718,6 +1718,7 @@ mod highlight_ffi { } extern "Rust" { type HighlightSpecListFFI; + fn new_highlight_spec_list() -> Box; fn highlight_shell_ffi( bff: &CxxWString, ctx: &OperationContext<'_>, @@ -1732,6 +1733,7 @@ mod highlight_ffi { colors: &HighlightSpecListFFI, vars: &EnvStackRefFFI, ) -> Vec; + fn push(&mut self, highlight: &HighlightSpec); } } @@ -1743,8 +1745,17 @@ fn colorize_ffi( colorize(text.as_wstr(), &colors.0, &*vars.0) } -struct HighlightSpecListFFI(Vec); +#[derive(Default)] +pub struct HighlightSpecListFFI(pub Vec); +unsafe impl cxx::ExternType for HighlightSpecListFFI { + type Id = cxx::type_id!("HighlightSpecListFFI"); + type Kind = cxx::kind::Opaque; +} + +fn new_highlight_spec_list() -> Box { + Box::default() +} impl HighlightSpecListFFI { fn size(&self) -> usize { self.0.len() @@ -1752,6 +1763,9 @@ impl HighlightSpecListFFI { fn at(&self, index: usize) -> &HighlightSpec { &self.0[index] } + fn push(&mut self, highlight: &HighlightSpec) { + self.0.push(*highlight) + } } fn highlight_shell_ffi( buff: &CxxWString, diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index 91994fbff..950d38419 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -58,6 +58,7 @@ mod fish_indent; mod flog; mod fork_exec; mod function; +mod future; mod future_feature_flags; mod global_safety; mod highlight; @@ -85,6 +86,7 @@ mod proc; mod re; mod reader; mod redirection; +mod screen; mod signal; mod smoke; mod termsize; diff --git a/fish-rust/src/output.rs b/fish-rust/src/output.rs index 44627f220..ceb78754d 100644 --- a/fish-rust/src/output.rs +++ b/fish-rust/src/output.rs @@ -6,7 +6,7 @@ use crate::env::EnvVar; use crate::wchar::prelude::*; use bitflags::bitflags; use std::cell::RefCell; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; use std::io::{Result, Write}; use std::os::fd::RawFd; use std::sync::atomic::{AtomicU8, Ordering}; @@ -406,13 +406,13 @@ impl Outputter { /// Begins buffering. Output will not be automatically flushed until a corresponding /// end_buffering() call. - fn begin_buffering(&mut self) { + pub fn begin_buffering(&mut self) { self.buffer_count += 1; assert!(self.buffer_count > 0, "buffer_count overflow"); } /// Balance a begin_buffering() call. - fn end_buffering(&mut self) { + pub fn end_buffering(&mut self) { assert!(self.buffer_count > 0, "buffer_count underflow"); self.buffer_count -= 1; self.maybe_flush(); @@ -467,9 +467,9 @@ impl Outputter { /// Convenience cover over tputs, in recognition of the fact that our Term has Optional fields. /// If `str` is Some, write it with tputs and return true. Otherwise, return false. - pub fn tputs_if_some(&mut self, str: &Option) -> bool { + pub fn tputs_if_some(&mut self, str: &Option>) -> bool { if let Some(str) = str { - self.tputs(str); + self.tputs(str.as_ref()); true } else { false diff --git a/fish-rust/src/screen.rs b/fish-rust/src/screen.rs new file mode 100644 index 000000000..0cf7b4762 --- /dev/null +++ b/fish-rust/src/screen.rs @@ -0,0 +1,2019 @@ +//! High level library for handling the terminal screen +//! +//! The screen library allows the interactive reader to write its output to screen efficiently by +//! keeping an internal representation of the current screen contents and trying to find a reasonably +//! efficient way for transforming that to the desired screen content. +//! +//! The current implementation is less smart than ncurses allows and can not for example move blocks +//! of text around to handle text insertion. + +use std::collections::LinkedList; +use std::ffi::{CStr, CString}; +use std::io::Write; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Mutex; + +use cxx::{CxxVector, CxxWString, UniquePtr}; +use libc::{ONLCR, STDERR_FILENO, STDOUT_FILENO}; + +use crate::common::{ + get_ellipsis_char, get_omitted_newline_str, get_omitted_newline_width, + has_working_tty_timestamps, shell_modes, str2wcstring, wcs2string, write_loop, ScopeGuard, + ScopeGuarding, +}; +use crate::curses::{term, tparm0, tparm1}; +use crate::env::{EnvStackRef, Environment, TERM_HAS_XN}; +use crate::fallback::fish_wcwidth; +use crate::ffi::{self, Repin}; +use crate::flog::FLOGF; +use crate::future::IsSomeAnd; +use crate::global_safety::RelaxedAtomicBool; +use crate::highlight::{HighlightColorResolver, HighlightSpecListFFI}; +use crate::output::Outputter; +use crate::termsize::{termsize_last, Termsize}; +use crate::wchar::prelude::*; +use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}; +use crate::wcstringutil::string_prefixes_string; +use crate::{highlight::HighlightSpec, wcstringutil::fish_wcwidth_visible}; +use std::pin::Pin; + +#[derive(Clone, Default)] +pub struct HighlightedChar { + highlight: HighlightSpec, + character: char, +} + +/// A class representing a single line of a screen. +#[derive(Clone, Default)] +pub struct Line { + /// A pair of a character, and the color with which to draw it. + pub text: Vec, + pub is_soft_wrapped: bool, + pub indentation: usize, +} + +impl Line { + fn new() -> Self { + Default::default() + } + + /// Clear the line's contents. + fn clear(&mut self) { + self.text.clear(); + } + + /// Append a single character \p txt to the line with color \p c. + pub fn append(&mut self, character: char, highlight: HighlightSpec) { + self.text.push(HighlightedChar { + highlight, + character, + }) + } + + /// Append a nul-terminated string \p txt to the line, giving each character \p color. + pub fn append_str(&mut self, txt: &wstr, highlight: HighlightSpec) { + for c in txt.chars() { + self.append(c, highlight); + } + } + + /// \return the number of characters. + pub fn size(&self) -> usize { + self.text.len() + } + + /// \return the character at a char index. + pub fn char_at(&self, idx: usize) -> char { + self.text[idx].character + } + + /// \return the color at a char index. + pub fn color_at(&self, idx: usize) -> HighlightSpec { + self.text[idx].highlight + } + + /// Append the contents of \p line to this line. + pub fn append_line(&mut self, line: &Line) { + self.text.extend_from_slice(&line.text); + } + + /// \return the width of this line, counting up to no more than \p max characters. + /// This follows fish_wcswidth() semantics, except that characters whose width would be -1 are + /// treated as 0. + pub fn wcswidth_min_0(&self, max: usize /* = usize::MAX */) -> usize { + let mut result = 0; + for c in &self.text[..max.min(self.text.len())] { + let w = fish_wcwidth_visible(c.character); + // A backspace at the start of the line does nothing. + if w > 0 || result > 0 { + result = + usize::try_from(isize::try_from(result).unwrap() + isize::try_from(w).unwrap()) + .unwrap(); + } + } + result + } +} + +/// Where the cursor is in (x, y) coordinates. +#[derive(Clone, Copy, Default)] +pub struct Cursor { + x: usize, + y: usize, +} + +/// A class representing screen contents. +#[derive(Clone, Default)] +pub struct ScreenData { + line_datas: Vec, + + /// The width of the screen in this rendering. + /// -1 if not set, i.e. we have not rendered before. + screen_width: Option, + + cursor: Cursor, +} + +impl ScreenData { + fn new() -> Self { + Default::default() + } + + pub fn add_line(&mut self) -> &mut Line { + self.line_datas.push(Line::new()); + self.line_datas.last_mut().unwrap() + } + + pub fn resize(&mut self, size: usize) { + self.line_datas.resize(size, Default::default()) + } + + pub fn create_line(&mut self, idx: usize) { + if idx >= self.line_datas.len() { + self.line_datas.resize(idx + 1, Default::default()) + } + } + + pub fn insert_line_at_index(&mut self, idx: usize) -> &mut Line { + assert!(idx <= self.line_datas.len()); + self.line_datas.insert(idx, Default::default()); + &mut self.line_datas[idx] + } + + pub fn line(&self, idx: usize) -> &Line { + &self.line_datas[idx] + } + + pub fn line_mut(&mut self, idx: usize) -> &mut Line { + &mut self.line_datas[idx] + } + + pub fn line_count(&self) -> usize { + self.line_datas.len() + } + + pub fn append_lines(&mut self, d: &ScreenData) { + self.line_datas.extend_from_slice(&d.line_datas); + } + + pub fn is_empty(&self) -> bool { + self.line_datas.is_empty() + } +} + +/// The class representing the current and desired screen contents. +pub struct Screen<'a> { + /// Whether the last-drawn autosuggestion (if any) is truncated, or hidden entirely. + pub autosuggestion_is_truncated: bool, + + /// Receiver for our output. + outp: &'a mut Outputter, + + /// The internal representation of the desired screen contents. + desired: ScreenData, + /// The internal representation of the actual screen contents. + actual: ScreenData, + /// A string containing the prompt which was last printed to the screen. + actual_left_prompt: WString, + /// Last right prompt width. + last_right_prompt_width: usize, + /// If we support soft wrapping, we can output to this location without any cursor motion. + soft_wrap_location: Option, + /// This flag is set to true when there is reason to suspect that the parts of the screen lines + /// where the actual content is not filled in may be non-empty. This means that a clr_eol + /// command has to be sent to the terminal at the end of each line, including + /// actual_lines_before_reset. + need_clear_lines: bool, + /// Whether there may be yet more content after the lines, and we issue a clr_eos if possible. + need_clear_screen: bool, + /// If we need to clear, this is how many lines the actual screen had, before we reset it. This + /// is used when resizing the window larger: if the cursor jumps to the line above, we need to + /// remember to clear the subsequent lines. + actual_lines_before_reset: usize, + /// These status buffers are used to check if any output has occurred other than from fish's + /// main loop, in which case we need to redraw. + prev_buff_1: libc::stat, + prev_buff_2: libc::stat, +} + +impl<'a> Screen<'a> { + pub fn new() -> Self { + Self { + outp: Outputter::stdoutput().get_mut(), + autosuggestion_is_truncated: Default::default(), + desired: Default::default(), + actual: Default::default(), + actual_left_prompt: Default::default(), + last_right_prompt_width: Default::default(), + soft_wrap_location: Default::default(), + need_clear_lines: Default::default(), + need_clear_screen: Default::default(), + actual_lines_before_reset: Default::default(), + prev_buff_1: unsafe { std::mem::zeroed() }, + prev_buff_2: unsafe { std::mem::zeroed() }, + } + } + + /// This is the main function for the screen output library. It is used to define the desired + /// contents of the screen. The screen command will use its knowledge of the current contents of + /// the screen in order to render the desired output using as few terminal commands as possible. + /// + /// \param left_prompt the prompt to prepend to the command line + /// \param right_prompt the right prompt, or NULL if none + /// \param commandline the command line + /// \param explicit_len the number of characters of the "explicit" (non-autosuggestion) portion + /// of the command line \param colors the colors to use for the commanad line \param indent the + /// indent to use for the command line \param cursor_pos where the cursor is \param pager the + /// pager to render below the command line \param page_rendering to cache the current pager view + /// \param cursor_is_within_pager whether the position is within the pager line (first line) + pub fn write( + &mut self, + left_prompt: &wstr, + right_prompt: &wstr, + commandline: &wstr, + explicit_len: usize, + colors: &[HighlightSpec], + indent: &[usize], + cursor_pos: usize, + vars: &dyn Environment, + pager: Pin<&mut ffi::pager_t>, + page_rendering: Pin<&mut ffi::page_rendering_t>, + cursor_is_within_pager: bool, + ) { + let curr_termsize = termsize_last(); + let screen_width = curr_termsize.width; + static REPAINTS: AtomicU32 = AtomicU32::new(0); + FLOGF!( + screen, + "Repaint %u", + 1 + REPAINTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + ); + let mut cursor_arr = Cursor::default(); + + // Turn the command line into the explicit portion and the autosuggestion. + let (explicit_command_line, autosuggestion) = commandline.split_at(explicit_len); + + // If we are using a dumb terminal, don't try any fancy stuff, just print out the text. + // right_prompt not supported. + if is_dumb() { + let prompt_narrow = wcs2string(left_prompt); + let command_line_narrow = wcs2string(explicit_command_line); + + let _ = write_loop(&STDOUT_FILENO, b"\r"); + let _ = write_loop(&STDOUT_FILENO, &prompt_narrow); + let _ = write_loop(&STDOUT_FILENO, &command_line_narrow); + + return; + } + + self.check_status(); + + // Completely ignore impossibly small screens. + if screen_width < 4 { + return; + } + let screen_width = usize::try_from(screen_width).unwrap(); + + // Compute a layout. + let layout = compute_layout( + screen_width, + left_prompt, + right_prompt, + explicit_command_line, + autosuggestion, + ); + + // Determine whether, if we have an autosuggestion, it was truncated. + self.autosuggestion_is_truncated = + !autosuggestion.is_empty() && autosuggestion != layout.autosuggestion; + + // Clear the desired screen and set its width. + self.desired.screen_width = Some(screen_width); + self.desired.resize(0); + self.desired.cursor.x = 0; + self.desired.cursor.y = 0; + + // Append spaces for the left prompt. + for _ in 0..layout.left_prompt_space { + self.desired_append_char(' ', HighlightSpec::new(), 0, layout.left_prompt_space, 1); + } + + // If overflowing, give the prompt its own line to improve the situation. + let first_line_prompt_space = layout.left_prompt_space; + + // Reconstruct the command line. + let effective_commandline = explicit_command_line.to_owned() + &layout.autosuggestion[..]; + + // Output the command line. + let mut i = 0; + while i < effective_commandline.len() { + // Grab the current cursor's x,y position if this character matches the cursor's offset. + if !cursor_is_within_pager && i == cursor_pos { + cursor_arr = self.desired.cursor; + } + self.desired_append_char( + effective_commandline.as_char_slice()[i], + colors[i], + indent[i], + first_line_prompt_space, + usize::try_from(fish_wcwidth_visible( + effective_commandline.as_char_slice()[i], + )) + .unwrap(), + ); + i += 1; + } + + // Cursor may have been at the end too. + if !cursor_is_within_pager && i == cursor_pos { + cursor_arr = self.desired.cursor; + } + + let full_line_count = self.desired.cursor.y + 1; + + // Now that we've output everything, set the cursor to the position that we saved in the loop + // above. + self.desired.cursor = cursor_arr; + + if cursor_is_within_pager { + self.desired.cursor.x = cursor_pos; + self.desired.cursor.y = self.desired.line_count(); + } + + // Re-render our completions page if necessary. Limit the term size of the pager to the true + // term size, minus the number of lines consumed by our string. + let pager = pager.unpin(); + let page_rendering = page_rendering.unpin(); + crate::ffi::pager_set_term_size_ffi( + pager.pin(), + &Termsize::new( + std::cmp::max(1, curr_termsize.width), + std::cmp::max( + 1, + curr_termsize + .height + .saturating_sub_unsigned(full_line_count), + ), + ) as *const Termsize as *const autocxx::c_void, + ); + + crate::ffi::pager_update_rendering_ffi(pager.pin(), page_rendering.pin()); + // Append pager_data (none if empty). + self.desired + .append_lines(unsafe { &*(page_rendering.screen_data_ffi() as *const ScreenData) }); + + self.update(&layout.left_prompt, &layout.right_prompt, vars); + self.save_status(); + } + + /// Resets the screen buffer's internal knowledge about the contents of the screen, + /// optionally repainting the prompt as well. + /// This function assumes that the current line is still valid. + pub fn reset_line(&mut self, repaint_prompt: bool /* = false */) { + // Remember how many lines we had output to, so we can clear the remaining lines in the next + // call to s_update. This prevents leaving junk underneath the cursor when resizing a window + // wider such that it reduces our desired line count. + self.actual_lines_before_reset = + std::cmp::max(self.actual_lines_before_reset, self.actual.line_count()); + + if repaint_prompt { + // If the prompt is multi-line, we need to move up to the prompt's initial line. We do this + // by lying to ourselves and claiming that we're really below what we consider "line 0" + // (which is the last line of the prompt). This will cause us to move up to try to get back + // to line 0, but really we're getting back to the initial line of the prompt. + let prompt_line_count = calc_prompt_lines(&self.actual_left_prompt); + self.actual.cursor.y += prompt_line_count.checked_sub(1).unwrap(); + self.actual_left_prompt.clear(); + } + self.actual.resize(0); + self.need_clear_lines = true; + + // This should prevent resetting the cursor position during the next repaint. + let _ = write_loop(&STDOUT_FILENO, b"\r"); + self.actual.cursor.x = 0; + + self.save_status(); + } + + /// 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, + /// The screen width must be provided for the PROMPT_SP hack. + pub fn reset_abandoning_line(&mut self, screen_width: usize) { + self.actual.cursor.y = 0; + self.actual.resize(0); + self.actual_left_prompt.clear(); + self.need_clear_lines = true; + + // Do the PROMPT_SP hack. + let mut abandon_line_string = WString::with_capacity(screen_width + 32); + + // Don't need to check for fish_wcwidth errors; this is done when setting up + // omitted_newline_char in common.cpp. + let non_space_width = get_omitted_newline_width(); + let term = term(); + let term = term.as_ref(); + // We do `>` rather than `>=` because the code below might require one extra space. + if screen_width > non_space_width { + let mut justgrey = true; + let add = |abandon_line_string: &mut WString, s: Option| { + let Some(s) = s else { + return false; + }; + abandon_line_string.push_utfstr(&str2wcstring(s.as_bytes())); + true + }; + if let Some(enter_dim_mode) = term.and_then(|term| term.enter_dim_mode.as_ref()) { + if add(&mut abandon_line_string, tparm0(enter_dim_mode)) { + // Use dim if they have it, so the color will be based on their actual normal + // color and the background of the terminal. + justgrey = false; + } + } + if let (true, Some(set_a_foreground)) = ( + justgrey, + term.and_then(|term| term.set_a_foreground.as_ref()), + ) { + let max_colors = term.unwrap().max_colors.unwrap_or_default(); + if max_colors >= 238 { + // draw the string in a particular grey + add(&mut abandon_line_string, tparm1(set_a_foreground, 237)); + } else if max_colors >= 9 { + // bright black (the ninth color, looks grey) + add(&mut abandon_line_string, tparm1(set_a_foreground, 8)); + } else if max_colors >= 2 { + if let Some(enter_bold_mode) = term.unwrap().enter_bold_mode.as_ref() { + // we might still get that color by setting black and going bold for bright + add(&mut abandon_line_string, tparm0(enter_bold_mode)); + add(&mut abandon_line_string, tparm1(set_a_foreground, 0)); + } + } + } + + abandon_line_string.push_utfstr(&get_omitted_newline_str()); + + if let Some(exit_attribute_mode) = + term.and_then(|term| term.exit_attribute_mode.as_ref()) + { + // normal text ANSI escape sequence + add(&mut abandon_line_string, tparm0(exit_attribute_mode)); + } + + let newline_glitch_width = if TERM_HAS_XN.load(Ordering::Relaxed) { + 0 + } else { + 1 + }; + for _ in 0..screen_width - non_space_width - newline_glitch_width { + abandon_line_string.push(' '); + } + } + + abandon_line_string.push('\r'); + abandon_line_string.push_utfstr(get_omitted_newline_str()); + // Now we are certainly on a new line. But we may have dropped the omitted newline char on + // it. So append enough spaces to overwrite the omitted newline char, and then clear all the + // spaces from the new line. + for _ in 0..non_space_width { + abandon_line_string.push(' '); + } + abandon_line_string.push('\r'); + // Clear entire line. Zsh doesn't do this. Fish added this with commit 4417a6ee: If you have + // a prompt preceded by a new line, you'll get a line full of spaces instead of an empty + // line above your prompt. This doesn't make a difference in normal usage, but copying and + // pasting your terminal log becomes a pain. This commit clears that line, making it an + // actual empty line. + if !is_dumb() { + if let Some(clr_eol) = term.unwrap().clr_eol.as_ref() { + abandon_line_string.push_utfstr(&str2wcstring(clr_eol.as_bytes())); + } + } + + let narrow_abandon_line_string = wcs2string(&abandon_line_string); + let _ = write_loop(&STDOUT_FILENO, &narrow_abandon_line_string); + self.actual.cursor.x = 0; + + self.save_status(); + } + + /// Stat stdout and stderr and save result as the current timestamp. + /// This is used to avoid reacting to changes that we ourselves made to the screen. + pub fn save_status(&mut self) { + unsafe { + libc::fstat(STDOUT_FILENO, &mut self.prev_buff_1); + libc::fstat(STDERR_FILENO, &mut self.prev_buff_2); + } + } + + /// \return whether we believe the cursor is wrapped onto the last line, and that line is + /// otherwise empty. This includes both soft and hard wrapping. + pub fn cursor_is_wrapped_to_own_line(&self) -> bool { + // Note == comparison against the line count is correct: we do not create a line just for the + // cursor. If there is a line containing the cursor, then it means that line has contents and we + // should return false. + // Don't consider dumb terminals to have wrapping for the purposes of this function. + self.actual.cursor.x == 0 && self.actual.cursor.y == self.actual.line_count() && !is_dumb() + } + + /// Appends a character to the end of the line that the output cursor is on. This function + /// automatically handles linebreaks and lines longer than the screen width. + fn desired_append_char( + &mut self, + b: char, + c: HighlightSpec, + indent: usize, + prompt_width: usize, + bwidth: usize, + ) { + let mut line_no = self.desired.cursor.y; + + if b == '\n' { + // Current line is definitely hard wrapped. + // Create the next line. + self.desired.create_line(self.desired.cursor.y + 1); + self.desired.line_mut(self.desired.cursor.y).is_soft_wrapped = false; + self.desired.cursor.y += 1; + let line_no = self.desired.cursor.y; + self.desired.cursor.x = 0; + let indentation = prompt_width + indent * INDENT_STEP; + let line = self.desired.line_mut(line_no); + line.indentation = indentation; + for _ in 0..indentation { + self.desired_append_char(' ', HighlightSpec::default(), indent, prompt_width, 1); + } + } else if b == '\r' { + let current = self.desired.line_mut(line_no); + current.clear(); + self.desired.cursor.x = 0; + } else { + let screen_width = self.desired.screen_width; + let cw = bwidth; + + self.desired.create_line(line_no); + + // Check if we are at the end of the line. If so, continue on the next line. + if screen_width.is_none_or(|sw| (self.desired.cursor.x + cw) > sw) { + // Current line is soft wrapped (assuming we support it). + self.desired.line_mut(self.desired.cursor.y).is_soft_wrapped = true; + + line_no = self.desired.line_count(); + self.desired.add_line(); + self.desired.cursor.y += 1; + self.desired.cursor.x = 0; + } + + self.desired.line_mut(line_no).append(b, c); + self.desired.cursor.x += cw; + + // Maybe wrap the cursor to the next line, even if the line itself did not wrap. This + // avoids wonkiness in the last column. + if screen_width.is_none_or(|sw| self.desired.cursor.x >= sw) { + self.desired.line_mut(line_no).is_soft_wrapped = true; + self.desired.cursor.x = 0; + self.desired.cursor.y += 1; + } + } + } + + /// Stat stdout and stderr and compare result to previous result in reader_save_status. Repaint + /// if modification time has changed. + fn check_status(&mut self) { + let _ = std::io::stdout().flush(); + let _ = std::io::stderr().flush(); + if !has_working_tty_timestamps() { + // We can't reliably determine if the terminal has been written to behind our back so we + // just assume that hasn't happened and hope for the best. This is important for multi-line + // prompts to work correctly. + return; + } + + let mut post_buff_1: libc::stat = unsafe { std::mem::zeroed() }; + let mut post_buff_2: libc::stat = unsafe { std::mem::zeroed() }; + unsafe { libc::fstat(STDOUT_FILENO, &mut post_buff_1) }; + unsafe { libc::fstat(STDERR_FILENO, &mut post_buff_2) }; + + let changed = self.prev_buff_1.st_mtime != post_buff_1.st_mtime + || self.prev_buff_1.st_mtime_nsec != post_buff_1.st_mtime_nsec + || self.prev_buff_2.st_mtime != post_buff_2.st_mtime + || self.prev_buff_2.st_mtime_nsec != post_buff_2.st_mtime_nsec; + + if changed { + // Ok, someone has been messing with our screen. We will want to repaint. However, we do not + // know where the cursor is. It is our best bet that we are still on the same line, so we + // move to the beginning of the line, reset the modelled screen contents, and then set the + // modeled cursor y-pos to its earlier value. + let prev_line = self.actual.cursor.y; + self.reset_line(true /* repaint prompt */); + self.actual.cursor.y = prev_line; + } + } + + /// Write the bytes needed to move screen cursor to the specified position to the specified + /// buffer. The actual_cursor field of the specified screen_t will be updated. + /// + /// \param new_x the new x position + /// \param new_y the new y position + fn r#move(&mut self, new_x: usize, new_y: usize) { + if self.actual.cursor.x == new_x && self.actual.cursor.y == new_y { + return; + } + + let mut outp = ScopedBuffer::new(self.outp); + + // If we are at the end of our window, then either the cursor stuck to the edge or it didn't. We + // don't know! We can fix it up though. + if self + .actual + .screen_width + .is_some_and(|sw| self.actual.cursor.x == sw) + { + // Either issue a cr to go back to the beginning of this line, or a nl to go to the + // beginning of the next one, depending on what we think is more efficient. + if new_y <= self.actual.cursor.y { + outp.push(b'\r'); + } else { + outp.push(b'\n'); + self.actual.cursor.y += 1; + } + // Either way we're not in the first column. + self.actual.cursor.x = 0; + } + + let y_steps = + isize::try_from(new_y).unwrap() - isize::try_from(self.actual.cursor.y).unwrap(); + + let Some(term) = term() else { + return; + }; + let term = term.as_ref(); + + let s = if y_steps < 0 { + term.cursor_up.as_ref() + } else if y_steps > 0 { + let s = term.cursor_down.as_ref(); + if (shell_modes().c_oflag & ONLCR) != 0 && s.is_some_and(|s| s.as_bytes() == b"\n") { + // See GitHub issue #4505. + // Most consoles use a simple newline as the cursor down escape. + // If ONLCR is enabled (which it normally is) this will of course + // also move the cursor to the beginning of the line. + // We could do: + // if (std::strcmp(cursor_up, "\x1B[A") == 0) str = "\x1B[B"; + // else ... but that doesn't work for unknown reasons. + self.actual.cursor.x = 0; + } + s + } else { + None + }; + + for _ in 0..y_steps.abs_diff(0) { + outp.tputs_if_some(&s); + } + + let mut x_steps = + isize::try_from(new_x).unwrap() - isize::try_from(self.actual.cursor.x).unwrap(); + if x_steps != 0 && new_x == 0 { + outp.push(b'\r'); + x_steps = 0; + } + + let (s, multi_str) = if x_steps < 0 { + (term.cursor_left.as_ref(), term.parm_left_cursor.as_ref()) + } else { + (term.cursor_right.as_ref(), term.parm_right_cursor.as_ref()) + }; + + // Use the bulk ('multi') output for cursor movement if it is supported and it would be shorter + // Note that this is required to avoid some visual glitches in iTerm (issue #1448). + let use_multi = multi_str.is_some_and(|ms| !ms.as_bytes().is_empty()) + && x_steps.abs_diff(0) * s.map_or(0, |s| s.as_bytes().len()) + > multi_str.unwrap().as_bytes().len(); + if use_multi { + let multi_param = tparm1( + multi_str.as_ref().unwrap(), + i32::try_from(x_steps.abs_diff(0)).unwrap(), + ); + outp.tputs_if_some(&multi_param); + } else { + for _ in 0..x_steps.abs_diff(0) { + outp.tputs_if_some(&s); + } + } + + self.actual.cursor.x = new_x; + self.actual.cursor.y = new_y; + } + + /// Convert a wide character to a multibyte string and append it to the buffer. + fn write_char(&mut self, c: char, width: isize) { + let mut outp = ScopedBuffer::new(self.outp); + self.actual.cursor.x = self.actual.cursor.x.wrapping_add(width as usize); + outp.writech(c); + if Some(self.actual.cursor.x) == self.actual.screen_width && allow_soft_wrap() { + self.soft_wrap_location = Some(Cursor { + x: 0, + y: self.actual.cursor.y + 1, + }); + + // Note that our cursor position may be a lie: Apple Terminal makes the right cursor stick + // to the margin, while Ubuntu makes it "go off the end" (but still doesn't wrap). We rely + // on s_move to fix this up. + } else { + self.soft_wrap_location = None; + } + } + + /// Send the specified string through tputs and append the output to the screen's outputter. + fn write_mbs(&mut self, s: &CStr) { + self.outp.tputs(s) + } + + fn write_mbs_if_some(&mut self, s: &Option>) -> bool { + self.outp.tputs_if_some(s) + } + + /// Convert a wide string to a multibyte string and append it to the buffer. + fn write_str(&mut self, s: &wstr) { + self.outp.write_wstr(s) + } + + /// Update the cursor as if soft wrapping had been performed. + /// We are about to output one or more characters onto the screen at the given x, y. If we are at the + /// end of previous line, and the previous line is marked as soft wrapping, then tweak the screen so + /// we believe we are already in the target position. This lets the terminal take care of wrapping, + /// which means that if you copy and paste the text, it won't have an embedded newline. + fn handle_soft_wrap(&mut self, x: usize, y: usize) { + if self + .soft_wrap_location + .as_ref() + .is_some_and(|swl| (x, y) == (swl.x, swl.y)) + { + // We can soft wrap; but do we want to? + if self.desired.line(y - 1).is_soft_wrapped && allow_soft_wrap() { + // Yes. Just update the actual cursor; that will cause us to elide emitting the commands + // to move here, so we will just output on "one big line" (which the terminal soft + // wraps. + self.actual.cursor = self.soft_wrap_location.unwrap(); + } + } + } + + fn scoped_buffer(&mut self) -> impl ScopeGuarding> { + self.outp.begin_buffering(); + ScopeGuard::new(self, |zelf| { + zelf.outp.end_buffering(); + }) + } + + /// Update the screen to match the desired output. + fn update(&mut self, left_prompt: &wstr, right_prompt: &wstr, vars: &dyn Environment) { + // Helper function to set a resolved color, using the caching resolver. + let mut color_resolver = HighlightColorResolver::new(); + let mut set_color = |zelf: &mut Self, c| { + let fg = color_resolver.resolve_spec(&c, false, vars); + let bg = color_resolver.resolve_spec(&c, true, vars); + zelf.outp.set_color(fg, bg); + }; + + let mut cached_layouts = unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() }; + let mut zelf = self.scoped_buffer(); + + // Determine size of left and right prompt. Note these have already been truncated. + let left_prompt_layout = cached_layouts.calc_prompt_layout(left_prompt, None, usize::MAX); + let left_prompt_width = left_prompt_layout.last_line_width; + let right_prompt_width = cached_layouts + .calc_prompt_layout(right_prompt, None, usize::MAX) + .last_line_width; + + // Figure out how many following lines we need to clear (probably 0). + let actual_lines_before_reset = zelf.actual_lines_before_reset; + zelf.actual_lines_before_reset = 0; + + let mut need_clear_lines = zelf.need_clear_lines; + let mut need_clear_screen = zelf.need_clear_screen; + let mut has_cleared_screen = false; + + let screen_width = zelf.desired.screen_width; + + if zelf.actual.screen_width != screen_width { + // Ensure we don't issue a clear screen for the very first output, to avoid issue #402. + if zelf.actual.screen_width.is_some_and(|sw| sw > 0) { + need_clear_screen = true; + zelf.r#move(0, 0); + zelf.reset_line(false); + + need_clear_lines |= zelf.need_clear_lines; + need_clear_screen |= zelf.need_clear_screen; + } + zelf.actual.screen_width = screen_width; + } + + zelf.need_clear_lines = false; + zelf.need_clear_screen = false; + + // Determine how many lines have stuff on them; we need to clear lines with stuff that we don't + // want. + let lines_with_stuff = std::cmp::max(actual_lines_before_reset, zelf.actual.line_count()); + if zelf.desired.line_count() < lines_with_stuff { + need_clear_screen = true; + } + + let term = term(); + let term = term.as_ref(); + + // Output the left prompt if it has changed. + if left_prompt != zelf.actual_left_prompt { + zelf.r#move(0, 0); + let mut start = 0; + for line_break in left_prompt_layout.line_breaks { + zelf.write_str(&left_prompt[start..line_break]); + zelf.outp + .tputs_if_some(&term.and_then(|term| term.clr_eol.as_ref())); + start = line_break; + } + zelf.write_str(&left_prompt[start..]); + zelf.actual_left_prompt = left_prompt.to_owned(); + zelf.actual.cursor.x = left_prompt_width; + } + + fn o_line<'b>(zelf: &'b Screen<'_>, i: usize) -> &'b Line { + zelf.desired.line(i) + } + fn o_line_mut<'b>(zelf: &'b mut Screen<'_>, i: usize) -> &'b mut Line { + zelf.desired.line_mut(i) + } + fn s_line<'b>(zelf: &'b Screen<'_>, i: usize) -> &'b Line { + zelf.actual.line(i) + } + fn s_line_mut<'b>(zelf: &'b mut Screen<'_>, i: usize) -> &'b mut Line { + zelf.actual.line_mut(i) + } + + // Output all lines. + for i in 0..zelf.desired.line_count() { + zelf.actual.create_line(i); + + let start_pos = if i == 0 { left_prompt_width } else { 0 }; + let mut current_width = 0; + let mut has_cleared_line = false; + + // If this is the last line, maybe we should clear the screen. + // Don't issue clr_eos if we think the cursor will end up in the last column - see #6951. + let should_clear_screen_this_line = need_clear_screen + && i + 1 == zelf.desired.line_count() + && term.is_some_and(|term| term.clr_eos.is_some()) + && !(zelf.desired.cursor.x == 0 + && zelf.desired.cursor.y == zelf.desired.line_count()); + + // skip_remaining is how many columns are unchanged on this line. + // Note that skip_remaining is a width, not a character count. + let mut skip_remaining = start_pos; + + let shared_prefix = line_shared_prefix(o_line(&zelf, i), s_line(&zelf, i)); + let mut skip_prefix = shared_prefix; + if shared_prefix < o_line(&zelf, i).indentation { + if o_line(&zelf, i).indentation > s_line(&zelf, i).indentation + && !has_cleared_screen + && term.is_some_and(|term| term.clr_eol.is_some() && term.clr_eos.is_some()) + { + set_color(&mut zelf, HighlightSpec::new()); + zelf.r#move(0, i); + let term = term.unwrap(); + zelf.write_mbs_if_some(if should_clear_screen_this_line { + &term.clr_eos + } else { + &term.clr_eol + }); + has_cleared_screen = should_clear_screen_this_line; + has_cleared_line = true; + } + skip_prefix = o_line(&zelf, i).indentation; + } + + // Compute how much we should skip. At a minimum we skip over the prompt. But also skip + // over the shared prefix of what we want to output now, and what we output before, to + // avoid repeatedly outputting it. + if skip_prefix > 0 { + let skip_width = if shared_prefix < skip_prefix { + skip_prefix + } else { + o_line(&zelf, i).wcswidth_min_0(shared_prefix) + }; + if skip_width > skip_remaining { + skip_remaining = skip_width; + } + } + + if !should_clear_screen_this_line { + // If we're soft wrapped, and if we're going to change the first character of the next + // line, don't skip over the last two characters so that we maintain soft-wrapping. + if o_line(&zelf, i).is_soft_wrapped && i + 1 < zelf.desired.line_count() { + let mut next_line_will_change = true; + if i + 1 < zelf.actual.line_count() { + if line_shared_prefix(zelf.desired.line(i + 1), zelf.actual.line(i + 1)) > 0 + { + next_line_will_change = false; + } + } + if next_line_will_change { + skip_remaining = + std::cmp::min(skip_remaining, zelf.actual.screen_width.unwrap() - 2); + } + } + } + + // Skip over skip_remaining width worth of characters. + let mut j = 0; + while j < o_line(&zelf, i).size() { + let width = + usize::try_from(fish_wcwidth_visible(o_line(&zelf, i).char_at(j))).unwrap(); + if skip_remaining < width { + break; + } + skip_remaining -= width; + current_width += width; + j += 1; + } + + // Skip over zero-width characters (e.g. combining marks at the end of the prompt). + while j < o_line(&zelf, i).size() { + let width = fish_wcwidth_visible(o_line(&zelf, i).char_at(j)); + if width > 0 { + break; + } + j += 1; + } + + // Now actually output stuff. + loop { + let done = j >= o_line(&zelf, i).size(); + // Clear the screen if we have not done so yet. + // If we are about to output into the last column, clear the screen first. If we clear + // the screen after we output into the last column, it can erase the last character due + // to the sticky right cursor. If we clear the screen too early, we can defeat soft + // wrapping. + if should_clear_screen_this_line + && !has_cleared_screen + && (done || Some(j + 1) == screen_width) + { + set_color(&mut zelf, HighlightSpec::new()); + zelf.r#move(current_width, i); + zelf.write_mbs_if_some(&term.and_then(|term| term.clr_eos.as_ref())); + has_cleared_screen = true; + } + if done { + break; + } + + zelf.handle_soft_wrap(current_width, i); + zelf.r#move(current_width, i); + let color = o_line(&zelf, i).color_at(j); + set_color(&mut zelf, color); + let ch = o_line(&zelf, i).char_at(j); + let width = usize::try_from(fish_wcwidth_visible(ch)).unwrap(); + zelf.write_char(ch, isize::try_from(width).unwrap()); + current_width += width; + j += 1; + } + + let mut clear_remainder = false; + // Clear the remainder of the line if we need to clear and if we didn't write to the end of + // the line. If we did write to the end of the line, the "sticky right edge" (as part of + // auto_right_margin) means that we'll be clearing the last character we wrote! + if has_cleared_screen || has_cleared_line { + // Already cleared everything. + clear_remainder = false; + } else if need_clear_lines && screen_width.is_some_and(|sw| current_width < sw) { + clear_remainder = true; + } else if right_prompt_width < zelf.last_right_prompt_width { + clear_remainder = true; + } else { + // This wcswidth shows up strong in the profile. + // Only do it if the previous line could conceivably be wider. + // That means if it is a prefix of the current one we can skip it. + if s_line(&zelf, i).text.len() != shared_prefix { + let prev_width = s_line(&zelf, i).wcswidth_min_0(usize::MAX); + clear_remainder = prev_width > current_width; + } + } + + // We unset the color even if we don't clear the line. + // This means that we switch background correctly on the next, + // including our weird implicit bolding. + set_color(&mut zelf, HighlightSpec::new()); + if let (true, Some(clr_eol)) = + (clear_remainder, term.and_then(|term| term.clr_eol.as_ref())) + { + zelf.r#move(current_width, i); + zelf.write_mbs(clr_eol); + } + + // Output any rprompt if this is the first line. + if i == 0 && right_prompt_width > 0 { + // Move the cursor to the beginning of the line first to be independent of the width. + // This helps prevent staircase effects if fish and the terminal disagree. + zelf.r#move(0, 0); + zelf.r#move(screen_width.unwrap() - right_prompt_width, i); + set_color(&mut zelf, HighlightSpec::new()); + zelf.write_str(right_prompt); + zelf.actual.cursor.x += right_prompt_width; + + // We output in the last column. Some terms (Linux) push the cursor further right, past + // the window. Others make it "stick." Since we don't really know which is which, issue + // a cr so it goes back to the left. + // + // However, if the user is resizing the window smaller, then it's possible the cursor + // wrapped. If so, then a cr will go to the beginning of the following line! So instead + // issue a bunch of "move left" commands to get back onto the line, and then jump to the + // front of it. + let Cursor { x, y } = zelf.actual.cursor; + zelf.r#move(x - right_prompt_width, y); + zelf.write_str(L!("\r")); + zelf.actual.cursor.x = 0; + } + } + + // Also move the cursor to the beginning of the line here, + // in case we're wrong about the width anywhere. + // Don't do it when running in midnight_commander because of + // https://midnight-commander.org/ticket/4258. + if !MIDNIGHT_COMMANDER_HACK.load() { + zelf.r#move(0, 0); + } + + // Clear remaining lines (if any) if we haven't cleared the screen. + if let (false, true, Some(clr_eol)) = ( + has_cleared_screen, + need_clear_screen, + term.and_then(|term| term.clr_eol.as_ref()), + ) { + set_color(&mut zelf, HighlightSpec::new()); + for i in zelf.desired.line_count()..lines_with_stuff { + zelf.r#move(0, i); + zelf.write_mbs(clr_eol); + } + } + + let Cursor { x, y } = zelf.desired.cursor; + zelf.r#move(x, y); + set_color(&mut zelf, HighlightSpec::new()); + + // We have now synced our actual screen against our desired screen. Note that this is a big + // assignment! + zelf.actual = zelf.desired.clone(); + zelf.last_right_prompt_width = right_prompt_width; + } +} + +/// Issues an immediate clr_eos. +pub fn screen_force_clear_to_end() { + Outputter::stdoutput() + .get_mut() + .tputs_if_some(&term().unwrap().clr_eos); +} + +/// Information about the layout of a prompt. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PromptLayout { + /// line breaks when rendering the prompt + pub line_breaks: Vec, + /// width of the longest line + pub max_line_width: usize, + /// width of the last line + pub last_line_width: usize, +} + +// Fields exposed for testing. +pub struct PromptCacheEntry { + /// Original prompt string. + pub text: WString, + /// Max line width when computing layout (for truncation). + pub max_line_width: usize, + /// Resulting truncated prompt string. + pub trunc_text: WString, + /// Resulting layout. + pub layout: PromptLayout, +} + +// Maintain a mapping of escape sequences to their widths for fast lookup. +#[derive(Default)] +pub struct LayoutCache { + // Cached escape sequences we've already detected in the prompt and similar strings, ordered + // lexicographically. + esc_cache: Vec, + // LRU-list of prompts and their layouts. + // Use a list so we can promote to the front on a cache hit. + // Exposed for testing. + pub prompt_cache: LinkedList, +} + +// Singleton of the cached escape sequences seen in prompts and similar strings. +// Note this is deliberately exported so that init_curses can clear it. +pub static mut LAYOUT_CACHE_SHARED: Mutex = Mutex::new(LayoutCache::new()); + +impl LayoutCache { + pub const fn new() -> Self { + Self { + esc_cache: vec![], + prompt_cache: LinkedList::new(), + } + } + + pub const PROMPT_CACHE_MAX_SIZE: usize = 12; + + /// \return the size of the escape code cache. + pub fn esc_cache_size(&self) -> usize { + self.esc_cache.len() + } + + /// Insert the entry \p str in its sorted position, if it is not already present in the cache. + pub fn add_escape_code(&mut self, s: WString) { + if let Err(pos) = self.esc_cache.binary_search(&s) { + self.esc_cache.insert(pos, s); + } + } + + /// \return the length of an escape code, accessing and perhaps populating the cache. + pub fn escape_code_length(&mut self, code: &wstr) -> usize { + if code.char_at(0) != '\x1B' { + return 0; + } + + let mut esc_seq_len = self.find_escape_code(code); + if esc_seq_len != 0 { + return esc_seq_len; + } + + if let Some(len) = escape_code_length(code) { + self.add_escape_code(code[..len].to_owned()); + esc_seq_len = len; + } + esc_seq_len + } + + /// \return the length of a string that matches a prefix of \p entry. + pub fn find_escape_code(&self, entry: &wstr) -> usize { + // Do a binary search and see if the escape code right before our entry is a prefix of our + // entry. Note this assumes that escape codes are prefix-free: no escape code is a prefix of + // another one. This seems like a safe assumption. + match self.esc_cache.binary_search_by(|e| e[..].cmp(entry)) { + Ok(_) => return entry.len(), + Err(pos) => { + if pos != 0 { + let candidate = &self.esc_cache[pos - 1]; + if string_prefixes_string(candidate, entry) { + return candidate.len(); + } + } + } + } + 0 + } + + /// Computes a prompt layout for \p prompt_str, perhaps truncating it to \p max_line_width. + /// \return the layout, and optionally the truncated prompt itself, by reference. + pub fn calc_prompt_layout( + &mut self, + prompt_str: &wstr, + out_trunc_prompt: Option<&mut WString>, + max_line_width: usize, /* = usize::MAX */ + ) -> PromptLayout { + // FIXME: we could avoid allocating trunc_prompt if max_line_width is SIZE_T_MAX. + if self.find_prompt_layout(prompt_str, max_line_width) { + let entry = self.prompt_cache.front().unwrap(); + out_trunc_prompt.map(|prompt| *prompt = entry.trunc_text.clone()); + return entry.layout.clone(); + } + + let mut layout = PromptLayout::default(); + let mut trunc_prompt = WString::new(); + + let mut run_start = 0; + while run_start < prompt_str.len() { + let mut run_end = 0; + let mut line_width = measure_run_from(prompt_str, run_start, Some(&mut run_end), self); + if line_width <= max_line_width { + // No truncation needed on this line. + trunc_prompt.extend(prompt_str[run_start..run_end].chars()); + } else { + // Truncation needed on this line. + let mut run_storage = prompt_str[run_start..run_end].to_owned(); + truncate_run(&mut run_storage, max_line_width, &mut line_width, self); + trunc_prompt.extend(run_storage.chars()); + } + layout.max_line_width = std::cmp::max(layout.max_line_width, line_width); + layout.last_line_width = line_width; + + let endc = prompt_str.char_at(run_end); + if endc != '\0' { + if endc == '\n' || endc == '\x0C' { + layout.line_breaks.push(trunc_prompt.len()); + // If the prompt ends in a new line, that's one empy last line. + if run_end == prompt_str.len() - 1 { + layout.last_line_width = 0; + } + } + trunc_prompt.push(endc); + run_start = run_end + 1; + } else { + break; + } + } + out_trunc_prompt.map(|prompt| *prompt = trunc_prompt.clone()); + self.add_prompt_layout(PromptCacheEntry { + text: prompt_str.to_owned(), + max_line_width, + trunc_text: trunc_prompt, + layout: layout.clone(), + }); + layout + } + + pub fn clear(&mut self) { + self.esc_cache.clear(); + self.prompt_cache.clear(); + } + + /// Add a cache entry. + /// Exposed for testing. + pub fn add_prompt_layout(&mut self, entry: PromptCacheEntry) { + self.prompt_cache.push_front(entry); + if self.prompt_cache.len() > Self::PROMPT_CACHE_MAX_SIZE { + self.prompt_cache.pop_back(); + } + } + + /// Finds the layout for a prompt, promoting it to the front. Returns nullptr if not found. + /// Note this points into our cache; do not modify the cache while the pointer lives. + /// Exposed for testing. + pub fn find_prompt_layout( + &mut self, + input: &wstr, + max_line_width: usize, /* = usize::MAX */ + ) -> bool { + let mut i = 0; + for entry in &self.prompt_cache { + if entry.text == input && entry.max_line_width == max_line_width { + break; + } + i += 1; + } + if i < self.prompt_cache.len() { + // Found it. Move it to the front if not already there. + if i > 0 { + let mut tail = self.prompt_cache.split_off(i); + let extracted = tail.pop_front().unwrap(); + self.prompt_cache.append(&mut tail); + self.prompt_cache.push_front(extracted); + } + return true; + } + false + } +} + +/// Returns the number of characters in the escape code starting at 'code'. We only handle sequences +/// that begin with \x1B. If it doesn't we return zero. We also return zero if we don't recognize +/// the escape sequence based on querying terminfo and other heuristics. +pub fn escape_code_length(code: &wstr) -> Option { + if code.char_at(0) != '\x1B' { + return None; + } + + is_visual_escape_seq(code) + .or_else(|| is_screen_name_escape_seq(code)) + .or_else(|| is_osc_escape_seq(code)) + .or_else(|| is_three_byte_escape_seq(code)) + .or_else(|| is_csi_style_escape_seq(code)) + .or_else(|| is_two_byte_escape_seq(code)) +} + +pub fn screen_clear() -> WString { + term() + .unwrap() + .clear_screen + .as_ref() + .map(|clear_screen| str2wcstring(clear_screen.as_bytes())) + .unwrap_or_default() +} + +static MIDNIGHT_COMMANDER_HACK: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +pub fn screen_set_midnight_commander_hack() { + MIDNIGHT_COMMANDER_HACK.store(true) +} + +/// The number of characters to indent new blocks. +const INDENT_STEP: usize = 4; + +/// RAII class to begin and end buffering around an outputter. +struct ScopedBuffer<'a> { + outp: &'a mut Outputter, +} + +impl<'a> ScopedBuffer<'a> { + fn new(outp: &'a mut Outputter) -> Self { + outp.begin_buffering(); + Self { outp } + } +} +impl<'a> Drop for ScopedBuffer<'a> { + fn drop(&mut self) { + self.outp.end_buffering() + } +} + +impl<'a> std::ops::Deref for ScopedBuffer<'a> { + type Target = Outputter; + + fn deref(&self) -> &Self::Target { + self.outp + } +} + +impl<'a> std::ops::DerefMut for ScopedBuffer<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.outp + } +} + +/// Tests if the specified narrow character sequence is present at the specified position of the +/// specified wide character string. All of \c seq must match, but str may be longer than seq. +fn try_sequence(seq: &[u8], s: &wstr) -> usize { + let mut i = 0; + loop { + if i == seq.len() { + return i; + } + if char::from(seq[i]) != s.char_at(i) { + return 0; + } + i += 1; + } +} + +/// Returns the number of columns left until the next tab stop, given the current cursor position. +fn next_tab_stop(current_line_width: usize) -> usize { + // Assume tab stops every 8 characters if undefined. + let tab_width = term().unwrap().init_tabs.unwrap_or(8); + ((current_line_width / tab_width) + 1) * tab_width +} + +/// Whether we permit soft wrapping. If so, in some cases we don't explicitly move to the second +/// physical line on a wrapped logical line; instead we just output it. +fn allow_soft_wrap() -> bool { + // Should we be looking at eat_newline_glitch as well? + term().unwrap().auto_right_margin +} + +/// Does this look like the escape sequence for setting a screen name? +fn is_screen_name_escape_seq(code: &wstr) -> Option { + // Tmux escapes start with `\ePtmux;` and end also in `\e\\`, + // so we can just handle them here. + let tmux_seq = L!("Ptmux;"); + let mut is_tmux = false; + if code.char_at(1) != 'k' { + if code.starts_with(tmux_seq) { + is_tmux = true; + } else { + return None; + } + } + let screen_name_end_sentinel = L!("\x1B\\"); + let mut offset = 2; + let escape_sequence_end; + loop { + let Some(pos) = code[offset..].find(screen_name_end_sentinel) else { + // Consider just k to be the code. + // (note: for the tmux sequence this is broken, but since we have no idea...) + escape_sequence_end = 2; + break; + }; + let screen_name_end = offset + pos; + // The tmux sequence requires that all escapes in the payload sequence + // be doubled. So if we have \e\e\\ that's still not the end. + if is_tmux { + let mut esc_count = 0; + let mut i = screen_name_end; + while i > 0 && code.as_char_slice()[i - 1] == '\x1B' { + i -= 1; + if i > 0 { + esc_count += 1; + } + } + if esc_count % 2 == 1 { + offset = screen_name_end + 1; + continue; + } + } + escape_sequence_end = screen_name_end + screen_name_end_sentinel.len(); + break; + } + Some(escape_sequence_end) +} + +/// Operating System Command (OSC) escape codes, used by iTerm2 and others: +/// ESC followed by ], terminated by either BEL or escape + backslash. +/// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +/// and https://iterm2.com/documentation-escape-codes.html . +fn is_osc_escape_seq(code: &wstr) -> Option { + if code.char_at(1) == ']' { + // Start at 2 to skip over ]. + let mut cursor = 2; + while cursor < code.len() { + let code = code.as_char_slice(); + // Consume a sequence of characters up to \ or . + if code[cursor] == '\x07' || (code[cursor] == '\\' && code[cursor - 1] == '\x1B') { + return Some(cursor + 1); + } + cursor += 1; + } + } + None +} + +/// Generic VT100 three byte sequence: CSI followed by something in the range @ through _. +fn is_three_byte_escape_seq(code: &wstr) -> Option { + if code.char_at(1) == '[' && (code.char_at(2) >= '@' && code.char_at(2) <= '_') { + return Some(3); + } + None +} + +/// Generic VT100 two byte sequence: followed by something in the range @ through _. +fn is_two_byte_escape_seq(code: &wstr) -> Option { + if code.char_at(1) >= '@' && code.char_at(1) <= '_' { + return Some(2); + } + None +} + +/// Generic VT100 CSI-style sequence. , followed by zero or more ASCII characters NOT in +/// the range [@,_], followed by one character in that range. +/// This will also catch color sequences. +fn is_csi_style_escape_seq(code: &wstr) -> Option { + if code.char_at(1) != '[' { + return None; + } + + // Start at 2 to skip over [ + let mut cursor = 2; + while cursor < code.len() { + // Consume a sequence of ASCII characters not in the range [@, ~]. + let widechar = code.as_char_slice()[cursor]; + + // If we're not in ASCII, just stop. + if !widechar.is_ascii() { + break; + } + + // If we're the end character, then consume it and then stop. + if ('@'..'~').contains(&widechar) { + cursor += 1; + break; + } + cursor += 1; + } + // cursor now indexes just beyond the end of the sequence (or at the terminating zero). + Some(cursor) +} + +/// Detect whether the escape sequence sets one of the terminal attributes that affects how text is +/// displayed other than the color. +fn is_visual_escape_seq(code: &wstr) -> Option { + let term = term()?; + let esc2 = [ + &term.enter_bold_mode, + &term.exit_attribute_mode, + &term.enter_underline_mode, + &term.exit_underline_mode, + &term.enter_standout_mode, + &term.exit_standout_mode, + &term.enter_blink_mode, + &term.enter_protected_mode, + &term.enter_italics_mode, + &term.exit_italics_mode, + &term.enter_reverse_mode, + &term.enter_shadow_mode, + &term.exit_shadow_mode, + &term.enter_secure_mode, + &term.enter_dim_mode, + &term.enter_alt_charset_mode, + &term.exit_alt_charset_mode, + ]; + + for p in &esc2 { + let Some(p) = p else { continue }; + // Test both padded and unpadded version, just to be safe. Most versions of fish_tparm don't + // actually seem to do anything these days. + let esc_seq_len = std::cmp::max( + try_sequence(tparm0(p).unwrap().as_bytes(), code), + try_sequence(p.as_bytes(), code), + ); + if esc_seq_len != 0 { + return Some(esc_seq_len); + } + } + + None +} + +/// \return whether \p c ends a measuring run. +fn is_run_terminator(c: char) -> bool { + matches!(c, '\0' | '\n' | '\r' | '\x0C') +} + +/// Measure a run of characters in \p input starting at \p start. +/// Stop when we reach a run terminator, and return its index in \p out_end (if not null). +/// Note \0 is a run terminator so there will always be one. +/// We permit escape sequences to have run terminators other than \0. That is, escape sequences may +/// have embedded newlines, etc.; it's unclear if this is possible but we allow it. +fn measure_run_from( + input: &wstr, + start: usize, + out_end: Option<&mut usize>, + cache: &mut LayoutCache, +) -> usize { + let mut width = 0; + let mut idx = start; + while !is_run_terminator(input.char_at(idx)) { + if input.char_at(idx) == '\x1B' { + // This is the start of an escape code; we assume it has width 0. + // -1 because we are going to increment in the loop. + let len = cache.escape_code_length(&input[idx..]); + if len > 0 { + idx += len - 1; + } + } else if input.char_at(idx) == '\t' { + width = next_tab_stop(width); + } else { + // Ordinary char. Add its width with care to ignore control chars which have width -1. + let w = fish_wcwidth_visible(input.char_at(idx)); + // A backspace at the start of the line does nothing. + if w != -1 || width > 0 { + width = + usize::try_from(isize::try_from(width).unwrap() + isize::try_from(w).unwrap()) + .unwrap(); + } + } + idx += 1; + } + out_end.map(|end| *end = idx); + width +} + +/// Attempt to truncate the prompt run \p run, which has width \p width, to \p no more than +/// desired_width. \return the resulting width and run by reference. +fn truncate_run( + run: &mut WString, + desired_width: usize, + width: &mut usize, + cache: &mut LayoutCache, +) { + let mut curr_width = *width; + if curr_width < desired_width { + return; + } + + // Bravely prepend ellipsis char and skip it. + // Ellipsis is always width 1. + let ellipsis = get_ellipsis_char(); + run.insert(0, ellipsis); + curr_width += 1; + + // Start removing characters after ellipsis. + // Note we modify 'run' inside this loop. + let mut idx = 1; + while curr_width > desired_width && idx < run.len() { + let c = run.as_char_slice()[idx]; + assert!( + !is_run_terminator(c), + "Should not have run terminator inside run" + ); + if c == '\x1B' { + let len = cache.escape_code_length(&run[idx..]); + idx += std::cmp::max(len, 1); + } else if c == '\t' { + // Tabs would seem to be quite annoying to measure while truncating. + // We simply remove these and start over. + run.remove(idx); + curr_width = measure_run_from(run, 0, None, cache); + idx = 0; + } else { + let char_width = fish_wcwidth_visible(c) as usize; + curr_width -= std::cmp::min(curr_width, char_width); + run.remove(idx); + } + } + *width = curr_width; +} + +fn calc_prompt_lines(prompt: &wstr) -> usize { + // Hack for the common case where there's no newline at all. I don't know if a newline can + // appear in an escape sequence, so if we detect a newline we have to defer to + // calc_prompt_width_and_lines. + let mut result = 1; + if prompt.chars().any(|c| matches!(c, '\n' | '\x0C')) { + result = unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() } + .calc_prompt_layout(prompt, None, usize::MAX) + .line_breaks + .len() + + 1; + } + result +} + +/// Returns the length of the "shared prefix" of the two lines, which is the run of matching text +/// and colors. If the prefix ends on a combining character, do not include the previous character +/// in the prefix. +fn line_shared_prefix(a: &Line, b: &Line) -> usize { + let mut idx = 0; + let max = std::cmp::min(a.size(), b.size()); + while idx < max { + let ac = a.char_at(idx); + let bc = b.char_at(idx); + + // We're done if the text or colors are different. + if ac != bc || a.color_at(idx) != b.color_at(idx) { + if idx > 0 { + let mut c = None; + // Possible combining mark, go back until we hit _two_ printable characters or idx + // of 0. + if fish_wcwidth(a.char_at(idx)) < 1 { + c = Some(&a); + } else if fish_wcwidth(b.char_at(idx)) < 1 { + c = Some(&b); + } + + if let Some(c) = c { + while idx > 1 + && (fish_wcwidth(c.char_at(idx - 1)) < 1 + || fish_wcwidth(c.char_at(idx)) < 1) + { + idx -= 1; + } + if idx == 1 && fish_wcwidth(c.char_at(idx)) < 1 { + idx = 0; + } + } + } + break; + } + idx += 1; + } + idx +} + +/// Returns true if we are using a dumb terminal. +fn is_dumb() -> bool { + term().is_none_or(|term| { + term.cursor_up.is_none() + || term.cursor_down.is_none() + || term.cursor_left.is_none() + || term.cursor_right.is_none() + }) +} + +#[derive(Default)] +struct ScreenLayout { + // The left prompt that we're going to use. + left_prompt: WString, + // How much space to leave for it. + left_prompt_space: usize, + // The right prompt. + right_prompt: WString, + // The autosuggestion. + autosuggestion: WString, +} + +// Given a vector whose indexes are offsets and whose values are the widths of the string if +// truncated at that offset, return the offset that fits in the given width. Returns +// width_by_offset.size() - 1 if they all fit. The first value in width_by_offset is assumed to be +// 0. +fn truncation_offset_for_width(width_by_offset: &[usize], max_width: usize) -> usize { + assert!(width_by_offset[0] == 0); + let mut i = 1; + while i < width_by_offset.len() { + if width_by_offset[i] > max_width { + break; + } + i += 1; + } + // i is the first index that did not fit; i-1 is therefore the last that did. + i - 1 +} + +fn compute_layout( + screen_width: usize, + left_untrunc_prompt: &wstr, + right_untrunc_prompt: &wstr, + commandline: &wstr, + autosuggestion_str: &wstr, +) -> ScreenLayout { + let mut result = ScreenLayout::default(); + + // Truncate both prompts to screen width (#904). + let mut left_prompt = WString::new(); + let left_prompt_layout = unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() }.calc_prompt_layout( + left_untrunc_prompt, + Some(&mut left_prompt), + screen_width, + ); + + let mut right_prompt = WString::new(); + let right_prompt_layout = unsafe { LAYOUT_CACHE_SHARED.lock().unwrap() }.calc_prompt_layout( + right_untrunc_prompt, + Some(&mut right_prompt), + screen_width, + ); + + let left_prompt_width = left_prompt_layout.last_line_width; + let mut right_prompt_width = right_prompt_layout.last_line_width; + + if left_prompt_width + right_prompt_width > screen_width { + // Nix right_prompt. + right_prompt.truncate(0); + right_prompt_width = 0; + } + + // Now we should definitely fit. + assert!(left_prompt_width + right_prompt_width <= screen_width); + + // Get the width of the first line, and if there is more than one line. + let mut multiline = false; + let mut first_line_width = 0; + for c in commandline.chars() { + if c == '\n' { + multiline = true; + break; + } else { + first_line_width += usize::try_from(fish_wcwidth_visible(c)).unwrap(); + } + } + let first_command_line_width = first_line_width; + + // If we have more than one line, ensure we have no autosuggestion. + let mut autosuggestion = autosuggestion_str; + let mut autosuggest_total_width = 0; + let mut autosuggest_truncated_widths = vec![]; + if multiline { + autosuggestion = L!(""); + } else { + autosuggest_truncated_widths.reserve(1 + autosuggestion_str.len()); + for c in autosuggestion.chars() { + autosuggest_truncated_widths.push(autosuggest_total_width); + autosuggest_total_width += usize::try_from(fish_wcwidth_visible(c)).unwrap(); + } + } + + // Here are the layouts we try in turn: + // + // 1. Left prompt visible, right prompt visible, command line visible, autosuggestion visible. + // + // 2. Left prompt visible, right prompt visible, command line visible, autosuggestion truncated + // (possibly to zero). + // + // 3. Left prompt visible, right prompt hidden, command line visible, autosuggestion visible + // + // 4. Left prompt visible, right prompt hidden, command line visible, autosuggestion truncated + // + // 5. Newline separator (left prompt visible, right prompt hidden, command line visible, + // autosuggestion visible). + // + // A remark about layout #4: if we've pushed the command line to a new line, why can't we draw + // the right prompt? The issue is resizing: if you resize the window smaller, then the right + // prompt will wrap to the next line. This means that we can't go back to the line that we were + // on, and things turn to chaos very quickly. + + // Case 1 + let calculated_width = + left_prompt_width + right_prompt_width + first_command_line_width + autosuggest_total_width; + if calculated_width <= screen_width { + result.left_prompt = left_prompt; + result.left_prompt_space = left_prompt_width; + result.right_prompt = right_prompt; + result.autosuggestion = autosuggestion.to_owned(); + return result; + } + + // Case 2. Note that we require strict inequality so that there's always at least one space + // between the left edge and the rprompt. + let calculated_width = left_prompt_width + right_prompt_width + first_command_line_width; + if calculated_width <= screen_width { + result.left_prompt = left_prompt; + result.left_prompt_space = left_prompt_width; + result.right_prompt = right_prompt; + + // Need at least two characters to show an autosuggestion. + let available_autosuggest_space = + screen_width - (left_prompt_width + right_prompt_width + first_command_line_width); + if autosuggest_total_width > 0 && available_autosuggest_space > 2 { + let truncation_offset = truncation_offset_for_width( + &autosuggest_truncated_widths, + available_autosuggest_space - 2, + ); + result.autosuggestion = autosuggestion[..truncation_offset].to_owned(); + result.autosuggestion.push(get_ellipsis_char()); + } + return result; + } + + // Case 3 + let calculated_width = left_prompt_width + first_command_line_width + autosuggest_total_width; + if calculated_width <= screen_width { + result.left_prompt = left_prompt; + result.left_prompt_space = left_prompt_width; + result.autosuggestion = autosuggestion.to_owned(); + return result; + } + + // Case 4 + let calculated_width = left_prompt_width + first_command_line_width; + if calculated_width <= screen_width { + result.left_prompt = left_prompt; + result.left_prompt_space = left_prompt_width; + + // Need at least two characters to show an autosuggestion. + let available_autosuggest_space = + screen_width - (left_prompt_width + first_command_line_width); + if autosuggest_total_width > 0 && available_autosuggest_space > 2 { + let truncation_offset = truncation_offset_for_width( + &autosuggest_truncated_widths, + available_autosuggest_space - 2, + ); + result.autosuggestion = autosuggestion[..truncation_offset].to_owned(); + result.autosuggestion.push(get_ellipsis_char()); + } + return result; + } + + // Case 5 + result.left_prompt = left_prompt; + result.left_prompt_space = left_prompt_width; + result.autosuggestion = autosuggestion.to_owned(); + result +} + +#[allow(clippy::needless_lifetimes)] // add odds with cxx +#[cxx::bridge] +mod screen_ffi { + extern "C++" { + include!("screen.h"); + include!("highlight.h"); + pub type HighlightSpec = crate::highlight::HighlightSpec; + pub type HighlightSpecListFFI = crate::highlight::HighlightSpecListFFI; + pub type pager_t = crate::ffi::pager_t; + pub type page_rendering_t = crate::ffi::page_rendering_t; + pub type highlight_spec_t = crate::ffi::highlight_spec_t; + } + extern "Rust" { + type Line; + fn new_line() -> Box; + #[cxx_name = "append"] + fn append_ffi(&mut self, character: u32, highlight: &HighlightSpec); + #[cxx_name = "append_str"] + fn append_str_ffi(&mut self, txt: &CxxWString, highlight: &HighlightSpec); + fn append_line(&mut self, line: &Line); + fn text_characters_ffi(&self) -> UniquePtr; + } + extern "Rust" { + type ScreenData; + fn new_screen_data() -> Box; + #[cxx_name = "create_line"] + fn create_line_ffi(&mut self, index: usize) -> *mut Line; + fn add_line(&mut self) -> &mut Line; + fn insert_line_at_index(&mut self, index: usize) -> &mut Line; + fn empty(&self) -> bool; + fn resize(&mut self, size: usize); + fn line_count(&self) -> usize; + fn line_ffi(&self, index: usize) -> *const Line; + } + extern "Rust" { + type Screen<'a>; + fn new_screen() -> Box>; + #[cxx_name = "write"] + fn write_ffi( + &mut self, + left_prompt: &CxxWString, + right_prompt: &CxxWString, + commandline: &CxxWString, + explicit_len: usize, + colors: &HighlightSpecListFFI, + indent: &CxxVector, + cursor_pos: usize, + vars: *mut u8, + pager: Pin<&mut pager_t>, + page_rendering: Pin<&mut page_rendering_t>, + cursor_is_within_pager: bool, + ); + fn reset_abandoning_line(&mut self, screen_width: usize); + fn cursor_is_wrapped_to_own_line(&self) -> bool; + fn save_status(&mut self); + fn reset_line(&mut self, repaint_prompt: bool); + fn autosuggestion_is_truncated(&self) -> bool; + } + extern "Rust" { + type PromptLayout; + } + extern "Rust" { + type LayoutCache; + } + extern "Rust" { + #[cxx_name = "screen_clear"] + fn screen_clear_ffi() -> UniquePtr; + fn screen_force_clear_to_end(); + } +} +fn new_line() -> Box { + Box::new(Line::new()) +} +impl Line { + fn append_ffi(&mut self, character: u32, highlight: &HighlightSpec) { + self.append(char::try_from(character).unwrap(), *highlight) + } + fn append_str_ffi(&mut self, txt: &CxxWString, highlight: &HighlightSpec) { + self.append_str(&txt.from_ffi(), *highlight) + } + fn text_characters_ffi(&self) -> UniquePtr { + WString::from_iter(self.text.iter().map(|hc| hc.character)).to_ffi() + } +} +fn new_screen_data() -> Box { + Box::new(ScreenData::new()) +} +impl ScreenData { + fn create_line_ffi(&mut self, index: usize) -> *mut Line { + self.create_line(index); + self.line_mut(index) as *mut Line + } + fn empty(&self) -> bool { + self.is_empty() + } + fn line_ffi(&self, index: usize) -> *const Line { + self.line(index) as *const Line + } +} +fn new_screen() -> Box> { + Box::new(Screen::new()) +} +impl<'a> Screen<'a> { + fn write_ffi( + &mut self, + left_prompt: &CxxWString, + right_prompt: &CxxWString, + commandline: &CxxWString, + explicit_len: usize, + colors: &HighlightSpecListFFI, + indent: &CxxVector, + cursor_pos: usize, + vars: *mut u8, + pager: Pin<&mut ffi::pager_t>, + page_rendering: Pin<&mut ffi::page_rendering_t>, + cursor_is_within_pager: bool, + ) { + let vars = unsafe { Box::from_raw(vars as *mut EnvStackRef) }; + let mut my_indent = vec![]; + for n in indent.as_slice() { + my_indent.push(usize::try_from(*n).unwrap()); + } + self.write( + left_prompt.as_wstr(), + right_prompt.as_wstr(), + commandline.as_wstr(), + explicit_len, + &colors.0, + &my_indent, + cursor_pos, + vars.as_ref().as_ref().get_ref(), + pager, + page_rendering, + cursor_is_within_pager, + ); + } + fn autosuggestion_is_truncated(&self) -> bool { + self.autosuggestion_is_truncated + } +} +fn screen_clear_ffi() -> UniquePtr { + screen_clear().to_ffi() +} diff --git a/fish-rust/src/tests/mod.rs b/fish-rust/src/tests/mod.rs index bcdcb8dea..2cf179658 100644 --- a/fish-rust/src/tests/mod.rs +++ b/fish-rust/src/tests/mod.rs @@ -12,6 +12,7 @@ mod history; mod parser; #[cfg(test)] mod redirection; +mod screen; mod string_escape; #[cfg(test)] mod tokenizer; diff --git a/fish-rust/src/tests/screen.rs b/fish-rust/src/tests/screen.rs new file mode 100644 index 000000000..3261e1d43 --- /dev/null +++ b/fish-rust/src/tests/screen.rs @@ -0,0 +1,238 @@ +use crate::common::get_ellipsis_char; +use crate::ffi_tests::add_test; +use crate::screen::{LayoutCache, PromptCacheEntry, PromptLayout}; +use crate::wchar::prelude::*; +use crate::wcstringutil::join_strings; + +add_test!("test_complete", || { + let mut lc = LayoutCache::new(); + assert_eq!(lc.escape_code_length(L!("")), 0); + assert_eq!(lc.escape_code_length(L!("abcd")), 0); + assert_eq!(lc.escape_code_length(L!("\x1B[2J")), 4); + assert_eq!( + lc.escape_code_length(L!("\x1B[38;5;123mABC")), + "\x1B[38;5;123m".len() + ); + assert_eq!(lc.escape_code_length(L!("\x1B@")), 2); + + // iTerm2 escape sequences. + assert_eq!( + lc.escape_code_length(L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE")), + 25 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]50;SetMark\x07NOT_PART_OF_SEQUENCE")), + 13 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]6;1;bg;red;brightness;255\x07NOT_PART_OF_SEQUENCE")), + 28 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]Pg4040ff\x1B\\NOT_PART_OF_SEQUENCE")), + 12 + ); + assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x1B\\")), 16); + assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x07")), 15); +}); + +add_test!("test_layout_cache", || { + let mut seqs = LayoutCache::new(); + + // Verify escape code cache. + assert_eq!(seqs.find_escape_code(L!("abc")), 0); + seqs.add_escape_code(L!("abc").to_owned()); + seqs.add_escape_code(L!("abc").to_owned()); + assert_eq!(seqs.esc_cache_size(), 1); + assert_eq!(seqs.find_escape_code(L!("abc")), 3); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("abcde")), 3); + assert_eq!(seqs.find_escape_code(L!("xabcde")), 0); + seqs.add_escape_code(L!("ac").to_owned()); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("acbd")), 2); + seqs.add_escape_code(L!("wxyz").to_owned()); + assert_eq!(seqs.find_escape_code(L!("abc")), 3); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("wxyz123")), 4); + assert_eq!(seqs.find_escape_code(L!("qwxyz123")), 0); + assert_eq!(seqs.esc_cache_size(), 3); + seqs.clear(); + assert_eq!(seqs.esc_cache_size(), 0); + assert_eq!(seqs.find_escape_code(L!("abcd")), 0); + + let huge = usize::MAX; + + // Verify prompt layout cache. + for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { + let input = i.to_wstring(); + assert!(!seqs.find_prompt_layout(&input, usize::MAX)); + seqs.add_prompt_layout(PromptCacheEntry { + text: input.clone(), + max_line_width: huge, + trunc_text: input.clone(), + layout: PromptLayout { + line_breaks: vec![], + max_line_width: i, + last_line_width: 0, + }, + }); + assert!(seqs.find_prompt_layout(&input, usize::MAX)); + assert_eq!(seqs.prompt_cache.front().unwrap().layout.max_line_width, i); + } + + let expected_evictee = 3; + for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { + if i != expected_evictee { + assert!(seqs.find_prompt_layout(&i.to_wstring(), usize::MAX)); + assert_eq!(seqs.prompt_cache.front().unwrap().layout.max_line_width, i); + } + } + + seqs.add_prompt_layout(PromptCacheEntry { + text: "whatever".into(), + max_line_width: huge, + trunc_text: "whatever".into(), + layout: PromptLayout { + line_breaks: vec![], + max_line_width: 100, + last_line_width: 0, + }, + }); + assert!(!seqs.find_prompt_layout(&expected_evictee.to_wstring(), usize::MAX)); + assert!(seqs.find_prompt_layout(L!("whatever"), huge)); + assert_eq!( + seqs.prompt_cache.front().unwrap().layout.max_line_width, + 100 + ); +}); + +add_test!("test_prompt_truncation", || { + let mut cache = LayoutCache::new(); + let mut trunc = WString::new(); + + let ellipsis = || WString::from_chars([get_ellipsis_char()]); + + // No truncation. + let layout = cache.calc_prompt_layout(L!("abcd"), Some(&mut trunc), usize::MAX); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![], + max_line_width: 4, + last_line_width: 4, + } + ); + assert_eq!(trunc, L!("abcd")); + + // Line break calculation. + let layout = cache.calc_prompt_layout( + L!(concat!( + "0123456789ABCDEF\n", + "012345\n", + "0123456789abcdef\n", + "xyz" + )), + Some(&mut trunc), + 80, + ); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![16, 23, 40], + max_line_width: 16, + last_line_width: 3, + } + ); + + // Basic truncation. + let layout = cache.calc_prompt_layout(L!("0123456789ABCDEF"), Some(&mut trunc), 8); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![], + max_line_width: 8, + last_line_width: 8, + }, + ); + assert_eq!(trunc, ellipsis() + L!("9ABCDEF")); + + // Multiline truncation. + let layout = cache.calc_prompt_layout( + L!(concat!( + "0123456789ABCDEF\n", + "012345\n", + "0123456789abcdef\n", + "xyz" + )), + Some(&mut trunc), + 8, + ); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![8, 15, 24], + max_line_width: 8, + last_line_width: 3, + }, + ); + assert_eq!( + trunc, + join_strings( + &[ + ellipsis() + L!("9ABCDEF"), + L!("012345").to_owned(), + ellipsis() + L!("9abcdef"), + L!("xyz").to_owned(), + ], + '\n', + ), + ); + + // Escape sequences are not truncated. + let layout = cache.calc_prompt_layout( + L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE"), + Some(&mut trunc), + 4, + ); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![], + max_line_width: 4, + last_line_width: 4, + }, + ); + assert_eq!(trunc, ellipsis() + L!("\x1B]50;CurrentDir=test/foo\x07NCE")); + + // Newlines in escape sequences are skipped. + let layout = cache.calc_prompt_layout( + L!("\x1B]50;CurrentDir=\ntest/foo\x07NOT_PART_OF_SEQUENCE"), + Some(&mut trunc), + 4, + ); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![], + max_line_width: 4, + last_line_width: 4, + }, + ); + assert_eq!( + trunc, + ellipsis() + L!("\x1B]50;CurrentDir=\ntest/foo\x07NCE") + ); + + // We will truncate down to one character if we have to. + let layout = cache.calc_prompt_layout(L!("Yay"), Some(&mut trunc), 1); + assert_eq!( + layout, + PromptLayout { + line_breaks: vec![], + max_line_width: 1, + last_line_width: 1, + }, + ); + assert_eq!(trunc, ellipsis()); +}); diff --git a/src/common.cpp b/src/common.cpp index edc317e37..60319dfed 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -58,6 +58,8 @@ struct termios shell_modes; +struct termios *shell_modes_ffi() { return &shell_modes; } + const wcstring g_empty_string{}; const std::vector g_empty_string_list{}; diff --git a/src/common.h b/src/common.h index 0200e520f..649392970 100644 --- a/src/common.h +++ b/src/common.h @@ -656,4 +656,6 @@ __attribute__((always_inline)) bool inline iswdigit(const wchar_t c) { #include "common.rs.h" #endif +struct termios *shell_modes_ffi(); + #endif // FISH_COMMON_H diff --git a/src/ffi_baggage.h b/src/ffi_baggage.h index e3a809599..1787fea18 100644 --- a/src/ffi_baggage.h +++ b/src/ffi_baggage.h @@ -17,7 +17,6 @@ void mark_as_used(const parser_t& parser, env_stack_t& env_stack) { wcstring s; - escape_code_length_ffi({}); event_fire_generic(parser, {}); event_fire_generic(parser, {}, {}); expand_tilde(s, env_stack); @@ -34,8 +33,6 @@ void mark_as_used(const parser_t& parser, env_stack_t& env_stack) { reader_status_count(); restore_term_mode(); rgb_color_t{}; - screen_clear_layout_cache_ffi(); - screen_set_midnight_commander_hack(); setenv_lock({}, {}, {}); set_inheriteds_ffi(); term_copy_modes(); diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index dc7e28566..d412e9b91 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -993,36 +993,6 @@ static void test_utility_functions() { test_const_strcmp(); } -// todo!("port this"); -static void test_escape_sequences() { - say(L"Testing escape_sequences"); - layout_cache_t lc; - if (lc.escape_code_length(L"") != 0) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"abcd") != 0) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B[2J") != 4) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B[38;5;123mABC") != strlen("\x1B[38;5;123m")) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B@") != 2) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - - // iTerm2 escape sequences. - if (lc.escape_code_length(L"\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE") != 25) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B]50;SetMark\x07NOT_PART_OF_SEQUENCE") != 13) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B]6;1;bg;red;brightness;255\x07NOT_PART_OF_SEQUENCE") != 28) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B]Pg4040ff\x1B\\NOT_PART_OF_SEQUENCE") != 12) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B]blahblahblah\x1B\\") != 16) - err(L"test_escape_sequences failed on line %d\n", __LINE__); - if (lc.escape_code_length(L"\x1B]blahblahblah\x07") != 15) - err(L"test_escape_sequences failed on line %d\n", __LINE__); -} - class test_lru_t : public lru_cache_t { public: static constexpr size_t test_capacity = 16; @@ -1300,7 +1270,7 @@ struct pager_layout_testcase_t { void run(pager_t &pager) const { pager.set_term_size(termsize_t{this->width, 24}); page_rendering_t rendering = pager.render(); - const screen_data_t &sd = rendering.screen_data; + const screen_data_t &sd = *rendering.screen_data; do_test(sd.line_count() == 1); if (sd.line_count() > 0) { wcstring expected = this->expected; @@ -1311,10 +1281,7 @@ struct pager_layout_testcase_t { std::replace(expected.begin(), expected.end(), L'\x2026', ellipsis_char); } - wcstring text; - for (const auto &p : sd.line(0).text) { - text.push_back(p.character); - } + wcstring text = *(sd.line_ffi(0)->text_characters_ffi()); if (text != expected) { std::fwprintf(stderr, L"width %d got %zu<%ls>, expected %zu<%ls>\n", this->width, text.length(), text.c_str(), expected.length(), expected.c_str()); @@ -2315,129 +2282,6 @@ void test_maybe() { do_test(c2.value_or("derp") == "derp"); } -// todo!("delete this") -void test_layout_cache() { - layout_cache_t seqs; - - // Verify escape code cache. - do_test(seqs.find_escape_code(L"abc") == 0); - seqs.add_escape_code(L"abc"); - seqs.add_escape_code(L"abc"); - do_test(seqs.esc_cache_size() == 1); - do_test(seqs.find_escape_code(L"abc") == 3); - do_test(seqs.find_escape_code(L"abcd") == 3); - do_test(seqs.find_escape_code(L"abcde") == 3); - do_test(seqs.find_escape_code(L"xabcde") == 0); - seqs.add_escape_code(L"ac"); - do_test(seqs.find_escape_code(L"abcd") == 3); - do_test(seqs.find_escape_code(L"acbd") == 2); - seqs.add_escape_code(L"wxyz"); - do_test(seqs.find_escape_code(L"abc") == 3); - do_test(seqs.find_escape_code(L"abcd") == 3); - do_test(seqs.find_escape_code(L"wxyz123") == 4); - do_test(seqs.find_escape_code(L"qwxyz123") == 0); - do_test(seqs.esc_cache_size() == 3); - seqs.clear(); - do_test(seqs.esc_cache_size() == 0); - do_test(seqs.find_escape_code(L"abcd") == 0); - - auto huge = std::numeric_limits::max(); - - // Verify prompt layout cache. - for (size_t i = 0; i < layout_cache_t::prompt_cache_max_size; i++) { - wcstring input = std::to_wstring(i); - do_test(!seqs.find_prompt_layout(input)); - seqs.add_prompt_layout({input, huge, input, {{}, i, 0}}); - do_test(seqs.find_prompt_layout(input)->layout.max_line_width == i); - } - - size_t expected_evictee = 3; - for (size_t i = 0; i < layout_cache_t::prompt_cache_max_size; i++) { - if (i != expected_evictee) - do_test(seqs.find_prompt_layout(std::to_wstring(i))->layout.max_line_width == i); - } - - seqs.add_prompt_layout({L"whatever", huge, L"whatever", {{}, 100, 0}}); - do_test(!seqs.find_prompt_layout(std::to_wstring(expected_evictee))); - do_test(seqs.find_prompt_layout(L"whatever", huge)->layout.max_line_width == 100); -} - -// todo!("port this") -void test_prompt_truncation() { - layout_cache_t cache; - wcstring trunc; - prompt_layout_t layout; - - /// Helper to return 'layout' formatted as a string for easy comparison. - auto format_layout = [&] { - wcstring line_breaks; - bool first = true; - for (const size_t line_break : layout.line_breaks) { - if (!first) { - line_breaks.push_back(L','); - } - line_breaks.append(format_string(L"%lu", (unsigned long)line_break)); - first = false; - } - return format_string(L"[%ls],%lu,%lu", line_breaks.c_str(), - (unsigned long)layout.max_line_width, - (unsigned long)layout.last_line_width); - }; - - /// Join some strings with newline. - auto join = [](std::initializer_list vals) { return join_strings(vals, L'\n'); }; - - wcstring ellipsis = {get_ellipsis_char()}; - - // No truncation. - layout = cache.calc_prompt_layout(L"abcd", &trunc); - do_test(format_layout() == L"[],4,4"); - do_test(trunc == L"abcd"); - - // Line break calculation. - layout = cache.calc_prompt_layout(join({ - L"0123456789ABCDEF", // - L"012345", // - L"0123456789abcdef", // - L"xyz" // - }), - &trunc, 80); - do_test(format_layout() == L"[16,23,40],16,3"); - - // Basic truncation. - layout = cache.calc_prompt_layout(L"0123456789ABCDEF", &trunc, 8); - do_test(format_layout() == L"[],8,8"); - do_test(trunc == ellipsis + L"9ABCDEF"); - - // Multiline truncation. - layout = cache.calc_prompt_layout(join({ - L"0123456789ABCDEF", // - L"012345", // - L"0123456789abcdef", // - L"xyz" // - }), - &trunc, 8); - do_test(format_layout() == L"[8,15,24],8,3"); - do_test(trunc == join({ellipsis + L"9ABCDEF", L"012345", ellipsis + L"9abcdef", L"xyz"})); - - // Escape sequences are not truncated. - layout = - cache.calc_prompt_layout(L"\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE", &trunc, 4); - do_test(format_layout() == L"[],4,4"); - do_test(trunc == ellipsis + L"\x1B]50;CurrentDir=test/foo\x07NCE"); - - // Newlines in escape sequences are skipped. - layout = cache.calc_prompt_layout(L"\x1B]50;CurrentDir=\ntest/foo\x07NOT_PART_OF_SEQUENCE", - &trunc, 4); - do_test(format_layout() == L"[],4,4"); - do_test(trunc == ellipsis + L"\x1B]50;CurrentDir=\ntest/foo\x07NCE"); - - // We will truncate down to one character if we have to. - layout = cache.calc_prompt_layout(L"Yay", &trunc, 1); - do_test(format_layout() == L"[],1,1"); - do_test(trunc == ellipsis); -} - // todo!("already ported, delete this") void test_normalize_path() { say(L"Testing path normalization"); @@ -2649,7 +2493,6 @@ static const test_t s_tests[]{ {TEST_GROUP("autosuggestion"), test_autosuggestion_combining}, {TEST_GROUP("new_parser_ll2"), test_new_parser_ll2}, {TEST_GROUP("test_abbreviations"), test_abbreviations}, - {TEST_GROUP("test_escape_sequences"), test_escape_sequences}, {TEST_GROUP("new_parser_fuzzing"), test_new_parser_fuzzing}, {TEST_GROUP("new_parser_correctness"), test_new_parser_correctness}, {TEST_GROUP("new_parser_ad_hoc"), test_new_parser_ad_hoc}, @@ -2676,8 +2519,6 @@ static const test_t s_tests[]{ {TEST_GROUP("completion_insertions"), test_completion_insertions}, {TEST_GROUP("illegal_command_exit_code"), test_illegal_command_exit_code}, {TEST_GROUP("maybe"), test_maybe}, - {TEST_GROUP("layout_cache"), test_layout_cache}, - {TEST_GROUP("prompt"), test_prompt_truncation}, {TEST_GROUP("normalize"), test_normalize_path}, {TEST_GROUP("dirname"), test_dirname_basename}, {TEST_GROUP("pipes"), test_pipes}, diff --git a/src/highlight.h b/src/highlight.h index b037c3240..442d13ebd 100644 --- a/src/highlight.h +++ b/src/highlight.h @@ -28,6 +28,7 @@ struct highlight_spec_t; #else struct HighlightSpec; enum class HighlightRole : uint8_t; +struct HighlightSpecListFFI; #endif using highlight_role_t = HighlightRole; diff --git a/src/pager.cpp b/src/pager.cpp index 560d9a604..23b068480 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -134,14 +134,15 @@ static size_t print_max(const wcstring &str, highlight_spec_t color, size_t max, } /// Print the specified item using at the specified amount of space. -line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, - size_t column, size_t width, bool secondary, bool selected, - page_rendering_t *rendering) const { +rust::Box pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, + size_t column, size_t width, bool secondary, + bool selected, page_rendering_t *rendering) const { UNUSED(column); UNUSED(row); UNUSED(rendering); size_t comp_width; - line_t line_data; + rust::Box line_data_box = new_line(); + auto &line_data = *line_data_box; if (c->preferred_width() <= width) { // The entry fits, we give it as much space as it wants. @@ -228,7 +229,7 @@ line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, s print_max(wcstring(desc_remaining, L' '), bg, desc_remaining, false, &line_data); } - return line_data; + return line_data_box; } /// Print the specified part of the completion list, using the specified column offsets and quoting @@ -261,16 +262,16 @@ void pager_t::completion_print(size_t cols, const size_t *width_by_column, size_ bool is_selected = (idx == effective_selected_idx); // Print this completion on its own "line". - line_t line = completion_print_item(prefix, el, row, col, width_by_column[col], row % 2, - is_selected, rendering); + auto line = completion_print_item(prefix, el, row, col, width_by_column[col], row % 2, + is_selected, rendering); // If there's more to come, append two spaces. if (col + 1 < cols) { - line.append(PAGER_SPACER_STRING, highlight_spec_t{}); + line->append_str(PAGER_SPACER_STRING, highlight_spec_t{}); } // Append this to the real line. - rendering->screen_data.create_line(row - row_start).append_line(line); + rendering->screen_data->create_line(row - row_start)->append_line(*line); } } } @@ -447,11 +448,19 @@ void pager_t::set_prefix(const wcstring &pref, bool highlight) { highlight_prefix = highlight; } -void pager_t::set_term_size(termsize_t ts) { +void pager_t::set_term_size(const termsize_t &ts) { available_term_width = ts.width > 0 ? ts.width : 0; available_term_height = ts.height > 0 ? ts.height : 0; } +void pager_set_term_size_ffi(pager_t &pager, const void *ts) { + pager.set_term_size(*reinterpret_cast(ts)); +} + +void pager_update_rendering_ffi(pager_t &pager, page_rendering_t &rendering) { + pager.update_rendering(&rendering); +} + /// Try to print the list of completions lst with the prefix prefix using cols as the number of /// columns. Return true if the completion list was printed, false if the terminal is too narrow for /// the specified number of columns. Always succeeds if cols is 1. @@ -571,7 +580,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co } if (!progress_text.empty()) { - line_t &line = rendering->screen_data.add_line(); + line_t &line = rendering->screen_data->add_line(); highlight_spec_t spec = {highlight_role_t::pager_progress, highlight_role_t::pager_progress}; print_max(progress_text, spec, term_width, true /* has_more */, &line); @@ -587,7 +596,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co if (search_field_text.size() < PAGER_SEARCH_FIELD_WIDTH) { search_field_text.append(PAGER_SEARCH_FIELD_WIDTH - search_field_text.size(), L' '); } - line_t *search_field = &rendering->screen_data.insert_line_at_index(0); + line_t *search_field = &rendering->screen_data->insert_line_at_index(0); // We limit the width to term_width - 1. highlight_spec_t underline{}; @@ -613,7 +622,7 @@ page_rendering_t pager_t::render() const { for (size_t cols = PAGER_MAX_COLS; cols > 0; cols--) { // Initially empty rendering. - rendering.screen_data.resize(0); + rendering.screen_data->resize(0); // Determine how many rows we would need if we had 'cols' columns. Then determine how many // columns we want from that. For example, say we had 19 completions. We can fit them into 6 @@ -645,9 +654,9 @@ page_rendering_t pager_t::render() const { bool pager_t::rendering_needs_update(const page_rendering_t &rendering) const { if (have_unrendered_completions) return true; // Common case is no pager. - if (this->empty() && rendering.screen_data.empty()) return false; + if (this->empty() && rendering.screen_data->empty()) return false; - return (this->empty() && !rendering.screen_data.empty()) || // Do update after clear(). + return (this->empty() && !rendering.screen_data->empty()) || // Do update after clear(). rendering.term_width != this->available_term_width || // rendering.term_height != this->available_term_height || // rendering.selected_completion_idx != @@ -956,4 +965,4 @@ size_t pager_t::cursor_position() const { return result; } -page_rendering_t::page_rendering_t() = default; +page_rendering_t::page_rendering_t() : screen_data(new_screen_data()) {} diff --git a/src/pager.h b/src/pager.h index f57e34be1..4125f4d52 100644 --- a/src/pager.h +++ b/src/pager.h @@ -28,8 +28,9 @@ class page_rendering_t { size_t row_start{0}; size_t row_end{0}; size_t selected_completion_idx{size_t(-1)}; - screen_data_t screen_data{}; + rust::Box screen_data; + const screen_data_t *screen_data_ffi() const { return &*screen_data; } size_t remaining_to_disclose{0}; bool search_field_shown{false}; @@ -37,6 +38,10 @@ class page_rendering_t { // Returns a rendering with invalid data, useful to indicate "no rendering". page_rendering_t(); + page_rendering_t(const page_rendering_t &) = delete; + page_rendering_t(page_rendering_t &&) = default; + page_rendering_t &operator=(const page_rendering_t &) = delete; + page_rendering_t &operator=(page_rendering_t &&) = default; }; enum class selection_motion_t { @@ -87,8 +92,10 @@ class pager_t { std::vector comp{}; /// The description. wcstring desc{}; +#if INCLUDE_RUST_HEADERS /// The representative completion. rust::Box representative = new_completion(); +#endif /// The per-character highlighting, used when this is a full shell command. std::vector colors{}; /// On-screen width of the completion string. @@ -144,9 +151,9 @@ class pager_t { void completion_print(size_t cols, const size_t *width_by_column, size_t row_start, size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst, page_rendering_t *rendering) const; - line_t completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, size_t column, - size_t width, bool secondary, bool selected, - page_rendering_t *rendering) const; + rust::Box completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, + size_t column, size_t width, bool secondary, + bool selected, page_rendering_t *rendering) const; public: // The text of the search field. @@ -162,7 +169,7 @@ class pager_t { void set_prefix(const wcstring &pref, bool highlight = true); // Sets the terminal size. - void set_term_size(termsize_t ts); + void set_term_size(const termsize_t &ts); // Changes the selected completion in the given direction according to the layout of the given // rendering. Returns true if the selection changed. @@ -218,4 +225,7 @@ class pager_t { ~pager_t(); }; +void pager_set_term_size_ffi(pager_t &pager, const void *ts); +void pager_update_rendering_ffi(pager_t &pager, page_rendering_t &rendering); + #endif diff --git a/src/reader.cpp b/src/reader.cpp index e14b79adb..508dae30e 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -740,7 +740,7 @@ class reader_data_t : public std::enable_shared_from_this { std::chrono::time_point last_flash; /// The representation of the current screen contents. - screen_t screen; + rust::Box screen; /// The source of input events. inputter_t inputter; @@ -859,6 +859,7 @@ class reader_data_t : public std::enable_shared_from_this { reader_data_t(rust::Box parser, HistorySharedPtr &hist, reader_config_t &&conf) : conf(std::move(conf)), parser_ref(std::move(parser)), + screen(new_screen()), inputter(parser_ref->deref(), conf.in), history(hist.clone()) {} @@ -1239,10 +1240,12 @@ void reader_data_t::paint_layout(const wchar_t *reason) { std::vector indents = parse_util_compute_indents(cmd_line->text()); indents.resize(full_line.size(), 0); + auto ffi_colors = new_highlight_spec_list(); + for (auto color : colors) ffi_colors->push(color); // Prepend the mode prompt to the left prompt. - screen.write(mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line, - cmd_line->size(), colors, indents, data.position, parser().vars_boxed(), pager, - current_page_rendering, data.focused_on_pager); + screen->write(mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line, + cmd_line->size(), *ffi_colors, indents, data.position, parser().vars_boxed(), + pager, current_page_rendering, data.focused_on_pager); } /// Internal helper function for handling killing parts of text. @@ -3052,7 +3055,7 @@ void reader_pop() { reader_interactive_destroy(); *commandline_state_snapshot() = commandline_state_t{}; } else { - new_reader->screen.reset_abandoning_line(termsize_last().width); + new_reader->screen->reset_abandoning_line(termsize_last().width); new_reader->update_commandline_state(); } } @@ -3434,7 +3437,7 @@ maybe_t reader_data_t::read_normal_chars(readline_loop_state_t &rl if (last_exec_count != exec_count()) { last_exec_count = exec_count(); - screen.save_status(); + screen->save_status(); } } @@ -3453,7 +3456,7 @@ maybe_t reader_data_t::read_normal_chars(readline_loop_state_t &rl if (last_exec_count != exec_count()) { last_exec_count = exec_count(); - screen.save_status(); + screen->save_status(); } return event_needing_handling; @@ -3512,7 +3515,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat outp.push_back('\n'); set_command_line_and_position(&command_line, L"", 0); - screen.reset_abandoning_line(termsize_last().width - command_line.size()); + screen->reset_abandoning_line(termsize_last().width - command_line.size()); // Post fish_cancel. event_fire_generic(parser(), L"fish_cancel"); @@ -3551,7 +3554,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat exec_mode_prompt(); if (!mode_prompt_buff.empty()) { if (this->is_repaint_needed()) { - screen.reset_line(true /* redraw prompt */); + screen->reset_line(true /* redraw prompt */); this->layout_and_repaint(L"mode"); } parser().libdata_pods_mut().is_repaint = false; @@ -3564,7 +3567,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::repaint: { parser().libdata_pods_mut().is_repaint = true; exec_prompt(); - screen.reset_line(true /* redraw prompt */); + screen->reset_line(true /* redraw prompt */); this->layout_and_repaint(L"readline"); force_exec_prompt_and_repaint = false; parser().libdata_pods_mut().is_repaint = false; @@ -3734,7 +3737,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::execute: { if (!this->handle_execute(rls)) { event_fire_generic(parser(), L"fish_posterror", {command_line.text()}); - screen.reset_abandoning_line(termsize_last().width); + screen->reset_abandoning_line(termsize_last().width); } break; } @@ -3776,7 +3779,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Skip the autosuggestion in the history unless it was truncated. const wcstring &suggest = autosuggestion.text; - if (!suggest.empty() && !screen.autosuggestion_is_truncated && + if (!suggest.empty() && !screen->autosuggestion_is_truncated() && mode != reader_history_search_t::prefix) { history_search.add_skip(suggest); } @@ -4326,7 +4329,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::clear_screen_and_repaint: { parser().libdata_pods_mut().is_repaint = true; - auto clear = screen_clear(); + auto clear = *screen_clear(); if (!clear.empty()) { // Clear the screen if we can. // This is subtle: We first clear, draw the old prompt, @@ -4335,11 +4338,11 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // while keeping the prompt up-to-date. outputter_t &outp = stdoutput(); outp.writestr(clear.c_str()); - screen.reset_line(true /* redraw prompt */); + screen->reset_line(true /* redraw prompt */); this->layout_and_repaint(L"readline"); } exec_prompt(); - screen.reset_line(true /* redraw prompt */); + screen->reset_line(true /* redraw prompt */); this->layout_and_repaint(L"readline"); force_exec_prompt_and_repaint = false; parser().libdata_pods_mut().is_repaint = false; @@ -4529,7 +4532,7 @@ maybe_t reader_data_t::readline(int nchars_or_0) { // // I can't see a good way around this. if (!first_prompt) { - screen.reset_abandoning_line(termsize_last().width); + screen->reset_abandoning_line(termsize_last().width); } first_prompt = false; @@ -4662,7 +4665,7 @@ maybe_t reader_data_t::readline(int nchars_or_0) { // Emit a newline so that the output is on the line after the command. // But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826. - if (!screen.cursor_is_wrapped_to_own_line()) { + if (!screen->cursor_is_wrapped_to_own_line()) { ignore_result(write(STDOUT_FILENO, "\n", 1)); } diff --git a/src/screen.cpp b/src/screen.cpp deleted file mode 100644 index c22834e5d..000000000 --- a/src/screen.cpp +++ /dev/null @@ -1,1368 +0,0 @@ -// High level library for handling the terminal screen. -// -// The screen library allows the interactive reader to write its output to screen efficiently by -// keeping an internal representation of the current screen contents and trying to find the most -// efficient way for transforming that to the desired screen content. -// -#include "config.h" - -#include -#include -#include -#include -#include - -#include -#include -#include - -#if HAVE_CURSES_H -#include // IWYU pragma: keep -#elif HAVE_NCURSES_H -#include -#elif HAVE_NCURSES_CURSES_H -#include -#endif -#if HAVE_TERM_H -#include -#elif HAVE_NCURSES_TERM_H -#include -#endif - -#include -#include -#include - -#include "common.h" -#include "env.h" -#include "fallback.h" // IWYU pragma: keep -#include "flog.h" -#include "global_safety.h" -#include "highlight.h" -#include "output.h" -#include "pager.h" -#include "screen.h" -#include "termsize.h" - -/// The number of characters to indent new blocks. -#define INDENT_STEP 4u - -/// RAII class to begin and end buffering around an outputter. -namespace { -class scoped_buffer_t : noncopyable_t, nonmovable_t { - outputter_t &outp_; - - public: - explicit scoped_buffer_t(outputter_t &outp) : outp_(outp) { outp_.begin_buffering(); } - - ~scoped_buffer_t() { outp_.end_buffering(); } -}; -} // namespace - -// Singleton of the cached escape sequences seen in prompts and similar strings. -// Note this is deliberately exported so that init_curses can clear it. -layout_cache_t layout_cache_t::shared; - -void screen_clear_layout_cache_ffi() { layout_cache_t::shared.clear(); } - -/// Tests if the specified narrow character sequence is present at the specified position of the -/// specified wide character string. All of \c seq must match, but str may be longer than seq. -static size_t try_sequence(const char *seq, const wchar_t *str) { - for (size_t i = 0;; i++) { - if (!seq[i]) return i; - if (seq[i] != str[i]) return 0; - } - - DIE("unexpectedly fell off end of try_sequence()"); - return 0; // this should never be executed -} - -static bool midnight_commander_hack = false; - -void screen_set_midnight_commander_hack() { midnight_commander_hack = true; } - -/// Returns the number of columns left until the next tab stop, given the current cursor position. -static size_t next_tab_stop(size_t current_line_width) { - // Assume tab stops every 8 characters if undefined. - size_t tab_width = init_tabs > 0 ? static_cast(init_tabs) : 8; - return ((current_line_width / tab_width) + 1) * tab_width; -} - -int line_t::wcswidth_min_0(size_t max) const { - int result = 0; - for (size_t idx = 0, end = std::min(max, text.size()); idx < end; idx++) { - auto w = fish_wcwidth_visible(text[idx].character); - // A backspace at the start of the line does nothing. - if (w > 0 || result > 0) { - result += w; - } - } - return result; -} - -/// Whether we permit soft wrapping. If so, in some cases we don't explicitly move to the second -/// physical line on a wrapped logical line; instead we just output it. -static bool allow_soft_wrap() { - // Should we be looking at eat_newline_glitch as well? - return auto_right_margin; -} - -/// Does this look like the escape sequence for setting a screen name? -static bool is_screen_name_escape_seq(const wchar_t *code, size_t *resulting_length) { - // Tmux escapes start with `\ePtmux;` and end also in `\e\\`, - // so we can just handle them here. - static const wchar_t *tmux_seq = L"Ptmux;"; - static const size_t tmux_seq_len = std::wcslen(tmux_seq); - bool is_tmux = false; - if (code[1] != L'k') { - if (wcsncmp(&code[1], tmux_seq, tmux_seq_len) == 0) { - is_tmux = true; - } else { - return false; - } - } - const wchar_t *const screen_name_end_sentinel = L"\x1B\\"; - size_t offset = 2; - while (true) { - const wchar_t *screen_name_end = std::wcsstr(&code[offset], screen_name_end_sentinel); - if (screen_name_end == nullptr) { - // Consider just k to be the code. - // (note: for the tmux sequence this is broken, but since we have no idea...) - *resulting_length = 2; - break; - } else { - // The tmux sequence requires that all escapes in the payload sequence - // be doubled. So if we have \e\e\\ that's still not the end. - if (is_tmux) { - size_t esc_count = 0; - const wchar_t *i = screen_name_end; - while (i > code && *(i - 1) == L'\x1B' && --i) esc_count++; - if (esc_count % 2 == 1) { - offset = screen_name_end - code + 1; - continue; - } - } - const wchar_t *escape_sequence_end = - screen_name_end + std::wcslen(screen_name_end_sentinel); - *resulting_length = escape_sequence_end - code; - break; - } - } - return true; -} - -/// Operating System Command (OSC) escape codes, used by iTerm2 and others: -/// ESC followed by ], terminated by either BEL or escape + backslash. -/// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html -/// and https://iterm2.com/documentation-escape-codes.html . -static bool is_osc_escape_seq(const wchar_t *code, size_t *resulting_length) { - bool found = false; - if (code[1] == ']') { - // Start at 2 to skip over ]. - size_t cursor = 2; - for (; code[cursor] != L'\0'; cursor++) { - // Consume a sequence of characters up to \ or . - if (code[cursor] == '\x07' || (code[cursor] == '\\' && code[cursor - 1] == '\x1B')) { - found = true; - break; - } - } - if (found) { - *resulting_length = cursor + 1; - } - } - return found; -} - -/// Generic VT100 three byte sequence: CSI followed by something in the range @ through _. -static bool is_three_byte_escape_seq(const wchar_t *code, size_t *resulting_length) { - bool found = false; - if (code[1] == L'[' && (code[2] >= L'@' && code[2] <= L'_')) { - *resulting_length = 3; - found = true; - } - return found; -} - -/// Generic VT100 two byte sequence: followed by something in the range @ through _. -static bool is_two_byte_escape_seq(const wchar_t *code, size_t *resulting_length) { - bool found = false; - if (code[1] >= L'@' && code[1] <= L'_') { - *resulting_length = 2; - found = true; - } - return found; -} - -/// Generic VT100 CSI-style sequence. , followed by zero or more ASCII characters NOT in -/// the range [@,_], followed by one character in that range. -/// This will also catch color sequences. -static bool is_csi_style_escape_seq(const wchar_t *code, size_t *resulting_length) { - if (code[1] != L'[') { - return false; - } - - // Start at 2 to skip over [ - size_t cursor = 2; - for (; code[cursor] != L'\0'; cursor++) { - // Consume a sequence of ASCII characters not in the range [@, ~]. - wchar_t widechar = code[cursor]; - - // If we're not in ASCII, just stop. - if (widechar > 127) break; - - // If we're the end character, then consume it and then stop. - if (widechar >= L'@' && widechar <= L'~') { - cursor++; - break; - } - } - // cursor now indexes just beyond the end of the sequence (or at the terminating zero). - *resulting_length = cursor; - return true; -} - -/// Detect whether the escape sequence sets one of the terminal attributes that affects how text is -/// displayed other than the color. -static bool is_visual_escape_seq(const wchar_t *code, size_t *resulting_length) { - if (!cur_term) return false; - const char *const esc2[] = { - enter_bold_mode, exit_attribute_mode, enter_underline_mode, exit_underline_mode, - enter_standout_mode, exit_standout_mode, enter_blink_mode, enter_protected_mode, - enter_italics_mode, exit_italics_mode, enter_reverse_mode, enter_shadow_mode, - exit_shadow_mode, enter_standout_mode, exit_standout_mode, enter_secure_mode, - enter_dim_mode, enter_blink_mode, enter_alt_charset_mode, exit_alt_charset_mode}; - - for (auto p : esc2) { - if (!p) continue; - // Test both padded and unpadded version, just to be safe. Most versions of fish_tparm don't - // actually seem to do anything these days. - size_t esc_seq_len = - std::max(try_sequence(fish_tparm(const_cast(p)), code), try_sequence(p, code)); - if (esc_seq_len) { - *resulting_length = esc_seq_len; - return true; - } - } - - return false; -} - -/// Returns the number of characters in the escape code starting at 'code'. We only handle sequences -/// that begin with \x1B. If it doesn't we return zero. We also return zero if we don't recognize -/// the escape sequence based on querying terminfo and other heuristics. -maybe_t escape_code_length(const wchar_t *code) { - assert(code != nullptr); - if (*code != L'\x1B') return none(); - - size_t esc_seq_len = 0; - bool found = is_visual_escape_seq(code, &esc_seq_len); - if (!found) found = is_screen_name_escape_seq(code, &esc_seq_len); - if (!found) found = is_osc_escape_seq(code, &esc_seq_len); - if (!found) found = is_three_byte_escape_seq(code, &esc_seq_len); - if (!found) found = is_csi_style_escape_seq(code, &esc_seq_len); - if (!found) found = is_two_byte_escape_seq(code, &esc_seq_len); - - return found ? maybe_t{esc_seq_len} : none(); -} - -wcstring screen_clear() { - if (clear_screen) { - return str2wcstring(clear_screen); - } - return wcstring{}; -} - -long escape_code_length_ffi(const wchar_t *code) { - auto found = escape_code_length(code); - return found.has_value() ? (long)*found : -1; -} - -size_t layout_cache_t::escape_code_length(const wchar_t *code) { - assert(code != nullptr); - if (*code != L'\x1B') return 0; - - size_t esc_seq_len = this->find_escape_code(code); - if (esc_seq_len) return esc_seq_len; - - auto found = ::escape_code_length(code); - if (found.has_value()) { - this->add_escape_code(wcstring(code, *found)); - esc_seq_len = *found; - } - return esc_seq_len; -} - -const layout_cache_t::prompt_cache_entry_t *layout_cache_t::find_prompt_layout( - const wcstring &input, size_t max_line_width) { - auto start = prompt_cache_.begin(); - auto end = prompt_cache_.end(); - for (auto iter = start; iter != end; ++iter) { - if (iter->text == input && iter->max_line_width == max_line_width) { - // Found it. Move it to the front if not already there. - if (iter != start) prompt_cache_.splice(start, prompt_cache_, iter); - return &*prompt_cache_.begin(); - } - } - return nullptr; -} - -void layout_cache_t::add_prompt_layout(prompt_cache_entry_t entry) { - prompt_cache_.emplace_front(std::move(entry)); - if (prompt_cache_.size() > prompt_cache_max_size) { - prompt_cache_.pop_back(); - } -} - -/// \return whether \p c ends a measuring run. -static bool is_run_terminator(wchar_t c) { - return c == L'\0' || c == L'\n' || c == L'\r' || c == L'\f'; -} - -/// Measure a run of characters in \p input starting at \p start. -/// Stop when we reach a run terminator, and return its index in \p out_end (if not null). -/// Note \0 is a run terminator so there will always be one. -/// We permit escape sequences to have run terminators other than \0. That is, escape sequences may -/// have embedded newlines, etc.; it's unclear if this is possible but we allow it. -static size_t measure_run_from(const wchar_t *input, size_t start, size_t *out_end, - layout_cache_t &cache) { - size_t width = 0; - size_t idx = start; - for (; !is_run_terminator(input[idx]); idx++) { - if (input[idx] == L'\x1B') { - // This is the start of an escape code; we assume it has width 0. - // -1 because we are going to increment in the loop. - size_t len = cache.escape_code_length(&input[idx]); - if (len > 0) idx += len - 1; - } else if (input[idx] == L'\t') { - width = next_tab_stop(width); - } else { - // Ordinary char. Add its width with care to ignore control chars which have width -1. - auto w = fish_wcwidth_visible(input[idx]); - // A backspace at the start of the line does nothing. - if (w != -1 || width > 0) { - width += w; - } - } - } - if (out_end) *out_end = idx; - return width; -} - -/// Attempt to truncate the prompt run \p run, which has width \p width, to \p no more than -/// desired_width. \return the resulting width and run by reference. -static void truncate_run(wcstring *run, size_t desired_width, size_t *width, - layout_cache_t &cache) { - size_t curr_width = *width; - if (curr_width <= desired_width) { - return; - } - - // Bravely prepend ellipsis char and skip it. - // Ellipsis is always width 1. - wchar_t ellipsis = get_ellipsis_char(); - run->insert(0, 1, ellipsis); // index, count, char - curr_width += 1; - - // Start removing characters after ellipsis. - // Note we modify 'run' inside this loop. - size_t idx = 1; - while (curr_width > desired_width && idx < run->size()) { - wchar_t c = run->at(idx); - assert(!is_run_terminator(c) && "Should not have run terminator inside run"); - if (c == L'\x1B') { - size_t len = cache.escape_code_length(run->c_str() + idx); - idx += std::max(len, (size_t)1); - } else if (c == '\t') { - // Tabs would seem to be quite annoying to measure while truncating. - // We simply remove these and start over. - run->erase(idx, 1); - curr_width = measure_run_from(run->c_str(), 0, nullptr, cache); - idx = 0; - } else { - size_t char_width = fish_wcwidth_visible(c); - curr_width -= std::min(curr_width, char_width); - run->erase(idx, 1); - } - } - *width = curr_width; -} - -prompt_layout_t layout_cache_t::calc_prompt_layout(const wcstring &prompt_str, - wcstring *out_trunc_prompt, - size_t max_line_width) { - // FIXME: we could avoid allocating trunc_prompt if max_line_width is SIZE_T_MAX. - if (const auto *entry = this->find_prompt_layout(prompt_str, max_line_width)) { - if (out_trunc_prompt) out_trunc_prompt->assign(entry->trunc_text); - return entry->layout; - } - - size_t prompt_len = prompt_str.size(); - const wchar_t *prompt = prompt_str.c_str(); - - prompt_layout_t layout = {{}, 0, 0}; - wcstring trunc_prompt; - - size_t run_start = 0; - while (run_start < prompt_len) { - size_t run_end; - size_t line_width = measure_run_from(prompt, run_start, &run_end, *this); - if (line_width <= max_line_width) { - // No truncation needed on this line. - trunc_prompt.append(&prompt[run_start], run_end - run_start); - } else { - // Truncation needed on this line. - wcstring run_storage(&prompt[run_start], run_end - run_start); - truncate_run(&run_storage, max_line_width, &line_width, *this); - trunc_prompt.append(run_storage); - } - layout.max_line_width = std::max(layout.max_line_width, line_width); - layout.last_line_width = line_width; - - wchar_t endc = prompt[run_end]; - if (endc) { - if (endc == L'\n' || endc == L'\f') { - layout.line_breaks.push_back(trunc_prompt.size()); - // If the prompt ends in a new line, that's one empy last line. - if (run_end == prompt_str.size() - 1) layout.last_line_width = 0; - } - trunc_prompt.push_back(endc); - run_start = run_end + 1; - } else { - break; - } - } - this->add_prompt_layout(prompt_cache_entry_t{prompt, max_line_width, trunc_prompt, layout}); - if (out_trunc_prompt) { - *out_trunc_prompt = std::move(trunc_prompt); - } - return layout; -} - -static size_t calc_prompt_lines(const wcstring &prompt) { - // Hack for the common case where there's no newline at all. I don't know if a newline can - // appear in an escape sequence, so if we detect a newline we have to defer to - // calc_prompt_width_and_lines. - size_t result = 1; - if (prompt.find_first_of(L"\n\f") != wcstring::npos) { - result = layout_cache_t::shared.calc_prompt_layout(prompt).line_breaks.size() + 1; - } - return result; -} - -/// Stat stdout and stderr and save result. This should be done before calling a function that may -/// cause output. -void screen_t::save_status() { - fstat(STDOUT_FILENO, &this->prev_buff_1); - fstat(STDERR_FILENO, &this->prev_buff_2); -} - -/// Stat stdout and stderr and compare result to previous result in reader_save_status. Repaint if -/// modification time has changed. -void screen_t::check_status() { - fflush(stdout); - fflush(stderr); - if (!has_working_tty_timestamps) { - // We can't reliably determine if the terminal has been written to behind our back so we - // just assume that hasn't happened and hope for the best. This is important for multi-line - // prompts to work correctly. - return; - } - - struct stat post_buff_1 {}; - struct stat post_buff_2 {}; - fstat(STDOUT_FILENO, &post_buff_1); - fstat(STDERR_FILENO, &post_buff_2); - - bool changed = (this->prev_buff_1.st_mtime != post_buff_1.st_mtime) || - (this->prev_buff_2.st_mtime != post_buff_2.st_mtime); - -#if defined HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC - changed = changed || - this->prev_buff_1.st_mtimespec.tv_nsec != post_buff_1.st_mtimespec.tv_nsec || - this->prev_buff_2.st_mtimespec.tv_nsec != post_buff_2.st_mtimespec.tv_nsec; -#elif defined HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC - changed = changed || this->prev_buff_1.st_mtim.tv_nsec != post_buff_1.st_mtim.tv_nsec || - this->prev_buff_2.st_mtim.tv_nsec != post_buff_2.st_mtim.tv_nsec; -#endif - - if (changed) { - // Ok, someone has been messing with our screen. We will want to repaint. However, we do not - // know where the cursor is. It is our best bet that we are still on the same line, so we - // move to the beginning of the line, reset the modelled screen contents, and then set the - // modeled cursor y-pos to its earlier value. - int prev_line = this->actual.cursor.y; - this->reset_line(true /* repaint prompt */); - this->actual.cursor.y = prev_line; - } -} - -void screen_t::desired_append_char(wchar_t b, highlight_spec_t c, int indent, size_t prompt_width, - size_t bwidth) { - int line_no = this->desired.cursor.y; - - if (b == L'\n') { - // Current line is definitely hard wrapped. - // Create the next line. - this->desired.create_line(this->desired.cursor.y + 1); - this->desired.line(this->desired.cursor.y).is_soft_wrapped = false; - int line_no = ++this->desired.cursor.y; - this->desired.cursor.x = 0; - size_t indentation = prompt_width + static_cast(indent) * INDENT_STEP; - line_t &line = this->desired.line(line_no); - line.indentation = indentation; - for (size_t i = 0; i < indentation; i++) { - desired_append_char(L' ', highlight_spec_t{}, indent, prompt_width, 1); - } - } else if (b == L'\r') { - line_t ¤t = this->desired.line(line_no); - current.clear(); - this->desired.cursor.x = 0; - } else { - int screen_width = this->desired.screen_width; - int cw = bwidth; - - this->desired.create_line(line_no); - - // Check if we are at the end of the line. If so, continue on the next line. - if ((this->desired.cursor.x + cw) > screen_width) { - // Current line is soft wrapped (assuming we support it). - this->desired.line(this->desired.cursor.y).is_soft_wrapped = true; - - line_no = static_cast(this->desired.line_count()); - this->desired.add_line(); - this->desired.cursor.y++; - this->desired.cursor.x = 0; - } - - line_t &line = this->desired.line(line_no); - line.append(b, c); - this->desired.cursor.x += cw; - - // Maybe wrap the cursor to the next line, even if the line itself did not wrap. This - // avoids wonkiness in the last column. - if (this->desired.cursor.x >= screen_width) { - line.is_soft_wrapped = true; - this->desired.cursor.x = 0; - this->desired.cursor.y++; - } - } -} - -void screen_t::move(int new_x, int new_y) { - if (this->actual.cursor.x == new_x && this->actual.cursor.y == new_y) return; - - const scoped_buffer_t buffering(outp()); - - // If we are at the end of our window, then either the cursor stuck to the edge or it didn't. We - // don't know! We can fix it up though. - if (this->actual.cursor.x == this->actual.screen_width) { - // Either issue a cr to go back to the beginning of this line, or a nl to go to the - // beginning of the next one, depending on what we think is more efficient. - if (new_y <= this->actual.cursor.y) { - this->outp().push_back('\r'); - } else { - this->outp().push_back('\n'); - this->actual.cursor.y++; - } - // Either way we're not in the first column. - this->actual.cursor.x = 0; - } - - int i; - int x_steps, y_steps; - - const char *str; - auto &outp = this->outp(); - - y_steps = new_y - this->actual.cursor.y; - - if (y_steps < 0) { - str = cursor_up; - } else if (y_steps > 0) { - str = cursor_down; - if ((shell_modes.c_oflag & ONLCR) != 0 && - std::strcmp(str, "\n") == 0) { // See GitHub issue #4505. - // Most consoles use a simple newline as the cursor down escape. - // If ONLCR is enabled (which it normally is) this will of course - // also move the cursor to the beginning of the line. - // We could do: - // if (std::strcmp(cursor_up, "\x1B[A") == 0) str = "\x1B[B"; - // else ... but that doesn't work for unknown reasons. - this->actual.cursor.x = 0; - } - } - - for (i = 0; i < abs(y_steps); i++) { - outp.writembs(str); - } - - x_steps = new_x - this->actual.cursor.x; - - if (x_steps && new_x == 0) { - outp.push_back('\r'); - x_steps = 0; - } - - const char *multi_str = nullptr; - if (x_steps < 0) { - str = cursor_left; - multi_str = parm_left_cursor; - } else { - str = cursor_right; - multi_str = parm_right_cursor; - } - - // Use the bulk ('multi') output for cursor movement if it is supported and it would be shorter - // Note that this is required to avoid some visual glitches in iTerm (issue #1448). - bool use_multi = multi_str != nullptr && multi_str[0] != '\0' && - abs(x_steps) * std::strlen(str) > std::strlen(multi_str); - if (use_multi && cur_term) { - char *multi_param = fish_tparm(const_cast(multi_str), abs(x_steps)); - writembs(outp, multi_param); - } else { - for (i = 0; i < abs(x_steps); i++) { - writembs(outp, str); - } - } - - this->actual.cursor.x = new_x; - this->actual.cursor.y = new_y; -} - -/// Convert a wide character to a multibyte string and append it to the buffer. -void screen_t::write_char(wchar_t c, size_t width) { - scoped_buffer_t buffering(outp()); - this->actual.cursor.x += width; - this->outp().writech(c); - if (this->actual.cursor.x == this->actual.screen_width && allow_soft_wrap()) { - this->soft_wrap_location = screen_data_t::cursor_t{0, this->actual.cursor.y + 1}; - - // Note that our cursor position may be a lie: Apple Terminal makes the right cursor stick - // to the margin, while Ubuntu makes it "go off the end" (but still doesn't wrap). We rely - // on s_move to fix this up. - } else { - this->soft_wrap_location = none(); - } -} - -/// Send the specified string through tputs and append the output to the screen's outputter. -void screen_t::write_mbs(const char *s) { writembs(this->outp(), s); } - -/// Convert a wide string to a multibyte string and append it to the buffer. -void screen_t::write_str(const wchar_t *s) { this->outp().writestr(s); } -void screen_t::write_str(const wcstring &s) { this->outp().writestr(s.c_str()); } - -/// Returns the length of the "shared prefix" of the two lines, which is the run of matching text -/// and colors. If the prefix ends on a combining character, do not include the previous character -/// in the prefix. -static size_t line_shared_prefix(const line_t &a, const line_t &b) { - size_t idx, max = std::min(a.size(), b.size()); - for (idx = 0; idx < max; idx++) { - wchar_t ac = a.char_at(idx), bc = b.char_at(idx); - - // We're done if the text or colors are different. - if (ac != bc || a.color_at(idx) != b.color_at(idx)) { - if (idx > 0) { - const line_t *c = nullptr; - // Possible combining mark, go back until we hit _two_ printable characters or idx - // of 0. - if (fish_wcwidth(a.char_at(idx)) < 1) { - c = &a; - } else if (fish_wcwidth(b.char_at(idx)) < 1) { - c = &b; - } - - if (c) { - while (idx > 1 && (fish_wcwidth(c->char_at(idx - 1)) < 1 || - fish_wcwidth(c->char_at(idx)) < 1)) - idx--; - if (idx == 1 && fish_wcwidth(c->char_at(idx)) < 1) idx = 0; - } - } - break; - } - } - return idx; -} - -// We are about to output one or more characters onto the screen at the given x, y. If we are at the -// end of previous line, and the previous line is marked as soft wrapping, then tweak the screen so -// we believe we are already in the target position. This lets the terminal take care of wrapping, -// which means that if you copy and paste the text, it won't have an embedded newline. -bool screen_t::handle_soft_wrap(int x, int y) { - if (this->soft_wrap_location && x == this->soft_wrap_location->x && - y == this->soft_wrap_location->y) { //!OCLINT - // We can soft wrap; but do we want to? - if (this->desired.line(y - 1).is_soft_wrapped && allow_soft_wrap()) { - // Yes. Just update the actual cursor; that will cause us to elide emitting the commands - // to move here, so we will just output on "one big line" (which the terminal soft - // wraps. - this->actual.cursor = this->soft_wrap_location.value(); - } - } - return false; -} - -/// Update the screen to match the desired output. -void screen_t::update(const wcstring &left_prompt, const wcstring &right_prompt, - const env_stack_t &vars) { - // Helper function to set a resolved color, using the caching resolver. - auto color_resolver = new_highlight_color_resolver(); - auto set_color = [&](highlight_spec_t c) { - rgb_color_t fg; - rgb_color_t bg; - color_resolver->resolve_spec(c, false, vars.get_impl_ffi(), fg); - color_resolver->resolve_spec(c, true, vars.get_impl_ffi(), bg); - this->outp().set_color(fg, bg); - }; - - layout_cache_t &cached_layouts = layout_cache_t::shared; - const scoped_buffer_t buffering(outp()); - - // Determine size of left and right prompt. Note these have already been truncated. - const prompt_layout_t left_prompt_layout = cached_layouts.calc_prompt_layout(left_prompt); - const size_t left_prompt_width = left_prompt_layout.last_line_width; - const size_t right_prompt_width = - cached_layouts.calc_prompt_layout(right_prompt).last_line_width; - - // Figure out how many following lines we need to clear (probably 0). - size_t actual_lines_before_reset = this->actual_lines_before_reset; - this->actual_lines_before_reset = 0; - - bool need_clear_lines = this->need_clear_lines; - bool need_clear_screen = this->need_clear_screen; - bool has_cleared_screen = false; - - const int screen_width = this->desired.screen_width; - - if (this->actual.screen_width != screen_width) { - // Ensure we don't issue a clear screen for the very first output, to avoid issue #402. - if (this->actual.screen_width > 0) { - need_clear_screen = true; - this->move(0, 0); - this->reset_line(); - - need_clear_lines = need_clear_lines || this->need_clear_lines; - need_clear_screen = need_clear_screen || this->need_clear_screen; - } - this->actual.screen_width = screen_width; - } - - this->need_clear_lines = false; - this->need_clear_screen = false; - - // Determine how many lines have stuff on them; we need to clear lines with stuff that we don't - // want. - const size_t lines_with_stuff = std::max(actual_lines_before_reset, this->actual.line_count()); - if (this->desired.line_count() < lines_with_stuff) need_clear_screen = true; - - // Output the left prompt if it has changed. - if (left_prompt != this->actual_left_prompt) { - this->move(0, 0); - size_t start = 0; - for (const size_t line_break : left_prompt_layout.line_breaks) { - this->write_str(left_prompt.substr(start, line_break - start)); - if (clr_eol) { - this->write_mbs(clr_eol); - } - start = line_break; - } - this->write_str(left_prompt.substr(start)); - this->actual_left_prompt = left_prompt; - this->actual.cursor.x = static_cast(left_prompt_width); - } - - // Output all lines. - for (size_t i = 0; i < this->desired.line_count(); i++) { - const line_t &o_line = this->desired.line(i); - line_t &s_line = this->actual.create_line(i); - size_t start_pos = i == 0 ? left_prompt_width : 0; - int current_width = 0; - bool has_cleared_line = false; - - // If this is the last line, maybe we should clear the screen. - // Don't issue clr_eos if we think the cursor will end up in the last column - see #6951. - const bool should_clear_screen_this_line = - need_clear_screen && i + 1 == this->desired.line_count() && clr_eos != nullptr && - !(this->desired.cursor.x == 0 && - this->desired.cursor.y == static_cast(this->desired.line_count())); - - // skip_remaining is how many columns are unchanged on this line. - // Note that skip_remaining is a width, not a character count. - size_t skip_remaining = start_pos; - - const size_t shared_prefix = line_shared_prefix(o_line, s_line); - size_t skip_prefix = shared_prefix; - if (shared_prefix < o_line.indentation) { - if (o_line.indentation > s_line.indentation && !has_cleared_screen && clr_eol && - clr_eos) { - set_color(highlight_spec_t{}); - this->move(0, static_cast(i)); - this->write_mbs(should_clear_screen_this_line ? clr_eos : clr_eol); - has_cleared_screen = should_clear_screen_this_line; - has_cleared_line = true; - } - skip_prefix = o_line.indentation; - } - - // Compute how much we should skip. At a minimum we skip over the prompt. But also skip - // over the shared prefix of what we want to output now, and what we output before, to - // avoid repeatedly outputting it. - if (skip_prefix > 0) { - size_t skip_width = - shared_prefix < skip_prefix ? skip_prefix : o_line.wcswidth_min_0(shared_prefix); - if (skip_width > skip_remaining) skip_remaining = skip_width; - } - - if (!should_clear_screen_this_line) { - // If we're soft wrapped, and if we're going to change the first character of the next - // line, don't skip over the last two characters so that we maintain soft-wrapping. - if (o_line.is_soft_wrapped && i + 1 < this->desired.line_count()) { - bool next_line_will_change = true; - if (i + 1 < this->actual.line_count()) { //!OCLINT - if (line_shared_prefix(this->desired.line(i + 1), this->actual.line(i + 1)) > - 0) { - next_line_will_change = false; - } - } - if (next_line_will_change) { - skip_remaining = std::min(skip_remaining, - static_cast(this->actual.screen_width - 2)); - } - } - } - - // Skip over skip_remaining width worth of characters. - size_t j = 0; - for (; j < o_line.size(); j++) { - size_t width = fish_wcwidth_visible(o_line.char_at(j)); - if (skip_remaining < width) break; - skip_remaining -= width; - current_width += width; - } - - // Skip over zero-width characters (e.g. combining marks at the end of the prompt). - for (; j < o_line.size(); j++) { - int width = fish_wcwidth_visible(o_line.char_at(j)); - if (width > 0) break; - } - - // Now actually output stuff. - for (;; j++) { - bool done = j >= o_line.size(); - // Clear the screen if we have not done so yet. - // If we are about to output into the last column, clear the screen first. If we clear - // the screen after we output into the last column, it can erase the last character due - // to the sticky right cursor. If we clear the screen too early, we can defeat soft - // wrapping. - if (should_clear_screen_this_line && !has_cleared_screen && - (done || j + 1 == static_cast(screen_width))) { - set_color(highlight_spec_t{}); - this->move(current_width, static_cast(i)); - this->write_mbs(clr_eos); - has_cleared_screen = true; - } - if (done) break; - - this->handle_soft_wrap(current_width, static_cast(i)); - this->move(current_width, static_cast(i)); - set_color(o_line.color_at(j)); - auto width = fish_wcwidth_visible(o_line.char_at(j)); - this->write_char(o_line.char_at(j), width); - current_width += width; - } - - bool clear_remainder = false; - // Clear the remainder of the line if we need to clear and if we didn't write to the end of - // the line. If we did write to the end of the line, the "sticky right edge" (as part of - // auto_right_margin) means that we'll be clearing the last character we wrote! - if (has_cleared_screen || has_cleared_line) { - // Already cleared everything. - clear_remainder = false; - } else if (need_clear_lines && current_width < screen_width) { - clear_remainder = true; - } else if (right_prompt_width < this->last_right_prompt_width) { - clear_remainder = true; - } else { - // This wcswidth shows up strong in the profile. - // Only do it if the previous line could conceivably be wider. - // That means if it is a prefix of the current one we can skip it. - if (s_line.text.size() != shared_prefix) { - int prev_width = s_line.wcswidth_min_0(); - clear_remainder = prev_width > current_width; - } - } - - // We unset the color even if we don't clear the line. - // This means that we switch background correctly on the next, - // including our weird implicit bolding. - set_color(highlight_spec_t{}); - if (clear_remainder && clr_eol) { - this->move(current_width, static_cast(i)); - this->write_mbs(clr_eol); - } - - // Output any rprompt if this is the first line. - if (i == 0 && right_prompt_width > 0) { //!OCLINT(Use early exit/continue) - // Move the cursor to the beginning of the line first to be independent of the width. - // This helps prevent staircase effects if fish and the terminal disagree. - this->move(0, 0); - this->move(static_cast(screen_width - right_prompt_width), static_cast(i)); - set_color(highlight_spec_t{}); - this->write_str(right_prompt); - this->actual.cursor.x += right_prompt_width; - - // We output in the last column. Some terms (Linux) push the cursor further right, past - // the window. Others make it "stick." Since we don't really know which is which, issue - // a cr so it goes back to the left. - // - // However, if the user is resizing the window smaller, then it's possible the cursor - // wrapped. If so, then a cr will go to the beginning of the following line! So instead - // issue a bunch of "move left" commands to get back onto the line, and then jump to the - // front of it. - this->move(this->actual.cursor.x - static_cast(right_prompt_width), - this->actual.cursor.y); - this->write_str(L"\r"); - this->actual.cursor.x = 0; - } - } - - // Also move the cursor to the beginning of the line here, - // in case we're wrong about the width anywhere. - // Don't do it when running in midnight_commander because of - // https://midnight-commander.org/ticket/4258. - if (!midnight_commander_hack) { - this->move(0, 0); - } - - // Clear remaining lines (if any) if we haven't cleared the screen. - if (!has_cleared_screen && need_clear_screen && clr_eol) { - set_color(highlight_spec_t{}); - for (size_t i = this->desired.line_count(); i < lines_with_stuff; i++) { - this->move(0, static_cast(i)); - this->write_mbs(clr_eol); - } - } - - this->move(this->desired.cursor.x, this->desired.cursor.y); - set_color(highlight_spec_t{}); - - // We have now synced our actual screen against our desired screen. Note that this is a big - // assignment! - this->actual = this->desired; - this->last_right_prompt_width = right_prompt_width; -} - -/// Returns true if we are using a dumb terminal. -static bool is_dumb() { - if (!cur_term) return true; - return !cursor_up || !cursor_down || !cursor_left || !cursor_right; -} - -namespace { -struct screen_layout_t { - // The left prompt that we're going to use. - wcstring left_prompt; - // How much space to leave for it. - size_t left_prompt_space; - // The right prompt. - wcstring right_prompt; - // The autosuggestion. - wcstring autosuggestion; -}; -} // namespace - -// Given a vector whose indexes are offsets and whose values are the widths of the string if -// truncated at that offset, return the offset that fits in the given width. Returns -// width_by_offset.size() - 1 if they all fit. The first value in width_by_offset is assumed to be -// 0. -static size_t truncation_offset_for_width(const std::vector &width_by_offset, - size_t max_width) { - assert(!width_by_offset.empty() && width_by_offset.at(0) == 0); - size_t i; - for (i = 1; i < width_by_offset.size(); i++) { - if (width_by_offset.at(i) > max_width) break; - } - // i is the first index that did not fit; i-1 is therefore the last that did. - return i - 1; -} - -static screen_layout_t compute_layout(screen_t *s, size_t screen_width, - const wcstring &left_untrunc_prompt, - const wcstring &right_untrunc_prompt, - const wcstring &commandline, - const wcstring &autosuggestion_str) { - UNUSED(s); - screen_layout_t result = {}; - - // Truncate both prompts to screen width (#904). - wcstring left_prompt; - prompt_layout_t left_prompt_layout = - layout_cache_t::shared.calc_prompt_layout(left_untrunc_prompt, &left_prompt, screen_width); - - wcstring right_prompt; - prompt_layout_t right_prompt_layout = layout_cache_t::shared.calc_prompt_layout( - right_untrunc_prompt, &right_prompt, screen_width); - - size_t left_prompt_width = left_prompt_layout.last_line_width; - size_t right_prompt_width = right_prompt_layout.last_line_width; - - if (left_prompt_width + right_prompt_width > screen_width) { - // Nix right_prompt. - right_prompt = L""; - right_prompt_width = 0; - } - - // Now we should definitely fit. - assert(left_prompt_width + right_prompt_width <= screen_width); - - // Get the width of the first line, and if there is more than one line. - bool multiline = false; - size_t first_line_width = 0; - for (auto c : commandline) { - if (c == L'\n') { - multiline = true; - break; - } else { - first_line_width += fish_wcwidth_visible(c); - } - } - const size_t first_command_line_width = first_line_width; - - // If we have more than one line, ensure we have no autosuggestion. - const wchar_t *autosuggestion = autosuggestion_str.c_str(); - size_t autosuggest_total_width = 0; - std::vector autosuggest_truncated_widths; - if (multiline) { - autosuggestion = L""; - } else { - autosuggest_truncated_widths.reserve(1 + autosuggestion_str.size()); - for (size_t i = 0; autosuggestion[i] != L'\0'; i++) { - autosuggest_truncated_widths.push_back(autosuggest_total_width); - autosuggest_total_width += fish_wcwidth_visible(autosuggestion[i]); - } - } - - // Here are the layouts we try in turn: - // - // 1. Left prompt visible, right prompt visible, command line visible, autosuggestion visible. - // - // 2. Left prompt visible, right prompt visible, command line visible, autosuggestion truncated - // (possibly to zero). - // - // 3. Left prompt visible, right prompt hidden, command line visible, autosuggestion visible - // - // 4. Left prompt visible, right prompt hidden, command line visible, autosuggestion truncated - // - // 5. Newline separator (left prompt visible, right prompt hidden, command line visible, - // autosuggestion visible). - // - // A remark about layout #4: if we've pushed the command line to a new line, why can't we draw - // the right prompt? The issue is resizing: if you resize the window smaller, then the right - // prompt will wrap to the next line. This means that we can't go back to the line that we were - // on, and things turn to chaos very quickly. - size_t calculated_width; - bool done = false; - - // Case 1 - if (!done) { - calculated_width = left_prompt_width + right_prompt_width + first_command_line_width + - autosuggest_total_width; - if (calculated_width <= screen_width) { - result.left_prompt = left_prompt; - result.left_prompt_space = left_prompt_width; - result.right_prompt = right_prompt; - result.autosuggestion = autosuggestion; - done = true; - } - } - - // Case 2. Note that we require strict inequality so that there's always at least one space - // between the left edge and the rprompt. - if (!done) { - calculated_width = left_prompt_width + right_prompt_width + first_command_line_width; - if (calculated_width <= screen_width) { - result.left_prompt = left_prompt; - result.left_prompt_space = left_prompt_width; - result.right_prompt = right_prompt; - - // Need at least two characters to show an autosuggestion. - size_t available_autosuggest_space = - screen_width - (left_prompt_width + right_prompt_width + first_command_line_width); - if (autosuggest_total_width > 0 && available_autosuggest_space > 2) { - size_t truncation_offset = truncation_offset_for_width( - autosuggest_truncated_widths, available_autosuggest_space - 2); - result.autosuggestion = wcstring(autosuggestion, truncation_offset); - result.autosuggestion.push_back(get_ellipsis_char()); - } - done = true; - } - } - - // Case 3 - if (!done) { - calculated_width = left_prompt_width + first_command_line_width + autosuggest_total_width; - if (calculated_width <= screen_width) { - result.left_prompt = left_prompt; - result.left_prompt_space = left_prompt_width; - result.autosuggestion = autosuggestion; - done = true; - } - } - - // Case 4 - if (!done) { - calculated_width = left_prompt_width + first_command_line_width; - if (calculated_width <= screen_width) { - result.left_prompt = left_prompt; - result.left_prompt_space = left_prompt_width; - - // Need at least two characters to show an autosuggestion. - size_t available_autosuggest_space = - screen_width - (left_prompt_width + first_command_line_width); - if (autosuggest_total_width > 0 && available_autosuggest_space > 2) { - size_t truncation_offset = truncation_offset_for_width( - autosuggest_truncated_widths, available_autosuggest_space - 2); - result.autosuggestion = wcstring(autosuggestion, truncation_offset); - result.autosuggestion.push_back(get_ellipsis_char()); - } - done = true; - } - } - - // Case 5 - if (!done) { - result.left_prompt = left_prompt; - result.left_prompt_space = left_prompt_width; - result.autosuggestion = autosuggestion; - } - - return result; -} - -void screen_t::write(const wcstring &left_prompt, const wcstring &right_prompt, - const wcstring &commandline, size_t explicit_len, - const std::vector &colors, const std::vector &indent, - size_t cursor_pos, - // todo!("should be environment_t") - const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering, - bool cursor_is_within_pager) { - termsize_t curr_termsize = termsize_last(); - int screen_width = curr_termsize.width; - static relaxed_atomic_t s_repaints{0}; - FLOGF(screen, "Repaint %u", static_cast(++s_repaints)); - screen_data_t::cursor_t cursor_arr; - - // Turn the command line into the explicit portion and the autosuggestion. - const wcstring explicit_command_line = commandline.substr(0, explicit_len); - const wcstring autosuggestion = commandline.substr(explicit_len); - - // If we are using a dumb terminal, don't try any fancy stuff, just print out the text. - // right_prompt not supported. - if (is_dumb()) { - const std::string prompt_narrow = wcs2string(left_prompt); - const std::string command_line_narrow = wcs2string(explicit_command_line); - - write_loop(STDOUT_FILENO, "\r", 1); - write_loop(STDOUT_FILENO, prompt_narrow.c_str(), prompt_narrow.size()); - write_loop(STDOUT_FILENO, command_line_narrow.c_str(), command_line_narrow.size()); - - return; - } - - this->check_status(); - - // Completely ignore impossibly small screens. - if (screen_width < 4) { - return; - } - - // Compute a layout. - const screen_layout_t layout = compute_layout(this, screen_width, left_prompt, right_prompt, - explicit_command_line, autosuggestion); - - // Determine whether, if we have an autosuggestion, it was truncated. - this->autosuggestion_is_truncated = - !autosuggestion.empty() && autosuggestion != layout.autosuggestion; - - // Clear the desired screen and set its width. - this->desired.screen_width = screen_width; - this->desired.resize(0); - this->desired.cursor.x = this->desired.cursor.y = 0; - - // Append spaces for the left prompt. - for (size_t i = 0; i < layout.left_prompt_space; i++) { - desired_append_char(L' ', highlight_spec_t{}, 0, layout.left_prompt_space, 1); - } - - // If overflowing, give the prompt its own line to improve the situation. - size_t first_line_prompt_space = layout.left_prompt_space; - - // Reconstruct the command line. - wcstring effective_commandline = explicit_command_line + layout.autosuggestion; - - // Output the command line. - size_t i; - for (i = 0; i < effective_commandline.size(); i++) { - // Grab the current cursor's x,y position if this character matches the cursor's offset. - if (!cursor_is_within_pager && i == cursor_pos) { - cursor_arr = this->desired.cursor; - } - desired_append_char(effective_commandline.at(i), colors[i], indent[i], - first_line_prompt_space, - fish_wcwidth_visible(effective_commandline.at(i))); - } - - // Cursor may have been at the end too. - if (!cursor_is_within_pager && i == cursor_pos) { - cursor_arr = this->desired.cursor; - } - - int full_line_count = this->desired.cursor.y + 1; - - // Now that we've output everything, set the cursor to the position that we saved in the loop - // above. - this->desired.cursor = cursor_arr; - - if (cursor_is_within_pager) { - this->desired.cursor.x = static_cast(cursor_pos); - this->desired.cursor.y = static_cast(this->desired.line_count()); - } - - // Re-render our completions page if necessary. Limit the term size of the pager to the true - // term size, minus the number of lines consumed by our string. - pager.set_term_size( - termsize_t{std::max((rust::isize)1, curr_termsize.width), - std::max((rust::isize)1, curr_termsize.height - full_line_count)}); - pager.update_rendering(&page_rendering); - // Append pager_data (none if empty). - this->desired.append_lines(page_rendering.screen_data); - - this->update(layout.left_prompt, layout.right_prompt, vars); - this->save_status(); -} - -void screen_t::reset_line(bool repaint_prompt) { - // Remember how many lines we had output to, so we can clear the remaining lines in the next - // call to s_update. This prevents leaving junk underneath the cursor when resizing a window - // wider such that it reduces our desired line count. - this->actual_lines_before_reset = - std::max(this->actual_lines_before_reset, this->actual.line_count()); - - if (repaint_prompt) { - // If the prompt is multi-line, we need to move up to the prompt's initial line. We do this - // by lying to ourselves and claiming that we're really below what we consider "line 0" - // (which is the last line of the prompt). This will cause us to move up to try to get back - // to line 0, but really we're getting back to the initial line of the prompt. - const size_t prompt_line_count = calc_prompt_lines(this->actual_left_prompt); - assert(prompt_line_count >= 1); - this->actual.cursor.y += (prompt_line_count - 1); - this->actual_left_prompt.clear(); - } - this->actual.resize(0); - this->need_clear_lines = true; - - // This should prevent resetting the cursor position during the next repaint. - write_loop(STDOUT_FILENO, "\r", 1); - this->actual.cursor.x = 0; - - fstat(STDOUT_FILENO, &this->prev_buff_1); - fstat(STDERR_FILENO, &this->prev_buff_2); -} - -void screen_t::reset_abandoning_line(int screen_width) { - this->actual.cursor.y = 0; - this->actual.resize(0); - this->actual_left_prompt.clear(); - this->need_clear_lines = true; - - // Do the PROMPT_SP hack. - wcstring abandon_line_string; - abandon_line_string.reserve(screen_width + 32); - - // Don't need to check for fish_wcwidth errors; this is done when setting up - // omitted_newline_char in common.cpp. - int non_space_width = get_omitted_newline_width(); - // We do `>` rather than `>=` because the code below might require one extra space. - if (screen_width > non_space_width) { - bool justgrey = true; - if (cur_term && enter_dim_mode) { - std::string dim = fish_tparm(const_cast(enter_dim_mode)); - if (!dim.empty()) { - // Use dim if they have it, so the color will be based on their actual normal - // color and the background of the terminal. - abandon_line_string.append(str2wcstring(dim)); - justgrey = false; - } - } - if (cur_term && justgrey && set_a_foreground) { - if (max_colors >= 238) { - // draw the string in a particular grey - abandon_line_string.append( - str2wcstring(fish_tparm(const_cast(set_a_foreground), 237))); - } else if (max_colors >= 9) { - // bright black (the ninth color, looks grey) - abandon_line_string.append( - str2wcstring(fish_tparm(const_cast(set_a_foreground), 8))); - } else if (max_colors >= 2 && enter_bold_mode) { - // we might still get that color by setting black and going bold for bright - abandon_line_string.append( - str2wcstring(fish_tparm(const_cast(enter_bold_mode)))); - abandon_line_string.append( - str2wcstring(fish_tparm(const_cast(set_a_foreground), 0))); - } - } - - abandon_line_string.append(get_omitted_newline_str()); - - if (cur_term && exit_attribute_mode) { - abandon_line_string.append(str2wcstring(fish_tparm( - const_cast(exit_attribute_mode)))); // normal text ANSI escape sequence - } - - int newline_glitch_width = TERM_HAS_XN ? 0 : 1; - abandon_line_string.append(screen_width - non_space_width - newline_glitch_width, L' '); - } - - abandon_line_string.push_back(L'\r'); - abandon_line_string.append(get_omitted_newline_str()); - // Now we are certainly on a new line. But we may have dropped the omitted newline char on - // it. So append enough spaces to overwrite the omitted newline char, and then clear all the - // spaces from the new line. - abandon_line_string.append(non_space_width, L' '); - abandon_line_string.push_back(L'\r'); - // Clear entire line. Zsh doesn't do this. Fish added this with commit 4417a6ee: If you have - // a prompt preceded by a new line, you'll get a line full of spaces instead of an empty - // line above your prompt. This doesn't make a difference in normal usage, but copying and - // pasting your terminal log becomes a pain. This commit clears that line, making it an - // actual empty line. - if (!is_dumb() && clr_eol) { - abandon_line_string.append(str2wcstring(clr_eol)); - } - - const std::string narrow_abandon_line_string = wcs2string(abandon_line_string); - write_loop(STDOUT_FILENO, narrow_abandon_line_string.c_str(), - narrow_abandon_line_string.size()); - this->actual.cursor.x = 0; - - fstat(STDOUT_FILENO, &this->prev_buff_1); - fstat(STDERR_FILENO, &this->prev_buff_2); -} - -void screen_force_clear_to_end() { - if (clr_eos) { - writembs(stdoutput(), clr_eos); - } -} - -screen_t::screen_t() : outp_(stdoutput()) {} - -bool screen_t::cursor_is_wrapped_to_own_line() const { - // Note == comparison against the line count is correct: we do not create a line just for the - // cursor. If there is a line containing the cursor, then it means that line has contents and we - // should return false. - // Don't consider dumb terminals to have wrapping for the purposes of this function. - return actual.cursor.x == 0 && static_cast(actual.cursor.y) == actual.line_count() && - !is_dumb(); -} diff --git a/src/screen.h b/src/screen.h index 66179778a..2ff2863c6 100644 --- a/src/screen.h +++ b/src/screen.h @@ -1,343 +1,24 @@ -// High level library for handling the terminal screen -// -// The screen library allows the interactive reader to write its output to screen efficiently by -// keeping an internal representation of the current screen contents and trying to find a reasonably -// efficient way for transforming that to the desired screen content. -// -// The current implementation is less smart than ncurses allows and can not for example move blocks -// of text around to handle text insertion. #ifndef FISH_SCREEN_H #define FISH_SCREEN_H #include "config.h" // IWYU pragma: keep -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "common.h" -#include "env.h" -#include "highlight.h" -#include "maybe.h" -#include "wcstringutil.h" - class pager_t; class page_rendering_t; -/// A class representing a single line of a screen. -struct line_t { - struct highlighted_char_t { - highlight_spec_t highlight; - wchar_t character; - }; - - /// A pair of a character, and the color with which to draw it. - std::vector text{}; - bool is_soft_wrapped{false}; - size_t indentation{0}; - - line_t() = default; - - /// Clear the line's contents. - void clear(void) { text.clear(); } - - /// Append a single character \p txt to the line with color \p c. - void append(wchar_t c, highlight_spec_t color) { text.push_back({color, c}); } - - /// Append a nul-terminated string \p txt to the line, giving each character \p color. - void append(const wchar_t *txt, highlight_spec_t color) { - for (size_t i = 0; txt[i]; i++) { - text.push_back({color, txt[i]}); - } - } - - /// \return the number of characters. - size_t size() const { return text.size(); } - - /// \return the character at a char index. - wchar_t char_at(size_t idx) const { return text.at(idx).character; } - - /// \return the color at a char index. - highlight_spec_t color_at(size_t idx) const { return text.at(idx).highlight; } - - /// Append the contents of \p line to this line. - void append_line(const line_t &line) { - text.insert(text.end(), line.text.begin(), line.text.end()); - } - - /// \return the width of this line, counting up to no more than \p max characters. - /// This follows fish_wcswidth() semantics, except that characters whose width would be -1 are - /// treated as 0. - int wcswidth_min_0(size_t max = std::numeric_limits::max()) const; -}; - -/// A class representing screen contents. -class screen_data_t { - std::vector line_datas; - - public: - /// The width of the screen in this rendering. - /// -1 if not set, i.e. we have not rendered before. - int screen_width{-1}; - - /// Where the cursor is in (x, y) coordinates. - struct cursor_t { - int x{0}; - int y{0}; - cursor_t() = default; - cursor_t(int a, int b) : x(a), y(b) {} - }; - cursor_t cursor; - - line_t &add_line(void) { - line_datas.resize(line_datas.size() + 1); - return line_datas.back(); - } - - void resize(size_t size) { line_datas.resize(size); } - - line_t &create_line(size_t idx) { - if (idx >= line_datas.size()) { - line_datas.resize(idx + 1); - } - return line_datas.at(idx); - } - - line_t &insert_line_at_index(size_t idx) { - assert(idx <= line_datas.size()); - return *line_datas.insert(line_datas.begin() + idx, line_t()); - } - - line_t &line(size_t idx) { return line_datas.at(idx); } - - const line_t &line(size_t idx) const { return line_datas.at(idx); } - - size_t line_count() const { return line_datas.size(); } - - void append_lines(const screen_data_t &d) { - this->line_datas.insert(this->line_datas.end(), d.line_datas.begin(), d.line_datas.end()); - } - - bool empty() const { return line_datas.empty(); } -}; - -struct outputter_t; - -/// The class representing the current and desired screen contents. -class screen_t { - public: - screen_t(); - - /// This is the main function for the screen output library. It is used to define the desired - /// contents of the screen. The screen command will use its knowledge of the current contents of - /// the screen in order to render the desired output using as few terminal commands as possible. - /// - /// \param left_prompt the prompt to prepend to the command line - /// \param right_prompt the right prompt, or NULL if none - /// \param commandline the command line - /// \param explicit_len the number of characters of the "explicit" (non-autosuggestion) portion - /// of the command line \param colors the colors to use for the commanad line \param indent the - /// indent to use for the command line \param cursor_pos where the cursor is \param pager the - /// pager to render below the command line \param page_rendering to cache the current pager view - /// \param cursor_is_within_pager whether the position is within the pager line (first line) - void write(const wcstring &left_prompt, const wcstring &right_prompt, - const wcstring &commandline, size_t explicit_len, - const std::vector &colors, const std::vector &indent, - size_t cursor_pos, - // todo!("this should be environment_t") - const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering, - bool cursor_is_within_pager); - - /// Resets the screen buffer's internal knowledge about the contents of the screen, - /// optionally repainting the prompt as well. - /// This function assumes that the current line is still valid. - void reset_line(bool repaint_prompt = false); - - /// 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, - /// The screen width must be provided for the PROMPT_SP hack. - void reset_abandoning_line(int screen_width); - - /// Stat stdout and stderr and save result as the current timestamp. - /// This is used to avoid reacting to changes that we ourselves made to the screen. - void save_status(); - - /// \return whether we believe the cursor is wrapped onto the last line, and that line is - /// otherwise empty. This includes both soft and hard wrapping. - bool cursor_is_wrapped_to_own_line() const; - - /// Whether the last-drawn autosuggestion (if any) is truncated, or hidden entirely. - bool autosuggestion_is_truncated{false}; - - private: - /// Appends a character to the end of the line that the output cursor is on. This function - /// automatically handles linebreaks and lines longer than the screen width. - void desired_append_char(wchar_t b, highlight_spec_t c, int indent, size_t prompt_width, - size_t bwidth); - - /// Stat stdout and stderr and compare result to previous result in reader_save_status. Repaint - /// if modification time has changed. - void check_status(); - - /// Write the bytes needed to move screen cursor to the specified position to the specified - /// buffer. The actual_cursor field of the specified screen_t will be updated. - /// - /// \param new_x the new x position - /// \param new_y the new y position - void move(int new_x, int new_y); - - /// Convert a wide character to a multibyte string and append it to the buffer. - void write_char(wchar_t c, size_t width); - - /// Send the specified string through tputs and append the output to the screen's outputter. - void write_mbs(const char *s); - - /// Convert a wide string to a multibyte string and append it to the buffer. - void write_str(const wchar_t *s); - void write_str(const wcstring &s); - - /// Update the cursor as if soft wrapping had been performed. - bool handle_soft_wrap(int x, int y); - - /// Receiver for our output. - outputter_t &outp_; - - /// The internal representation of the desired screen contents. - screen_data_t desired{}; - /// The internal representation of the actual screen contents. - screen_data_t actual{}; - /// A string containing the prompt which was last printed to the screen. - wcstring actual_left_prompt{}; - /// Last right prompt width. - size_t last_right_prompt_width{0}; - /// If we support soft wrapping, we can output to this location without any cursor motion. - maybe_t soft_wrap_location{}; - /// This flag is set to true when there is reason to suspect that the parts of the screen lines - /// where the actual content is not filled in may be non-empty. This means that a clr_eol - /// command has to be sent to the terminal at the end of each line, including - /// actual_lines_before_reset. - bool need_clear_lines{false}; - /// Whether there may be yet more content after the lines, and we issue a clr_eos if possible. - bool need_clear_screen{false}; - /// If we need to clear, this is how many lines the actual screen had, before we reset it. This - /// is used when resizing the window larger: if the cursor jumps to the line above, we need to - /// remember to clear the subsequent lines. - size_t actual_lines_before_reset{0}; - /// These status buffers are used to check if any output has occurred other than from fish's - /// main loop, in which case we need to redraw. - struct stat prev_buff_1 {}; - struct stat prev_buff_2 {}; - - /// \return the outputter for this screen. - outputter_t &outp() { return outp_; } - - /// Update the screen to match the desired output. - void update(const wcstring &left_prompt, const wcstring &right_prompt, - // todo!("this should be environment_t") - const env_stack_t &vars); -}; - -/// Issues an immediate clr_eos. -void screen_force_clear_to_end(); - -void screen_clear_layout_cache_ffi(); - -// Information about the layout of a prompt. -struct prompt_layout_t { - std::vector line_breaks; // line breaks when rendering the prompt - size_t max_line_width; // width of the longest line - size_t last_line_width; // width of the last line -}; - -// Maintain a mapping of escape sequences to their widths for fast lookup. -class layout_cache_t : noncopyable_t { - private: - // Cached escape sequences we've already detected in the prompt and similar strings, ordered - // lexicographically. - std::vector esc_cache_; - - // LRU-list of prompts and their layouts. - // Use a list so we can promote to the front on a cache hit. - struct prompt_cache_entry_t { - wcstring text; // Original prompt string. - size_t max_line_width; // Max line width when computing layout (for truncation). - wcstring trunc_text; // Resulting truncated prompt string. - prompt_layout_t layout; // Resulting layout. - }; - std::list prompt_cache_; - - public: - static constexpr size_t prompt_cache_max_size = 12; - - /// \return the size of the escape code cache. - size_t esc_cache_size() const { return esc_cache_.size(); } - - /// Insert the entry \p str in its sorted position, if it is not already present in the cache. - void add_escape_code(wcstring str) { - auto where = std::upper_bound(esc_cache_.begin(), esc_cache_.end(), str); - if (where == esc_cache_.begin() || where[-1] != str) { - esc_cache_.emplace(where, std::move(str)); - } - } - - /// \return the length of an escape code, accessing and perhaps populating the cache. - size_t escape_code_length(const wchar_t *code); - - /// \return the length of a string that matches a prefix of \p entry. - size_t find_escape_code(const wchar_t *entry) const { - // Do a binary search and see if the escape code right before our entry is a prefix of our - // entry. Note this assumes that escape codes are prefix-free: no escape code is a prefix of - // another one. This seems like a safe assumption. - auto where = std::upper_bound(esc_cache_.begin(), esc_cache_.end(), entry); - // 'where' is now the first element that is greater than entry. Thus where-1 is the last - // element that is less than or equal to entry. - if (where != esc_cache_.begin()) { - const wcstring &candidate = where[-1]; - if (string_prefixes_string(candidate.c_str(), entry)) return candidate.size(); - } - return 0; - } - - /// Computes a prompt layout for \p prompt_str, perhaps truncating it to \p max_line_width. - /// \return the layout, and optionally the truncated prompt itself, by reference. - prompt_layout_t calc_prompt_layout(const wcstring &prompt_str, - wcstring *out_trunc_prompt = nullptr, - size_t max_line_width = std::numeric_limits::max()); - - void clear() { - esc_cache_.clear(); - prompt_cache_.clear(); - } - - // Singleton that is exposed so that the cache can be invalidated when terminal related - // variables change by calling `cached_esc_sequences.clear()`. - static layout_cache_t shared; - - layout_cache_t() = default; - - private: - // Add a cache entry. - void add_prompt_layout(prompt_cache_entry_t entry); - - // Finds the layout for a prompt, promoting it to the front. Returns nullptr if not found. - // Note this points into our cache; do not modify the cache while the pointer lives. - const prompt_cache_entry_t *find_prompt_layout( - const wcstring &input, size_t max_line_width = std::numeric_limits::max()); - - friend void test_layout_cache(); -}; - -maybe_t escape_code_length(const wchar_t *code); -// Always return a value, by moving checking of sequence start to the caller. -long escape_code_length_ffi(const wchar_t *code); - -wcstring screen_clear(); -void screen_set_midnight_commander_hack(); +#if INCLUDE_RUST_HEADERS +#include "screen.rs.h" +#else +struct Line; +struct ScreenData; +struct Screen; +struct PromptLayout; +struct LayoutCache; +#endif + +using line_t = Line; +using screen_data_t = ScreenData; +using screen_t = Screen; +using prompt_layout_t = PromptLayout; +using layout_cache_t = LayoutCache; + #endif