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;