Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/XTerm.NET.Tests/Esc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public static class Esc
public const string ClearScreen = "\u001b[2J";
public const string ClearLine = "\u001b[2K";

// mouse reporting (DECSET)
public const string EnableMouseTracking = "\u001b[?1000h";
public const string EnableSgrMouse = "\u001b[?1006h";
public const string EnableSgrPixelsMouse = "\u001b[?1016h";

/// <summary>A control sequence: ESC [ followed by the body.</summary>
public static string Csi(string body)
{
Expand All @@ -43,6 +48,19 @@ public static string SetCursorPosition(int x, int y)
return $"\u001b[{y + 1};{x + 1}H";
}

/// <summary>
/// An SGR mouse report (1006, or 1016 in pixels), taking a zero-based cell or pixel.
/// </summary>
/// <remarks>
/// One-based on the wire like CUP, and converted here for the same reason. The terminator
/// is the only thing telling a press from a release, so it comes from a flag rather than a
/// hand-typed M or m.
/// </remarks>
public static string SgrMouseReport(int button, int x, int y, bool press = true)
{
return $"\u001b[<{button};{x + 1};{y + 1}{(press ? 'M' : 'm')}";
}

/// <summary>
/// An application programming command, which is how the Kitty graphics protocol arrives.
/// </summary>
Expand Down
42 changes: 42 additions & 0 deletions src/XTerm.NET.Tests/MouseTrackingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,48 @@ public void MouseEvent_SGRFormat_Release_UsesLowercaseM()

#endregion

#region SGR-Pixels Format

[Fact]
public void MouseEvent_SGRPixels_ReportsOneBasedPixelPosition()
{
var terminal = CreateTerminal();
terminal.Write(Esc.EnableMouseTracking + Esc.EnableSgrPixelsMouse);

var down = terminal.GenerateMouseEvent(MouseButton.Left, 5, 10, 57, 213, MouseEventType.Down);
var up = terminal.GenerateMouseEvent(MouseButton.Left, 5, 10, 57, 213, MouseEventType.Up);

Assert.Equal(MouseEncoding.SGRPixels, terminal.MouseEncoding);
Assert.Equal(Esc.SgrMouseReport(0, 57, 213), down);
Assert.Equal(Esc.SgrMouseReport(0, 57, 213, press: false), up);
}

[Fact]
public void MouseEvent_SGRPixels_FromCellOnlyHost_ReportsCellCorner()
{
// A host that never learned the pixel overload still answers in pixels, at the clicked
// cell's top-left, so dividing by the CSI 16 t cell size lands back on that cell.
var terminal = new Terminal(new TerminalOptions { Cols = 80, Rows = 24, CellWidthPixels = 9, CellHeightPixels = 18 });
terminal.Write(Esc.EnableMouseTracking + Esc.EnableSgrPixelsMouse);

var sequence = terminal.GenerateMouseEvent(MouseButton.Left, 5, 10, MouseEventType.Down);

Assert.Equal(Esc.SgrMouseReport(0, 5 * 9, 10 * 18), sequence);
}

[Fact]
public void MouseEvent_PixelOverload_UnderSGR_StillReportsCells()
{
var terminal = CreateTerminal();
terminal.Write(Esc.EnableMouseTracking + Esc.EnableSgrMouse);

var sequence = terminal.GenerateMouseEvent(MouseButton.Left, 5, 10, 57, 213, MouseEventType.Down);

Assert.Equal(Esc.SgrMouseReport(0, 5, 10), sequence);
}

#endregion

#region Modifiers

[Fact]
Expand Down
26 changes: 25 additions & 1 deletion src/XTerm.NET.Tests/RequestModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public void Reports_each_mode_as_set_after_setting_it_and_reset_after_resetting_
[InlineData(6)] // origin
[InlineData(1000)] // VT200 mouse
[InlineData(1006)] // SGR mouse encoding
[InlineData(1016)] // SGR-Pixels mouse encoding
[InlineData(1049)] // alternate buffer
[InlineData(2004)] // bracketed paste
[InlineData(2026)] // synchronized output
Expand Down Expand Up @@ -201,6 +202,30 @@ public void Reports_only_the_mouse_encoding_currently_selected()
replies);
}

/// <summary>
/// SGR-Pixels is one more choice in that same selection. An application probing for it with
/// DECRQM before switching has to hear "supported" -- a 0 reply sends it back to cell reports --
/// and once it is on, 1006 is not, because the reports no longer carry cells.
/// </summary>
[Fact]
public void Reports_sgr_pixels_as_a_mouse_encoding_selection()
{
var terminal = Fresh();

terminal.Write(Set(1016));
var replies = Replies(terminal);
terminal.Write(Query(1016));
terminal.Write(Query(1006));

Assert.Equal(new[] { Report(1016, true), Report(1006, false) }, replies);

replies.Clear();
terminal.Write(Reset(1016));
terminal.Write(Query(1016));

Assert.Equal(new[] { Report(1016, false) }, replies);
}

/// <summary>
/// The three alternate-buffer modes differ only in the cursor and erase work they do on the way
/// in and out; there is one buffer, so they read alike. An application that entered with 1049
Expand Down Expand Up @@ -239,7 +264,6 @@ public void Reports_the_alternate_buffer_alike_for_all_three_modes_that_switch_i
[Theory]
[InlineData(1035)] // NumLock modifiers
[InlineData(1001)] // highlight mouse tracking, not implemented
[InlineData(1016)] // pixel-position mouse, not implemented
[InlineData(64738)] // not a mode at all
public void Reports_not_recognised_for_modes_it_keeps_no_state_for(int mode)
{
Expand Down
1 change: 1 addition & 0 deletions src/XTerm.NET/Input/MouseInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ public enum MouseEncoding
Utf8, // UTF-8 encoding (CSI ? 1005 h)
SGR, // SGR format (CSI ? 1006 h) - recommended
URXVT, // URXVT format (CSI ? 1015 h)
SGRPixels, // SGR format with pixel coordinates (CSI ? 1016 h)
}
22 changes: 22 additions & 0 deletions src/XTerm.NET/Input/MouseTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ public MouseTracker(Terminal terminal)
/// Generates a mouse event sequence.
/// </summary>
public string GenerateMouseEvent(MouseButton button, int x, int y, MouseEventType eventType, KeyModifiers modifiers = KeyModifiers.None)
{
// A host that only knows the cell still gets an answer in pixel mode: the cell's top-left
// corner, from the same cell metrics a CSI 16 t query reports. An application dividing by
// those metrics recovers exactly the cell that was clicked -- which is all this host knows.
return GenerateMouseEvent(button, x, y,
x * Math.Max(1, _terminal.Options.CellWidthPixels),
y * Math.Max(1, _terminal.Options.CellHeightPixels),
eventType, modifiers);
}

/// <summary>
/// Generates a mouse event sequence for a host that knows the pointer's pixel position.
/// </summary>
/// <remarks>
/// <paramref name="pixelX"/> and <paramref name="pixelY"/> are 0-based offsets from the top-left
/// of the cell area, in the same device pixels as <c>CellWidthPixels</c>. They are used only
/// under SGR-Pixels (DECSET 1016); every other encoding reports the cell.
/// </remarks>
public string GenerateMouseEvent(MouseButton button, int x, int y, int pixelX, int pixelY, MouseEventType eventType, KeyModifiers modifiers = KeyModifiers.None)
{
// Check if this mode supports this event type
if (!ShouldReportEvent(button, eventType))
Expand All @@ -39,6 +58,9 @@ public string GenerateMouseEvent(MouseButton button, int x, int y, MouseEventTyp
return Encoding switch
{
MouseEncoding.SGR => GenerateSGRSequence(button, x, y, eventType, modifiers),
// 1016 is 1006 with the coordinates swapped for pixels -- same button byte, same M/m
// terminator, and 1-based like xterm's (EmitMousePosition adds one in both modes).
MouseEncoding.SGRPixels => GenerateSGRSequence(button, Math.Max(0, pixelX), Math.Max(0, pixelY), eventType, modifiers),
MouseEncoding.URXVT => GenerateURXVTSequence(button, x, y, eventType, modifiers),
MouseEncoding.Utf8 => GenerateUTF8Sequence(button, x, y, eventType, modifiers),
_ => GenerateDefaultSequence(button, x, y, eventType, modifiers)
Expand Down
4 changes: 4 additions & 0 deletions src/XTerm.NET/InputHandler.Modes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ private bool TryGetPrivateModeState(int mode, out bool set)
case (int)TerminalMode.MouseReportUrxvt:
set = mouseTracker.Encoding == MouseEncoding.URXVT;
return true;
case (int)TerminalMode.MouseReportPixel:
set = mouseTracker.Encoding == MouseEncoding.SGRPixels;
return true;
case (int)TerminalMode.SendFocusEvents:
set = _terminal.SendFocusEvents;
return true;
Expand Down Expand Up @@ -449,6 +452,7 @@ private void ResetCSIMode(int mode, bool isPrivate)
case TerminalMode.MouseReportUtf8:
case TerminalMode.MouseReportSgr:
case TerminalMode.MouseReportUrxvt:
case TerminalMode.MouseReportPixel:
_terminal.GetMouseTracker().Encoding = MouseEncoding.Default;
break;

Expand Down
4 changes: 4 additions & 0 deletions src/XTerm.NET/InputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,10 @@ private void SetCSIMode(int mode, bool isPrivate)
_terminal.GetMouseTracker().Encoding = MouseEncoding.URXVT;
break;

case TerminalMode.MouseReportPixel:
_terminal.GetMouseTracker().Encoding = MouseEncoding.SGRPixels;
break;

case TerminalMode.EightBitInput:
_terminal.EightBitInput = true;
break;
Expand Down
17 changes: 17 additions & 0 deletions src/XTerm.NET/Terminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,23 @@ public string GenerateMouseEvent(MouseButton button, int x, int y, MouseEventTyp
return _mouseTracker.GenerateMouseEvent(button, x, y, eventType, modifiers);
}

/// <summary>
/// Generates an escape sequence for a mouse event, with the pointer's pixel position for
/// SGR-Pixels reporting (DECSET 1016).
/// </summary>
/// <param name="button">The mouse button</param>
/// <param name="x">The column position (0-based)</param>
/// <param name="y">The row position (0-based)</param>
/// <param name="pixelX">Horizontal offset from the left of the cell area, in device pixels (0-based)</param>
/// <param name="pixelY">Vertical offset from the top of the cell area, in device pixels (0-based)</param>
/// <param name="eventType">The type of mouse event</param>
/// <param name="modifiers">Modifier keys held during the event</param>
/// <returns>The escape sequence string to send to the application</returns>
public string GenerateMouseEvent(MouseButton button, int x, int y, int pixelX, int pixelY, MouseEventType eventType, KeyModifiers modifiers = KeyModifiers.None)
{
return _mouseTracker.GenerateMouseEvent(button, x, y, pixelX, pixelY, eventType, modifiers);
}

/// <summary>
/// Generates an escape sequence for a focus event (focus in/out).
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/XTerm.NET/XTerm.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<RootNamespace>XTerm</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<Version>2.0.3</Version>
<AssemblyVersion>2.0.3.0</AssemblyVersion>
<Version>2.0.4</Version>
<AssemblyVersion>2.0.4.0</AssemblyVersion>
<Description>Dotnet port of xterm.js, giving you ability to host console applications, with Sixel and Kitty graphics.</Description>
<Company>Iciclecreek</Company>
<Authors>Tom Laird-McConnell</Authors>
Expand Down
Loading