From 8464257c062255372c8ce375c6157f0467e96f4d Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Mon, 31 Aug 2026 22:04:16 -0400 Subject: [PATCH] Answer the last five vttest queries, and trim blanks from area checksums Fixes #123 Fixes #128 Fixes #139 Fixes #142 Fixes #145 The remaining open issues, all found sweeping vttest. Four are queries that produced silence; the fifth is an answer that was wrong by a fixed amount. DA3, CSI = c, was not in the identifier table, so it never reached the handler that was already shaped for it. It now replies DCS ! | 00000000 ST -- xterm's DECRPTUI, zeros for the site code and serial number, because there is no unit to identify. VT400 and up, as in xterm: the control arrived with the VT420, and a program that dropped the level with DECSCL is entitled to a VT200's silence. A non-zero parameter is still ignored, since that is another terminal's reply arriving on our input. DECRQCRA counted every blank cell. The correct rule is that a run of blanks at the end of a row is trimmed rather than summed, so a blank is worth 0x20 only when something further along its own row follows it. That is the whole of the discrepancy vttest reported: its own test computes the two rows it checks with the blanks worth nothing, and this emulator came back exactly 95 spaces high. The first cell counted is exempt and adds whatever it holds. That is not decoration -- esctest reads content back one cell at a time and expects a written space as 0x20, so trimming unconditionally would answer zero for every space on screen. vttest builds its expectation with the same exception in the same place. xterm and its own documentation disagree here, and the divergence is recorded on the method. xtermCheckRect only ever accumulates its held-back run under csNOTRIM -- the flag meaning do not trim at all, under which the trimmed total is then discarded -- so its default mode drops interior blanks too. The two readings agree on every screen either tool checks; this follows the documented one. Attributes still contribute nothing, where xterm weights six of them into the cell value, because esctest compares a cell's checksum to the bare codepoint of the character it expects. DECRQDE answers CSI rows ; cols ; 1 ; 1 ; 1 " w. The window is the page, so the corner is 1;1 and there is one page -- the DEC spelling of what CSI 18 t has always answered in the dtterm dialect, and the only one of the two forms that stayed quiet. DECRQUPSS answers DCS 1 ! u A ST. The choice UPSS offers is DEC Supplemental or ISO Latin-1, and a UTF-8 terminal reaches Latin-1, which is the conclusion xterm reaches for the same reason. DECAUPSS is not implemented, so the default is the only value this can report; saying so still beats the silence a client waits on forever. DECRQTSR answers DCS 0 $ s ST. DECTSR serialises state for DECRSTS to restore, there is no DECRSTS, and a terminal that cannot restore state has no business claiming it can report it -- so this is a refusal rather than a payload nothing can consume. But refusing is not saying nothing: DECRQSS already declines with DCS 0 $ r ST, and this is that shape one final character over. Ps absent or zero asks for nothing and gets nothing, per the VT510 manual. DECCARA and DECRARA were the two members of the rectangle family that change attributes rather than characters, and both were missing. They honour DECSACE, which until now was parsed, stored, reported back by DECRQSS and read by nothing: 2 is the rectangle the coordinates describe, anything else the stream running from the top-left position to the bottom-right one across whole intervening lines. Only DEC's set is touched -- 0, 1, 4, 5, 7, 8 and the resets 22, 24, 25, 27, 28, with the resets ignored under DECRARA because reversing an attribute already says both directions. Parameter 0 covers xterm's SGR_MASK, which is bold, underline, blink and inverse, and deliberately not invisible. Two things fell out of giving DECSACE teeth. It now resets on RIS and not on DECSTR, which is where xterm clears it -- the full-reset branch of ReallyReset and nowhere else -- and a stale rectangle setting would otherwise turn the next program's stream into a box. And unlike xterm every cell in the area is marked, the trailing half of a wide character included: xterm skips cells it has never drawn, but a line here is born full of spaces, so the only cell that reads as empty is that half, and skipping it would leave one character disagreeing with itself about its own rendition. OSC 50 asks the host through a new FontQueryRequested event, on the OSC 52 model -- the emulator has no font, whatever draws its cells does. The part worth having is what happens when the host declines: xterm answers a font query it cannot satisfy with a NAMELESS OSC 50, and vttest reads exactly that, skipping replies that carry no name. An indexed query is always nameless, since there is no font menu to index, and the host is not asked. Setting a font is left to the host and reported as unrecognised, so a listener on OscReceived can act on it without having to work out whether the terminal already did. Co-Authored-By: Claude Opus 5 --- src/XTerm.NET.Tests/ChecksumReportTests.cs | 59 ++++++- .../Common/CsiCommandExtensionsTests.cs | 3 +- src/XTerm.NET.Tests/DeviceReportTests.cs | 63 +++++++ src/XTerm.NET.Tests/InputHandlerTests.cs | 49 +++++- src/XTerm.NET.Tests/OscSequenceTests.cs | 89 ++++++++++ src/XTerm.NET.Tests/RectangleOpsTests.cs | 164 ++++++++++++++++++ src/XTerm.NET/Common/CommandExtensions.cs | 6 + src/XTerm.NET/Common/CsiCommand.cs | 18 +- src/XTerm.NET/Common/OscCommand.cs | 8 + src/XTerm.NET/Events/TerminalEvents.cs | 21 +++ src/XTerm.NET/InputHandler.Csi.cs | 130 ++++++++++++-- src/XTerm.NET/InputHandler.Osc.cs | 41 +++++ src/XTerm.NET/InputHandler.Rectangles.cs | 117 ++++++++++++- src/XTerm.NET/InputHandler.StoredModes.cs | 19 +- src/XTerm.NET/InputHandler.cs | 44 ++++- src/XTerm.NET/Terminal.cs | 23 +++ 16 files changed, 814 insertions(+), 40 deletions(-) diff --git a/src/XTerm.NET.Tests/ChecksumReportTests.cs b/src/XTerm.NET.Tests/ChecksumReportTests.cs index 66713b9..ef7578c 100644 --- a/src/XTerm.NET.Tests/ChecksumReportTests.cs +++ b/src/XTerm.NET.Tests/ChecksumReportTests.cs @@ -50,13 +50,57 @@ public void A_rects_checksum_is_the_sum_of_its_characters() } [Fact] - public void A_cell_nothing_ever_wrote_counts_as_a_space() + public void A_run_of_blanks_counts_once_for_the_first_cell() { - // Erased and never-written alike: DEC terminals trim trailing blanks and esctest's client - // side reasons that away, but only if blanks come back as spaces rather than zeros. + // DEC terminals trim the blanks at the end of a row rather than counting them, and the + // first cell of the area is the documented exception -- it counts whatever it holds, which + // is what lets esctest read a written space back as 0x20 one cell at a time. var terminal = NewTerminal(); - Assert.Equal(Report(2, 3 * 0x20), Reply(terminal, $"{Esc}[2;0;2;1;2;3*y")); + Assert.Equal(Report(2, 0x20), Reply(terminal, $"{Esc}[2;0;2;1;2;3*y")); + } + + [Fact] + public void A_single_blank_cell_is_a_space() + { + // The shape esctest reads content back in: one cell at a time, expecting the character it + // put there. Trimming that unconditionally would answer zero for every space on screen. + var terminal = NewTerminal(); + + Assert.Equal(Report(8, 0x20), Reply(terminal, $"{Esc}[8;0;2;1;2;1*y")); + } + + [Fact] + public void A_blank_between_two_characters_counts() + { + // Only TRAILING blanks are trimmed. One with text still to come on its row is interior, + // and vttest computes its expectation the same way. + var terminal = NewTerminal(); + terminal.Write("a b"); + + Assert.Equal(Report(9, Sum("a b")), Reply(terminal, $"{Esc}[9;0;1;1;1;3*y")); + } + + [Fact] + public void Blanks_trailing_a_row_are_trimmed() + { + // The same three cells as above with the tail cut off: 'a', a blank, and nothing after it + // on the row. + var terminal = NewTerminal(); + terminal.Write("a b"); + + Assert.Equal(Report(10, 'a'), Reply(terminal, $"{Esc}[10;0;1;1;1;2*y")); + } + + [Fact] + public void A_rows_trailing_blanks_do_not_carry_into_the_next() + { + // Trimming is per row: the blanks after "hi" end with row 1 rather than being revived by + // the "there" on row 2. + var terminal = NewTerminal(cols: 8, rows: 2); + terminal.Write($"hi{Esc}[2;1Hthere"); + + Assert.Equal(Report(11, Sum("hi") + Sum("there")), Reply(terminal, $"{Esc}[11*y")); } [Fact] @@ -76,8 +120,9 @@ public void Coordinates_are_clamped_to_the_screen() var terminal = NewTerminal(cols: 10, rows: 3); terminal.Write("AB"); - // A rect hanging off every edge still answers, for what the screen actually holds. - Assert.Equal(Report(4, Sum("AB") + (3 * 10 - 2) * 0x20), + // A rect hanging off every edge still answers, for what the screen actually holds -- the + // blanks after "AB" trail their row and the two rows below it, so none of them count. + Assert.Equal(Report(4, Sum("AB")), Reply(terminal, $"{Esc}[4;0;1;1;99;99*y")); } @@ -87,7 +132,7 @@ public void Omitted_coordinates_mean_the_whole_screen() var terminal = NewTerminal(cols: 4, rows: 2); terminal.Write("hi"); - Assert.Equal(Report(5, Sum("hi") + 6 * 0x20), Reply(terminal, $"{Esc}[5*y")); + Assert.Equal(Report(5, Sum("hi")), Reply(terminal, $"{Esc}[5*y")); } [Fact] diff --git a/src/XTerm.NET.Tests/Common/CsiCommandExtensionsTests.cs b/src/XTerm.NET.Tests/Common/CsiCommandExtensionsTests.cs index 3161bb9..7b87fd0 100644 --- a/src/XTerm.NET.Tests/Common/CsiCommandExtensionsTests.cs +++ b/src/XTerm.NET.Tests/Common/CsiCommandExtensionsTests.cs @@ -77,10 +77,9 @@ public void ToCsiCommand_BareQ_IsDecllAndReturnsUnknown() /// /// '<' and '=' were never stripped, so they are recognised only where the map lists them -- - /// the Kitty keyboard pop and set forms, and nothing else. + /// the Kitty keyboard pop and set forms, the tertiary DA, and nothing else. /// [Theory] - [InlineData("=c")] [InlineData(" + /// DECRQDE, vttest menu 11.2.5 -> 6. The window IS the page, so the corner is 1;1 and there is + /// one page; the size is the same one CSI 18 t already reports in the dtterm dialect. + /// + [Fact] + public void Decrqde_ReportsTheDisplayedExtent() + { + var (terminal, replies) = Create(); + terminal.Write(Esc + "[\"v"); + Assert.Equal(Esc + "[24;80;1;1;1\"w", Assert.Single(replies)); + } + + [Fact] + public void Decrqde_IsSilentBelowVt300() + { + var (terminal, replies) = Create(); + terminal.Write(Esc + "[62\"p"); // DECSCL: VT200 + replies.Clear(); + terminal.Write(Esc + "[\"v"); + Assert.Empty(replies); + } + + /// + /// DECRQUPSS, vttest menu 11.2.5 -> 5. A UTF-8 terminal's supplemental set is ISO Latin-1, + /// a 96-character set, which is the Ps = 1 form and the designator 'A'. + /// + [Fact] + public void Decrqupss_ReportsIsoLatin1() + { + var (terminal, replies) = Create(); + terminal.Write(Esc + "[&u"); + Assert.Equal(Esc + "P1!uA" + Esc + "\\", Assert.Single(replies)); + } + + /// + /// DECRQTSR, vttest menu 11.2.5 -> 4 -> 2. There is no DECRSTS to consume a terminal state + /// report, so the answer is the invalid-request form rather than a payload nothing can restore + /// -- and rather than the silence a client blocks on, which is how DECRQSS already declines. + /// + [Fact] + public void Decrqtsr_DeclinesInsteadOfStayingSilent() + { + var (terminal, replies) = Create(); + terminal.Write(Esc + "[1$u"); + Assert.Equal(Esc + "P0$s" + Esc + "\\", Assert.Single(replies)); + } + + [Fact] + public void Decrqtsr_WithNoParameterAsksForNothing() + { + var (terminal, replies) = Create(); + terminal.Write(Esc + "[$u"); + Assert.Empty(replies); + } } diff --git a/src/XTerm.NET.Tests/InputHandlerTests.cs b/src/XTerm.NET.Tests/InputHandlerTests.cs index 7adcef4..9fc0d93 100644 --- a/src/XTerm.NET.Tests/InputHandlerTests.cs +++ b/src/XTerm.NET.Tests/InputHandlerTests.cs @@ -1171,7 +1171,7 @@ public void HandleCsi_DA_IgnoresNonZeroParameter(string identifier) } [Fact] - public void HandleCsi_DA_Tertiary_IsNotAnswered() + public void HandleCsi_DA_Tertiary_ReportsAZeroUnitId() { // Arrange var terminal = CreateTerminal(); @@ -1185,9 +1185,50 @@ public void HandleCsi_DA_Tertiary_IsNotAnswered() // Act - "=c" is the tertiary DA, asking for a unit ID handler.HandleCsi("=c", params_); - // Assert - there is no unit ID to report, and terminals without DECRPTUI stay quiet. - // Answering a question nobody asked is worse than silence: the program would read a DA - // reply where it expected DECRPTUI, while still waiting for the reply it did ask for. + // Assert - DECRPTUI, with the site code and serial number as the zeros xterm reports. + // There is no unit to identify, but vttest and anything else that asks decodes this and + // waits forever for the silence it used to get. + Assert.Equal("\u001bP!|00000000\u001b\\", receivedData); + } + + [Fact] + public void HandleCsi_DA_Tertiary_IsSilentBelowVt400() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + terminal.ConformanceLevel = 62; // VT200 + var params_ = new Params(); + params_.AddParam(0); + + string? receivedData = null; + terminal.DataReceived += (_, e) => receivedData = e.Data; + + // Act + handler.HandleCsi("=c", params_); + + // Assert - DECRPTUI is a VT420 control, and a program that put the terminal back to a + // VT200 level with DECSCL gets the silence a terminal of that vintage would have given it. + Assert.Null(receivedData); + } + + [Fact] + public void HandleCsi_DA_Tertiary_IgnoresAReply() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + var params_ = new Params(); + params_.AddParam(1); + + string? receivedData = null; + terminal.DataReceived += (_, e) => receivedData = e.Data; + + // Act + handler.HandleCsi("=c", params_); + + // Assert - a non-zero parameter is another terminal's reply arriving on our input, and + // answering it starts a ping-pong. The primary and secondary DA already refuse it. Assert.Null(receivedData); } diff --git a/src/XTerm.NET.Tests/OscSequenceTests.cs b/src/XTerm.NET.Tests/OscSequenceTests.cs index 8ab8277..6056b08 100644 --- a/src/XTerm.NET.Tests/OscSequenceTests.cs +++ b/src/XTerm.NET.Tests/OscSequenceTests.cs @@ -980,4 +980,93 @@ public void OscColorQueries_Sequential_AllRespond() Assert.Equal(3, responses.Count); Assert.All(responses, r => Assert.Contains("rgb:", r)); } + + /// + /// OSC 50 ; ? -- vttest menu 11.8.4.2. The emulator has no font; whatever draws its cells does, + /// so the question goes to the host the way OSC 52's does. + /// + [Fact] + public void OscFontQuery_AnswersWithTheNameTheHostGives() + { + var terminal = CreateTerminal(); + var responses = new List(); + terminal.DataReceived += (_, e) => responses.Add(e.Data); + terminal.FontQueryRequested += (_, e) => + { + e.FontName = "Cascadia Mono"; + e.Handled = true; + }; + + terminal.Write("\x1B]50;?\x1B\\"); + + Assert.Equal("\x1B]50;Cascadia Mono\x1B\\", Assert.Single(responses)); + } + + /// + /// The whole point of handling this at all: a host that will not say still produces a REPLY. + /// xterm answers a font query it cannot satisfy with a nameless OSC 50, and vttest reads + /// exactly that -- it skips entries whose reply carries no name. Silence leaves the asking + /// program waiting forever. + /// + [Fact] + public void OscFontQuery_AnswersNamelesslyWhenNoHostDoes() + { + var terminal = CreateTerminal(); + var responses = new List(); + terminal.DataReceived += (_, e) => responses.Add(e.Data); + + terminal.Write("\x1B]50;?\x1B\\"); + + Assert.Equal("\x1B]50\x1B\\", Assert.Single(responses)); + } + + [Fact] + public void OscFontQuery_AnswersNamelesslyForAMenuIndex() + { + // There is no font menu to index, so every indexed form is declined -- and never put to + // the host, which has no menu either. + var terminal = CreateTerminal(); + var responses = new List(); + var asked = false; + terminal.DataReceived += (_, e) => responses.Add(e.Data); + terminal.FontQueryRequested += (_, _) => asked = true; + + terminal.Write("\x1B]50;?#2\x1B\\"); + terminal.Write("\x1B]50;?+1\x1B\\"); + + Assert.False(asked); + Assert.Equal(new[] { "\x1B]50\x1B\\", "\x1B]50\x1B\\" }, responses); + } + + [Fact] + public void OscFontQuery_TerminatesTheReplyTheWayTheRequestWasTerminated() + { + var terminal = CreateTerminal(); + var responses = new List(); + terminal.DataReceived += (_, e) => responses.Add(e.Data); + + terminal.Write("\x1B]50;?\x07"); + + Assert.Equal("\x1B]50\x07", Assert.Single(responses)); + } + + [Fact] + public void OscFontSet_IsLeftToTheHostAndReportedUnrecognised() + { + // Setting a font is the host's business, so the terminal answers nothing and says it did + // not act -- a listener on OscReceived can, without having to work out whether it already + // had been. + var terminal = CreateTerminal(); + var responses = new List(); + TerminalEvents.OscReceivedEventArgs? received = null; + terminal.DataReceived += (_, e) => responses.Add(e.Data); + terminal.OscReceived += (_, e) => received = e; + + terminal.Write("\x1B]50;9x15\x1B\\"); + + Assert.Empty(responses); + Assert.NotNull(received); + Assert.False(received!.Recognized); + Assert.Equal("9x15", received.Data); + } } diff --git a/src/XTerm.NET.Tests/RectangleOpsTests.cs b/src/XTerm.NET.Tests/RectangleOpsTests.cs index 76ef8aa..e4e1618 100644 --- a/src/XTerm.NET.Tests/RectangleOpsTests.cs +++ b/src/XTerm.NET.Tests/RectangleOpsTests.cs @@ -1,3 +1,4 @@ +using XTerm.Buffer; using XTerm.Options; namespace XTerm.Tests; @@ -137,4 +138,167 @@ public void SelectiveErase_spares_DecscaProtected_cells_only() // DECSCA protection holds; the ISO guard belongs to the other erase family and does not. Assert.Equal(" CD ", Row(terminal, 0, 6)); } + + private static AttributeData AttrAt(Terminal t, int row, int col) => + t.Buffer.Lines[row]![col].Attributes; + + [Fact] + public void ChangeAttributes_sets_the_named_attributes_and_leaves_the_text_alone() + { + var terminal = NewTerminal(cols: 10, rows: 4); + terminal.Write("XXXXXXXXXX\r\nXXXXXXXXXX\r\nXXXXXXXXXX"); + terminal.Write($"{Esc}[2;2;2;4;1;4$r"); // DECCARA: bold + underline over row 2, cols 2-4 + + Assert.Equal("XXXXXXXXXX", Row(terminal, 1, 10)); // characters untouched + Assert.False(AttrAt(terminal, 1, 0).IsBold()); + Assert.True(AttrAt(terminal, 1, 1).IsBold()); + Assert.True(AttrAt(terminal, 1, 1).IsUnderline()); + Assert.True(AttrAt(terminal, 1, 3).IsBold()); + Assert.False(AttrAt(terminal, 1, 4).IsBold()); + Assert.False(AttrAt(terminal, 0, 1).IsBold()); // the row above is not in the area + } + + /// + /// DECSACE's default is the STREAM: the area runs from the top-left position to the bottom-right + /// one across whole intervening lines, not as a box. This is the setting that was accepted, + /// reported back by DECRQSS and then read by nothing, because the controls it governs did not + /// exist. + /// + [Fact] + public void ChangeAttributes_runs_as_a_stream_by_default() + { + var terminal = NewTerminal(cols: 10, rows: 4); + terminal.Write($"{Esc}[2;3;3;6;1$r"); // rows 2-3, cols 3..6 + + // Row 2 runs from its column to the end of the line. + Assert.False(AttrAt(terminal, 1, 1).IsBold()); + Assert.True(AttrAt(terminal, 1, 2).IsBold()); + Assert.True(AttrAt(terminal, 1, 9).IsBold()); + + // Row 3 runs from the start of the line to its column. + Assert.True(AttrAt(terminal, 2, 0).IsBold()); + Assert.True(AttrAt(terminal, 2, 5).IsBold()); + Assert.False(AttrAt(terminal, 2, 6).IsBold()); + } + + [Fact] + public void ChangeAttributes_confines_itself_to_the_rectangle_under_Decsace_2() + { + var terminal = NewTerminal(cols: 10, rows: 4); + terminal.Write($"{Esc}[2*x"); // DECSACE 2 -- rectangle + terminal.Write($"{Esc}[2;3;3;6;1$r"); + + foreach (var row in new[] { 1, 2 }) + { + Assert.False(AttrAt(terminal, row, 1).IsBold()); + Assert.True(AttrAt(terminal, row, 2).IsBold()); + Assert.True(AttrAt(terminal, row, 5).IsBold()); + Assert.False(AttrAt(terminal, row, 6).IsBold()); + } + } + + [Fact] + public void ChangeAttributes_0_clears_the_four_DEC_attributes_but_not_invisible() + { + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write($"{Esc}[1;4;5;7;8mXXXX"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;4;0$r"); + + var attributes = AttrAt(terminal, 0, 0); + Assert.False(attributes.IsBold()); + Assert.False(attributes.IsUnderline()); + Assert.False(attributes.IsBlink()); + Assert.False(attributes.IsInverse()); + + // xterm leaves invisible out of the SGR_MASK that parameter 0 covers; it has its own + // 8 and 28, which is the extension the documentation calls out. + Assert.True(attributes.IsInvisible()); + } + + [Fact] + public void ChangeAttributes_ignores_everything_outside_the_DEC_set() + { + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write("XXXX"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;4;31;3;1$r"); // red and italic are not DECCARA's + + Assert.True(AttrAt(terminal, 0, 0).IsBold()); + Assert.False(AttrAt(terminal, 0, 0).IsItalic()); + Assert.Equal(AttributeData.Default.Fg, AttrAt(terminal, 0, 0).Fg); + } + + [Fact] + public void ReverseAttributes_toggles_each_cell_from_what_it_already_had() + { + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write($"{Esc}[1mXX{Esc}[0mXX"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;4;1$t"); // DECRARA: reverse bold + + Assert.False(AttrAt(terminal, 0, 0).IsBold()); + Assert.False(AttrAt(terminal, 0, 1).IsBold()); + Assert.True(AttrAt(terminal, 0, 2).IsBold()); + Assert.True(AttrAt(terminal, 0, 3).IsBold()); + Assert.Equal("XXXX ", Row(terminal, 0, 6)); + } + + [Fact] + public void ReverseAttributes_0_reverses_the_four_together() + { + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write($"{Esc}[1;5mXX"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;2;0$t"); + + var attributes = AttrAt(terminal, 0, 0); + Assert.False(attributes.IsBold()); // was on + Assert.False(attributes.IsBlink()); // was on + Assert.True(attributes.IsUnderline()); // was off + Assert.True(attributes.IsInverse()); // was off + } + + /// + /// The resets say nothing under DECRARA -- reversing an attribute already covers both + /// directions -- so xterm reads 22, 24, 25, 27 and 28 only when setting. + /// + [Fact] + public void ReverseAttributes_ignores_the_reset_parameters() + { + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write($"{Esc}[1mXX"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;2;22$t"); + + Assert.True(AttrAt(terminal, 0, 0).IsBold()); + } + + [Fact] + public void ChangeAttributes_marks_both_halves_of_a_wide_character() + { + // The trailing half holds no character of its own, which is exactly the cell xterm's + // never-drawn test would skip. Skipping it here would leave one character disagreeing + // with itself about how it is drawn. + var terminal = NewTerminal(cols: 6, rows: 2); + terminal.Write("δΈ–"); + terminal.Write($"{Esc}[2*x{Esc}[1;1;1;2;1$r"); + + Assert.True(AttrAt(terminal, 0, 0).IsBold()); + Assert.True(AttrAt(terminal, 0, 1).IsBold()); + } + + /// + /// DECSACE survives a soft reset and not a hard one, which is how xterm clears it. It mattered + /// only once DECCARA and DECRARA read it: a stale rectangle setting turns the next program's + /// stream into a box. + /// + [Fact] + public void Decsace_survives_a_soft_reset_and_not_a_hard_one() + { + var terminal = NewTerminal(cols: 10, rows: 4); + + terminal.Write($"{Esc}[2*x{Esc}[!p"); // DECSACE 2, then DECSTR + terminal.Write($"{Esc}[2;3;3;6;1$r"); + Assert.False(AttrAt(terminal, 1, 9).IsBold()); // still a rectangle + + terminal.Write($"{Esc}c"); // RIS + terminal.Write($"{Esc}[2;3;3;6;1$r"); + Assert.True(AttrAt(terminal, 1, 9).IsBold()); // back to a stream + } } diff --git a/src/XTerm.NET/Common/CommandExtensions.cs b/src/XTerm.NET/Common/CommandExtensions.cs index f0b92a6..56174fa 100644 --- a/src/XTerm.NET/Common/CommandExtensions.cs +++ b/src/XTerm.NET/Common/CommandExtensions.cs @@ -54,6 +54,7 @@ public static class CsiCommandExtensions { "x", CsiCommand.RequestTerminalParameters }, // DECREQTPARM { "c", CsiCommand.DeviceAttributes }, // DA1 - primary { ">c", CsiCommand.DeviceAttributes }, // DA2 - secondary + { "=c", CsiCommand.DeviceAttributes }, // DA3 - tertiary, the unit ID { "d", CsiCommand.LinePositionAbsolute }, { "f", CsiCommand.CursorPosition }, // HVP - same as CUP { "g", CsiCommand.TabClear }, @@ -83,6 +84,8 @@ public static class CsiCommandExtensions { "!p", CsiCommand.SoftReset }, // DECSTR { "$|", CsiCommand.SetColumnsPerPage }, // DECSCPP { "$v", CsiCommand.CopyRectangularArea }, // DECCRA + { "$r", CsiCommand.ChangeAttributesRectangularArea }, // DECCARA + { "$t", CsiCommand.ReverseAttributesRectangularArea }, // DECRARA { "$x", CsiCommand.FillRectangularArea }, // DECFRA { "$z", CsiCommand.EraseRectangularArea }, // DECERA { "${", CsiCommand.SelectiveEraseRectangularArea }, // DECSERA @@ -91,6 +94,9 @@ public static class CsiCommandExtensions { "\"q", CsiCommand.SelectCharacterProtection }, // DECSCA { "\"p", CsiCommand.SelectConformanceLevel }, // DECSCL { "*x", CsiCommand.SelectAttributeChangeExtent }, // DECSACE + { "\"v", CsiCommand.RequestDisplayedExtent }, // DECRQDE + { "&u", CsiCommand.RequestUserPreferredSupplementalSet }, // DECRQUPSS + { "$u", CsiCommand.RequestTerminalStateReport }, // DECRQTSR { "$}", CsiCommand.SelectActiveStatusDisplay }, // DECSASD { "$~", CsiCommand.SelectStatusDisplayType }, // DECSSDT { "*|", CsiCommand.SetLinesPerScreen }, // DECSNLS diff --git a/src/XTerm.NET/Common/CsiCommand.cs b/src/XTerm.NET/Common/CsiCommand.cs index 6946427..883a4cb 100644 --- a/src/XTerm.NET/Common/CsiCommand.cs +++ b/src/XTerm.NET/Common/CsiCommand.cs @@ -116,7 +116,8 @@ public enum CsiCommand TabClear, /// - /// Device Attributes (CSI c is the primary request, CSI > c the secondary). + /// Device Attributes (CSI c is the primary request, CSI > c the secondary, CSI = c the + /// tertiary). /// DeviceAttributes, @@ -192,6 +193,12 @@ public enum CsiCommand /// DECCRA -- copy a rectangular area (CSI ... $ v). CopyRectangularArea, + /// DECCARA -- set SGR attributes over an area, leaving its characters alone (CSI Pt;Pl;Pb;Pr;Pm $ r). + ChangeAttributesRectangularArea, + + /// DECRARA -- toggle SGR attributes over an area (CSI Pt;Pl;Pb;Pr;Pm $ t). + ReverseAttributesRectangularArea, + /// DECFRA -- fill a rectangular area with a character (CSI Pch;Pt;Pl;Pb;Pr $ x). FillRectangularArea, @@ -234,6 +241,15 @@ public enum CsiCommand /// DECRQCRA -- request a checksum of a rectangular area (CSI Pid;Pp;Pt;Pl;Pb;Pr * y). RequestChecksumRectangularArea, + /// DECRQDE -- request the displayed extent (CSI " v); answered with DECRPDE. + RequestDisplayedExtent, + + /// DECRQUPSS -- request the user-preferred supplemental set (CSI & u); answered with DECAUPSS. + RequestUserPreferredSupplementalSet, + + /// DECRQTSR -- request a terminal state report (CSI Ps $ u); answered with DECTSR. + RequestTerminalStateReport, + /// /// Set Kitty keyboard protocol flags (CSI = Ps ; Pm u). /// diff --git a/src/XTerm.NET/Common/OscCommand.cs b/src/XTerm.NET/Common/OscCommand.cs index 2973adb..4965327 100644 --- a/src/XTerm.NET/Common/OscCommand.cs +++ b/src/XTerm.NET/Common/OscCommand.cs @@ -81,6 +81,14 @@ public enum OscCommand /// PointerShape = 22, + /// + /// Font operations (OSC 50), xterm. + /// Format: OSC 50 ; ? ST - which font is in use + /// OSC 50 ; ? #n ST - which font entry n is + /// OSC 50 ; name ST - set the font + /// + FontOps = 50, + /// /// Clipboard operations (OSC 52). /// Format: OSC 52 ; c ; data ST diff --git a/src/XTerm.NET/Events/TerminalEvents.cs b/src/XTerm.NET/Events/TerminalEvents.cs index 6d8277e..3de73cd 100644 --- a/src/XTerm.NET/Events/TerminalEvents.cs +++ b/src/XTerm.NET/Events/TerminalEvents.cs @@ -573,6 +573,27 @@ public WindowInfoRequestedEventArgs(WindowInfoRequest request) } } + /// + /// A request for the name of the font the terminal is displayed in (OSC 50 ; ? ST). The + /// emulator has no fonts -- whatever draws its cells does -- so this is the seam that asks + /// whatever does. + /// + /// + /// Answer by setting and . A handler that does not + /// answer, or that answers with an empty name, is a decline, and the terminal sends xterm's own + /// "no font to report" reply -- OSC 50 with no name -- rather than nothing. Silence is what a + /// client blocking on the report waits on forever; a reply saying the terminal will not say is + /// an answer it can act on. + /// + public class FontQueryEventArgs : EventArgs + { + /// True once the handler has supplied a name. + public bool Handled { get; set; } + + /// The font the terminal is drawn in, in whatever form the host names it. + public string? FontName { get; set; } + } + /// /// Buffer change event - fired when the active buffer switches. /// diff --git a/src/XTerm.NET/InputHandler.Csi.cs b/src/XTerm.NET/InputHandler.Csi.cs index e1e2203..3689f8e 100644 --- a/src/XTerm.NET/InputHandler.Csi.cs +++ b/src/XTerm.NET/InputHandler.Csi.cs @@ -771,6 +771,69 @@ private void RequestTerminalParameters(Params parameters) _terminal.RaiseDataReceived($"\u001b[{sol};1;1;128;128;1;0x"); } + /// + /// DECRQDE (CSI " v) -- reports the displayed extent as + /// CSI Ph ; Pw ; Pc ; Pr ; Pp " w: the page's height and width, the column and row of + /// the window's top-left corner within it, and the page number. + /// + /// + /// The window IS the page here, so the corner is always 1;1 and there is one page. That makes + /// this the DEC spelling of what CSI 18 t already answers in the dtterm dialect -- + /// a question this terminal has always known the answer to, and was the only one of the two + /// forms not answering. VT300 and up, as in xterm. + /// + private void RequestDisplayedExtent() + { + if (_terminal.ConformanceLevel < 63) + return; + + _terminal.RaiseDataReceived($"\u001b[{_terminal.Rows};{_terminal.Cols};1;1;1\"w"); + } + + /// + /// DECRQUPSS (CSI & u) -- reports the user-preferred supplemental set as + /// DCS Ps ! u <designator> ST, where Ps is 0 for a 94-character set and 1 for a + /// 96-character one. + /// + /// + /// The choice UPSS offers is DEC Supplemental Graphic or ISO Latin-1, and this terminal decodes + /// UTF-8: the supplemental half of Latin-1 is a pass-through here, and DEC Supplemental is not + /// reachable at all. So the answer is ISO Latin-1 -- A, a 96-character set -- which is + /// the same conclusion xterm reaches for the same reason. DECAUPSS, the assignment half, is not + /// implemented, so the default is the only value this can ever report; saying so is still worth + /// more than the silence a client waits on forever. VT300 and up. + /// + private void RequestUserPreferredSupplementalSet() + { + if (_terminal.ConformanceLevel < 63) + return; + + _terminal.RaiseDataReceived("\u001bP1!uA\u001b\\"); + } + + /// + /// DECRQTSR (CSI Ps $ u) -- the terminal state report, answered with the + /// cannot-report form DCS 0 $ s ST. + /// + /// + /// DECTSR serialises the whole terminal state into a payload DECRSTS restores later. This + /// terminal has no DECRSTS, and a terminal that cannot restore state has no business claiming + /// it can report it -- a report nothing can consume is a worse answer than an honest refusal, + /// because the client acts on it. + /// Refusing is not the same as saying nothing, though. DECRQSS already answers a request + /// it does not recognise with DCS 0 $ r ST rather than silence, and this is that same + /// shape one final character over: Ps = 0 is the invalid-request form of the same reply, and a + /// client reading it stops waiting. Ps = 0 or absent asks for nothing and gets nothing, which + /// is what the VT510 manual specifies for it. + /// + private void RequestTerminalStateReport(Params parameters) + { + if (parameters.GetParam(0, 0) == 0) + return; + + _terminal.RaiseDataReceived("\u001bP0$s\u001b\\"); + } + private void CursorForwardTab(Params parameters) => Tab(Math.Max(parameters.GetParam(0, 1), 1)); @@ -1651,14 +1714,31 @@ private void RestoreCursor() /// /// /// The sum follows the DEC/xterm convention esctest's default expects: each cell - /// contributes its character's codepoints, a cell that holds nothing contributes a SPACE -- - /// erased and never-written alike, which is also what lets DEC's trailing-blank trimming be - /// reasoned away by the client -- and the report carries the NEGATED total (0x10000 - sum), - /// which is what xterm sent before patch #279 and what esctest's default + /// contributes its character's codepoints, and the report carries the NEGATED total + /// (0x10000 - sum), which is what xterm sent before patch #279 and what esctest's default /// --xterm-checksum 0 undoes on its side. - /// Attributes deliberately contribute nothing. esctest compares a cell's checksum to - /// the bare codepoint of the character it expects, so a weight per attribute bit would fail - /// every assertion on styled text. + /// TRAILING BLANKS ARE TRIMMED, which is the part this used to get wrong. DEC terminals + /// drop the run of blanks at the end of a row rather than counting it, so a blank cell inside + /// an area is worth 0x20 only when something further along the same row follows it. Counting + /// every blank instead put this emulator exactly 95 spaces above what vttest's own DECRQCRA + /// test computes for the two rows it checks -- the whole of the discrepancy, and the whole of + /// the reason to trim. + /// The FIRST cell counted is added whatever it holds, blank or not. That exception is + /// not decoration: esctest reads content back one cell at a time and expects a space to come + /// back as 0x20, so a rule that trimmed unconditionally would answer zero for every space on + /// the screen. vttest builds its expectation with the same exception, in the same place. + /// xterm's xtermCheckRect is the reference for the rest of this and diverges here, + /// in a way worth naming. Its held-back run is only ever accumulated under + /// csNOTRIM -- the flag meaning "do not trim at all", under which the trimmed total is + /// then discarded -- so in its default mode the run is always empty and EVERY blank drops, + /// interior ones included. That is neither what DEC documents nor what vttest predicts; the two + /// readings agree on every screen either tool checks, and this follows the documented one. + /// Attributes deliberately contribute nothing, where xterm weights six of them into the + /// cell's value (bold 0x80, blink 0x40, inverse 0x20, underline 0x10, invisible 0x8, protected + /// 0x4). esctest compares a cell's checksum to the bare codepoint of the character it expects, + /// so a weight per attribute bit would fail every assertion on styled text. The visible + /// consequence is confined to the trimming above: xterm never trims a styled space, because its + /// value is no longer 0x20, and this trims it like any other. /// The page parameter is accepted and ignored: there is one screen. Coordinates are /// 1-based screen positions, clamped, whole screen when omitted. /// @@ -1676,6 +1756,13 @@ private void RequestChecksumRectangularArea(Params parameters) var right = Math.Min(_terminal.Cols, parameters.GetParam(5, _terminal.Cols - originX) + originX); var sum = 0; + // Blanks seen since the last counted character. They join the sum the moment something + // else on their row does, and are thrown away if the row ends first. + var pending = 0; + // Whether anything has been counted yet, which is the first-cell exemption above. A cell + // skipped outright does not spend it. + var first = true; + for (var row = top; row <= bottom; row++) { var line = _buffer.Lines[_buffer.YBase + row - 1]; @@ -1686,19 +1773,32 @@ private void RequestChecksumRectangularArea(Params parameters) { var cell = line[col - 1]; var content = cell.Content; + + // The trailing half of a wide character is a placeholder, not a blank: its + // character was already counted in full one cell to the left. A cell nothing has + // ever written to reads the same way, and neither adds anything. if (string.IsNullOrEmpty(content)) - { - // The trailing half of a wide character is a placeholder, not a blank: its - // character was already counted in full one cell to the left. - if (cell.Width == 0) - continue; - sum += 0x20; continue; - } + var value = 0; foreach (var ch in content) - sum += ch; + value += ch; + + if (first || value != 0x20) + { + sum += value + pending; + pending = 0; + } + else + { + pending += value; + } + + first = false; } + + // The row ended on blanks, so they were trailing ones. + pending = 0; } _terminal.RaiseDataReceived($"\u001bP{id}!~{(0x10000 - sum) & 0xFFFF:X4}\u001b\\"); diff --git a/src/XTerm.NET/InputHandler.Osc.cs b/src/XTerm.NET/InputHandler.Osc.cs index 1249629..b1a4b6a 100644 --- a/src/XTerm.NET/InputHandler.Osc.cs +++ b/src/XTerm.NET/InputHandler.Osc.cs @@ -724,6 +724,47 @@ private void AnswerPointerShapeQuery(string query) _terminal.RaiseDataReceived($"\u001b]22;{string.Join(",", answers)}\u001b\\"); } + /// + /// OSC 50 -- the font controls. Only the QUERY half is answered here. + /// + /// + /// A font query is a question about something the emulator does not own: whatever draws + /// the cells picks the font, exactly as whatever owns the clipboard answers OSC 52. So it is + /// put to the host through , and the host may decline + /// by not answering. + /// Declining is not silence, and that is the whole point of this method existing. xterm + /// answers a font query it cannot satisfy with a NAMELESS OSC 50 -- the reply with its + /// semicolon and name left off -- and vttest reads exactly that, skipping entries whose reply + /// carries no name. A client blocking on the report is told "not this one" instead of waiting + /// forever, which is the same argument that settled DECRQM and the pixel reports. + /// An INDEXED query -- ?#2, ?+1 and the rest, asking after an entry of + /// xterm's font menu -- is always answered namelessly: there is no font menu to index. And a + /// bare OSC 50 ; name SETS the font, which is the host's business and none of the + /// emulator's; it is reported as unrecognised so a listener on OscReceived can act on it + /// without having to work out whether the terminal already did. + /// + /// False for the set form, which this does not act on. + private bool HandleFontOps(string data) + { + if (!data.StartsWith('?')) + return false; + + // The name is asked for only when the query is bare. Anything trailing the '?' names a + // menu entry, and there is no menu. + string? name = null; + if (data.Length == 1) + { + var args = _terminal.RaiseFontQueryRequested(); + if (args.Handled && !string.IsNullOrEmpty(args.FontName)) + name = args.FontName; + } + + _terminal.RaiseDataReceived(name is null + ? $"\u001b]50{_terminal.OscReplyTerminator}" + : $"\u001b]50;{name}{_terminal.OscReplyTerminator}"); + return true; + } + private void HandleClipboard(string data) { var parts = data.Split(new[] { ';' }, 2); diff --git a/src/XTerm.NET/InputHandler.Rectangles.cs b/src/XTerm.NET/InputHandler.Rectangles.cs index 9e32c07..22e4a48 100644 --- a/src/XTerm.NET/InputHandler.Rectangles.cs +++ b/src/XTerm.NET/InputHandler.Rectangles.cs @@ -5,8 +5,9 @@ namespace XTerm; /// -/// The DEC rectangular-area operations: copy, fill and erase (DECCRA, DECFRA, DECERA). One file -/// because they share one coordinate discipline, spelled out on . +/// The DEC rectangular-area operations: copy, fill, erase and the two that change attributes +/// rather than characters (DECCRA, DECFRA, DECERA, DECSERA, DECCARA, DECRARA). One file because +/// they share one coordinate discipline, spelled out on . /// public partial class InputHandler { @@ -169,4 +170,116 @@ private void CopyRectangularArea(Params parameters) } } } + + /// + /// DECCARA (CSI Pt;Pl;Pb;Pr;Pm $ r) and DECRARA (CSI Pt;Pl;Pb;Pr;Pm $ t) -- set + /// or toggle the named SGR attributes over an area, leaving the characters alone. + /// + /// + /// The attribute half of the rectangle family, and the only consumer DECSACE has. That + /// setting was parsed, stored and read back by DECRQSS while nothing acted on it, because the + /// two controls it governs did not exist: a terminal reporting a rectangle-or-stream choice it + /// then ignored. + /// DECSACE 2 means the RECTANGLE the four coordinates describe. Anything else -- the + /// default included -- means the STREAM running from the top-left position to the bottom-right + /// one, so the first row runs from its column to the end of the line, the last row from the + /// start of the line to its column, and every row between them runs whole. + /// Only the six attributes DEC defines are touched: 1 bold, 4 underline, 5 blink, + /// 7 inverse and their resets 22, 24, 25, 27, plus xterm's 8/28 for invisible. Parameter 0 + /// means the first four together -- NOT invisible, which xterm leaves out of its SGR_MASK -- + /// and reverses rather than clears them under DECRARA. Everything else in the list is ignored; + /// colours are not in the standard, and honouring an SGR parameter here that a real VT420 would + /// not is how a program's careful rectangle ends up recoloured on one terminal only. + /// Every cell in the area is marked, the trailing half of a wide character included. xterm + /// skips cells it has never drawn -- it tracks that per cell, and a blank it has never touched + /// is not a blank it will colour -- but a line here is born full of spaces, so there is no such + /// state to test for and the only cell that reads as empty is a wide character's second half. + /// Skipping THAT would leave a character's two halves disagreeing about their own rendition. + /// + private void MarkRectangularArea(Params parameters, bool reverse) + { + if (!TryReadRectangle(parameters, 0, out var top, out var left, out var bottom, out var right)) + return; + + var exact = _attributeChangeExtent == 2; + + for (var row = top; row <= bottom; row++) + { + var line = _buffer.Lines[_buffer.YBase + row]; + if (line is null) + continue; + + var from = exact || row == top ? left : 0; + var to = exact || row == bottom ? right : _terminal.Cols - 1; + + for (var col = from; col <= to && col < line.Length; col++) + { + var cell = line[col]; + ApplyAreaAttributes(parameters, 4, ref cell.Attributes, reverse); + line.SetCell(col, ref cell); + } + } + } + + /// + /// Applies the DECCARA/DECRARA attribute list starting at to one + /// cell's rendition. + /// + private static void ApplyAreaAttributes(Params parameters, int first, ref AttributeData attributes, bool reverse) + { + for (var i = first; i < parameters.Length; i++) + { + switch (parameters.GetParam(i, 0)) + { + case 0: + if (reverse) + { + attributes.SetBold(!attributes.IsBold()); + attributes.SetUnderline(!attributes.IsUnderline()); + attributes.SetBlink(!attributes.IsBlink()); + attributes.SetInverse(!attributes.IsInverse()); + } + else + { + attributes.SetBold(false); + attributes.SetUnderline(false); + attributes.SetBlink(false); + attributes.SetInverse(false); + } + break; + case 1: + attributes.SetBold(reverse ? !attributes.IsBold() : true); + break; + case 4: + attributes.SetUnderline(reverse ? !attributes.IsUnderline() : true); + break; + case 5: + attributes.SetBlink(reverse ? !attributes.IsBlink() : true); + break; + case 7: + attributes.SetInverse(reverse ? !attributes.IsInverse() : true); + break; + case 8: + attributes.SetInvisible(reverse ? !attributes.IsInvisible() : true); + break; + // The resets have no meaning under DECRARA -- reversing an attribute already says + // both directions -- so xterm reads them only when setting, and so does this. + case 22 when !reverse: + attributes.SetBold(false); + break; + case 24 when !reverse: + attributes.SetUnderline(false); + break; + case 25 when !reverse: + attributes.SetBlink(false); + break; + case 27 when !reverse: + attributes.SetInverse(false); + break; + case 28 when !reverse: + attributes.SetInvisible(false); + break; + } + } + } } diff --git a/src/XTerm.NET/InputHandler.StoredModes.cs b/src/XTerm.NET/InputHandler.StoredModes.cs index 4dd0b47..83f7e69 100644 --- a/src/XTerm.NET/InputHandler.StoredModes.cs +++ b/src/XTerm.NET/InputHandler.StoredModes.cs @@ -50,16 +50,27 @@ public partial class InputHandler // DECHCCM because the hardware it coupled is gone. /// - /// Stored display SETTINGS, kept for the same reason as the stored modes: DECRQSS answers - /// them, and "recognised, at its default" beats a denial. Extent is DECSACE's -- the rect - /// operations read it when the standard grows teeth here; the status-display pair have no - /// status line to point at and never will. + /// DECSACE's extent, which DECCARA and DECRARA read to decide between a rectangle and a + /// stream, and which DECRQSS reports. /// private int _attributeChangeExtent; // DECSACE (* x) // DECSASD ($ }) and DECSSDT ($ ~) were cached here for DECRQSS to report. They are the // terminal's state now, and DECRQSS reads it there -- a second copy is what let RIS undo the // status line while the report went on describing the one that had been undone. + /// + /// Puts DECSACE back to its default, for RIS. + /// + /// + /// Separate from , which the SOFT reset calls too. DECSACE + /// survives DECSTR on a real terminal and in xterm -- it is cleared in the full-reset branch + /// of ReallyReset and nowhere else -- and now that DECCARA and DECRARA read it, the + /// difference between the two resets is the difference between a program's rectangle landing + /// as a rectangle and landing as a stream. The status-display pair reset with the status line + /// itself, in the terminal that now owns them. + /// + internal void ResetAttributeChangeExtent() => _attributeChangeExtent = 0; + /// Sets or resets a stored mode; false when the mode is not one of the stored set. private bool TrySetStoredMode(int mode, bool isPrivate, bool value) { diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index e70fad4..650861f 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -716,6 +716,14 @@ public void HandleCsi(string identifier, Params parameters) CopyRectangularArea(parameters); break; + case CsiCommand.ChangeAttributesRectangularArea: + MarkRectangularArea(parameters, reverse: false); + break; + + case CsiCommand.ReverseAttributesRectangularArea: + MarkRectangularArea(parameters, reverse: true); + break; + case CsiCommand.FillRectangularArea: FillRectangularArea(parameters); break; @@ -805,6 +813,18 @@ public void HandleCsi(string identifier, Params parameters) RequestChecksumRectangularArea(parameters); break; + case CsiCommand.RequestDisplayedExtent: + RequestDisplayedExtent(); + break; + + case CsiCommand.RequestUserPreferredSupplementalSet: + RequestUserPreferredSupplementalSet(); + break; + + case CsiCommand.RequestTerminalStateReport: + RequestTerminalStateReport(parameters); + break; + case CsiCommand.DeviceStatusReport: DeviceStatusReport(parameters, isPrivate); break; @@ -1240,6 +1260,10 @@ public void HandleOsc(string data) HandlePointerShape(arg); break; + case OscCommand.FontOps: + recognized = HandleFontOps(arg); + break; + case OscCommand.Clipboard: HandleClipboard(arg); break; @@ -1559,6 +1583,18 @@ private void DeviceAttributes(string identifier, Params parameters) // "no cartridge ROM". _terminal.RaiseDataReceived(SecondaryDeviceAttributes); } + else if (identifier.StartsWith('=')) + { + // Tertiary DA: CSI = Pp c, answered with DECRPTUI -- DCS ! | <8 hex digits> ST, the + // "terminal unit ID". There is no unit to identify, so the site code and serial number + // are zeros, which is exactly what xterm reports and what vttest decodes and displays. + // + // VT400 and up only, as in xterm: DECRPTUI arrived with the VT420, and a program that + // has put the terminal into a VT100 or VT200 conformance level with DECSCL is entitled + // to the silence a terminal of that vintage would have given it. + if (_terminal.ConformanceLevel >= 64) + _terminal.RaiseDataReceived("\u001bP!|00000000\u001b\\"); + } else if (identifier.Length == 1) { // Primary DA: CSI ? Pl ; ... c, from the DECSCL operating level. @@ -1567,11 +1603,9 @@ private void DeviceAttributes(string identifier, Params parameters) // Any other prefix is left unanswered. "?c" is the one that used to go wrong: it is not the // secondary DA, but it sets isPrivate, so it was handed the secondary reply -- the answer to - // a question the program had not asked, while it was still waiting for the one it had. - // Neither it nor the tertiary DA, "=c", reaches this method any more: the lookup matches the - // whole identifier and only "c" and ">c" are listed, so both resolve to Unknown. Silence is - // the right outcome for the tertiary regardless: it asks for a unit ID this terminal does - // not have, and terminals without DECRPTUI say nothing. + // a question the program had not asked, while it was still waiting for the one it had. It + // does not reach this method any more: the lookup matches the whole identifier and "?c" is + // not listed, so it resolves to Unknown. } /// diff --git a/src/XTerm.NET/Terminal.cs b/src/XTerm.NET/Terminal.cs index 37a3758..66693d9 100644 --- a/src/XTerm.NET/Terminal.cs +++ b/src/XTerm.NET/Terminal.cs @@ -731,6 +731,18 @@ private void RaisePointerShapeChanged(string? before) /// public event EventHandler? WindowInfoRequested; + /// + /// Fired when a program asks which font the terminal is displayed in (OSC 50 ; ? ST). + /// + /// + /// The emulator has no font of its own; the host that draws its cells does. Answer by setting + /// and + /// . Declining -- by not subscribing, + /// or by leaving the name unset -- still sends a reply, xterm's nameless OSC 50, so the asking + /// program is told rather than left waiting. + /// + public event EventHandler? FontQueryRequested; + /// /// Fired when the active buffer is changed. /// @@ -1232,6 +1244,9 @@ public void Reset() // RIS had already put the behaviour back. The report and the behaviour now agree. _inputHandler.ResetStoredModes(); + // DECSACE, which a SOFT reset deliberately leaves alone. + _inputHandler.ResetAttributeChangeExtent(); + // And the charset designations, with the SO/SI shift state. InputHandler.ResetCharsets // existed for exactly this and was called from nowhere, so a program that designated line // drawing into G0 and died left the next one printing box characters for letters. @@ -1935,6 +1950,13 @@ internal void RaiseWindowRefreshed() => internal void RaiseWindowFullscreened() => WindowFullscreened?.Invoke(this, EventArgs.Empty); + internal TerminalEvents.FontQueryEventArgs RaiseFontQueryRequested() + { + var args = new TerminalEvents.FontQueryEventArgs(); + FontQueryRequested?.Invoke(this, args); + return args; + } + internal TerminalEvents.WindowInfoRequestedEventArgs RaiseWindowInfoRequested(WindowInfoRequest request) { var args = new TerminalEvents.WindowInfoRequestedEventArgs(request); @@ -2166,6 +2188,7 @@ public void Dispose() DataReceived = null; ClipboardWriteRequested = null; ClipboardReadRequested = null; + FontQueryRequested = null; CursorStyleChanged = null; SynchronizedOutputChanged = null; BufferChanged = null;