From 74d411fd94b5e6645011f82e35a38e1c5b473d9c Mon Sep 17 00:00:00 2001 From: panicz Date: Thu, 2 Apr 2026 18:32:12 +0200 Subject: [PATCH] Fixed mouse movement handling when the position is greater than 127 --- .../lanterna/input/MouseCharacterPattern.java | 16 +- .../input/MouseTranslatingInputStream.java | 148 ++++++++++++++++++ .../terminal/ansi/StreamBasedTerminal.java | 4 +- 3 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/googlecode/lanterna/input/MouseTranslatingInputStream.java diff --git a/src/main/java/com/googlecode/lanterna/input/MouseCharacterPattern.java b/src/main/java/com/googlecode/lanterna/input/MouseCharacterPattern.java index 0548d8a0f..500f8cf29 100644 --- a/src/main/java/com/googlecode/lanterna/input/MouseCharacterPattern.java +++ b/src/main/java/com/googlecode/lanterna/input/MouseCharacterPattern.java @@ -93,12 +93,12 @@ public Matching match(List seq) { } else { button = 5; } - } else if((item & 0x2) != 0) { - button = 3; - } else if((item & 0x1) != 0) { - button = 1; - } else if((item & 0x1) == 0) { - button = 2; + } else if((item & 0x3) == 2) { + button = 3; // right + } else if((item & 0x3) == 1) { + button = 2; // middle + } else { + button = 1; // left } // Get the modifier keys (it seems that they do not are always reported correctly depending on the terminal) @@ -127,9 +127,9 @@ public Matching match(List seq) { // Get the move and drag actions if((item & 0x20) != 0) { - if((item & 0x3) != 0) + if((item & 0x3) == 3) { - // In move mode, the bits 0, 1 are set in addition to the 6th bit + // button bits 11 with motion flag = no button held = MOVE actionType=MouseActionType.MOVE; button=0; } else { diff --git a/src/main/java/com/googlecode/lanterna/input/MouseTranslatingInputStream.java b/src/main/java/com/googlecode/lanterna/input/MouseTranslatingInputStream.java new file mode 100644 index 000000000..b37c1bd72 --- /dev/null +++ b/src/main/java/com/googlecode/lanterna/input/MouseTranslatingInputStream.java @@ -0,0 +1,148 @@ +/* + * X10 mouse fallback for terminals that don't support SGR mode 1006 + * (e.g. ConPTY on WSL2). Lanterna 3.2+ only parses SGR format + * (ESC [ < button ; col ; row M/m), so X10 events (ESC [ M Cb Cx Cy) + * would be silently ignored. This InputStream wrapper converts X10 + * to SGR at the byte level, before UTF-8 decoding. SGR events + * (ESC [ <) pass through unchanged. + */ +package com.googlecode.lanterna.input; + +import java.io.IOException; +import java.io.InputStream; + +public class MouseTranslatingInputStream extends InputStream { + + private final InputStream source; + + // Output buffer for converted SGR sequences + private final byte[] outBuf = new byte[48]; + private int outPos = 0; + private int outLen = 0; + + public MouseTranslatingInputStream(InputStream source) { + this.source = source; + } + + @Override + public int available() throws IOException { + return (outLen - outPos) + source.available(); + } + + @Override + public void close() throws IOException { + source.close(); + } + + private void bufferByte(int b) { + outBuf[outLen++] = (byte) b; + } + + private void bufferDecimal(int n) { + String s = Integer.toString(n); + for (int i = 0; i < s.length(); i++) { + bufferByte(s.charAt(i)); + } + } + + private int readOne() throws IOException { + // Return from buffer if non-empty + if (outPos < outLen) { + int b = outBuf[outPos++] & 0xFF; + if (outPos >= outLen) { + outPos = 0; + outLen = 0; + } + return b; + } + + int b = source.read(); + if (b != 27) { // not ESC, pass through + return b; + } + + // Saw ESC - look for [ M (X10 mouse) + int b2 = source.read(); + if (b2 == -1) { + return 27; + } + if (b2 != '[') { + outPos = 0; + outLen = 0; + bufferByte(b2); + return 27; + } + + // Saw ESC [ - check for M + int b3 = source.read(); + if (b3 == -1) { + outPos = 0; + outLen = 0; + bufferByte('['); + return 27; + } + if (b3 != 'M') { + // Not X10 mouse, pass through ESC [ + outPos = 0; + outLen = 0; + bufferByte('['); + bufferByte(b3); + return 27; + } + + // X10 mouse event: ESC [ M Cb Cx Cy + int cb = source.read(); + int cx = source.read(); + int cy = source.read(); + if (cb == -1 || cx == -1 || cy == -1) { + return -1; + } + + // Convert to SGR: ESC [ < button ; col ; row M/m + int button = cb - 32; + int col = cx - 32; + int row = cy - 32; + boolean isRelease = (button & 3) == 3 && (button & 64) == 0; + + outPos = 0; + outLen = 0; + bufferByte('['); + bufferByte('<'); + bufferDecimal(button); + bufferByte(';'); + bufferDecimal(col); + bufferByte(';'); + bufferDecimal(row); + bufferByte(isRelease ? 'm' : 'M'); + return 27; // return ESC + } + + @Override + public int read() throws IOException { + return readOne(); + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + if (len <= 0) { + return 0; + } + // First byte: blocking + int first = readOne(); + if (first == -1) { + return -1; + } + buf[off] = (byte) first; + // Remaining bytes: only if immediately available + int i = 1; + while (i < len && available() > 0) { + int b = readOne(); + if (b == -1) { + break; + } + buf[off + i] = (byte) b; + i++; + } + return i; + } +} diff --git a/src/main/java/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java b/src/main/java/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java index 9b82f3a28..c652244b7 100644 --- a/src/main/java/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java +++ b/src/main/java/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java @@ -77,7 +77,9 @@ public StreamBasedTerminal(InputStream terminalInput, OutputStream terminalOutpu else { this.terminalCharset = terminalCharset; } - this.inputDecoder = new InputDecoder(new InputStreamReader(this.terminalInput, this.terminalCharset)); + this.inputDecoder = new InputDecoder(new InputStreamReader( + new com.googlecode.lanterna.input.MouseTranslatingInputStream(this.terminalInput), + this.terminalCharset)); this.keyQueue = new LinkedList<>(); this.readLock = new ReentrantLock(); this.lastReportedCursorPosition = null;