Port screen.cpp

This commit is contained in:
Johannes Altmanninger 2023-11-25 19:40:31 +01:00
parent 5f1499cd67
commit 31ad182aa5
24 changed files with 2462 additions and 1932 deletions

View file

@ -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

View file

@ -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",

View file

@ -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<usize> {
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.

View file

@ -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<Mutex<libc::termios>> = 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.

View file

@ -113,18 +113,37 @@ pub struct Term {
pub exit_underline_mode: Option<CString>,
pub enter_reverse_mode: Option<CString>,
pub enter_standout_mode: Option<CString>,
pub exit_standout_mode: Option<CString>,
pub enter_blink_mode: Option<CString>,
pub enter_protected_mode: Option<CString>,
pub enter_shadow_mode: Option<CString>,
pub exit_shadow_mode: Option<CString>,
pub enter_secure_mode: Option<CString>,
pub enter_alt_charset_mode: Option<CString>,
pub exit_alt_charset_mode: Option<CString>,
pub set_a_foreground: Option<CString>,
pub set_foreground: Option<CString>,
pub set_a_background: Option<CString>,
pub set_background: Option<CString>,
pub exit_attribute_mode: Option<CString>,
pub set_title: Option<CString>,
pub clear_screen: Option<CString>,
pub cursor_up: Option<CString>,
pub cursor_down: Option<CString>,
pub cursor_left: Option<CString>,
pub cursor_right: Option<CString>,
pub parm_left_cursor: Option<CString>,
pub parm_right_cursor: Option<CString>,
pub clr_eol: Option<CString>,
pub clr_eos: Option<CString>,
// Number capabilities
pub max_colors: Option<usize>,
pub init_tabs: Option<usize>,
// 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<CString> {
// Take the lock because tparm races with del_curterm, etc.
let _term: std::sync::MutexGuard<Option<Arc<Term>>> = 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<CString> {
// Take the lock because tparm races with del_curterm, etc.

View file

@ -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);
}

View file

@ -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::*;

25
fish-rust/src/future.rs Normal file
View file

@ -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<T> IsSomeAnd for Option<T> {
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,
}
}
}

View file

@ -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<HighlightSpecListFFI>;
fn highlight_shell_ffi(
bff: &CxxWString,
ctx: &OperationContext<'_>,
@ -1732,6 +1733,7 @@ mod highlight_ffi {
colors: &HighlightSpecListFFI,
vars: &EnvStackRefFFI,
) -> Vec<u8>;
fn push(&mut self, highlight: &HighlightSpec);
}
}
@ -1743,8 +1745,17 @@ fn colorize_ffi(
colorize(text.as_wstr(), &colors.0, &*vars.0)
}
struct HighlightSpecListFFI(Vec<HighlightSpec>);
#[derive(Default)]
pub struct HighlightSpecListFFI(pub Vec<HighlightSpec>);
unsafe impl cxx::ExternType for HighlightSpecListFFI {
type Id = cxx::type_id!("HighlightSpecListFFI");
type Kind = cxx::kind::Opaque;
}
fn new_highlight_spec_list() -> Box<HighlightSpecListFFI> {
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,

View file

@ -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;

View file

@ -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<CString>) -> bool {
pub fn tputs_if_some(&mut self, str: &Option<impl AsRef<CStr>>) -> bool {
if let Some(str) = str {
self.tputs(str);
self.tputs(str.as_ref());
true
} else {
false

2019
fish-rust/src/screen.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ mod history;
mod parser;
#[cfg(test)]
mod redirection;
mod screen;
mod string_escape;
#[cfg(test)]
mod tokenizer;

View file

@ -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());
});

View file

@ -58,6 +58,8 @@
struct termios shell_modes;
struct termios *shell_modes_ffi() { return &shell_modes; }
const wcstring g_empty_string{};
const std::vector<wcstring> g_empty_string_list{};

View file

@ -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

View file

@ -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();

View file

@ -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<int> {
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<size_t>::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<wcstring> 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},

View file

@ -28,6 +28,7 @@ struct highlight_spec_t;
#else
struct HighlightSpec;
enum class HighlightRole : uint8_t;
struct HighlightSpecListFFI;
#endif
using highlight_role_t = HighlightRole;

View file

@ -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<Line> 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> 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<const termsize_t *>(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()) {}

View file

@ -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<ScreenData> 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<wcstring> comp{};
/// The description.
wcstring desc{};
#if INCLUDE_RUST_HEADERS
/// The representative completion.
rust::Box<completion_t> representative = new_completion();
#endif
/// The per-character highlighting, used when this is a full shell command.
std::vector<highlight_spec_t> 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<Line> 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

View file

@ -740,7 +740,7 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
std::chrono::time_point<std::chrono::steady_clock> last_flash;
/// The representation of the current screen contents.
screen_t screen;
rust::Box<Screen> 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> {
reader_data_t(rust::Box<ParserRef> 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<int> 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<char_event_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<char_event_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<wcstring> 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<wcstring> 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));
}

File diff suppressed because it is too large Load diff

View file

@ -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 <stddef.h>
#include <sys/stat.h>
#include <algorithm>
#include <cstddef>
#include <limits>
#include <list>
#include <string>
#include <utility>
#include <vector>
#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<highlighted_char_t> 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<size_t>::max()) const;
};
/// A class representing screen contents.
class screen_data_t {
std::vector<line_t> 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<highlight_spec_t> &colors, const std::vector<int> &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<screen_data_t::cursor_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<size_t> 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<wcstring> 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_entry_t> 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<size_t>::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<size_t>::max());
friend void test_layout_cache();
};
maybe_t<size_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