Skip to content

Commit f2efe36

Browse files
Merge pull request #1890 from contour-terminal/feature/97-complete-deccir
Complete DECCIR (Cursor Information Report) response
2 parents 91fb661 + 38c4036 commit f2efe36

File tree

5 files changed

+243
-8
lines changed

5 files changed

+243
-8
lines changed

.github/actions/spelling/allow/terminal-terms.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ANSISYSSC
33
APC
44
APPCURSOR
55
APPKEYPAD
6+
BBBB
67
BSU
78
CELLBACKGROUND
89
CELLFOREGROUND
@@ -101,7 +102,9 @@ MODIFYOTHERKEYS
101102
NEL
102103
OSC
103104
Pgl
105+
pgl
104106
Pgr
107+
pgr
105108
RCOLORBG
106109
RCOLORCURSOR
107110
RCOLORFG
@@ -126,6 +129,7 @@ Setulc
126129
Satt
127130
satt
128131
Sdesig
132+
sdesig
129133
Sflag
130134
sflag
131135
Sixel

metainfo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
<li>Adds kitty's unscroll extension (CSI n + T) to restore scrolled-off content from scrollback history</li>
143143
<li>Adds natural momentum scrolling for touchpad gestures with configurable falloff via momentum_scrolling profile setting</li>
144144
<li>Adds Kitty OSC 99 desktop notification protocol with D-Bus backend on Linux, supporting structured metadata, chunked payloads, base64 encoding, urgency levels, display occasion filtering, bidirectional close/activation events, and query/alive responses</li>
145+
<li>Adds complete DECCIR (Cursor Information Report) response including character set designations, GL/GR mappings, and wrap-pending state (#97)</li>
145146
</ul>
146147
</description>
147148
</release>

src/vtbackend/Charset.h

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,33 @@ enum class CharsetTable : std::uint8_t
3434
G3 = 3
3535
};
3636

37-
/// @returns the charset
37+
/// @returns the charset mapping table for the given charset identifier.
3838
CharsetMap const* charsetMap(CharsetId id) noexcept;
3939

40+
/// @returns the SCS (Select Character Set) final character for the given charset identifier.
41+
///
42+
/// This is the final byte used in SCS escape sequences (e.g., 'B' for USASCII, '0' for Special).
43+
/// Used by DECCIR (Cursor Information Report) to encode the Sdesig field.
44+
constexpr char charsetDesignation(CharsetId id) noexcept
45+
{
46+
switch (id)
47+
{
48+
case CharsetId::Special: return '0';
49+
case CharsetId::British: return 'A';
50+
case CharsetId::Dutch: return '4';
51+
case CharsetId::Finnish: return 'C';
52+
case CharsetId::French: return 'R';
53+
case CharsetId::FrenchCanadian: return 'Q';
54+
case CharsetId::German: return 'K';
55+
case CharsetId::NorwegianDanish: return 'E';
56+
case CharsetId::Spanish: return 'Z';
57+
case CharsetId::Swedish: return 'H';
58+
case CharsetId::Swiss: return '=';
59+
case CharsetId::USASCII: return 'B';
60+
}
61+
return 'B'; // fallback to USASCII
62+
}
63+
4064
/// Charset mapping API for tables G0, G1, G2, and G3.
4165
///
4266
/// Relevant VT sequences are: SCS, SS2, SS3.
@@ -95,10 +119,24 @@ class CharsetMapping
95119
return isSelected(_tableForNextGraphic, id);
96120
}
97121

98-
// Selects a given designated character set into the table G0, G1, G2, or G3.
122+
/// Selects a given designated character set into the table G0, G1, G2, or G3.
99123
void select(CharsetTable table, CharsetId id) noexcept
100124
{
101125
_tables[static_cast<std::size_t>(table)] = charsetMap(id);
126+
_charsetIds[static_cast<std::size_t>(table)] = id;
127+
}
128+
129+
/// @returns the G-set table currently mapped to GL (the active locking shift).
130+
[[nodiscard]] constexpr CharsetTable selectedTable() const noexcept { return _selectedTable; }
131+
132+
/// @returns the G-set table used for the next graphic character (differs from selectedTable() after
133+
/// SS2/SS3).
134+
[[nodiscard]] constexpr CharsetTable tableForNextGraphic() const noexcept { return _tableForNextGraphic; }
135+
136+
/// @returns the CharsetId designated for the given G-set table.
137+
[[nodiscard]] constexpr CharsetId charsetIdOf(CharsetTable table) const noexcept
138+
{
139+
return _charsetIds[static_cast<std::size_t>(table)];
102140
}
103141

104142
private:
@@ -107,6 +145,9 @@ class CharsetMapping
107145

108146
using Tables = std::array<CharsetMap const*, 4>;
109147
Tables _tables;
148+
std::array<CharsetId, 4> _charsetIds = {
149+
CharsetId::USASCII, CharsetId::USASCII, CharsetId::USASCII, CharsetId::USASCII
150+
};
110151
};
111152

112153
} // namespace vtbackend

src/vtbackend/Screen.cpp

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2766,13 +2766,15 @@ namespace impl
27662766
else if (seq.param(0) == 1)
27672767
{
27682768
// DECCIR — Cursor Information Report
2769-
// Response: DCS 1 $ u <Pr>;<Pc>;<Pp>;<Srend>;<Satt>;<Sflag>;<Pgl>;<Pgr>;<Scss>;<Sdesig> ST
2769+
// Response: DCS 1 $ u Pr;Pc;Pp;Srend;Satt;Sflag;Pgl;Pgr;Scss;Sdesig ST
2770+
// See: https://vt100.net/docs/vt510-rm/DECCIR.html
27702771
auto const& cursor = screen.cursor();
27712772
auto const line = *cursor.position.line + 1;
27722773
auto const column = *cursor.position.column + 1;
27732774
auto const page = 1; // We only support one page
27742775

2775-
// Encode SGR flags as a bitmask character (0x40 base)
2776+
// Srend: visual attributes bitmask (0x40 base)
2777+
// Bit 1=Bold, Bit 2=Underline, Bit 3=Blinking, Bit 4=Inverse
27762778
auto const& flags = cursor.graphicsRendition.flags;
27772779
int srendBits = 0;
27782780
if (flags.test(CellFlag::Bold))
@@ -2785,19 +2787,54 @@ namespace impl
27852787
srendBits |= 8;
27862788
auto const srend = static_cast<char>(0x40 + srendBits);
27872789

2788-
// Protection attribute
2790+
// Satt: protection attribute (Bit 1 = DECSCA protection)
27892791
auto const satt = flags.test(CellFlag::CharacterProtected) ? static_cast<char>(0x41)
27902792
: static_cast<char>(0x40);
27912793

2792-
// Flags: origin mode, auto-wrap, selective erase
2794+
// Sflag: Bit 1=DECOM, Bit 2=SS2, Bit 3=SS3, Bit 4=wrap pending
2795+
auto const& charsets = cursor.charsets;
27932796
int sflagBits = 0;
27942797
if (cursor.originMode)
27952798
sflagBits |= 1;
2796-
if (cursor.autoWrap)
2799+
if (charsets.tableForNextGraphic() != charsets.selectedTable())
2800+
{
2801+
if (charsets.tableForNextGraphic() == CharsetTable::G2)
2802+
sflagBits |= 2; // SS2 active
2803+
else if (charsets.tableForNextGraphic() == CharsetTable::G3)
2804+
sflagBits |= 4; // SS3 active
2805+
}
2806+
if (cursor.wrapPending)
27972807
sflagBits |= 8;
27982808
auto const sflag = static_cast<char>(0x40 + sflagBits);
27992809

2800-
screen.reply("\033P1$u{};{};{};{};{};{}\033\\", line, column, page, srend, satt, sflag);
2810+
// Pgl: GL charset table index (0=G0, 1=G1, 2=G2, 3=G3)
2811+
auto const pgl = static_cast<int>(charsets.selectedTable());
2812+
2813+
// Pgr: GR charset table index (GR not tracked; default to G2 per VT standard)
2814+
auto const pgr = 2;
2815+
2816+
// Scss: character set size for each G-set (0x40 base)
2817+
// Bit N = G(N-1) size: 0=94-char, 1=96-char. All supported charsets are 94-char.
2818+
auto const scss = static_cast<char>(0x40);
2819+
2820+
// Sdesig: SCS designation final characters for G0 through G3
2821+
auto const sdesig =
2822+
std::string { charsetDesignation(charsets.charsetIdOf(CharsetTable::G0)),
2823+
charsetDesignation(charsets.charsetIdOf(CharsetTable::G1)),
2824+
charsetDesignation(charsets.charsetIdOf(CharsetTable::G2)),
2825+
charsetDesignation(charsets.charsetIdOf(CharsetTable::G3)) };
2826+
2827+
screen.reply("\033P1$u{};{};{};{};{};{};{};{};{};{}\033\\",
2828+
line,
2829+
column,
2830+
page,
2831+
srend,
2832+
satt,
2833+
sflag,
2834+
pgl,
2835+
pgr,
2836+
scss,
2837+
sdesig);
28012838
return ApplyResult::Ok;
28022839
}
28032840
else if (seq.param(0) == 2)

src/vtbackend/Screen_test.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4050,6 +4050,158 @@ TEST_CASE("HorizontalTab.AfterScreenClear", "[screen]")
40504050
CHECK("X Y \n \n" == screen.renderMainPageText());
40514051
}
40524052

4053+
// {{{ DECCIR — Cursor Information Report
4054+
4055+
TEST_CASE("DECCIR.default_state", "[screen]")
4056+
{
4057+
// Verify DECCIR response with all defaults: cursor at (1,1), no attributes, no wrap pending,
4058+
// GL=G0, GR=G2, all charsets USASCII.
4059+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4060+
4061+
mock.writeToScreen(DECRQPSR(1));
4062+
4063+
// Expected: DCS 1 $ u 1;1;1;@;@;@;0;2;@;BBBB ST
4064+
// Pr=1, Pc=1, Pp=1
4065+
// Srend='@' (0x40, no attributes)
4066+
// Satt='@' (0x40, no protection)
4067+
// Sflag='@' (0x40, no flags)
4068+
// Pgl=0 (G0 in GL)
4069+
// Pgr=2 (G2 in GR, default)
4070+
// Scss='@' (0x40, all 94-char sets)
4071+
// Sdesig="BBBB" (all USASCII)
4072+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;@;@;0;2;@;BBBB\033\\"));
4073+
}
4074+
4075+
TEST_CASE("DECCIR.cursor_position", "[screen]")
4076+
{
4077+
// Verify DECCIR correctly reports cursor position after movement.
4078+
auto mock = MockTerm { PageSize { LineCount(5), ColumnCount(10) } };
4079+
4080+
mock.writeToScreen(CUP(3, 7)); // Move to line 3, column 7
4081+
4082+
mock.writeToScreen(DECRQPSR(1));
4083+
4084+
// Pr=3, Pc=7
4085+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u3;7;1;@;@;@;0;2;@;BBBB\033\\"));
4086+
}
4087+
4088+
TEST_CASE("DECCIR.bold_and_underline", "[screen]")
4089+
{
4090+
// Verify Srend field encodes bold (bit 1) and underline (bit 2).
4091+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4092+
4093+
mock.writeToScreen(SGR(1)); // Bold
4094+
mock.writeToScreen(SGR(4)); // Underline
4095+
mock.writeToScreen(DECRQPSR(1));
4096+
4097+
// Srend = 0x40 + 0x01 (bold) + 0x02 (underline) = 0x43 = 'C'
4098+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;C;@;@;0;2;@;BBBB\033\\"));
4099+
}
4100+
4101+
TEST_CASE("DECCIR.blinking_and_inverse", "[screen]")
4102+
{
4103+
// Verify Srend field encodes blinking (bit 3) and inverse (bit 4).
4104+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4105+
4106+
mock.writeToScreen(SGR(5)); // Blinking
4107+
mock.writeToScreen(SGR(7)); // Inverse
4108+
mock.writeToScreen(DECRQPSR(1));
4109+
4110+
// Srend = 0x40 + 0x04 (blink) + 0x08 (inverse) = 0x4C = 'L'
4111+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;L;@;@;0;2;@;BBBB\033\\"));
4112+
}
4113+
4114+
TEST_CASE("DECCIR.all_rendition_attributes", "[screen]")
4115+
{
4116+
// Verify Srend field with all attributes enabled: bold+underline+blink+inverse.
4117+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4118+
4119+
mock.writeToScreen(SGR(1)); // Bold
4120+
mock.writeToScreen(SGR(4)); // Underline
4121+
mock.writeToScreen(SGR(5)); // Blinking
4122+
mock.writeToScreen(SGR(7)); // Inverse
4123+
mock.writeToScreen(DECRQPSR(1));
4124+
4125+
// Srend = 0x40 + 0x01 + 0x02 + 0x04 + 0x08 = 0x4F = 'O'
4126+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;O;@;@;0;2;@;BBBB\033\\"));
4127+
}
4128+
4129+
TEST_CASE("DECCIR.character_protection", "[screen]")
4130+
{
4131+
// Verify Satt field reports DECSCA character protection attribute.
4132+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4133+
4134+
mock.writeToScreen(DECSCA(1)); // Enable character protection
4135+
mock.writeToScreen(DECRQPSR(1));
4136+
4137+
// Satt = 0x41 = 'A' (bit 1 set for protection)
4138+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;A;@;0;2;@;BBBB\033\\"));
4139+
}
4140+
4141+
TEST_CASE("DECCIR.origin_mode", "[screen]")
4142+
{
4143+
// Verify Sflag bit 1 reports origin mode (DECOM).
4144+
auto mock = MockTerm { PageSize { LineCount(5), ColumnCount(10) } };
4145+
4146+
mock.writeToScreen(DECSM(toDECModeNum(DECMode::Origin)));
4147+
mock.writeToScreen(DECRQPSR(1));
4148+
4149+
// Sflag = 0x40 + 0x01 = 0x41 = 'A' (origin mode set)
4150+
// Note: cursor is at (1,1) because origin mode homes the cursor.
4151+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;@;A;0;2;@;BBBB\033\\"));
4152+
}
4153+
4154+
TEST_CASE("DECCIR.wrap_pending", "[screen]")
4155+
{
4156+
// Verify Sflag bit 4 reports wrap-pending state.
4157+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(5) } };
4158+
4159+
// Write exactly enough characters to reach the right margin and trigger wrap pending.
4160+
mock.writeToScreen("ABCDE");
4161+
mock.writeToScreen(DECRQPSR(1));
4162+
4163+
// Cursor is at column 5, wrap pending. Sflag = 0x40 + 0x08 = 0x48 = 'H'
4164+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;5;1;@;@;H;0;2;@;BBBB\033\\"));
4165+
}
4166+
4167+
TEST_CASE("DECCIR.charset_designation_special", "[screen]")
4168+
{
4169+
// Verify Sdesig reports DEC Special charset when designated into G0.
4170+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4171+
4172+
mock.writeToScreen(SCS_G0_SPECIAL()); // Designate G0 = DEC Special
4173+
mock.writeToScreen(DECRQPSR(1));
4174+
4175+
// Sdesig: G0='0' (Special), G1-G3='B' (USASCII)
4176+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;@;@;0;2;@;0BBB\033\\"));
4177+
}
4178+
4179+
TEST_CASE("DECCIR.charset_designation_g1", "[screen]")
4180+
{
4181+
// Verify Sdesig reports charset designated into G1.
4182+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4183+
4184+
mock.writeToScreen(SCS_G1_SPECIAL()); // Designate G1 = DEC Special
4185+
mock.writeToScreen(DECRQPSR(1));
4186+
4187+
// Sdesig: G0='B', G1='0' (Special), G2-G3='B'
4188+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;@;@;0;2;@;B0BB\033\\"));
4189+
}
4190+
4191+
TEST_CASE("DECCIR.gl_charset_after_locking_shift", "[screen]")
4192+
{
4193+
// Verify Pgl reports G1 after a locking shift (SO → LS1 maps G1 into GL).
4194+
auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(10) } };
4195+
4196+
mock.writeToScreen("\x0E"); // SO (Shift Out) = LS1 → map G1 into GL
4197+
mock.writeToScreen(DECRQPSR(1));
4198+
4199+
// Pgl=1 (G1 in GL)
4200+
CHECK(e(mock.terminal.peekInput()) == e("\033P1$u1;1;1;@;@;@;1;2;@;BBBB\033\\"));
4201+
}
4202+
4203+
// }}} DECCIR
4204+
40534205
// NOLINTEND(misc-const-correctness,readability-function-cognitive-complexity)
40544206

40554207
// NOLINTBEGIN(misc-const-correctness)

0 commit comments

Comments
 (0)