diff --git a/src/XTerm.NET.Tests/Esc.cs b/src/XTerm.NET.Tests/Esc.cs index 0cc6cf34..be503816 100644 --- a/src/XTerm.NET.Tests/Esc.cs +++ b/src/XTerm.NET.Tests/Esc.cs @@ -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"; + /// A control sequence: ESC [ followed by the body. public static string Csi(string body) { @@ -43,6 +48,19 @@ public static string SetCursorPosition(int x, int y) return $"\u001b[{y + 1};{x + 1}H"; } + /// + /// An SGR mouse report (1006, or 1016 in pixels), taking a zero-based cell or pixel. + /// + /// + /// 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. + /// + public static string SgrMouseReport(int button, int x, int y, bool press = true) + { + return $"\u001b[<{button};{x + 1};{y + 1}{(press ? 'M' : 'm')}"; + } + /// /// An application programming command, which is how the Kitty graphics protocol arrives. /// diff --git a/src/XTerm.NET.Tests/MouseTrackingTests.cs b/src/XTerm.NET.Tests/MouseTrackingTests.cs index 71a721c6..069863bf 100644 --- a/src/XTerm.NET.Tests/MouseTrackingTests.cs +++ b/src/XTerm.NET.Tests/MouseTrackingTests.cs @@ -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] diff --git a/src/XTerm.NET.Tests/RequestModeTests.cs b/src/XTerm.NET.Tests/RequestModeTests.cs index e2280a6d..e67b7be1 100644 --- a/src/XTerm.NET.Tests/RequestModeTests.cs +++ b/src/XTerm.NET.Tests/RequestModeTests.cs @@ -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 @@ -201,6 +202,30 @@ public void Reports_only_the_mouse_encoding_currently_selected() replies); } + /// + /// 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. + /// + [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); + } + /// /// 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 @@ -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) { diff --git a/src/XTerm.NET/Input/MouseInput.cs b/src/XTerm.NET/Input/MouseInput.cs index ec117915..ff91b71d 100644 --- a/src/XTerm.NET/Input/MouseInput.cs +++ b/src/XTerm.NET/Input/MouseInput.cs @@ -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) } diff --git a/src/XTerm.NET/Input/MouseTracker.cs b/src/XTerm.NET/Input/MouseTracker.cs index f8f2adb9..7e0c095b 100644 --- a/src/XTerm.NET/Input/MouseTracker.cs +++ b/src/XTerm.NET/Input/MouseTracker.cs @@ -27,6 +27,25 @@ public MouseTracker(Terminal terminal) /// Generates a mouse event sequence. /// 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); + } + + /// + /// Generates a mouse event sequence for a host that knows the pointer's pixel position. + /// + /// + /// and are 0-based offsets from the top-left + /// of the cell area, in the same device pixels as CellWidthPixels. They are used only + /// under SGR-Pixels (DECSET 1016); every other encoding reports the cell. + /// + 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)) @@ -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) diff --git a/src/XTerm.NET/InputHandler.Modes.cs b/src/XTerm.NET/InputHandler.Modes.cs index c7db77ba..7cd70839 100644 --- a/src/XTerm.NET/InputHandler.Modes.cs +++ b/src/XTerm.NET/InputHandler.Modes.cs @@ -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; @@ -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; diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index 650861f1..b25ce1b6 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -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; diff --git a/src/XTerm.NET/Terminal.cs b/src/XTerm.NET/Terminal.cs index 66693d99..14831a6c 100644 --- a/src/XTerm.NET/Terminal.cs +++ b/src/XTerm.NET/Terminal.cs @@ -1728,6 +1728,23 @@ public string GenerateMouseEvent(MouseButton button, int x, int y, MouseEventTyp return _mouseTracker.GenerateMouseEvent(button, x, y, eventType, modifiers); } + /// + /// Generates an escape sequence for a mouse event, with the pointer's pixel position for + /// SGR-Pixels reporting (DECSET 1016). + /// + /// The mouse button + /// The column position (0-based) + /// The row position (0-based) + /// Horizontal offset from the left of the cell area, in device pixels (0-based) + /// Vertical offset from the top of the cell area, in device pixels (0-based) + /// The type of mouse event + /// Modifier keys held during the event + /// The escape sequence string to send to the application + 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); + } + /// /// Generates an escape sequence for a focus event (focus in/out). /// diff --git a/src/XTerm.NET/XTerm.NET.csproj b/src/XTerm.NET/XTerm.NET.csproj index 85515d3e..4d5934c7 100644 --- a/src/XTerm.NET/XTerm.NET.csproj +++ b/src/XTerm.NET/XTerm.NET.csproj @@ -7,8 +7,8 @@ XTerm - 2.0.3 - 2.0.3.0 + 2.0.4 + 2.0.4.0 Dotnet port of xterm.js, giving you ability to host console applications, with Sixel and Kitty graphics. Iciclecreek Tom Laird-McConnell