Skip to content

Commit 51120ec

Browse files
Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31).
1 parent 3e441c9 commit 51120ec

File tree

8 files changed

+448
-44
lines changed

8 files changed

+448
-44
lines changed

metainfo.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@
108108
<li>Fixes vertical cursor movement for Sixel graphics with only newlines (#822).</li>
109109
<li>Fixes Sixel rendering for images with aspect ratios other than 1:1.</li>
110110
<li>Fixes Sixel rendering for images that show below but should be rendered above text (#831).</li>
111-
<li>Removes `images.sixel_cursor_conformance` config option.</li>
111+
<li>Removes `images.sixel_cursor_conformance` config option.</li>
112+
<li>Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31).</li>
112113
</ul>
113114
</description>
114115
</release>

src/terminal/Cell.h

+12-3
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ class CONTOUR_PACKED Cell
120120
uint8_t _width,
121121
HyperlinkId _hyperlink) noexcept;
122122

123+
void writeTextOnly(char32_t _ch, uint8_t _width) noexcept;
124+
123125
std::u32string codepoints() const;
124126
char32_t codepoint(size_t i) const noexcept;
125127
std::size_t codepointCount() const noexcept;
@@ -292,12 +294,11 @@ inline void Cell::write(GraphicsAttributes const& _attributes,
292294
uint8_t _width,
293295
HyperlinkId _hyperlink) noexcept
294296
{
295-
setWidth(_width);
296-
297-
codepoint_ = _ch;
297+
writeTextOnly(_ch, _width);
298298
if (extra_)
299299
{
300300
extra_->codepoints.clear();
301+
// Writing text into a cell destroys the image fragment (as least for Sixels).
301302
extra_->imageFragment = {};
302303
}
303304

@@ -314,6 +315,14 @@ inline void Cell::write(GraphicsAttributes const& _attributes,
314315
}
315316
}
316317

318+
inline void Cell::writeTextOnly(char32_t _ch, uint8_t _width) noexcept
319+
{
320+
setWidth(_width);
321+
codepoint_ = _ch;
322+
if (extra_)
323+
extra_->codepoints.clear();
324+
}
325+
317326
inline void Cell::reset(GraphicsAttributes const& _attributes, HyperlinkId _hyperlink) noexcept
318327
{
319328
codepoint_ = 0;

src/terminal/CellFlags.h

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
namespace terminal
2424
{
2525

26-
enum class CellFlags : uint16_t
26+
enum class CellFlags : uint32_t
2727
{
2828
None = 0,
2929

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

4849
constexpr CellFlags& operator|=(CellFlags& a, CellFlags b) noexcept
@@ -101,7 +102,7 @@ struct formatter<terminal::CellFlags>
101102
template <typename FormatContext>
102103
auto format(const terminal::CellFlags _flags, FormatContext& ctx)
103104
{
104-
static const std::array<std::pair<terminal::CellFlags, std::string_view>, 16> nameMap = {
105+
static const std::array<std::pair<terminal::CellFlags, std::string_view>, 17> nameMap = {
105106
std::pair { terminal::CellFlags::Bold, std::string_view("Bold") },
106107
std::pair { terminal::CellFlags::Faint, std::string_view("Faint") },
107108
std::pair { terminal::CellFlags::Italic, std::string_view("Italic") },
@@ -118,6 +119,7 @@ struct formatter<terminal::CellFlags>
118119
std::pair { terminal::CellFlags::Framed, std::string_view("Framed") },
119120
std::pair { terminal::CellFlags::Encircled, std::string_view("Encircled") },
120121
std::pair { terminal::CellFlags::Overline, std::string_view("Overline") },
122+
std::pair { terminal::CellFlags::CharacterProtected, std::string_view("CharacterProtected") },
121123
};
122124
std::string s;
123125
for (auto const& mapping: nameMap)

src/terminal/Functions.h

+8
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ constexpr inline auto DECERA = detail::CSI(std::nullopt, 0, 4, '$', 'z', VT
376376
constexpr inline auto DECFRA = detail::CSI(std::nullopt, 0, 4, '$', 'x', VTType::VT420, "DECFRA", "Fill rectangular area");
377377
constexpr inline auto DECDC = detail::CSI(std::nullopt, 0, 1, '\'', '~', VTType::VT420, "DECDC", "Delete column");
378378
constexpr inline auto DECIC = detail::CSI(std::nullopt, 0, 1, '\'', '}', VTType::VT420, "DECIC", "Insert column");
379+
constexpr inline auto DECSCA = detail::CSI(std::nullopt, 0, 1, '"', 'q', VTType::VT240, "DECSCA", "Select Character Protection Attribute");
380+
constexpr inline auto DECSED = detail::CSI('?', 0, 1, std::nullopt, 'J', VTType::VT240, "DECSED", "Selective Erase in Display");
381+
constexpr inline auto DECSERA = detail::CSI(std::nullopt, 0, 4, '$', '{', VTType::VT240, "DECSERA", "Selective Erase in Rectangular Area");
382+
constexpr inline auto DECSEL = detail::CSI('?', 0, 1, std::nullopt, 'K', VTType::VT240, "DECSEL", "Selective Erase in Line");
379383
constexpr inline auto XTRESTORE = detail::CSI('?', 0, ArgsMax, std::nullopt, 'r', VTExtension::XTerm, "XTRESTORE", "Restore DEC private modes.");
380384
constexpr inline auto XTSAVE = detail::CSI('?', 0, ArgsMax, std::nullopt, 's', VTExtension::XTerm, "XTSAVE", "Save DEC private modes.");
381385
constexpr inline auto DECRM = detail::CSI('?', 1, ArgsMax, std::nullopt, 'l', VTType::VT100, "DECRM", "Reset DEC-mode");
@@ -527,6 +531,10 @@ inline auto const& functions() noexcept
527531
DECERA,
528532
DECFRA,
529533
DECIC,
534+
DECSCA,
535+
DECSED,
536+
DECSERA,
537+
DECSEL,
530538
XTRESTORE,
531539
XTSAVE,
532540
DECPS,

src/terminal/Screen.cpp

+183-3
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,7 @@ void Screen<Cell>::sendTerminalId()
838838
_terminal.reply("\033[>{};{};{}c", Pp, Pv, Pc);
839839
}
840840

841+
// {{{ ED
841842
template <typename Cell>
842843
void Screen<Cell>::clearToEndOfScreen()
843844
{
@@ -871,6 +872,7 @@ void Screen<Cell>::clearScreen()
871872
// up in case the content is still needed.
872873
scrollUp(_state.pageSize.lines);
873874
}
875+
// }}}
874876

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

895+
// {{{ DECSEL
896+
template <typename Cell>
897+
void Screen<Cell>::selectiveEraseToEndOfLine()
898+
{
899+
if (_terminal.isFullHorizontalMargins() && _state.cursor.position.column.value == 0)
900+
selectiveEraseLine(_state.cursor.position.line);
901+
else
902+
selectiveErase(_state.cursor.position.line,
903+
_state.cursor.position.column,
904+
ColumnOffset::cast_from(_state.pageSize.columns));
905+
}
906+
907+
template <typename Cell>
908+
void Screen<Cell>::selectiveEraseToBeginOfLine()
909+
{
910+
if (_terminal.isFullHorizontalMargins()
911+
&& _state.cursor.position.column.value == _state.pageSize.columns.value)
912+
selectiveEraseLine(_state.cursor.position.line);
913+
else
914+
selectiveErase(_state.cursor.position.line, ColumnOffset(0), _state.cursor.position.column + 1);
915+
}
916+
917+
template <typename Cell>
918+
void Screen<Cell>::selectiveEraseLine(LineOffset line)
919+
{
920+
if (containsProtectedCharacters(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns)))
921+
{
922+
selectiveErase(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns));
923+
return;
924+
}
925+
926+
currentLine().reset(grid().defaultLineFlags(), _state.cursor.graphicsRendition);
927+
928+
auto const left = ColumnOffset(0);
929+
auto const right = boxed_cast<ColumnOffset>(_state.pageSize.columns - 1);
930+
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
931+
_terminal.markRegionDirty(area);
932+
}
933+
934+
template <typename Cell>
935+
void Screen<Cell>::selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end)
936+
{
937+
Cell* i = &at(line, begin);
938+
Cell const* e = i + unbox<uintptr_t>(end - begin);
939+
while (i != e)
940+
{
941+
if (i->isFlagEnabled(CellFlags::CharacterProtected))
942+
{
943+
++i;
944+
continue;
945+
}
946+
i->reset(_state.cursor.graphicsRendition);
947+
++i;
948+
}
949+
950+
auto const left = begin;
951+
auto const right = end - 1;
952+
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
953+
_terminal.markRegionDirty(area);
954+
}
955+
956+
template <typename Cell>
957+
bool Screen<Cell>::containsProtectedCharacters(LineOffset line, ColumnOffset begin, ColumnOffset end) const
958+
{
959+
Cell const* i = &at(line, begin);
960+
Cell const* e = i + unbox<uintptr_t>(end - begin);
961+
while (i != e)
962+
{
963+
if (i->isFlagEnabled(CellFlags::CharacterProtected))
964+
return true;
965+
++i;
966+
}
967+
return false;
968+
}
969+
// }}}
970+
// {{{ DECSED
971+
template <typename Cell>
972+
void Screen<Cell>::selectiveEraseToEndOfScreen()
973+
{
974+
selectiveEraseToEndOfLine();
975+
976+
auto const lineStart = unbox<int>(_state.cursor.position.line) + 1;
977+
auto const lineEnd = unbox<int>(_state.pageSize.lines);
978+
979+
for (auto const lineOffset: ranges::views::iota(lineStart, lineEnd))
980+
selectiveEraseLine(LineOffset::cast_from(lineOffset));
981+
}
982+
983+
template <typename Cell>
984+
void Screen<Cell>::selectiveEraseToBeginOfScreen()
985+
{
986+
selectiveEraseToBeginOfLine();
987+
988+
for (auto const lineOffset: ranges::views::iota(0, *_state.cursor.position.line))
989+
selectiveEraseLine(LineOffset::cast_from(lineOffset));
990+
}
991+
992+
template <typename Cell>
993+
void Screen<Cell>::selectiveEraseScreen()
994+
{
995+
for (auto const lineOffset: ranges::views::iota(0, *_state.pageSize.lines))
996+
selectiveEraseLine(LineOffset::cast_from(lineOffset));
997+
}
998+
// }}}
999+
// {{{ DECSERA
1000+
template <typename Cell>
1001+
void Screen<Cell>::selectiveEraseArea(Rect area)
1002+
{
1003+
auto const [top, left, bottom, right] = applyOriginMode(area).clampTo(_state.pageSize);
1004+
assert(unbox<int>(right) <= unbox<int>(_state.pageSize.columns));
1005+
assert(unbox<int>(bottom) <= unbox<int>(_state.pageSize.lines));
1006+
1007+
if (top.value > bottom.value || left.value > right.value)
1008+
return;
1009+
1010+
for (int y = top.value; y <= bottom.value; ++y)
1011+
{
1012+
for (Cell& cell: grid()
1013+
.lineAt(LineOffset::cast_from(y))
1014+
.useRange(ColumnOffset::cast_from(left),
1015+
ColumnCount::cast_from(right.value - left.value + 1)))
1016+
{
1017+
if (!cell.isFlagEnabled(CellFlags::CharacterProtected))
1018+
{
1019+
cell.writeTextOnly(L' ', 1);
1020+
cell.setHyperlink(HyperlinkId(0));
1021+
}
1022+
}
1023+
}
1024+
}
1025+
// }}}
1026+
1027+
// {{{ EL
8931028
template <typename Cell>
8941029
void Screen<Cell>::clearToEndOfLine()
8951030
{
@@ -943,6 +1078,7 @@ void Screen<Cell>::clearLine()
9431078
auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) };
9441079
_terminal.markRegionDirty(area);
9451080
}
1081+
// }}}
9461082

9471083
template <typename Cell>
9481084
void Screen<Cell>::moveCursorToNextLine(LineCount _n)
@@ -1821,9 +1957,11 @@ void Screen<Cell>::requestStatusString(RequestStatusString _value)
18211957
case RequestStatusString::DECSNLS: return fmt::format("{}*|", _state.pageSize.lines);
18221958
case RequestStatusString::SGR:
18231959
return fmt::format("0;{}m", vtSequenceParameterString(_state.cursor.graphicsRendition));
1824-
case RequestStatusString::DECSCA: // TODO
1825-
errorlog()(fmt::format("Requesting device status for {} not implemented yet.", _value));
1826-
break;
1960+
case RequestStatusString::DECSCA: {
1961+
auto const isProtected =
1962+
_state.cursor.graphicsRendition.styles & CellFlags::CharacterProtected;
1963+
return fmt::format("{}\"q", isProtected ? 1 : 2);
1964+
}
18271965
case RequestStatusString::DECSASD:
18281966
switch (_state.activeStatusDisplay)
18291967
{
@@ -3184,6 +3322,48 @@ ApplyResult Screen<Cell>::apply(FunctionDefinition const& function, Sequence con
31843322
break;
31853323
case DECDC: deleteColumns(seq.param_or(0, ColumnCount(1))); break;
31863324
case DECIC: insertColumns(seq.param_or(0, ColumnCount(1))); break;
3325+
case DECSCA: {
3326+
auto const Pc = seq.param_or(0, 0);
3327+
switch (Pc)
3328+
{
3329+
case 1:
3330+
_state.cursor.graphicsRendition.styles |= CellFlags::CharacterProtected;
3331+
return ApplyResult::Ok;
3332+
case 0:
3333+
case 2:
3334+
_state.cursor.graphicsRendition.styles &= ~CellFlags::CharacterProtected;
3335+
return ApplyResult::Ok;
3336+
default: return ApplyResult::Invalid;
3337+
}
3338+
}
3339+
case DECSED: {
3340+
switch (seq.param_or(0, Sequence::Parameter { 0 }))
3341+
{
3342+
case 0: selectiveEraseToEndOfScreen(); break;
3343+
case 1: selectiveEraseToBeginOfScreen(); break;
3344+
case 2: selectiveEraseScreen(); break;
3345+
default: return ApplyResult::Unsupported;
3346+
}
3347+
return ApplyResult::Ok;
3348+
}
3349+
case DECSERA: {
3350+
auto const top = seq.param_or(0, Top(1)) - 1;
3351+
auto const left = seq.param_or(1, Left(1)) - 1;
3352+
auto const bottom = seq.param_or(2, Bottom::cast_from(_state.pageSize.lines)) - 1;
3353+
auto const right = seq.param_or(3, Right::cast_from(_state.pageSize.columns)) - 1;
3354+
selectiveEraseArea(Rect { top, left, bottom, right });
3355+
return ApplyResult::Ok;
3356+
}
3357+
case DECSEL: {
3358+
switch (seq.param_or(0, Sequence::Parameter { 0 }))
3359+
{
3360+
case 0: selectiveEraseToEndOfLine(); break;
3361+
case 1: selectiveEraseToBeginOfLine(); break;
3362+
case 2: selectiveEraseLine(_state.cursor.position.line); break;
3363+
default: return ApplyResult::Invalid;
3364+
}
3365+
return ApplyResult::Ok;
3366+
}
31873367
case DECRM: {
31883368
ApplyResult r = ApplyResult::Ok;
31893369
crispy::for_each(crispy::times(seq.parameterCount()), [&](size_t i) {

src/terminal/Screen.h

+47
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase
144144
void clearToEndOfScreen();
145145
void clearScreen();
146146

147+
// DECSEL
148+
void selectiveEraseToBeginOfLine();
149+
void selectiveEraseToEndOfLine();
150+
void selectiveEraseLine(LineOffset line);
151+
152+
// DECSED
153+
void selectiveEraseToBeginOfScreen();
154+
void selectiveEraseToEndOfScreen();
155+
void selectiveEraseScreen();
156+
157+
void selectiveEraseArea(Rect area);
158+
159+
void selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end);
160+
[[nodiscard]] bool containsProtectedCharacters(LineOffset line,
161+
ColumnOffset begin,
162+
ColumnOffset end) const;
163+
147164
void eraseCharacters(ColumnCount _n); // ECH
148165
void insertCharacters(ColumnCount _n); // ICH
149166
void deleteCharacters(ColumnCount _n); // DCH
@@ -293,6 +310,36 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase
293310
return { pos.line + _state.margin.vertical.from, pos.column + _state.margin.horizontal.from };
294311
}
295312

313+
[[nodiscard]] LineOffset applyOriginMode(LineOffset line) const noexcept
314+
{
315+
if (!_state.cursor.originMode)
316+
return line;
317+
else
318+
return line + _state.margin.vertical.from;
319+
}
320+
321+
[[nodiscard]] ColumnOffset applyOriginMode(ColumnOffset column) const noexcept
322+
{
323+
if (!_state.cursor.originMode)
324+
return column;
325+
else
326+
return column + _state.margin.horizontal.from;
327+
}
328+
329+
[[nodiscard]] Rect applyOriginMode(Rect area) const noexcept
330+
{
331+
if (!_state.cursor.originMode)
332+
return area;
333+
334+
auto const top = Top::cast_from(area.top.value + _state.margin.vertical.from.value);
335+
auto const left = Left::cast_from(area.top.value + _state.margin.horizontal.from.value);
336+
auto const bottom = Bottom::cast_from(area.bottom.value + _state.margin.vertical.from.value);
337+
auto const right = Right::cast_from(area.right.value + _state.margin.horizontal.from.value);
338+
// TODO: Should this automatically clamp to margin's botom/right values?
339+
340+
return Rect { top, left, bottom, right };
341+
}
342+
296343
/// Clamps given coordinates, respecting DECOM (Origin Mode).
297344
CellLocation clampCoordinate(CellLocation coord) const noexcept
298345
{

0 commit comments

Comments
 (0)