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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*
* <ul>
* <li><b>CSI</b> ({@code ESC [}) – parameter/intermediate bytes followed by a final byte
* <li><b>Legacy X10 mouse events</b> ({@code ESC [ M}) – followed by exactly 3 data bytes
* <li><b>OSC/DCS/PM/APC/SOS</b> ({@code ESC ] P X ^ _}) – string commands terminated by {@code
* BEL} (0x07) or {@code ST} ({@code ESC \})
* <li><b>Charset designators</b> ({@code ESC ( ) * + - . /}) – one additional character
Expand All @@ -35,6 +36,10 @@ public final class AnsiParser {
private static final int S_STRING_CMD = 3;
private static final int S_ESC_IN_STR = 4;
private static final int S_CHARSET = 5;
private static final int S_X10_MOUSE_EVENT = 6;

/** Number of characters following the {@code M} in a legacy X10 mouse event (b, x, y). */
private static final int X10_MOUSE_DATA_LENGTH = 3;

private AnsiParser() {}

Expand Down Expand Up @@ -78,6 +83,7 @@ public static int parse(Appendable appendable, IntReader source, int maxLength)
throws IOException {
int state = S_INIT;
int len = 0;
int remaining = 0;
while (len < maxLength) {
int ch = source.read();
if (ch < 0) {
Expand Down Expand Up @@ -108,6 +114,12 @@ public static int parse(Appendable appendable, IntReader source, int maxLength)
return 0; // simple two-char ESC sequence

case S_CSI:
if (len == 3 && ch == 'M') {
// legacy X10 mouse event: ESC [ M <b> <x> <y>
state = S_X10_MOUSE_EVENT;
remaining = X10_MOUSE_DATA_LENGTH;
break;
}
// final byte: 0x40–0x7E → sequence complete
if (ch >= 0x40 && ch <= 0x7E) return 0;
// valid parameter/intermediate bytes: 0x20–0x3F → keep reading
Expand All @@ -128,6 +140,11 @@ public static int parse(Appendable appendable, IntReader source, int maxLength)

case S_CHARSET:
return 0; // one designator char consumed

case S_X10_MOUSE_EVENT:
remaining--;
if (remaining <= 0) return 0; // <b> <x> <y> all consumed
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ void csiAbortsOnControlCode() throws IOException {
assertThat(result).isEqualTo("\u001b[\u0007");
}

// ── legacy X10 mouse events ────────────────────────────────────────────────

@Test
void x10MouseEvent() throws IOException {
// ESC [ M <b> <x> <y> — exactly 6 characters total
assertThat(AnsiParser.parse(supplyString("\u001b[M !\""))).isEqualTo("\u001b[M !\"");
}

@Test
void x10MouseEventDataBytesInFinalByteRangeAreNotSpecial() throws IOException {
// data bytes that look like CSI final bytes (e.g. 'M', 'A') must still be consumed as data
assertThat(AnsiParser.parse(supplyString("\u001b[MMAM"))).isEqualTo("\u001b[MMAM");
}

@Test
void mNotAfterCsiStartIsOrdinaryFinalByte() throws IOException {
// ESC [ 1 M – 'M' is not the first CSI char here, so it's a normal final byte
assertThat(AnsiParser.parse(supplyString("\u001b[1M"))).isEqualTo("\u001b[1M");
}

@Test
void x10MouseEventTruncatedByEof() throws IOException {
// ESC [ M <b> then EOF – returns what was accumulated so far
assertThat(AnsiParser.parse(supply(0x1B, '[', 'M', 'b', -1))).isEqualTo("\u001b[Mb");
}

// ── OSC sequences ─────────────────────────────────────────────────────────

@Test
Expand Down
31 changes: 24 additions & 7 deletions examples/PrintMouse.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,43 @@ public class PrintMouse {
private static final String CURSOR_UP = CSI + "A";
private static final String ERASE_EOL = CSI + "K";

private static final int VIEW_LINES = 6;

public static void main(String[] args) {
try (Terminal terminal = Terminal.create()) {
terminal.enableRawMode();
MouseTracking.enable(terminal, MouseTracking.Protocol.ANY_MOTION);
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
boolean pixelMode = false;
MouseEvent lastEv = null;
try {
// Reserve 5 lines and print the initial (empty) view
printView(terminal, null);
// Reserve the view lines and print the initial (empty) view
printView(terminal, null, pixelMode);

AnsiReader reader = new AnsiReader(() -> terminal.read(-1));
String token;
while ((token = reader.read()) != null) {
if (token.isEmpty()) continue;
if (!token.startsWith("\033") && token.charAt(0) == 3) break; // Ctrl+C
if (MouseTracking.isMouseEvent(token)) {
MouseEvent ev = MouseTracking.parse(token);
// Move cursor back up 5 lines to overwrite the previous view
terminal.write(CURSOR_UP.repeat(5));
printView(terminal, ev);
lastEv = MouseTracking.parse(token);
} else if (!token.startsWith("\033") && (token.equals("t") || token.equals("T"))) {
pixelMode = !pixelMode;
if (pixelMode) {
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
} else {
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
}
} else {
continue;
}
// Move cursor back up to overwrite the previous view
terminal.write(CURSOR_UP.repeat(VIEW_LINES));
printView(terminal, lastEv, pixelMode);
}
} finally {
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
MouseTracking.disable(terminal, MouseTracking.Protocol.ANY_MOTION);
}
Expand All @@ -48,16 +63,18 @@ public static void main(String[] args) {
}
}

private static void printView(Terminal terminal, MouseEvent ev) throws IOException {
private static void printView(Terminal terminal, MouseEvent ev, boolean pixelMode) throws IOException {
String type = ev == null ? "-" : ev.type().name();
String button = ev == null ? "-" : ev.button().name();
String position = ev == null ? "-" : ev.x() + ", " + ev.y();
String mods = ev == null ? "-" : modifiers(ev);
String unit = pixelMode ? "PIXEL" : "CELL";

writeLine(terminal, "Type: " + type);
writeLine(terminal, "Button: " + button);
writeLine(terminal, "Position: " + position);
writeLine(terminal, "Modifiers: " + mods);
writeLine(terminal, "Coordinates: " + unit + " (press T to toggle cell/pixel coordinates)");
writeLine(terminal, "(move the mouse, click or scroll — Ctrl+C to exit)");
}

Expand Down
7 changes: 7 additions & 0 deletions mousetrack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ try {

Not all terminals support mode 1016; check the terminal's documentation before relying on it.

Some terminals reset `SGR` as a side effect of disabling `SGR_PIXELS`. If you toggle `SGR_PIXELS` off at runtime, re-enable `SGR` right after, e.g.:

```java
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
```

## Adding the dependency

### JBang
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,28 @@ public Button button() {
}

/**
* Returns the 1-based column at which the event occurred.
* Returns the horizontal coordinate at which the event occurred.
*
* @return column, &ge; 1
* <p>By default this is the 1-based column of the character cell. If {@link
* MouseTracking.Encoding#SGR_PIXELS} was enabled when the event was reported, this is instead a
* 0-based pixel offset from the left edge of the terminal window. This class cannot tell the
* two apart — callers must track which encoding is active.
*
* @return column or pixel offset, depending on the active encoding
*/
public int x() {
return x;
}

/**
* Returns the 1-based row at which the event occurred.
* Returns the vertical coordinate at which the event occurred.
*
* <p>By default this is the 1-based row of the character cell. If {@link
* MouseTracking.Encoding#SGR_PIXELS} was enabled when the event was reported, this is instead a
* 0-based pixel offset from the top edge of the terminal window. This class cannot tell the two
* apart — callers must track which encoding is active.
*
* @return row, &ge; 1
* @return row or pixel offset, depending on the active encoding
*/
public int y() {
return y;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@
*
* <p>{@link Encoding#SGR_PIXELS} (DEC mode 1016) extends {@link Encoding#SGR} to report pixel-level
* coordinates instead of cell-based coordinates. Enable it together with {@link Encoding#SGR}.
*
* <p>The wire format is unchanged by {@link Encoding#SGR_PIXELS} — only the meaning of the {@code
* Px}/{@code Py} fields changes, from 1-based character-cell column/row to pixel offsets from the
* terminal's top-left corner. {@link #parse(String)} decodes both the same way; callers must track
* which encoding is active to know how to interpret {@link MouseEvent#x()} and {@link
* MouseEvent#y()}.
*
* <p>Some terminals reset {@link Encoding#SGR} as a side effect of disabling {@link
* Encoding#SGR_PIXELS}. If you toggle {@link Encoding#SGR_PIXELS} off at runtime, re-enable {@link
* Encoding#SGR} right after to be safe.
*/
public final class MouseTracking {

Expand Down