Skip to content

Commit

Permalink
Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protec…
Browse files Browse the repository at this point in the history
…ted grid areas during erase operations (#29, #30, #31).
  • Loading branch information
christianparpart committed Sep 25, 2022
1 parent 3e441c9 commit 51120ec
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 44 deletions.
3 changes: 2 additions & 1 deletion metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@
<li>Fixes vertical cursor movement for Sixel graphics with only newlines (#822).</li>
<li>Fixes Sixel rendering for images with aspect ratios other than 1:1.</li>
<li>Fixes Sixel rendering for images that show below but should be rendered above text (#831).</li>
<li>Removes `images.sixel_cursor_conformance` config option.</li>
<li>Removes `images.sixel_cursor_conformance` config option.</li>
<li>Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31).</li>
</ul>
</description>
</release>
Expand Down
15 changes: 12 additions & 3 deletions src/terminal/Cell.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class CONTOUR_PACKED Cell
uint8_t _width,
HyperlinkId _hyperlink) noexcept;

void writeTextOnly(char32_t _ch, uint8_t _width) noexcept;

std::u32string codepoints() const;
char32_t codepoint(size_t i) const noexcept;
std::size_t codepointCount() const noexcept;
Expand Down Expand Up @@ -292,12 +294,11 @@ inline void Cell::write(GraphicsAttributes const& _attributes,
uint8_t _width,
HyperlinkId _hyperlink) noexcept
{
setWidth(_width);

codepoint_ = _ch;
writeTextOnly(_ch, _width);
if (extra_)
{
extra_->codepoints.clear();
// Writing text into a cell destroys the image fragment (as least for Sixels).
extra_->imageFragment = {};
}

Expand All @@ -314,6 +315,14 @@ inline void Cell::write(GraphicsAttributes const& _attributes,
}
}

inline void Cell::writeTextOnly(char32_t _ch, uint8_t _width) noexcept
{
setWidth(_width);
codepoint_ = _ch;
if (extra_)
extra_->codepoints.clear();
}

inline void Cell::reset(GraphicsAttributes const& _attributes, HyperlinkId _hyperlink) noexcept
{
codepoint_ = 0;
Expand Down
6 changes: 4 additions & 2 deletions src/terminal/CellFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
namespace terminal
{

enum class CellFlags : uint16_t
enum class CellFlags : uint32_t
{
None = 0,

Expand All @@ -43,6 +43,7 @@ enum class CellFlags : uint16_t
Encircled = (1 << 13),
Overline = (1 << 14),
RapidBlinking = (1 << 15),
CharacterProtected = (1 << 16), // Character is protected by selective erase operations.
};

constexpr CellFlags& operator|=(CellFlags& a, CellFlags b) noexcept
Expand Down Expand Up @@ -101,7 +102,7 @@ struct formatter<terminal::CellFlags>
template <typename FormatContext>
auto format(const terminal::CellFlags _flags, FormatContext& ctx)
{
static const std::array<std::pair<terminal::CellFlags, std::string_view>, 16> nameMap = {
static const std::array<std::pair<terminal::CellFlags, std::string_view>, 17> nameMap = {
std::pair { terminal::CellFlags::Bold, std::string_view("Bold") },
std::pair { terminal::CellFlags::Faint, std::string_view("Faint") },
std::pair { terminal::CellFlags::Italic, std::string_view("Italic") },
Expand All @@ -118,6 +119,7 @@ struct formatter<terminal::CellFlags>
std::pair { terminal::CellFlags::Framed, std::string_view("Framed") },
std::pair { terminal::CellFlags::Encircled, std::string_view("Encircled") },
std::pair { terminal::CellFlags::Overline, std::string_view("Overline") },
std::pair { terminal::CellFlags::CharacterProtected, std::string_view("CharacterProtected") },
};
std::string s;
for (auto const& mapping: nameMap)
Expand Down
8 changes: 8 additions & 0 deletions src/terminal/Functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ constexpr inline auto DECERA = detail::CSI(std::nullopt, 0, 4, '$', 'z', VT
constexpr inline auto DECFRA = detail::CSI(std::nullopt, 0, 4, '$', 'x', VTType::VT420, "DECFRA", "Fill rectangular area");
constexpr inline auto DECDC = detail::CSI(std::nullopt, 0, 1, '\'', '~', VTType::VT420, "DECDC", "Delete column");
constexpr inline auto DECIC = detail::CSI(std::nullopt, 0, 1, '\'', '}', VTType::VT420, "DECIC", "Insert column");
constexpr inline auto DECSCA = detail::CSI(std::nullopt, 0, 1, '"', 'q', VTType::VT240, "DECSCA", "Select Character Protection Attribute");
constexpr inline auto DECSED = detail::CSI('?', 0, 1, std::nullopt, 'J', VTType::VT240, "DECSED", "Selective Erase in Display");
constexpr inline auto DECSERA = detail::CSI(std::nullopt, 0, 4, '$', '{', VTType::VT240, "DECSERA", "Selective Erase in Rectangular Area");
constexpr inline auto DECSEL = detail::CSI('?', 0, 1, std::nullopt, 'K', VTType::VT240, "DECSEL", "Selective Erase in Line");
constexpr inline auto XTRESTORE = detail::CSI('?', 0, ArgsMax, std::nullopt, 'r', VTExtension::XTerm, "XTRESTORE", "Restore DEC private modes.");
constexpr inline auto XTSAVE = detail::CSI('?', 0, ArgsMax, std::nullopt, 's', VTExtension::XTerm, "XTSAVE", "Save DEC private modes.");
constexpr inline auto DECRM = detail::CSI('?', 1, ArgsMax, std::nullopt, 'l', VTType::VT100, "DECRM", "Reset DEC-mode");
Expand Down Expand Up @@ -527,6 +531,10 @@ inline auto const& functions() noexcept
DECERA,
DECFRA,
DECIC,
DECSCA,
DECSED,
DECSERA,
DECSEL,
XTRESTORE,
XTSAVE,
DECPS,
Expand Down
186 changes: 183 additions & 3 deletions src/terminal/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ void Screen<Cell>::sendTerminalId()
_terminal.reply("\033[>{};{};{}c", Pp, Pv, Pc);
}

// {{{ ED
template <typename Cell>
void Screen<Cell>::clearToEndOfScreen()
{
Expand Down Expand Up @@ -871,6 +872,7 @@ void Screen<Cell>::clearScreen()
// up in case the content is still needed.
scrollUp(_state.pageSize.lines);
}
// }}}

template <typename Cell>
void Screen<Cell>::eraseCharacters(ColumnCount _n)
Expand All @@ -890,6 +892,139 @@ void Screen<Cell>::eraseCharacters(ColumnCount _n)
line.useCellAt(_state.cursor.position.column + i).reset(_state.cursor.graphicsRendition);
}

// {{{ DECSEL
template <typename Cell>
void Screen<Cell>::selectiveEraseToEndOfLine()
{
if (_terminal.isFullHorizontalMargins() && _state.cursor.position.column.value == 0)
selectiveEraseLine(_state.cursor.position.line);
else
selectiveErase(_state.cursor.position.line,
_state.cursor.position.column,
ColumnOffset::cast_from(_state.pageSize.columns));
}

template <typename Cell>
void Screen<Cell>::selectiveEraseToBeginOfLine()
{
if (_terminal.isFullHorizontalMargins()
&& _state.cursor.position.column.value == _state.pageSize.columns.value)
selectiveEraseLine(_state.cursor.position.line);
else
selectiveErase(_state.cursor.position.line, ColumnOffset(0), _state.cursor.position.column + 1);
}

template <typename Cell>
void Screen<Cell>::selectiveEraseLine(LineOffset line)
{
if (containsProtectedCharacters(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns)))
{
selectiveErase(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns));
return;
}

currentLine().reset(grid().defaultLineFlags(), _state.cursor.graphicsRendition);

auto const left = ColumnOffset(0);
auto const right = boxed_cast<ColumnOffset>(_state.pageSize.columns - 1);
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
_terminal.markRegionDirty(area);
}

template <typename Cell>
void Screen<Cell>::selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end)
{
Cell* i = &at(line, begin);
Cell const* e = i + unbox<uintptr_t>(end - begin);
while (i != e)
{
if (i->isFlagEnabled(CellFlags::CharacterProtected))
{
++i;
continue;
}
i->reset(_state.cursor.graphicsRendition);
++i;
}

auto const left = begin;
auto const right = end - 1;
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
_terminal.markRegionDirty(area);
}

template <typename Cell>
bool Screen<Cell>::containsProtectedCharacters(LineOffset line, ColumnOffset begin, ColumnOffset end) const
{
Cell const* i = &at(line, begin);
Cell const* e = i + unbox<uintptr_t>(end - begin);
while (i != e)
{
if (i->isFlagEnabled(CellFlags::CharacterProtected))
return true;
++i;
}
return false;
}
// }}}
// {{{ DECSED
template <typename Cell>
void Screen<Cell>::selectiveEraseToEndOfScreen()
{
selectiveEraseToEndOfLine();

auto const lineStart = unbox<int>(_state.cursor.position.line) + 1;
auto const lineEnd = unbox<int>(_state.pageSize.lines);

for (auto const lineOffset: ranges::views::iota(lineStart, lineEnd))
selectiveEraseLine(LineOffset::cast_from(lineOffset));
}

template <typename Cell>
void Screen<Cell>::selectiveEraseToBeginOfScreen()
{
selectiveEraseToBeginOfLine();

for (auto const lineOffset: ranges::views::iota(0, *_state.cursor.position.line))
selectiveEraseLine(LineOffset::cast_from(lineOffset));
}

template <typename Cell>
void Screen<Cell>::selectiveEraseScreen()
{
for (auto const lineOffset: ranges::views::iota(0, *_state.pageSize.lines))
selectiveEraseLine(LineOffset::cast_from(lineOffset));
}
// }}}
// {{{ DECSERA
template <typename Cell>
void Screen<Cell>::selectiveEraseArea(Rect area)
{
auto const [top, left, bottom, right] = applyOriginMode(area).clampTo(_state.pageSize);
assert(unbox<int>(right) <= unbox<int>(_state.pageSize.columns));
assert(unbox<int>(bottom) <= unbox<int>(_state.pageSize.lines));

if (top.value > bottom.value || left.value > right.value)
return;

for (int y = top.value; y <= bottom.value; ++y)
{
for (Cell& cell: grid()
.lineAt(LineOffset::cast_from(y))
.useRange(ColumnOffset::cast_from(left),
ColumnCount::cast_from(right.value - left.value + 1)))
{
if (!cell.isFlagEnabled(CellFlags::CharacterProtected))
{
cell.writeTextOnly(L' ', 1);
cell.setHyperlink(HyperlinkId(0));
}
}
}
}
// }}}

// {{{ EL
template <typename Cell>
void Screen<Cell>::clearToEndOfLine()
{
Expand Down Expand Up @@ -943,6 +1078,7 @@ void Screen<Cell>::clearLine()
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
_terminal.markRegionDirty(area);
}
// }}}

template <typename Cell>
void Screen<Cell>::moveCursorToNextLine(LineCount _n)
Expand Down Expand Up @@ -1821,9 +1957,11 @@ void Screen<Cell>::requestStatusString(RequestStatusString _value)
case RequestStatusString::DECSNLS: return fmt::format("{}*|", _state.pageSize.lines);
case RequestStatusString::SGR:
return fmt::format("0;{}m", vtSequenceParameterString(_state.cursor.graphicsRendition));
case RequestStatusString::DECSCA: // TODO
errorlog()(fmt::format("Requesting device status for {} not implemented yet.", _value));
break;
case RequestStatusString::DECSCA: {
auto const isProtected =
_state.cursor.graphicsRendition.styles & CellFlags::CharacterProtected;
return fmt::format("{}\"q", isProtected ? 1 : 2);
}
case RequestStatusString::DECSASD:
switch (_state.activeStatusDisplay)
{
Expand Down Expand Up @@ -3184,6 +3322,48 @@ ApplyResult Screen<Cell>::apply(FunctionDefinition const& function, Sequence con
break;
case DECDC: deleteColumns(seq.param_or(0, ColumnCount(1))); break;
case DECIC: insertColumns(seq.param_or(0, ColumnCount(1))); break;
case DECSCA: {
auto const Pc = seq.param_or(0, 0);
switch (Pc)
{
case 1:
_state.cursor.graphicsRendition.styles |= CellFlags::CharacterProtected;
return ApplyResult::Ok;
case 0:
case 2:
_state.cursor.graphicsRendition.styles &= ~CellFlags::CharacterProtected;
return ApplyResult::Ok;
default: return ApplyResult::Invalid;
}
}
case DECSED: {
switch (seq.param_or(0, Sequence::Parameter { 0 }))
{
case 0: selectiveEraseToEndOfScreen(); break;
case 1: selectiveEraseToBeginOfScreen(); break;
case 2: selectiveEraseScreen(); break;
default: return ApplyResult::Unsupported;
}
return ApplyResult::Ok;
}
case DECSERA: {
auto const top = seq.param_or(0, Top(1)) - 1;
auto const left = seq.param_or(1, Left(1)) - 1;
auto const bottom = seq.param_or(2, Bottom::cast_from(_state.pageSize.lines)) - 1;
auto const right = seq.param_or(3, Right::cast_from(_state.pageSize.columns)) - 1;
selectiveEraseArea(Rect { top, left, bottom, right });
return ApplyResult::Ok;
}
case DECSEL: {
switch (seq.param_or(0, Sequence::Parameter { 0 }))
{
case 0: selectiveEraseToEndOfLine(); break;
case 1: selectiveEraseToBeginOfLine(); break;
case 2: selectiveEraseLine(_state.cursor.position.line); break;
default: return ApplyResult::Invalid;
}
return ApplyResult::Ok;
}
case DECRM: {
ApplyResult r = ApplyResult::Ok;
crispy::for_each(crispy::times(seq.parameterCount()), [&](size_t i) {
Expand Down
47 changes: 47 additions & 0 deletions src/terminal/Screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase
void clearToEndOfScreen();
void clearScreen();

// DECSEL
void selectiveEraseToBeginOfLine();
void selectiveEraseToEndOfLine();
void selectiveEraseLine(LineOffset line);

// DECSED
void selectiveEraseToBeginOfScreen();
void selectiveEraseToEndOfScreen();
void selectiveEraseScreen();

void selectiveEraseArea(Rect area);

void selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end);
[[nodiscard]] bool containsProtectedCharacters(LineOffset line,
ColumnOffset begin,
ColumnOffset end) const;

void eraseCharacters(ColumnCount _n); // ECH
void insertCharacters(ColumnCount _n); // ICH
void deleteCharacters(ColumnCount _n); // DCH
Expand Down Expand Up @@ -293,6 +310,36 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase
return { pos.line + _state.margin.vertical.from, pos.column + _state.margin.horizontal.from };
}

[[nodiscard]] LineOffset applyOriginMode(LineOffset line) const noexcept
{
if (!_state.cursor.originMode)
return line;
else
return line + _state.margin.vertical.from;
}

[[nodiscard]] ColumnOffset applyOriginMode(ColumnOffset column) const noexcept
{
if (!_state.cursor.originMode)
return column;
else
return column + _state.margin.horizontal.from;
}

[[nodiscard]] Rect applyOriginMode(Rect area) const noexcept
{
if (!_state.cursor.originMode)
return area;

auto const top = Top::cast_from(area.top.value + _state.margin.vertical.from.value);
auto const left = Left::cast_from(area.top.value + _state.margin.horizontal.from.value);
auto const bottom = Bottom::cast_from(area.bottom.value + _state.margin.vertical.from.value);
auto const right = Right::cast_from(area.right.value + _state.margin.horizontal.from.value);
// TODO: Should this automatically clamp to margin's botom/right values?

return Rect { top, left, bottom, right };
}

/// Clamps given coordinates, respecting DECOM (Origin Mode).
CellLocation clampCoordinate(CellLocation coord) const noexcept
{
Expand Down
Loading

0 comments on commit 51120ec

Please sign in to comment.