From 5e6f4274a319e5f8754eb12edb0233da27edbec9 Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Wed, 14 Feb 2024 22:51:47 +0100 Subject: [PATCH 1/5] [crispy] flags: add reduce(init, lambda) Signed-off-by: Christian Parpart --- src/crispy/flags.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/crispy/flags.h b/src/crispy/flags.h index bbe294f79d..6c81359476 100644 --- a/src/crispy/flags.h +++ b/src/crispy/flags.h @@ -126,6 +126,15 @@ class flags return flags::from_value(_value | static_cast(other)); } + [[nodiscard]] auto reduce(auto init, auto f) const + { + auto result = std::move(init); + for (auto i = 0u; i < sizeof(flag_type) * 8; ++i) + if (auto const flag = static_cast(1 << i); test(flag)) + result = f(std::move(result), flag); + return result; + } + private: value_type _value = 0; }; From d8b1dccaee87052fc42baea4db386b38d1d3c32b Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Mon, 5 Feb 2024 21:05:31 +0100 Subject: [PATCH 2/5] [vtbackend] Tiny code cleanup around reply() function Signed-off-by: Christian Parpart --- src/vtbackend/Screen.cpp | 112 ++++++++++++++++++++------------------- src/vtbackend/Screen.h | 12 +++++ 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/src/vtbackend/Screen.cpp b/src/vtbackend/Screen.cpp index 9e9b6c688d..bd5c831524 100644 --- a/src/vtbackend/Screen.cpp +++ b/src/vtbackend/Screen.cpp @@ -891,14 +891,14 @@ template CRISPY_REQUIRES(CellConcept) void Screen::deviceStatusReport() { - _terminal->reply("\033[0n"); + reply("\033[0n"); } template CRISPY_REQUIRES(CellConcept) void Screen::reportCursorPosition() { - _terminal->reply("\033[{};{}R", logicalCursorPosition().line + 1, logicalCursorPosition().column + 1); + reply("\033[{};{}R", logicalCursorPosition().line + 1, logicalCursorPosition().column + 1); } template @@ -910,7 +910,7 @@ void Screen::reportColorPaletteUpdate() auto const modeHint = isLightColor(_state->colorPalette.defaultForeground) ? DarkModeHint : LightModeHint; - _terminal->reply("\033[?{};{}n", ColorPaletteUpdateDsrReplyId, modeHint); + reply("\033[?{};{}n", ColorPaletteUpdateDsrReplyId, modeHint); _terminal->flushInput(); } @@ -919,8 +919,7 @@ CRISPY_REQUIRES(CellConcept) void Screen::reportExtendedCursorPosition() { auto const pageNum = 1; - _terminal->reply( - "\033[{};{};{}R", logicalCursorPosition().line + 1, logicalCursorPosition().column + 1, pageNum); + reply("\033[{};{};{}R", logicalCursorPosition().line + 1, logicalCursorPosition().column + 1, pageNum); } template @@ -964,7 +963,7 @@ void Screen::sendDeviceAttributes() // TODO: DeviceAttributes::TechnicalCharacters | DeviceAttributes::UserDefinedKeys); - _terminal->reply("\033[?{};{}c", id, attrs); + reply("\033[?{};{}c", id, attrs); } template @@ -985,7 +984,7 @@ void Screen::sendTerminalId() // ROM cardridge registration number (always 0) auto constexpr Pc = 0; - _terminal->reply("\033[>{};{};{}c", pp, Pv, Pc); + reply("\033[>{};{};{}c", pp, Pv, Pc); } // {{{ ED @@ -1626,15 +1625,15 @@ void Screen::captureBuffer(LineCount lineCount, bool logicalLines) if (data.empty()) return; if (currentChunkSize == 0) // initiate chunk - _terminal->reply("\033^{};", CaptureBufferCode); + reply("\033^{};", CaptureBufferCode); else if (currentChunkSize + data.size() >= MaxChunkSize) { vtCaptureBufferLog()("Transferred chunk of {} bytes.", currentChunkSize); - _terminal->reply("\033\\"); // ST - _terminal->reply("\033^{};", CaptureBufferCode); + reply("\033\\"); // ST + reply("\033^{};", CaptureBufferCode); currentChunkSize = 0; } - _terminal->reply(data); + reply(data); currentChunkSize += data.size(); }; LineOffset const bottomLine = boxed_cast(pageSize().lines - 1); @@ -1666,10 +1665,10 @@ void Screen::captureBuffer(LineCount lineCount, bool logicalLines) } if (currentChunkSize != 0) - _terminal->reply("\033\\"); // ST + reply("\033\\"); // ST vtCaptureBufferLog()("Capturing buffer finished."); - _terminal->reply("\033^{};\033\\", CaptureBufferCode); // mark the end + reply("\033^{};\033\\", CaptureBufferCode); // mark the end } template @@ -1835,7 +1834,7 @@ void Screen::requestAnsiMode(unsigned int mode) auto const code = toAnsiModeNum(static_cast(mode)); - _terminal->reply("\033[{};{}$y", code, static_cast(modeResponse)); + reply("\033[{};{}$y", code, static_cast(modeResponse)); } template @@ -1849,7 +1848,7 @@ void Screen::requestDECMode(unsigned int mode) auto const code = toDECModeNum(static_cast(mode)); - _terminal->reply("\033[?{};{}$y", code, static_cast(modeResponse)); + reply("\033[?{};{}$y", code, static_cast(modeResponse)); } template @@ -2043,8 +2042,7 @@ void Screen::requestDynamicColor(DynamicColorName name) if (color.has_value()) { - _terminal->reply( - "\033]{};{}\033\\", setDynamicColorCommand(name), setDynamicColorValue(color.value())); + reply("\033]{};{}\033\\", setDynamicColorCommand(name), setDynamicColorValue(color.value())); } } @@ -2057,12 +2055,12 @@ void Screen::requestPixelSize(RequestPixelSize area) case RequestPixelSize::WindowArea: [[fallthrough]]; // TODO case RequestPixelSize::TextArea: { // Result is CSI 4 ; height ; width t - _terminal->reply("\033[4;{};{}t", pixelSize().height, pixelSize().width); + reply("\033[4;{};{}t", pixelSize().height, pixelSize().width); break; } case RequestPixelSize::CellArea: // Result is CSI 6 ; height ; width t - _terminal->reply("\033[6;{};{}t", _state->cellPixelSize.height, _state->cellPixelSize.width); + reply("\033[6;{};{}t", _state->cellPixelSize.height, _state->cellPixelSize.width); break; } } @@ -2073,11 +2071,9 @@ void Screen::requestCharacterSize(RequestPixelSize area) { switch (area) { - case RequestPixelSize::TextArea: - _terminal->reply("\033[8;{};{}t", pageSize().lines, pageSize().columns); - break; + case RequestPixelSize::TextArea: reply("\033[8;{};{}t", pageSize().lines, pageSize().columns); break; case RequestPixelSize::WindowArea: - _terminal->reply("\033[9;{};{}t", pageSize().lines, pageSize().columns); + reply("\033[9;{};{}t", pageSize().lines, pageSize().columns); break; case RequestPixelSize::CellArea: Guarantee(false @@ -2173,7 +2169,7 @@ void Screen::requestStatusString(RequestStatusString value) return nullopt; }(value); - _terminal->reply("\033P{}$r{}\033\\", response.has_value() ? 1 : 0, response.value_or(""), "\"p"); + reply("\033P{}$r{}\033\\", response.has_value() ? 1 : 0, response.value_or(""), "\"p"); } template @@ -2200,7 +2196,7 @@ void Screen::requestTabStops() } dcs << "\033\\"sv; // ST - _terminal->reply(dcs.str()); + reply(dcs.str()); } namespace @@ -2219,18 +2215,18 @@ CRISPY_REQUIRES(CellConcept) void Screen::requestCapability(std::string_view name) { if (booleanCapability(name)) - _terminal->reply("\033P1+r{}\033\\", toHexString(name)); + reply("\033P1+r{}\033\\", toHexString(name)); else if (auto const value = numericCapability(name); value != Database::Npos) { auto hexValue = fmt::format("{:X}", value); if (hexValue.size() % 2) hexValue.insert(hexValue.begin(), '0'); - _terminal->reply("\033P1+r{}={}\033\\", toHexString(name), hexValue); + reply("\033P1+r{}={}\033\\", toHexString(name), hexValue); } else if (auto const value = stringCapability(name); !value.empty()) - _terminal->reply("\033P1+r{}={}\033\\", toHexString(name), asHex(value)); + reply("\033P1+r{}={}\033\\", toHexString(name), asHex(value)); else - _terminal->reply("\033P0+r\033\\"); + reply("\033P0+r\033\\"); } template @@ -2238,18 +2234,18 @@ CRISPY_REQUIRES(CellConcept) void Screen::requestCapability(capabilities::Code code) { if (booleanCapability(code)) - _terminal->reply("\033P1+r{}\033\\", code.hex()); + reply("\033P1+r{}\033\\", code.hex()); else if (auto const value = numericCapability(code); value >= 0) { auto hexValue = fmt::format("{:X}", value); if (hexValue.size() % 2) hexValue.insert(hexValue.begin(), '0'); - _terminal->reply("\033P1+r{}={}\033\\", code.hex(), hexValue); + reply("\033P1+r{}={}\033\\", code.hex(), hexValue); } else if (auto const value = stringCapability(code); !value.empty()) - _terminal->reply("\033P1+r{}={}\033\\", code.hex(), asHex(value)); + reply("\033P1+r{}={}\033\\", code.hex(), asHex(value)); else - _terminal->reply("\033P0+r\033\\"); + reply("\033P0+r\033\\"); } template @@ -2386,32 +2382,31 @@ void Screen::smGraphics(XtSmGraphics::Item item, XtSmGraphics::Action acti { case Action::Read: { auto const value = _state->imageColorPalette->size(); - _terminal->reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); break; } case Action::ReadLimit: { auto const value = _state->imageColorPalette->maxSize(); - _terminal->reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); break; } case Action::ResetToDefault: { auto const value = _state->maxImageColorRegisters; _state->imageColorPalette->setSize(value); - _terminal->reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, value); break; } case Action::SetToValue: visit(overloaded { [&](int number) { _state->imageColorPalette->setSize(static_cast(number)); - _terminal->reply( - "\033[?{};{};{}S", NumberOfColorRegistersItem, Success, number); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Success, number); }, [&](ImageSize) { - _terminal->reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Failure, 0); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Failure, 0); }, [&](monostate) { - _terminal->reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Failure, 0); + reply("\033[?{};{};{}S", NumberOfColorRegistersItem, Failure, 0); }, }, value); @@ -2424,19 +2419,19 @@ void Screen::smGraphics(XtSmGraphics::Item item, XtSmGraphics::Action acti { case Action::Read: { auto const viewportSize = pixelSize(); - _terminal->reply("\033[?{};{};{};{}S", - SixelItem, - Success, - min(viewportSize.width, _state->effectiveImageCanvasSize.width), - min(viewportSize.height, _state->effectiveImageCanvasSize.height)); + reply("\033[?{};{};{};{}S", + SixelItem, + Success, + min(viewportSize.width, _state->effectiveImageCanvasSize.width), + min(viewportSize.height, _state->effectiveImageCanvasSize.height)); } break; case Action::ReadLimit: - _terminal->reply("\033[?{};{};{};{}S", - SixelItem, - Success, - _settings->maxImageSize.width, - _settings->maxImageSize.height); + reply("\033[?{};{};{};{}S", + SixelItem, + Success, + _settings->maxImageSize.width, + _settings->maxImageSize.height); break; case Action::ResetToDefault: // The limit is the default at the same time. @@ -2449,10 +2444,10 @@ void Screen::smGraphics(XtSmGraphics::Item item, XtSmGraphics::Action acti size.width = min(size.width, _settings->maxImageSize.width); size.height = min(size.height, _settings->maxImageSize.height); _state->effectiveImageCanvasSize = size; - _terminal->reply("\033[?{};{};{};{}S", SixelItem, Success, size.width, size.height); + reply("\033[?{};{};{};{}S", SixelItem, Success, size.width, size.height); } else - _terminal->reply("\033[?{};{};{}S", SixelItem, Failure, 0); + reply("\033[?{};{};{}S", SixelItem, Failure, 0); break; } break; @@ -3397,6 +3392,13 @@ void Screen::restoreCursor(Cursor const& savedCursor) verifyState(); } +template +CRISPY_REQUIRES(CellConcept) +void Screen::reply(std::string_view text) +{ + _terminal->reply(text); +} + template CRISPY_REQUIRES(CellConcept) void Screen::processSequence(Sequence const& seq) @@ -3512,7 +3514,7 @@ ApplyResult Screen::apply(FunctionDefinition const& function, Sequence con case DA2: sendTerminalId(); break; case DA3: // terminal identification, 4 hex codes - _terminal->reply("\033P!|C0000000\033\\"); + reply("\033P!|C0000000\033\\"); break; case DCH: deleteCharacters(seq.param_or(0, ColumnCount { 1 })); break; case DECCARA: { @@ -3782,7 +3784,7 @@ ApplyResult Screen::apply(FunctionDefinition const& function, Sequence con case XTREPORTCOLORS: _terminal->reportColorPaletteStack(); return ApplyResult::Ok; case XTSMGRAPHICS: return impl::XTSMGRAPHICS(seq, *this); case XTVERSION: - _terminal->reply(fmt::format("\033P>|{} {}\033\\", LIBTERMINAL_NAME, LIBTERMINAL_VERSION_STRING)); + reply("\033P>|{} {}\033\\", LIBTERMINAL_NAME, LIBTERMINAL_VERSION_STRING); return ApplyResult::Ok; case DECSSDT: { // Changes the status line display type. @@ -3825,7 +3827,7 @@ ApplyResult Screen::apply(FunctionDefinition const& function, Sequence con return ApplyResult::Ok; } case CSIUQUERY: { - _terminal->reply("\033[?{}u", _terminal->keyboardProtocol().flags().value()); + reply("\033[?{}u", _terminal->keyboardProtocol().flags().value()); return ApplyResult::Ok; } case CSIUENHCE: { diff --git a/src/vtbackend/Screen.h b/src/vtbackend/Screen.h index 02a47d9b6c..18cc5c7908 100644 --- a/src/vtbackend/Screen.h +++ b/src/vtbackend/Screen.h @@ -600,6 +600,18 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase void restoreGraphicsRendition(); void saveGraphicsRendition(); + void reply(std::string_view text); + + template + void reply(fmt::format_string message, Ts const&... args) + { +#if defined(__APPLE__) || defined(_MSC_VER) + reply(fmt::vformat(message, fmt::make_format_args(args...))); +#else + reply(fmt::vformat(message, fmt::make_format_args(args...))); +#endif + } + private: void writeTextInternal(char32_t codepoint); From 7cfad5b94da076ac4f23978cce623283fe5cb36f Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Mon, 5 Feb 2024 22:50:04 +0100 Subject: [PATCH 3/5] [crispy] Add interpolated_string API Signed-off-by: Christian Parpart --- src/crispy/CMakeLists.txt | 2 + src/crispy/interpolated_string.cpp | 96 +++++++++++++++++++++++++ src/crispy/interpolated_string.h | 33 +++++++++ src/crispy/interpolated_string_test.cpp | 39 ++++++++++ 4 files changed, 170 insertions(+) create mode 100644 src/crispy/interpolated_string.cpp create mode 100644 src/crispy/interpolated_string.h create mode 100644 src/crispy/interpolated_string_test.cpp diff --git a/src/crispy/CMakeLists.txt b/src/crispy/CMakeLists.txt index ebf1684b83..c23d3ff6c7 100644 --- a/src/crispy/CMakeLists.txt +++ b/src/crispy/CMakeLists.txt @@ -24,6 +24,7 @@ set(crispy_SOURCES escape.h file_descriptor.h flags.h + interpolated_string.cpp interpolated_string.h logstore.cpp logstore.h overloaded.h reference.h @@ -129,6 +130,7 @@ if(CRISPY_TESTING) TrieMap_test.cpp base64_test.cpp compose_test.cpp + interpolated_string_test.cpp utils_test.cpp result_test.cpp ring_test.cpp diff --git a/src/crispy/interpolated_string.cpp b/src/crispy/interpolated_string.cpp new file mode 100644 index 0000000000..7e5d982deb --- /dev/null +++ b/src/crispy/interpolated_string.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +namespace crispy +{ + +namespace +{ + void parse_attribute(string_interpolation* interpolation, std::string_view attribute) + { + auto const equal = attribute.find('='); + if (equal != std::string_view::npos) + { + auto const key = attribute.substr(0, equal); + auto const value = attribute.substr(equal + 1); + interpolation->attributes[key] = value; + } + else + { + interpolation->flags.insert(attribute); + } + } +} // anonymous namespace + +string_interpolation parse_interpolation(std::string_view text) +{ + auto result = string_interpolation {}; + auto const colon = text.find(':'); + if (colon != std::string_view::npos) + { + result.name = text.substr(0, colon); + auto const attributes = text.substr(colon + 1); + size_t pos = 0; + while (pos < attributes.size()) + { + auto const comma = attributes.find(',', pos); + if (comma == std::string_view::npos) + { + parse_attribute(&result, attributes.substr(pos)); + break; + } + else + { + parse_attribute(&result, attributes.substr(pos, comma - pos)); + pos = comma + 1; + } + } + } + else + { + result.name = text; + } + return result; +} + +interpolated_string parse_interpolated_string(std::string_view text) +{ + // "< {Clock:Bold,Italic,Color=#FFFF00} | {VTType} | {InputMode} {Search:Bold,Color=Yellow} >" + + auto fragments = interpolated_string {}; + + size_t pos = 0; + while (pos < text.size()) + { + auto const openBrace = text.find('{', pos); + if (openBrace == std::string_view::npos) + { + // no more open braces found, so we're done. + fragments.emplace_back(text.substr(pos)); + return fragments; + } + + if (auto const textFragment = text.substr(pos, openBrace - pos); !textFragment.empty()) + // add text fragment before the open brace + fragments.emplace_back(textFragment); + + auto const closeBrace = text.find('}', openBrace); + if (closeBrace == std::string_view::npos) + { + // no matching close brace found, so we're done. + fragments.emplace_back(parse_interpolation(text.substr(openBrace))); + return fragments; + } + else + { + // add interpolation fragment + auto const fragment = text.substr(openBrace + 1, closeBrace - openBrace - 1); + fragments.emplace_back(parse_interpolation(fragment)); + pos = closeBrace + 1; + } + } + + return fragments; +} + +} // namespace crispy diff --git a/src/crispy/interpolated_string.h b/src/crispy/interpolated_string.h new file mode 100644 index 0000000000..171950aee9 --- /dev/null +++ b/src/crispy/interpolated_string.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include + +namespace crispy +{ + +struct string_interpolation +{ + std::string_view name; + std::set flags; + std::map attributes; + + bool operator==(string_interpolation const& rhs) const noexcept + { + return name == rhs.name && flags == rhs.flags && attributes == rhs.attributes; + } + + bool operator!=(string_interpolation const& rhs) const noexcept { return !(*this == rhs); } +}; + +using interpolated_string_fragment = std::variant; +using interpolated_string = std::vector; + +string_interpolation parse_interpolation(std::string_view text); +interpolated_string parse_interpolated_string(std::string_view text); + +} // namespace crispy diff --git a/src/crispy/interpolated_string_test.cpp b/src/crispy/interpolated_string_test.cpp new file mode 100644 index 0000000000..47649d1eb3 --- /dev/null +++ b/src/crispy/interpolated_string_test.cpp @@ -0,0 +1,39 @@ +#include + +#include + +#include + +TEST_CASE("interpolated_string.parse_interpolation") +{ + using crispy::parse_interpolation; + + auto const interpolation = parse_interpolation("Clock:Bold,Italic,Color=#FFFF00"); + CHECK(interpolation.name == "Clock"); + CHECK(interpolation.flags.size() == 2); + CHECK(interpolation.flags.count("Bold")); + CHECK(interpolation.flags.count("Italic") == 1); + CHECK(interpolation.attributes.size() == 1); + CHECK(interpolation.attributes.count("Color")); + CHECK(interpolation.attributes.at("Color") == "#FFFF00"); +} + +TEST_CASE("interpolated_string.parse_interpolated_string") +{ + using crispy::parse_interpolated_string; + + auto const interpolated = parse_interpolated_string( + "< {Clock:Bold,Italic,Color=#FFFF00} | {VTType}"); + + CHECK(interpolated.size() == 4); + + REQUIRE(std::holds_alternative(interpolated[0])); + REQUIRE(std::get(interpolated[0]) == "< "); + + REQUIRE(std::holds_alternative(interpolated[1])); + + REQUIRE(std::holds_alternative(interpolated[2])); + REQUIRE(std::get(interpolated[2]) == " | "); + + REQUIRE(std::holds_alternative(interpolated[3])); +} From 02bba831a7278f46fedd1734a1300520a9253183 Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Mon, 5 Feb 2024 22:50:51 +0100 Subject: [PATCH 4/5] [vtbackend] Color: default-initialize foreground/background with cell foreground/background for CellRGBColor[AndAlpha]Pair Signed-off-by: Christian Parpart --- src/vtbackend/Color.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vtbackend/Color.h b/src/vtbackend/Color.h index 1d4c6f5ae2..e3f00aac8b 100644 --- a/src/vtbackend/Color.h +++ b/src/vtbackend/Color.h @@ -357,15 +357,15 @@ using CellRGBColor = std::variant Date: Sun, 18 Feb 2024 22:15:06 +0100 Subject: [PATCH 5/5] [vtbackend] Add customizable indicator statusline Signed-off-by: Christian Parpart --- docs/configuration/indicator-statusline.md | 74 ++++ metainfo.xml | 1 + mkdocs.yml | 1 + src/contour/Config.cpp | 15 + src/contour/Config.h | 14 +- src/contour/ConfigDocumentation.h | 4 + src/contour/TerminalSession.cpp | 3 + src/crispy/interpolated_string_test.cpp | 3 +- src/vtbackend/CMakeLists.txt | 2 + src/vtbackend/Color.cpp | 44 +++ src/vtbackend/Color.h | 2 + src/vtbackend/ColorPalette.h | 4 +- src/vtbackend/Screen.cpp | 65 +-- src/vtbackend/Screen.h | 11 +- src/vtbackend/Sequencer.cpp | 23 +- src/vtbackend/Sequencer.h | 6 +- src/vtbackend/Settings.h | 7 + src/vtbackend/StatusLineBuilder.cpp | 424 ++++++++++++++++++++ src/vtbackend/StatusLineBuilder.h | 87 ++++ src/vtbackend/Terminal.cpp | 436 ++++++++++++++++----- src/vtbackend/Terminal.h | 11 + 21 files changed, 1048 insertions(+), 189 deletions(-) create mode 100644 docs/configuration/indicator-statusline.md create mode 100644 src/vtbackend/StatusLineBuilder.cpp create mode 100644 src/vtbackend/StatusLineBuilder.h diff --git a/docs/configuration/indicator-statusline.md b/docs/configuration/indicator-statusline.md new file mode 100644 index 0000000000..5cfbe754e1 --- /dev/null +++ b/docs/configuration/indicator-statusline.md @@ -0,0 +1,74 @@ +# Indicator Statusline + +The indicator statusline used to be a feature, from the old DEC VT level 4 terminals. +Contour revives this feature to prominently show the terminal status. + +## Configuration + +``` +profiles: + your_profile: + status_line: + indicator: + left: "{VTType} │ {InputMode:Bold,Color=#C0C030}{SearchPrompt:Left= │ }{TraceMode:Bold,Color=#FFFF00,Left= │ }{ProtectedMode:Bold,Left= │ }" + middle: "{Title:Left= « ,Right= » ,Color=#20c0c0}" + right: "{HistoryLineCount:Faint,Color=#c0c0c0} │ {Clock:Bold} " +``` + +Each segment, `left`, `middle`, and `right` may contain text to be displayed in the +left, middle, or right segment of the indicator statusline. + +This text may contain placeholders to be replaced by their respective dynamic content. + +## Variables + +Variable | Description +---------------------|-------------------------------------------------------------------- +`{Clock}` | current clock in HH:MM format +`{Command}` | yields the result of the given command prompt, as specified via parameter `Program=...` +`{HistoryLineCount}` | number of lines in history (only available in primary screen) +`{Hyperlink}` | reveals the hyperlink at the given mouse location +`{InputMode}` | current input mode (e.g. INSERT, NORMAL, VISUAL) +`{ProtectedMode}` | indicates protected mode, if currently enabled +`{SearchMode}` | indicates search highlight mode, if currently active +`{SearchPrompt}` | search input prompt, if currently active +`{Text}` | given text (makes only sense when customized with flags) +`{Title}` | current window title +`{VTType}` | currently active VT emulation type + +## Formatting Styles + +Each Variable, as specified above, can be parametrized for customizing the look of it. +The common syntax to these variables and their parameters looks as follows: + +``` +{VariableName:SomeFlag,SomeKey=SomeValue} +``` + +So parameters can be specified after a colon (`:`) as a comma separated list of flags and key/value pairs. +A key/value pair is further split by equal sign (`=`). + +The following list of formatting styles are supported: + +Parameter | Description +--------------------------|-------------------------------------------------------------------- +`Left=TEXT` | text to show on the left side, if the variable is to be shown +`Right=TEXT` | text to show on the right side, if the variable is to be shown +`Color=#RRGGBB` | text color in hexadecimal RGB notation +`BackgroundColor=#RRGGBB` | background color in hexadecimal RGB notation +`Bold` | text in bold font face +`Italic` | text in italic font face +`Underline` | underline text (only one underline style can be active) +`CurlyUnderline` | curly underline text (only one underline style can be active) +`DoubleUnderline` | double underline text (only one underline style can be active) +`DottedUnderline` | dotted underline text (only one underline style can be active) +`DashedUnderline` | dashed underline text (only one underline style can be active) +`Blinking` | blinking text +`RapidBlinking` | rapid blinking text +`Overline` | overline text +`Inverse` | inversed text/background coloring + +These parameters apply to all variables above. + +The `Command` variable is the only one that requires a special attribute, `Program` whose value +is the command to execute. diff --git a/metainfo.xml b/metainfo.xml index 5406088e8c..461bd49bc4 100644 --- a/metainfo.xml +++ b/metainfo.xml @@ -107,6 +107,7 @@
    +
  • Add ability to customize the indicator statusline through configuration (#687)
  • Add generation of config file from internal state (#1282)
  • Add SGRSAVE and SGRRESTORE VT sequences to save and restore SGR state (They intentionally conflict with XTPUSHSGR and XTPOPSGR)
  • Fixes corruption of sixel image on high resolution (#1049)
  • diff --git a/mkdocs.yml b/mkdocs.yml index f8aea3ace8..362230b913 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - configuration/index.md - configuration/profiles.md - configuration/colors.md + - configuration/indicator-statusline.md #- Images (Advanced) : configuration/advanced/images.md #- Mouse (Advanced) : configuration/advanced/images.md #- Misc (Advanced) : configuration/advanced/mouse.md diff --git a/src/contour/Config.cpp b/src/contour/Config.cpp index ca60f78cb7..6f9031df73 100644 --- a/src/contour/Config.cpp +++ b/src/contour/Config.cpp @@ -403,6 +403,13 @@ void YAMLConfigReader::loadFromEntry(YAML::Node const& node, std::string const& loadFromEntry(child["status_line"], "position", where.statusDisplayPosition); loadFromEntry(child["status_line"], "sync_to_window_title", where.syncWindowTitleWithHostWritableStatusDisplay); loadFromEntry(child["status_line"], "display", where.initialStatusDisplayType); + + if (child["status_line"]["indicator"]) + { + loadFromEntry(child["status_line"]["indicator"], "left", where.indicatorStatusLineLeft); + loadFromEntry(child["status_line"]["indicator"], "middle", where.indicatorStatusLineMiddle); + loadFromEntry(child["status_line"]["indicator"], "right", where.indicatorStatusLineRight); + } } if (child["background"]) { @@ -1933,6 +1940,14 @@ std::string YAMLConfigWriter::createString(Config const& c) process(entry.initialStatusDisplayType); process(entry.statusDisplayPosition); process(entry.syncWindowTitleWithHostWritableStatusDisplay); + + doc.append(addOffset("indicator:\n", Offset::levels * OneOffset)); + { + const auto _ = Offset {}; + process(entry.indicatorStatusLineLeft); + process(entry.indicatorStatusLineMiddle); + process(entry.indicatorStatusLineRight); + } } doc.append(addOffset("\n" diff --git a/src/contour/Config.h b/src/contour/Config.h index c77fc6925e..7f52e10749 100644 --- a/src/contour/Config.h +++ b/src/contour/Config.h @@ -319,6 +319,18 @@ struct TerminalProfile }; ConfigEntry statusDisplayPosition { vtbackend::StatusDisplayPosition::Bottom }; + ConfigEntry indicatorStatusLineLeft { + " {InputMode:Bold,Color=#FFFF00}" + "{SearchPrompt:Left= │ }" + "{TraceMode:Bold,Color=#FFFF00,Left= │ }" + "{ProtectedMode:Bold,Left= │ }" + }; + ConfigEntry indicatorStatusLineMiddle { + " {Clock:Bold} {Title:Left= « ,Right= » }" + }; + ConfigEntry indicatorStatusLineRight { + "{HistoryLineCount:Faint,Color=#c0c0c0} " + }; ConfigEntry syncWindowTitleWithHostWritableStatusDisplay { false }; ConfigEntry hideScrollbarInAltScreen { true }; @@ -351,7 +363,7 @@ struct TerminalProfile ConfigEntry highlightTimeout { 100 }; ConfigEntry highlightDoubleClickedWord { true }; ConfigEntry initialStatusDisplayType { - vtbackend::StatusDisplayType::None + vtbackend::StatusDisplayType::Indicator }; ConfigEntry backgroundOpacity { vtbackend::Opacity( 0xFF) }; diff --git a/src/contour/ConfigDocumentation.h b/src/contour/ConfigDocumentation.h index 8c85b13c23..7ba2e003aa 100644 --- a/src/contour/ConfigDocumentation.h +++ b/src/contour/ConfigDocumentation.h @@ -171,6 +171,10 @@ constexpr StringLiteral StatusDisplayPosition { "\n" }; +constexpr StringLiteral IndicatorStatusLineLeft { "left: \"{}\"\n" }; +constexpr StringLiteral IndicatorStatusLineMiddle { "middle: \"{}\"\n" }; +constexpr StringLiteral IndicatorStatusLineRight { "right: \"{}\"\n" }; + constexpr StringLiteral SyncWindowTitleWithHostWritableStatusDisplay { "{comment} Synchronize the window title with the Host Writable status_line if\n" "{comment} and only if the host writable status line was denied to be shown.\n" diff --git a/src/contour/TerminalSession.cpp b/src/contour/TerminalSession.cpp index 15ada27a35..d48e6e83b9 100644 --- a/src/contour/TerminalSession.cpp +++ b/src/contour/TerminalSession.cpp @@ -141,6 +141,9 @@ namespace settings.maxImageRegisterCount = config.maxImageColorRegisters.value(); settings.statusDisplayType = profile.initialStatusDisplayType.value(); settings.statusDisplayPosition = profile.statusDisplayPosition.value(); + settings.indicatorStatusLine.left = profile.indicatorStatusLineLeft.value(); + settings.indicatorStatusLine.middle = profile.indicatorStatusLineMiddle.value(); + settings.indicatorStatusLine.right = profile.indicatorStatusLineRight.value(); settings.syncWindowTitleWithHostWritableStatusDisplay = profile.syncWindowTitleWithHostWritableStatusDisplay.value(); if (auto const* p = preferredColorPalette(profile.colors.value(), colorPreference)) diff --git a/src/crispy/interpolated_string_test.cpp b/src/crispy/interpolated_string_test.cpp index 47649d1eb3..29b16bfa80 100644 --- a/src/crispy/interpolated_string_test.cpp +++ b/src/crispy/interpolated_string_test.cpp @@ -22,8 +22,7 @@ TEST_CASE("interpolated_string.parse_interpolated_string") { using crispy::parse_interpolated_string; - auto const interpolated = parse_interpolated_string( - "< {Clock:Bold,Italic,Color=#FFFF00} | {VTType}"); + auto const interpolated = parse_interpolated_string("< {Clock:Bold,Italic,Color=#FFFF00} | {VTType}"); CHECK(interpolated.size() == 4); diff --git a/src/vtbackend/CMakeLists.txt b/src/vtbackend/CMakeLists.txt index 6c5e0c2825..7d82498602 100644 --- a/src/vtbackend/CMakeLists.txt +++ b/src/vtbackend/CMakeLists.txt @@ -39,6 +39,7 @@ set(vtbackend_HEADERS Sequence.h Sequencer.h SixelParser.h + StatusLineBuilder.h Terminal.h VTType.h VTWriter.h @@ -69,6 +70,7 @@ set(vtbackend_SOURCES Sequence.cpp Sequencer.cpp SixelParser.cpp + StatusLineBuilder.cpp Terminal.cpp TerminalState.cpp VTType.cpp diff --git a/src/vtbackend/Color.cpp b/src/vtbackend/Color.cpp index c8a9d0f9e9..7273523ee8 100644 --- a/src/vtbackend/Color.cpp +++ b/src/vtbackend/Color.cpp @@ -2,6 +2,7 @@ #include #include +#include using namespace std; @@ -128,4 +129,47 @@ string to_string(RGBAColor c) return fmt::format("#{:02X}{:02X}{:02X}{:02X}", c.red(), c.green(), c.blue(), c.alpha()); } +optional parseColor(string_view const& value) +{ + try + { + // "rgb:RR/GG/BB" + // 0123456789a + if (value.size() == 12 && value.substr(0, 4) == "rgb:" && value[6] == '/' && value[9] == '/') + { + auto const r = crispy::to_integer<16, uint8_t>(value.substr(4, 2)); + auto const g = crispy::to_integer<16, uint8_t>(value.substr(7, 2)); + auto const b = crispy::to_integer<16, uint8_t>(value.substr(10, 2)); + return RGBColor { r.value(), g.value(), b.value() }; + } + + // "#RRGGBB" + if (value.size() == 7 && value[0] == '#') + { + auto const r = crispy::to_integer<16, uint8_t>(value.substr(1, 2)); + auto const g = crispy::to_integer<16, uint8_t>(value.substr(3, 2)); + auto const b = crispy::to_integer<16, uint8_t>(value.substr(5, 2)); + return RGBColor { r.value(), g.value(), b.value() }; + } + + // "#RGB" + if (value.size() == 4 && value[0] == '#') + { + auto const r = crispy::to_integer<16, uint8_t>(value.substr(1, 1)); + auto const g = crispy::to_integer<16, uint8_t>(value.substr(2, 1)); + auto const b = crispy::to_integer<16, uint8_t>(value.substr(3, 1)); + auto const rr = static_cast(r.value() << 4); + auto const gg = static_cast(g.value() << 4); + auto const bb = static_cast(b.value() << 4); + return RGBColor { rr, gg, bb }; + } + + return std::nullopt; + } + catch (...) + { + // that will be a formatting error in stoul() then. + return std::nullopt; + } +} } // namespace vtbackend diff --git a/src/vtbackend/Color.h b/src/vtbackend/Color.h index e3f00aac8b..8f8d6740e4 100644 --- a/src/vtbackend/Color.h +++ b/src/vtbackend/Color.h @@ -395,6 +395,8 @@ constexpr Opacity& operator--(Opacity& value) noexcept } // }}} +std::optional parseColor(std::string_view const& value); + } // namespace vtbackend // {{{ fmtlib custom formatter diff --git a/src/vtbackend/ColorPalette.h b/src/vtbackend/ColorPalette.h index 1633b5d262..c570cbb876 100644 --- a/src/vtbackend/ColorPalette.h +++ b/src/vtbackend/ColorPalette.h @@ -126,8 +126,8 @@ struct ColorPalette CellRGBColorAndAlphaPair normalModeCursorline = { 0xFFFFFF_rgb, 0.2f, 0x808080_rgb, 0.4f }; // clang-format on - RGBColorPair indicatorStatusLine = { 0x808080_rgb, 0x000000_rgb }; - RGBColorPair indicatorStatusLineInactive = { 0x808080_rgb, 0x000000_rgb }; + RGBColorPair indicatorStatusLine = { 0xFFFFFF_rgb, 0x0270c0_rgb }; + RGBColorPair indicatorStatusLineInactive = { 0xFFFFFF_rgb, 0x0270c0_rgb }; }; enum class ColorTarget diff --git a/src/vtbackend/Screen.cpp b/src/vtbackend/Screen.cpp index bd5c831524..d0f73815de 100644 --- a/src/vtbackend/Screen.cpp +++ b/src/vtbackend/Screen.cpp @@ -507,7 +507,7 @@ void Screen::writeTextEnd() return; if (vtTraceSequenceLog) - vtTraceSequenceLog()("text: \"{}\"", _pendingCharTraceLog); + vtTraceSequenceLog()("[{}] text: \"{}\"", _name, _pendingCharTraceLog); _pendingCharTraceLog.clear(); #endif @@ -1790,7 +1790,10 @@ template CRISPY_REQUIRES(CellConcept) void Screen::setGraphicsRendition(GraphicsRendition rendition) { - _terminal->setGraphicsRendition(rendition); + if (rendition == GraphicsRendition::Reset) + _cursor.graphicsRendition = {}; + else + _cursor.graphicsRendition.flags = CellUtil::makeCellFlags(rendition, _cursor.graphicsRendition.flags); } template @@ -2546,50 +2549,6 @@ namespace impl return ApplyResult::Invalid; } - optional parseColor(string_view const& value) - { - try - { - // "rgb:RR/GG/BB" - // 0123456789a - if (value.size() == 12 && value.substr(0, 4) == "rgb:" && value[6] == '/' && value[9] == '/') - { - auto const r = crispy::to_integer<16, uint8_t>(value.substr(4, 2)); - auto const g = crispy::to_integer<16, uint8_t>(value.substr(7, 2)); - auto const b = crispy::to_integer<16, uint8_t>(value.substr(10, 2)); - return RGBColor { r.value(), g.value(), b.value() }; - } - - // "#RRGGBB" - if (value.size() == 7 && value[0] == '#') - { - auto const r = crispy::to_integer<16, uint8_t>(value.substr(1, 2)); - auto const g = crispy::to_integer<16, uint8_t>(value.substr(3, 2)); - auto const b = crispy::to_integer<16, uint8_t>(value.substr(5, 2)); - return RGBColor { r.value(), g.value(), b.value() }; - } - - // "#RGB" - if (value.size() == 4 && value[0] == '#') - { - auto const r = crispy::to_integer<16, uint8_t>(value.substr(1, 1)); - auto const g = crispy::to_integer<16, uint8_t>(value.substr(2, 1)); - auto const b = crispy::to_integer<16, uint8_t>(value.substr(3, 1)); - auto const rr = static_cast(r.value() << 4); - auto const gg = static_cast(g.value() << 4); - auto const bb = static_cast(b.value() << 4); - return RGBColor { rr, gg, bb }; - } - - return std::nullopt; - } - catch (...) - { - // that will be a formatting error in stoul() then. - return std::nullopt; - } - } - Color parseColor(Sequence const& seq, size_t* pi) { // We are at parameter index `i`. @@ -2701,9 +2660,7 @@ namespace impl return Color {}; } - template - CRISPY_REQUIRES((CellConcept || std::is_same_v) ) - ApplyResult applySGR(Target& target, Sequence const& seq, size_t parameterStart, size_t parameterEnd) + ApplyResult applySGR(auto& target, Sequence const& seq, size_t parameterStart, size_t parameterEnd) { if (parameterStart == parameterEnd) { @@ -2916,7 +2873,7 @@ namespace impl auto const& value = seq.intermediateCharacters(); if (value == "?") screen.requestDynamicColor(name); - else if (auto color = parseColor(value); color.has_value()) + else if (auto color = vtbackend::parseColor(value); color.has_value()) screen.setDynamicColor(name, color.value()); else return ApplyResult::Invalid; @@ -2948,7 +2905,7 @@ namespace impl queryColor((uint8_t) index); index = -1; } - else if (auto const color = parseColor(value)) + else if (auto const color = vtbackend::parseColor(value)) { setColor((uint8_t) index, color.value()); index = -1; @@ -3408,10 +3365,10 @@ void Screen::processSequence(Sequence const& seq) { if (auto const* fd = seq.functionDefinition(_terminal->activeSequences())) { - vtTraceSequenceLog()("Processing {:<14} {}", fd->documentation.mnemonic, seq.text()); + vtTraceSequenceLog()("[{}] Processing {:<14} {}", _name, fd->documentation.mnemonic, seq.text()); } else - vtTraceSequenceLog()("Processing unknown sequence: {}", seq.text()); + vtTraceSequenceLog()("[{}] Processing unknown sequence: {}", _name, seq.text()); } #endif @@ -3750,7 +3707,7 @@ ApplyResult Screen::apply(FunctionDefinition const& function, Sequence con case SCOSC: saveCursor(); break; case SD: scrollDown(seq.param_or(0, LineCount { 1 })); break; case SETMARK: setMark(); break; - case SGR: return impl::applySGR(*_terminal, seq, 0, seq.parameterCount()); + case SGR: return impl::applySGR(*this, seq, 0, seq.parameterCount()); case SGRRESTORE: restoreGraphicsRendition(); return ApplyResult::Ok; case SGRSAVE: saveGraphicsRendition(); return ApplyResult::Ok; case SM: { diff --git a/src/vtbackend/Screen.h b/src/vtbackend/Screen.h index 18cc5c7908..dda7351d44 100644 --- a/src/vtbackend/Screen.h +++ b/src/vtbackend/Screen.h @@ -57,6 +57,7 @@ class ScreenBase: public SequenceHandler [[nodiscard]] virtual bool compareCellTextAt(CellLocation position, char32_t codepoint) const noexcept = 0; [[nodiscard]] virtual std::string cellTextAt(CellLocation position) const noexcept = 0; + [[nodiscard]] virtual CellFlags cellFlagsAt(CellLocation position) const noexcept = 0; [[nodiscard]] virtual LineFlags lineFlagsAt(LineOffset line) const noexcept = 0; virtual void enableLineFlags(LineOffset lineOffset, LineFlags flags, bool enable) noexcept = 0; [[nodiscard]] virtual bool isLineFlagEnabledAt(LineOffset line, LineFlags flags) const noexcept = 0; @@ -526,6 +527,12 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase return _grid.lineAt(position.line).inflatedBuffer().at(position.column.as()).toUtf8(); } + [[nodiscard]] CellFlags cellFlagsAt(CellLocation position) const noexcept override + { + // TODO: This is not efficient. We should have a direct access to the flags. + return _grid.lineAt(position.line).inflatedBuffer().at(position.column.as()).flags(); + } + [[nodiscard]] LineFlags lineFlagsAt(LineOffset line) const noexcept override { return _grid.lineAt(line).flags(); @@ -605,11 +612,7 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase template void reply(fmt::format_string message, Ts const&... args) { -#if defined(__APPLE__) || defined(_MSC_VER) reply(fmt::vformat(message, fmt::make_format_args(args...))); -#else - reply(fmt::vformat(message, fmt::make_format_args(args...))); -#endif } private: diff --git a/src/vtbackend/Sequencer.cpp b/src/vtbackend/Sequencer.cpp index f9e73d078b..a805d96d10 100644 --- a/src/vtbackend/Sequencer.cpp +++ b/src/vtbackend/Sequencer.cpp @@ -6,12 +6,9 @@ #include #include +#include #include -using std::get; -using std::holds_alternative; -using std::string_view; - using namespace std::string_view_literals; namespace vtbackend @@ -34,7 +31,7 @@ void Sequencer::print(char32_t codepoint) _terminal.sequenceHandler().writeText(codepoint); } -size_t Sequencer::print(string_view chars, size_t cellCount) +size_t Sequencer::print(std::string_view chars, size_t cellCount) { assert(!chars.empty()); @@ -45,7 +42,7 @@ size_t Sequencer::print(string_view chars, size_t cellCount) - _terminal.currentScreen().cursor().position.column.as(); } -void Sequencer::printEnd() +void Sequencer::printEnd() noexcept { _terminal.sequenceHandler().writeTextEnd(); } @@ -99,7 +96,7 @@ void Sequencer::dispatchCSI(char finalChar) handleSequence(); } -void Sequencer::startOSC() +void Sequencer::startOSC() noexcept { _sequence.setCategory(FunctionCategory::OSC); } @@ -145,17 +142,7 @@ void Sequencer::unhook() size_t Sequencer::maxBulkTextSequenceWidth() const noexcept { - if (!_terminal.isPrimaryScreen()) - return 0; - - if (!_terminal.primaryScreen().currentLine().isTrivialBuffer()) - return 0; - - assert(_terminal.state().mainScreenMargin.horizontal.to - >= _terminal.currentScreen().cursor().position.column); - - return unbox(_terminal.state().mainScreenMargin.horizontal.to - - _terminal.currentScreen().cursor().position.column); + return _terminal.maxBulkTextSequenceWidth(); } void Sequencer::handleSequence() diff --git a/src/vtbackend/Sequencer.h b/src/vtbackend/Sequencer.h index 2ba7f0feab..149e0f8c6d 100644 --- a/src/vtbackend/Sequencer.h +++ b/src/vtbackend/Sequencer.h @@ -13,11 +13,9 @@ #include #include -#include #include #include #include -#include namespace vtbackend { @@ -129,7 +127,7 @@ class Sequencer void error(std::string_view errorString); void print(char32_t codepoint); size_t print(std::string_view chars, size_t cellCount); - void printEnd(); + void printEnd() noexcept; void execute(char controlCode); void clear() noexcept; void collect(char ch); @@ -140,7 +138,7 @@ class Sequencer void paramSubSeparator() noexcept; void dispatchESC(char finalChar); void dispatchCSI(char finalChar); - void startOSC(); + void startOSC() noexcept; void putOSC(char ch); void dispatchOSC(); void hook(char finalChar); diff --git a/src/vtbackend/Settings.h b/src/vtbackend/Settings.h index 360c9ad1fe..08f00365b9 100644 --- a/src/vtbackend/Settings.h +++ b/src/vtbackend/Settings.h @@ -43,6 +43,13 @@ struct Settings unsigned maxImageRegisterCount = 256; StatusDisplayType statusDisplayType = StatusDisplayType::None; StatusDisplayPosition statusDisplayPosition = StatusDisplayPosition::Bottom; + struct + { + std::string left { "{VTType} │ {InputMode:Bold,Color=#C0C030}{SearchPrompt:Left= │ }" + "{TraceMode:Bold,Color=#FFFF00,Left= │ }{ProtectedMode:Bold,Left= │ }" }; + std::string middle { "{Title:Left= « ,Right= » ,Color=#20c0c0}" }; + std::string right { "{HistoryLineCount:Faint,Color=#c0c0c0} │ {Clock:Bold} " }; + } indicatorStatusLine; bool syncWindowTitleWithHostWritableStatusDisplay = true; CursorDisplay cursorDisplay = CursorDisplay::Steady; CursorShape cursorShape = CursorShape::Block; diff --git a/src/vtbackend/StatusLineBuilder.cpp b/src/vtbackend/StatusLineBuilder.cpp new file mode 100644 index 0000000000..cdfeaed3af --- /dev/null +++ b/src/vtbackend/StatusLineBuilder.cpp @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include + +using namespace std::string_view_literals; + +#if defined(_WIN32) + #define popen _popen + #define pclose _pclose +#endif + +namespace vtbackend +{ + +namespace // helper functions +{ + std::string_view modeString(ViMode mode) noexcept + { + switch (mode) + { + case ViMode::Normal: return "NORMAL"sv; + case ViMode::Insert: return "INSERT"sv; + case ViMode::Visual: return "VISUAL"sv; + case ViMode::VisualLine: return "VISUAL LINE"sv; + case ViMode::VisualBlock: return "VISUAL BLOCK"sv; + } + crispy::unreachable(); + } +} // namespace + +std::optional makeStatusLineItem( + crispy::interpolated_string_fragment const& fragment) +{ + if (std::holds_alternative(fragment)) + return StatusLineDefinitions::Text { StatusLineDefinitions::Styles {}, + std::string(std::get(fragment)) }; + + auto const& interpolation = std::get(fragment); + + auto styles = StatusLineDefinitions::Styles {}; + + auto constexpr FlagMappings = std::array { + std::pair { "Bold", CellFlag::Bold }, + std::pair { "Faint", CellFlag::Faint }, + std::pair { "Italic", CellFlag::Italic }, + std::pair { "Underline", CellFlag::Underline }, + std::pair { "Blinking", CellFlag::Blinking }, + std::pair { "Inverse", CellFlag::Inverse }, + std::pair { "CrossedOut", CellFlag::CrossedOut }, + std::pair { "DoubleUnderline", CellFlag::DoublyUnderlined }, + std::pair { "CurlyUnderline", CellFlag::CurlyUnderlined }, + std::pair { "DottedUnderline", CellFlag::DottedUnderline }, + std::pair { "DashedUnderline", CellFlag::DashedUnderline }, + std::pair { "RapidBlinking", CellFlag::RapidBlinking }, + std::pair { "Overline", CellFlag::Overline }, + }; + + for (auto&& [text, flag]: FlagMappings) + if (interpolation.flags.count(text)) + styles.flags.enable(flag); + + if (auto const i = interpolation.attributes.find("Color"); i != interpolation.attributes.end()) + { + if (auto const parsedColor = parseColor(i->second)) + styles.foregroundColor = parsedColor.value(); + } + + if (auto const i = interpolation.attributes.find("BackgroundColor"); i != interpolation.attributes.end()) + { + if (auto const parsedColor = parseColor(i->second)) + styles.backgroundColor = parsedColor.value(); + } + + if (auto const i = interpolation.attributes.find("Left"); i != interpolation.attributes.end()) + styles.textLeft = i->second; + + if (auto const i = interpolation.attributes.find("Right"); i != interpolation.attributes.end()) + styles.textRight = i->second; + + if (interpolation.name == "CellSGR") + return StatusLineDefinitions::CellSGR { styles }; + + if (interpolation.name == "CellTextUTF8") + return StatusLineDefinitions::CellTextUtf8 { styles }; + + if (interpolation.name == "CellTextUTF32") + return StatusLineDefinitions::CellTextUtf32 { styles }; + + if (interpolation.name == "Clock") + return StatusLineDefinitions::Clock { styles }; + + if (interpolation.name == "Command") + { + if (interpolation.attributes.count("Program")) + { + return StatusLineDefinitions::Command { + styles, + std::string(interpolation.attributes.at("Program")), + }; + } + else + return std::nullopt; + } + + if (interpolation.name == "HistoryLineCount") + return StatusLineDefinitions::HistoryLineCount { styles }; + + if (interpolation.name == "Hyperlink") + return StatusLineDefinitions::Hyperlink { styles }; + + if (interpolation.name == "InputMode") + return StatusLineDefinitions::InputMode { styles }; + + if (interpolation.name == "ProtectedMode") + return StatusLineDefinitions::ProtectedMode { styles }; + + if (interpolation.name == "SearchMode") + return StatusLineDefinitions::SearchMode { styles }; + + if (interpolation.name == "SearchPrompt") + return StatusLineDefinitions::SearchPrompt { styles }; + + if (interpolation.name == "Title") + return StatusLineDefinitions::Title { styles }; + + if (interpolation.name == "Text") + return StatusLineDefinitions::Text { + styles, + std::string(interpolation.attributes.at("text")), + }; + + if (interpolation.name == "VTType") + return StatusLineDefinitions::VTType { styles }; + + return std::nullopt; +} + +StatusLineSegment parseStatusLineSegment(std::string_view text) +{ + auto segment = StatusLineSegment {}; + + // Parses a string like: + // "{Clock:Bold,Italic,Color=#FFFF00} | {VTType} | {InputMode} {Search:Bold,Color=Yellow}" + + auto const interpolations = crispy::parse_interpolated_string(text); + + for (auto const& fragment: interpolations) + { + if (std::holds_alternative(fragment)) + { + segment.emplace_back(StatusLineDefinitions::Text { + StatusLineDefinitions::Styles {}, std::string(std::get(fragment)) }); + } + else if (auto const item = makeStatusLineItem(std::get(fragment))) + { + segment.emplace_back(*item); + } + } + + return segment; +} + +StatusLineDefinition parseStatusLineDefinition(std::string_view left, + std::string_view middle, + std::string_view right) +{ + return StatusLineDefinition { + .left = parseStatusLineSegment(left), + .middle = parseStatusLineSegment(middle), + .right = parseStatusLineSegment(right), + }; +} + +struct VTSerializer +{ + Terminal const& vt; + StatusLineStyling styling; + std::string result {}; + + void applyStyles(StatusLineDefinitions::Styles const& styles) // {{{ + { + if (styling == StatusLineStyling::Disabled) + return; + + if (styles.foregroundColor) + result += fmt::format("\033[38:2:{}:{}:{}m", + styles.foregroundColor->red, + styles.foregroundColor->green, + styles.foregroundColor->blue); + + if (styles.backgroundColor) + result += fmt::format("\033[48:2:{}:{}:{}m", + styles.backgroundColor->red, + styles.backgroundColor->green, + styles.backgroundColor->blue); + + result += styles.flags.reduce(std::string {}, [](std::string&& result, CellFlag flag) -> std::string { + switch (flag) + { + case CellFlag::None: return result; + case CellFlag::Bold: return std::move(result) + "\033[1m"; + case CellFlag::Italic: return std::move(result) + "\033[3m"; + case CellFlag::Underline: return std::move(result) + "\033[4m"; + case CellFlag::DottedUnderline: return std::move(result) + "\033[4:1m"; + case CellFlag::CurlyUnderlined: return std::move(result) + "\033[4:3m"; + case CellFlag::DoublyUnderlined: return std::move(result) + "\033[4:4m"; + case CellFlag::DashedUnderline: return std::move(result) + "\033[4:5m"; + case CellFlag::Blinking: return std::move(result) + "\033[5m"; + case CellFlag::RapidBlinking: return std::move(result) + "\033[6m"; + case CellFlag::Inverse: return std::move(result) + "\033[7m"; + case CellFlag::Hidden: return std::move(result) + "\033[8m"; + case CellFlag::CrossedOut: return std::move(result) + "\033[9m"; + case CellFlag::Framed: return std::move(result) + "\033[51m"; + case CellFlag::Encircled: return std::move(result) + "\033[52m"; + case CellFlag::Overline: return std::move(result) + "\033[53m"; + case CellFlag::Faint: return std::move(result) + "\033[2m"; + case CellFlag::CharacterProtected: + default: return result; + } + }); + } // }}} + + std::string operator()(StatusLineDefinitions::Item const& item) + { + std::visit( + [this](auto const& item) { + if (auto const text = visit(item); !text.empty()) + { + if constexpr (std::is_same_v) + result += text; + else + { + if (styling == StatusLineStyling::Enabled) + { + result += SGRSAVE(); + applyStyles(item); + } + result += item.textLeft; + result += text; + result += item.textRight; + if (styling == StatusLineStyling::Enabled) + result += SGRRESTORE(); + } + } + }, + item); + return result; + } + + std::string operator()(StatusLineSegment const& segment) + { + std::string result; + for (auto const& item: segment) + result += std::visit(*this, item); + return result; + } + + // {{{ + std::string visit(StatusLineDefinitions::Title const&) { return vt.windowTitle(); } + + std::string visit(StatusLineDefinitions::CellSGR const&) + { + auto const currentMousePosition = vt.currentMousePosition(); + auto const cellFlags = vt.currentScreen().cellFlagsAt(currentMousePosition); + return fmt::format("{}", cellFlags); + } + + std::string visit(StatusLineDefinitions::CellTextUtf32 const&) + { + auto const currentMousePosition = vt.currentMousePosition(); + if (!vt.contains(currentMousePosition)) + return {}; + + auto const cellText = vt.currentScreen().cellTextAt(currentMousePosition); + auto const cellText32 = unicode::convert_to(std::string_view(cellText)); + + return ranges::views::transform( + cellText32, [](char32_t ch) { return fmt::format("U+{:04X}", static_cast(ch)); }) + | ranges::views::join(" ") | ranges::to; + } + + std::string visit(StatusLineDefinitions::CellTextUtf8 const&) + { + auto const currentMousePosition = vt.currentMousePosition(); + if (!vt.contains(currentMousePosition)) + return {}; + return crispy::escape(vt.currentScreen().cellTextAt(currentMousePosition)); + } + + std::string visit(StatusLineDefinitions::Clock const&) + { + crispy::ignore_unused(this); + return fmt::format("{:%H:%M}", fmt::localtime(std::time(nullptr))); + } + + std::string visit(StatusLineDefinitions::HistoryLineCount const&) + { + if (!vt.isPrimaryScreen()) + return {}; + + if (vt.viewport().scrollOffset().value) + { + auto const pct = + double(vt.viewport().scrollOffset()) / double(vt.primaryScreen().historyLineCount()); + return fmt::format("{}/{} {:3}%", + vt.viewport().scrollOffset(), + vt.primaryScreen().historyLineCount(), + int(pct * 100)); + } + else + return fmt::format("{}", vt.primaryScreen().historyLineCount()); + } + + std::string visit(StatusLineDefinitions::Hyperlink const&) + { + if (auto const hyperlink = vt.currentScreen().hyperlinkAt(vt.currentMousePosition())) + return fmt::format("{}", hyperlink->uri); + + return {}; + } + + std::string visit(StatusLineDefinitions::InputMode const&) + { + return std::string(modeString(vt.inputHandler().mode())); + } + + std::string visit(StatusLineDefinitions::ProtectedMode const&) + { + if (vt.allowInput()) + return {}; + + return " (PROTECTED)"; + } + + std::string visit(StatusLineDefinitions::TraceMode const&) + { + std::string result; + + result += "TRACING"; + + if (!vt.traceHandler().pendingSequences().empty()) + result += fmt::format(" (#{}): {}", + vt.traceHandler().pendingSequences().size(), + vt.traceHandler().pendingSequences().front()); + return result; + } + + std::string visit(StatusLineDefinitions::SearchMode const&) + { + if (!vt.state().searchMode.pattern.empty() || vt.state().inputHandler.isEditingSearch()) + return " SEARCH"; + + return {}; + } + + std::string visit(StatusLineDefinitions::SearchPrompt const&) + { + if (vt.state().inputHandler.isEditingSearch()) + return fmt::format("Search: {}█", + unicode::convert_to(std::u32string_view(vt.state().searchMode.pattern))); + + return {}; + } + + std::string visit(StatusLineDefinitions::Command const& item) + { + crispy::ignore_unused(this); + + std::string result; + if (FILE* fp = popen(item.command.c_str(), "r"); fp) + { + char buffer[256] {}; + while (fgets(buffer, sizeof(buffer), fp) != nullptr) + { + result += buffer; + } + pclose(fp); + + // Only keep first line + if (auto const pos = result.find('\n'); pos != std::string::npos) + result.erase(pos); + } + else + result = std::strerror(errno); + return result; + } + + std::string visit(StatusLineDefinitions::Text const& item) + { + crispy::ignore_unused(this); + return item.text; + } + + std::string visit(StatusLineDefinitions::VTType const&) + { + return fmt::format("{}", vt.state().terminalId); + } + // }}} +}; + +std::string serializeToVT(Terminal const& vt, StatusLineSegment const& segment, StatusLineStyling styling) +{ + auto serializer = VTSerializer { vt, styling }; + for (auto const& item: segment) + serializer(item); + return serializer.result; +} + +} // namespace vtbackend diff --git a/src/vtbackend/StatusLineBuilder.h b/src/vtbackend/StatusLineBuilder.h new file mode 100644 index 0000000000..6bcbfcc532 --- /dev/null +++ b/src/vtbackend/StatusLineBuilder.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include +#include +#include + +namespace vtbackend +{ + +namespace StatusLineDefinitions +{ + struct Styles + { + std::optional foregroundColor; + std::optional backgroundColor; + CellFlags flags; + std::string textLeft; + std::string textRight; + }; + + // clang-format off + struct Title: Styles {}; + struct CellSGR: Styles {}; + struct CellTextUtf32: Styles {}; + struct CellTextUtf8: Styles {}; + struct Clock: Styles {}; + struct Command: Styles { std::string command; }; + struct HistoryLineCount: Styles {}; + struct Hyperlink: Styles {}; + struct InputMode: Styles {}; + struct ProtectedMode: Styles {}; + struct SearchMode: Styles {}; + struct SearchPrompt: Styles {}; + struct Text: Styles { std::string text; }; + struct TraceMode: Styles {}; + struct VTType: Styles {}; + + using Item = std::variant< + CellSGR, + CellTextUtf32, + CellTextUtf8, + Clock, + Command, + HistoryLineCount, + Hyperlink, + InputMode, + ProtectedMode, + SearchMode, + SearchPrompt, + Text, + Title, + TraceMode, + VTType + >; + // clang-format on +} // namespace StatusLineDefinitions + +using StatusLineSegment = std::vector; + +struct StatusLineDefinition +{ + StatusLineSegment left; + StatusLineSegment middle; + StatusLineSegment right; +}; + +// "{Clock:Bold,Italic,Color=#FFFF00} | {VTType} | {InputMode} {SearchPrompt:Bold,Color=Yellow}" +StatusLineSegment parseStatusLineSegment(std::string_view text); + +StatusLineDefinition parseStatusLineDefinition(std::string_view left, + std::string_view middle, + std::string_view right); + +enum class StatusLineStyling +{ + Disabled, + Enabled +}; + +class Terminal; +std::string serializeToVT(Terminal const& vt, StatusLineSegment const& segment, StatusLineStyling styling); + +} // namespace vtbackend diff --git a/src/vtbackend/Terminal.cpp b/src/vtbackend/Terminal.cpp index 56be4f7020..b1474335e7 100644 --- a/src/vtbackend/Terminal.cpp +++ b/src/vtbackend/Terminal.cpp @@ -4,11 +4,14 @@ #include #include #include +#include #include #include #include #include +#include + #include #include @@ -55,31 +58,6 @@ namespace // {{{ helpers value.pop_back(); } - string_view modeString(ViMode mode) noexcept - { - switch (mode) - { - case ViMode::Normal: return "NORMAL"sv; - case ViMode::Insert: return "INSERT"sv; - case ViMode::Visual: return "VISUAL"sv; - case ViMode::VisualLine: return "VISUAL LINE"sv; - case ViMode::VisualBlock: return "VISUAL BLOCK"sv; - } - crispy::unreachable(); - } - - std::string codepointText(std::u32string const& codepoints) - { - std::string text; - for (auto const codepoint: codepoints) - { - if (!text.empty()) - text += ' '; - text += fmt::format("U+{:X}", static_cast(codepoint)); - } - return text; - } - #if defined(CONTOUR_PERF_STATS) void logRenderBufferSwap(bool success, uint64_t frameID) { @@ -158,6 +136,9 @@ Terminal::Terminal(Events& eventListener, _currentScreen { &_primaryScreen }, _viewport { *this, std::bind(&Terminal::onViewportChanged, this) }, _traceHandler { *this }, + _indicatorStatusLineDefinition { parseStatusLineDefinition(_settings.indicatorStatusLine.left, + _settings.indicatorStatusLine.middle, + _settings.indicatorStatusLine.right) }, _selectionHelper { this }, _refreshInterval { _settings.refreshRate } { @@ -541,102 +522,60 @@ void Terminal::updateIndicatorStatusLine() { Require(_state.activeStatusDisplay != ActiveStatusDisplay::IndicatorStatusLine); - auto const _ = crispy::finally { [this]() { + auto const colors = + _state.focused ? colorPalette().indicatorStatusLine : colorPalette().indicatorStatusLineInactive; + + auto const backupForeground = _indicatorStatusScreen.colorPalette().defaultForeground; + auto const backupBackground = _indicatorStatusScreen.colorPalette().defaultBackground; + _indicatorStatusScreen.colorPalette().defaultForeground = colors.foreground; + _indicatorStatusScreen.colorPalette().defaultBackground = colors.background; + + auto const _ = crispy::finally { [&]() { // Cleaning up. + _indicatorStatusScreen.colorPalette().defaultForeground = backupForeground; + _indicatorStatusScreen.colorPalette().defaultBackground = backupBackground; verifyState(); } }; - auto const colors = - _state.focused ? colorPalette().indicatorStatusLine : colorPalette().indicatorStatusLineInactive; - // Prepare old status line's cursor position and some other flags. _indicatorStatusScreen.moveCursorTo({}, {}); _indicatorStatusScreen.cursor().graphicsRendition.foregroundColor = colors.foreground; _indicatorStatusScreen.cursor().graphicsRendition.backgroundColor = colors.background; - - // Run status-line update. - // We cannot use VT writing here, because we shall not interfere with the application's VT state. - // TODO: Future improvement would be to allow full VT sequence support for the Indicator-status-line, - // such that we can pass display-control partially over to some user/thirdparty configuration. _indicatorStatusScreen.clearLine(); - _indicatorStatusScreen.writeTextFromExternal( - fmt::format(" {} │ {}", _state.terminalId, modeString(inputHandler().mode()))); - if (!_state.searchMode.pattern.empty() || _state.inputHandler.isEditingSearch()) - _indicatorStatusScreen.writeTextFromExternal(" SEARCH"); + using Styling = StatusLineStyling; + auto const& definitions = _indicatorStatusLineDefinition; - if (!allowInput()) + if (!definitions.left.empty()) { - _indicatorStatusScreen.cursor().graphicsRendition.foregroundColor = BrightColor::Red; - _indicatorStatusScreen.cursor().graphicsRendition.flags.enable(CellFlag::Bold); - _indicatorStatusScreen.writeTextFromExternal(" (PROTECTED)"); - _indicatorStatusScreen.cursor().graphicsRendition.foregroundColor = colors.foreground; - _indicatorStatusScreen.cursor().graphicsRendition.flags.disable(CellFlag::Bold); - } - - if (_state.executionMode != ExecutionMode::Normal) - { - _indicatorStatusScreen.writeTextFromExternal(" | "); - _indicatorStatusScreen.cursor().graphicsRendition.foregroundColor = BrightColor::Yellow; - _indicatorStatusScreen.cursor().graphicsRendition.flags |= CellFlag::Bold; - _indicatorStatusScreen.writeTextFromExternal("TRACING"); - if (!_traceHandler.pendingSequences().empty()) - _indicatorStatusScreen.writeTextFromExternal( - fmt::format(" (#{}): {}", - _traceHandler.pendingSequences().size(), - _traceHandler.pendingSequences().front())); - - _indicatorStatusScreen.cursor().graphicsRendition.foregroundColor = colors.foreground; - _indicatorStatusScreen.cursor().graphicsRendition.flags.disable(CellFlag::Bold); + auto const leftVT = serializeToVT(*this, definitions.left, Styling::Enabled); + writeToScreenInternal(_indicatorStatusScreen, leftVT); } - // TODO: Disabled for now, but generally I want that functionality, but configurable somehow. - auto constexpr IndicatorLineShowCodepoints = false; - if (IndicatorLineShowCodepoints) - { - auto const cursorPosition = _state.inputHandler.mode() == ViMode::Insert - ? _indicatorStatusScreen.cursor().position - : _state.viCommands.cursorPosition; - auto const text = - codepointText(isPrimaryScreen() ? _primaryScreen.useCellAt(cursorPosition).codepoints() - : alternateScreen().useCellAt(cursorPosition).codepoints()); - _indicatorStatusScreen.writeTextFromExternal(fmt::format(" | {}", text)); - } - - if (_state.inputHandler.isEditingSearch()) - _indicatorStatusScreen.writeTextFromExternal(fmt::format( - " │ Search: {}█", unicode::convert_to(u32string_view(_state.searchMode.pattern)))); - - auto rightString = ""s; - - if (isPrimaryScreen()) + if (!definitions.middle.empty() || !definitions.right.empty()) { - if (viewport().scrollOffset().value) - rightString += fmt::format( - "{}/{} {:3}%", - viewport().scrollOffset(), - _primaryScreen.historyLineCount(), - int((double(viewport().scrollOffset()) / double(_primaryScreen.historyLineCount())) * 100)); - else - rightString += fmt::format("{}", _primaryScreen.historyLineCount()); - } + // Don't show the middle segment if text is too long. + auto const middleLength = serializeToVT(*this, definitions.middle, Styling::Disabled).size(); + auto const center = pageSize().columns / ColumnCount(2) - ColumnCount(1); + _indicatorStatusScreen.moveCursorToColumn( + ColumnOffset::cast_from(center - ColumnOffset::cast_from(middleLength / 2))); + auto const middleVT = serializeToVT(*this, definitions.middle, Styling::Enabled); + if (unbox(center) > middleLength) + writeToScreenInternal(_indicatorStatusScreen, middleVT); - if (!rightString.empty()) - rightString += " │ "; - - // NB: Cannot use std::chrono::system_clock::now() here, because MSVC can't handle it. - rightString += fmt::format("{:%H:%M} ", fmt::localtime(std::time(nullptr))); - - auto const columnsAvailable = _indicatorStatusScreen.pageSize().columns.as() - - _indicatorStatusScreen.cursor().position.column.as(); - if (rightString.size() <= static_cast(columnsAvailable)) - { - _indicatorStatusScreen.cursor().position.column = - ColumnOffset::cast_from(_indicatorStatusScreen.pageSize().columns) - - ColumnOffset::cast_from(rightString.size()) - ColumnOffset(1); - _indicatorStatusScreen.updateCursorIterator(); + // Don't show the right part if the left and middle segments are too long. + // That is, if the current cursor position is past the beginning of the right segment. + // Also don't show if the right text is too long. + auto const rightLength = serializeToVT(*this, definitions.right, Styling::Disabled).size(); + if (ColumnCount::cast_from(rightLength) < _indicatorStatusScreen.pageSize().columns) + { + _indicatorStatusScreen.moveCursorToColumn( + boxed_cast(_indicatorStatusScreen.pageSize().columns) + - ColumnOffset::cast_from(rightLength)); - _indicatorStatusScreen.writeTextFromExternal(rightString); + auto const rightVT = serializeToVT(*this, definitions.right, Styling::Enabled); + writeToScreenInternal(_indicatorStatusScreen, rightVT); + } } } @@ -1043,6 +982,295 @@ string_view Terminal::lockedWriteToPtyBuffer(string_view data) return string_view(ref.data(), ref.size()); } +// {{{ SequenceBuilder +// SequenceBuilder implements ParserEvents interface to handle parsed VT sequences. +template +class SequenceBuilder +{ + public: + explicit SequenceBuilder(Handler& handler): + _sequence {}, _parameterBuilder { _sequence.parameters() }, _handler { handler } + { + } + + // {{{ ParserEvents interface + void error(std::string_view errorString); + void print(char32_t codepoint); + size_t print(std::string_view chars, size_t cellCount); + void printEnd(); + void execute(char controlCode); + void clear() noexcept; + void collect(char ch); + void collectLeader(char leader) noexcept; + void param(char ch) noexcept; + void paramDigit(char ch) noexcept; + void paramSeparator() noexcept; + void paramSubSeparator() noexcept; + void dispatchESC(char finalChar); + void dispatchCSI(char finalChar); + void startOSC(); + void putOSC(char ch); + void dispatchOSC(); + void hook(char finalChar); + void put(char ch); + void unhook(); + void startAPC() {} + void putAPC(char) {} + void dispatchAPC() {} + void startPM() {} + void putPM(char) {} + void dispatchPM() {} + + [[nodiscard]] size_t maxBulkTextSequenceWidth() const noexcept; + // }}} + + private: + void handleSequence(); + + Sequence _sequence {}; + SequenceParameterBuilder _parameterBuilder; + Handler& _handler; + + std::unique_ptr _hookedParser {}; + std::unique_ptr _sixelImageBuilder {}; +}; + +template +inline void SequenceBuilder::clear() noexcept +{ + _sequence.clearExceptParameters(); + _parameterBuilder.reset(); +} + +template +inline void SequenceBuilder::paramDigit(char ch) noexcept +{ + _parameterBuilder.multiplyBy10AndAdd(static_cast(ch - '0')); +} + +template +inline void SequenceBuilder::paramSeparator() noexcept +{ + _parameterBuilder.nextParameter(); +} + +template +inline void SequenceBuilder::paramSubSeparator() noexcept +{ + _parameterBuilder.nextSubParameter(); +} + +template +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void SequenceBuilder::error(std::string_view errorString) +{ + if (vtParserLog) + vtParserLog()("Parser error: {}", errorString); +} + +template +void SequenceBuilder::print(char32_t codepoint) +{ + _handler.writeText(codepoint); +} + +template +size_t SequenceBuilder::print(string_view chars, size_t cellCount) +{ + return _handler.writeText(chars, cellCount); +} + +template +void SequenceBuilder::printEnd() +{ + _handler.writeTextEnd(); +} + +template +void SequenceBuilder::execute(char controlCode) +{ + _handler.executeControlCode(controlCode); +} + +template +void SequenceBuilder::collect(char ch) +{ + _sequence.intermediateCharacters().push_back(ch); +} + +template +void SequenceBuilder::collectLeader(char leader) noexcept +{ + _sequence.setLeader(leader); +} + +template +void SequenceBuilder::param(char ch) noexcept +{ + switch (ch) + { + case ';': paramSeparator(); break; + case ':': paramSubSeparator(); break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': paramDigit(ch); break; + default: crispy::unreachable(); + } +} + +template +void SequenceBuilder::dispatchESC(char finalChar) +{ + _sequence.setCategory(FunctionCategory::ESC); + _sequence.setFinalChar(finalChar); + handleSequence(); +} + +template +void SequenceBuilder::dispatchCSI(char finalChar) +{ + _sequence.setCategory(FunctionCategory::CSI); + _sequence.setFinalChar(finalChar); + handleSequence(); +} + +template +void SequenceBuilder::startOSC() +{ + _sequence.setCategory(FunctionCategory::OSC); +} + +template +void SequenceBuilder::putOSC(char ch) +{ + if (_sequence.intermediateCharacters().size() + 1 < Sequence::MaxOscLength) + _sequence.intermediateCharacters().push_back(ch); +} + +template +void SequenceBuilder::dispatchOSC() +{ + auto const [code, skipCount] = vtparser::extractCodePrefix(_sequence.intermediateCharacters()); + _parameterBuilder.set(static_cast(code)); + _sequence.intermediateCharacters().erase(0, skipCount); + handleSequence(); + clear(); +} + +template +void SequenceBuilder::hook(char finalChar) +{ + _handler.hook(finalChar); + _sequence.setCategory(FunctionCategory::DCS); + _sequence.setFinalChar(finalChar); + + handleSequence(); +} + +template +void SequenceBuilder::put(char ch) +{ + if (_hookedParser) + _hookedParser->pass(ch); +} + +template +void SequenceBuilder::unhook() +{ + if (_hookedParser) + { + _hookedParser->finalize(); + _hookedParser.reset(); + } +} + +template +size_t SequenceBuilder::maxBulkTextSequenceWidth() const noexcept +{ + return _handler.maxBulkTextSequenceWidth(); +} + +size_t Terminal::maxBulkTextSequenceWidth() const noexcept +{ + if (!isPrimaryScreen()) + return 0; + + if (!_primaryScreen.currentLine().isTrivialBuffer()) + return 0; + + assert(_state.mainScreenMargin.horizontal.to >= _currentScreen->cursor().position.column); + + return unbox(_state.mainScreenMargin.horizontal.to - _currentScreen->cursor().position.column); +} + +template +void SequenceBuilder::handleSequence() +{ + _parameterBuilder.fixiate(); + _handler.processSequence(_sequence); +} +// }}} + +struct LocalSequenceHandler // {{{ +{ + Terminal& terminal; + Screen& targetScreen; + uint64_t instructionCounter = 0; + + void executeControlCode(char controlCode) + { + instructionCounter++; + targetScreen.executeControlCode(controlCode); + } + + void processSequence(Sequence const& seq) + { + instructionCounter = 0; + targetScreen.processSequence(seq); + } + + void writeText(char32_t codepoint) + { + instructionCounter++; + targetScreen.writeText(codepoint); + } + + [[nodiscard]] size_t writeText(std::string_view chars, size_t cellCount) + { + assert(!chars.empty()); + instructionCounter += chars.size(); + targetScreen.writeText(chars, cellCount); + return terminal.settings().pageSize.columns.as() + - terminal.currentScreen().cursor().position.column.as(); + } + + void writeTextEnd() { targetScreen.writeTextEnd(); } + + void hook(char /*finalChar*/) { instructionCounter++; } + + [[nodiscard]] size_t maxBulkTextSequenceWidth() const noexcept + { + return terminal.maxBulkTextSequenceWidth(); + } +}; +// }}} + +void Terminal::writeToScreenInternal(Screen& screen, std::string_view vtStream) +{ + auto sequenceHandler = LocalSequenceHandler { *this, screen }; + auto sequenceBuilder = SequenceBuilder { sequenceHandler }; + auto parser = vtparser::Parser { sequenceBuilder }; + + parser.parseFragment(vtStream); +} + void Terminal::writeToScreenInternal(std::string_view vtStream) { while (!vtStream.empty()) diff --git a/src/vtbackend/Terminal.h b/src/vtbackend/Terminal.h index 7d2c2ff6a7..bf25056c10 100644 --- a/src/vtbackend/Terminal.h +++ b/src/vtbackend/Terminal.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -322,6 +323,10 @@ class Terminal /// Writes a given VT-sequence to screen - but without acquiring the lock (must be already acquired). void writeToScreenInternal(std::string_view vtStream); + /// Writes a given VT-sequence to screen - but without acquiring the lock (must be already acquired). + /// This version of the function is used to write to the status line and should not be used by the shell. + void writeToScreenInternal(Screen& screen, std::string_view vtStream); + // viewport management [[nodiscard]] Viewport& viewport() noexcept { return _viewport; } [[nodiscard]] Viewport const& viewport() const noexcept { return _viewport; } @@ -744,6 +749,10 @@ class Terminal return _supportedVTSequences.activeSequences(); } + size_t maxBulkTextSequenceWidth() const noexcept; + + TraceHandler const& traceHandler() const noexcept { return _traceHandler; } + private: void mainLoop(); void fillRenderBufferInternal(RenderBuffer& output, bool includeSelection); @@ -845,6 +854,8 @@ class Terminal gsl::not_null _currentScreen; Viewport _viewport; TraceHandler _traceHandler; + + StatusLineDefinition _indicatorStatusLineDefinition; // clang-format on // }}}