mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-03 23:11:14 -03:00
Port screen.cpp
This commit is contained in:
@@ -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{};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -28,6 +28,7 @@ struct highlight_spec_t;
|
||||
#else
|
||||
struct HighlightSpec;
|
||||
enum class HighlightRole : uint8_t;
|
||||
struct HighlightSpecListFFI;
|
||||
#endif
|
||||
|
||||
using highlight_role_t = HighlightRole;
|
||||
|
||||
@@ -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()) {}
|
||||
|
||||
20
src/pager.h
20
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<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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
1368
src/screen.cpp
1368
src/screen.cpp
File diff suppressed because it is too large
Load Diff
351
src/screen.h
351
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 <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
|
||||
|
||||
Reference in New Issue
Block a user