diff --git a/src/contour/Actions.h b/src/contour/Actions.h index 18a3c81570..e8b1b6a963 100644 --- a/src/contour/Actions.h +++ b/src/contour/Actions.h @@ -13,6 +13,8 @@ */ #pragma once +#include + #include #include @@ -22,12 +24,28 @@ namespace contour::actions { +// Defines the format to use when extracting a selection range from the terminal. +enum class CopyFormat +{ + // Copies purely the text (with their whitespaces, and newlines, but no formatting). + Text, + + // Copies the selection in HTML format. + HTML, + + // Copies the selection in escaped VT sequence format. + VT, + + // Copies the selection as PNG image. + PNG, +}; + // clang-format off struct CancelSelection{}; struct ChangeProfile{ std::string name; }; struct ClearHistoryAndReset{}; struct CopyPreviousMarkRange{}; -struct CopySelection{}; +struct CopySelection{ CopyFormat format = CopyFormat::Text; }; struct CreateDebugDump{}; struct DecreaseFontSize{}; struct DecreaseOpacity{}; @@ -255,6 +273,28 @@ struct formatter return fmt::format_to(ctx.out(), "UNKNOWN ACTION"); } }; +template <> +struct formatter +{ + template + constexpr auto parse(ParseContext& ctx) + { + return ctx.begin(); + } + template + auto format(contour::actions::CopyFormat value, FormatContext& ctx) + { + switch (value) + { + case contour::actions::CopyFormat::Text: return fmt::format_to(ctx.out(), "Text"); + case contour::actions::CopyFormat::HTML: return fmt::format_to(ctx.out(), "HTML"); + case contour::actions::CopyFormat::PNG: return fmt::format_to(ctx.out(), "PNG"); + case contour::actions::CopyFormat::VT: return fmt::format_to(ctx.out(), "VT"); + } + crispy::unreachable(); + } +}; + } // namespace fmt #undef HANDLE_ACTION // ]}} diff --git a/src/contour/Config.cpp b/src/contour/Config.cpp index f425fbbd1f..539d01fec4 100644 --- a/src/contour/Config.cpp +++ b/src/contour/Config.cpp @@ -43,6 +43,8 @@ #include #include +#include "contour/Actions.h" + #if defined(_WIN32) #include #elif defined(__APPLE__) @@ -876,6 +878,32 @@ namespace return nullopt; } + if (holds_alternative(action)) + { + if (auto node = _parent["format"]; node && node.IsScalar()) + { + _usedKeys.emplace(_prefix + ".format"); + auto const formatString = toUpper(node.as()); + static auto constexpr mappings = + std::array, 4> { { + { "TEXT", actions::CopyFormat::Text }, + { "HTML", actions::CopyFormat::HTML }, + { "PNG", actions::CopyFormat::PNG }, + { "VT", actions::CopyFormat::VT }, + } }; + if (auto const p = std::find_if(mappings.begin(), + mappings.end(), + [&](auto const& t) { return t.first == formatString; }); + p != mappings.end()) + { + return actions::CopySelection { p->second }; + } + errorlog()("Invalid format '{}' in CopySelection action. Defaulting to 'text'.", + node.as()); + return actions::CopySelection { actions::CopyFormat::Text }; + } + } + if (holds_alternative(action)) { if (auto node = _parent["strip"]; node && node.IsScalar()) diff --git a/src/contour/TerminalSession.cpp b/src/contour/TerminalSession.cpp index 4574895e07..3d891e6a59 100644 --- a/src/contour/TerminalSession.cpp +++ b/src/contour/TerminalSession.cpp @@ -703,9 +703,23 @@ bool TerminalSession::operator()(actions::CopyPreviousMarkRange) return true; } -bool TerminalSession::operator()(actions::CopySelection) +bool TerminalSession::operator()(actions::CopySelection copySelection) { - copyToClipboard(terminal().extractSelectionText()); + switch (copySelection.format) + { + case actions::CopyFormat::Text: + // Copy the selection in pure text, plus whitespaces and newline. + copyToClipboard(terminal().extractSelectionText()); + break; + case actions::CopyFormat::HTML: + // TODO: This requires walking through each selected cell and construct HTML+CSS for it. + case actions::CopyFormat::VT: + // TODO: Construct VT escape sequences. + case actions::CopyFormat::PNG: + // TODO: Copy to clipboard as rendered PNG for the selected area. + errorlog()("CopySelection format {} is not yet supported.", copySelection.format); + return false; + } return true; } diff --git a/src/text_shaper/fontconfig_locator.cpp b/src/text_shaper/fontconfig_locator.cpp index f8b1fcec0d..bd8df94820 100644 --- a/src/text_shaper/fontconfig_locator.cpp +++ b/src/text_shaper/fontconfig_locator.cpp @@ -88,7 +88,7 @@ namespace return nullopt; } - constexpr int fcWeight(font_weight _weight) noexcept + int fcWeight(font_weight _weight) noexcept { for (auto const& mapping: fontWeightMappings) if (mapping.first == _weight) diff --git a/src/vtbackend/RenderBufferBuilder.cpp b/src/vtbackend/RenderBufferBuilder.cpp index 947a521a81..982fa0a44e 100644 --- a/src/vtbackend/RenderBufferBuilder.cpp +++ b/src/vtbackend/RenderBufferBuilder.cpp @@ -117,14 +117,16 @@ RenderBufferBuilder::RenderBufferBuilder(Terminal const& _terminal, bool theReverseVideo, HighlightSearchMatches highlightSearchMatches, InputMethodData inputMethodData, - optional theCursorPosition): + optional theCursorPosition, + bool includeSelection): output { _output }, terminal { _terminal }, cursorPosition { theCursorPosition }, baseLine { base }, reverseVideo { theReverseVideo }, _highlightSearchMatches { highlightSearchMatches }, - _inputMethodData { std::move(inputMethodData) } + _inputMethodData { std::move(inputMethodData) }, + _includeSelection { includeSelection } { output.frameID = _terminal.lastFrameID(); @@ -256,7 +258,8 @@ RGBColorPair RenderBufferBuilder::makeColorsForCell(CellLocation gridPosit && output.cursor->shape == CursorShape::Block; // clang-format on - auto const selected = terminal.isSelected(CellLocation { gridPosition.line, gridPosition.column }); + auto const selected = + _includeSelection && terminal.isSelected(CellLocation { gridPosition.line, gridPosition.column }); auto const highlighted = terminal.isHighlighted(CellLocation { gridPosition.line, gridPosition.column }); auto const blink = terminal.blinkState(); auto const rapidBlink = terminal.rapidBlinkState(); @@ -345,7 +348,7 @@ void RenderBufferBuilder::renderTrivialLine(TrivialLineBuffer const& lineB // We're not testing for cursor shape (which should be done in order to be 100% correct) // because it's not really draining performance. bool const canRenderViaSimpleLine = - !terminal.isSelected(lineOffset) && !gridLineContainsCursor(lineOffset); + (!terminal.isSelected(lineOffset) || !_includeSelection) && !gridLineContainsCursor(lineOffset); if (canRenderViaSimpleLine) { diff --git a/src/vtbackend/RenderBufferBuilder.h b/src/vtbackend/RenderBufferBuilder.h index c7fced7d18..5bf1ba403f 100644 --- a/src/vtbackend/RenderBufferBuilder.h +++ b/src/vtbackend/RenderBufferBuilder.h @@ -22,7 +22,8 @@ class RenderBufferBuilder bool reverseVideo, HighlightSearchMatches highlightSearchMatches, InputMethodData inputMethodData, - std::optional theCursorPosition); + std::optional theCursorPosition, + bool includeSelection); /// Renders a single grid cell. /// This call is guaranteed to be invoked sequencially, from top line @@ -119,6 +120,7 @@ class RenderBufferBuilder bool reverseVideo; HighlightSearchMatches _highlightSearchMatches; InputMethodData _inputMethodData; + bool _includeSelection; ColumnCount _inputMethodSkipColumns = ColumnCount(0); int prevWidth = 0; diff --git a/src/vtbackend/Terminal.cpp b/src/vtbackend/Terminal.cpp index cf694cce87..0710cd0784 100644 --- a/src/vtbackend/Terminal.cpp +++ b/src/vtbackend/Terminal.cpp @@ -275,13 +275,21 @@ bool Terminal::ensureFreshRenderBuffer(bool _locked) break; renderBuffer_.state = RenderBufferState::RefreshBuffersAndTrySwap; [[fallthrough]]; - case RenderBufferState::RefreshBuffersAndTrySwap: + case RenderBufferState::RefreshBuffersAndTrySwap: { + auto& backBuffer = renderBuffer_.backBuffer(); + auto const lastCursorPos = std::move(backBuffer.cursor); if (!_locked) - refreshRenderBuffer(renderBuffer_.backBuffer()); + fillRenderBuffer(renderBuffer_.backBuffer(), true); else - refreshRenderBufferInternal(renderBuffer_.backBuffer()); + fillRenderBufferInternal(renderBuffer_.backBuffer(), true); + auto const cursorChanged = + lastCursorPos.has_value() != backBuffer.cursor.has_value() + || (backBuffer.cursor.has_value() && backBuffer.cursor->position != lastCursorPos->position); + if (cursorChanged) + eventListener_.cursorPositionChanged(); renderBuffer_.state = RenderBufferState::TrySwapBuffers; [[fallthrough]]; + } case RenderBufferState::TrySwapBuffers: { [[maybe_unused]] auto const success = renderBuffer_.swapBuffers(currentTime_); @@ -300,12 +308,6 @@ bool Terminal::ensureFreshRenderBuffer(bool _locked) return true; } -void Terminal::refreshRenderBuffer(RenderBuffer& _output) -{ - auto const _l = lock_guard { *this }; - refreshRenderBufferInternal(_output); -} - PageSize Terminal::SelectionHelper::pageSize() const noexcept { return terminal->pageSize(); @@ -368,12 +370,17 @@ void Terminal::updateInputMethodPreeditString(std::string preeditString) screenUpdated(); } -void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) +void Terminal::fillRenderBuffer(RenderBuffer& output, bool includeSelection) +{ + auto const _l = lock_guard { *this }; + fillRenderBufferInternal(output, includeSelection); +} + +void Terminal::fillRenderBufferInternal(RenderBuffer& output, bool includeSelection) { verifyState(); - auto const lastCursorPos = std::move(_output.cursor); - _output.clear(); + output.clear(); changes_.store(0); screenDirty_ = false; @@ -398,23 +405,25 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) if (isPrimaryScreen()) _lastRenderPassHints = primaryScreen_.render(RenderBufferBuilder { *this, - _output, + output, LineOffset(0), mainDisplayReverseVideo, HighlightSearchMatches::Yes, inputMethodData_, - theCursorPosition }, + theCursorPosition, + includeSelection }, viewport_.scrollOffset(), highlightSearchMatches); else _lastRenderPassHints = alternateScreen_.render(RenderBufferBuilder { *this, - _output, + output, LineOffset(0), mainDisplayReverseVideo, HighlightSearchMatches::Yes, inputMethodData_, - theCursorPosition }, + theCursorPosition, + includeSelection }, viewport_.scrollOffset(), highlightSearchMatches); @@ -427,32 +436,28 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) updateIndicatorStatusLine(); indicatorStatusScreen_.render( RenderBufferBuilder { *this, - _output, + output, pageSize().lines.as(), !mainDisplayReverseVideo, HighlightSearchMatches::No, InputMethodData {}, - nullopt }, + nullopt, + includeSelection }, ScrollOffset(0)); break; case StatusDisplayType::HostWritable: hostWritableStatusLineScreen_.render( RenderBufferBuilder { *this, - _output, + output, pageSize().lines.as(), !mainDisplayReverseVideo, HighlightSearchMatches::No, InputMethodData {}, - nullopt }, + nullopt, + includeSelection }, ScrollOffset(0)); break; } - - if (lastCursorPos.has_value() != _output.cursor.has_value() - || (_output.cursor.has_value() && _output.cursor->position != lastCursorPos->position)) - { - eventListener_.cursorPositionChanged(); - } } // }}} diff --git a/src/vtbackend/Terminal.h b/src/vtbackend/Terminal.h index 43895b1918..8e171cbb5b 100644 --- a/src/vtbackend/Terminal.h +++ b/src/vtbackend/Terminal.h @@ -658,10 +658,15 @@ class Terminal Settings const& settings() const noexcept { return settings_; } Settings& settings() noexcept { return settings_; } + // Renders current visual terminal state to the render buffer. + // + // @param output target render buffer to write the current visual state to. + // @param _includeSelection boolean to indicate whether or not to include colorize selection. + void fillRenderBuffer(RenderBuffer& output, bool includeSelection); // <- acquires the lock + private: void mainLoop(); - void refreshRenderBuffer(RenderBuffer& _output); // <- acquires the lock - void refreshRenderBufferInternal(RenderBuffer& _output); + void fillRenderBufferInternal(RenderBuffer& _output, bool includeSelection); void updateIndicatorStatusLine(); void updateCursorVisibilityState() const; bool updateCursorHoveringState();