Skip to content

Commit b2df538

Browse files
committed
feat: added new module mousetrack for mouse support
1 parent 939992c commit b2df538

8 files changed

Lines changed: 1451 additions & 9 deletions

File tree

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ Two variants are available:
66
- **[`miniterm`](miniterm/README.md)** — legacy implementation, works with Java 8+
77
- **[`miniterm-ffm`](miniterm-ffm/README.md)** — modern implementation using the Foreign Function & Memory API, requires Java 22+
88

9-
And then we have a utility module:
10-
- **[`ansiparser`](ansiparser/README.md)** — compact ANSI escape sequence parser, works with Java 8+
9+
And then we have utility modules:
10+
- **[`ansiparser`](ansiparser/README.md)** — compact ANSI escape sequence parser
11+
- **[`mousetrack`](mousetrack/README.md)** — terminal mouse-tracking helpers and event parser
1112

1213
**Philosophy** : this project has been expressly created to be as minimal as possible, it only offers the most essential functionality that is missing from Java to be able to use the features of a modern Terminal. Several other projects exist that do this as well, but they normally come with a whole bunch of other things that you might not need. `miniterm` on the other hand *only* does the work that you can't do with standard Java APIs. Everything else can be built on top.
1314

@@ -53,16 +54,33 @@ while (true) {
5354
```java
5455
terminal.enableRawMode();
5556
AnsiReader reader = new AnsiReader(() -> terminal.read(-1));
56-
String token;
57-
while ((token = reader.read()) != null) {
58-
if (token.startsWith("\u001b")) {
59-
if (token.equals("\u001b[A")) {
57+
String seq;
58+
while ((seq = reader.read()) != null) {
59+
if (seq.startsWith("\u001b")) {
60+
if (seq.equals("\u001b[A")) {
6061
System.out.println("Up arrow");
6162
} else { /* etc... */ }
62-
} else if (token.charAt(0) == 3) {
63+
} else if (seq.charAt(0) == 3) {
6364
break; // Ctrl+C
6465
} else {
65-
System.out.println("Key pressed: " + token);
66+
System.out.println("Key pressed: " + seq);
67+
}
68+
}
69+
```
70+
71+
### Handling mouse events
72+
73+
```java
74+
terminal.enableRawMode();
75+
MouseTracking.enable(terminal, MouseTracking.Protocol.NORMAL);
76+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
77+
AnsiReader reader = new AnsiReader(() -> terminal.read(-1));
78+
String seq;
79+
while ((seq = reader.read()) != null) {
80+
if (MouseTracking.isMouseEvent(seq)) {
81+
MouseEvent ev = MouseTracking.parse(seq);
82+
System.out.printf("%-8s %-12s at (%d, %d)%n",
83+
ev.type(), ev.button(), ev.x(), ev.y());
6684
}
6785
}
6886
```
@@ -76,6 +94,7 @@ Three artifacts are published independently:
7694
| [`miniterm`](miniterm/README.md) | Legacy terminal implementation, Java 8+ |
7795
| [`miniterm-ffm`](miniterm-ffm/README.md) | Modern FFM-based terminal implementation, Java 22+ |
7896
| [`ansiparser`](ansiparser/README.md) | Compact ANSI escape sequence parser, Java 8+ |
97+
| [`mousetrack`](mousetrack/README.md) | Terminal mouse-tracking helpers and event parser, Java 8+ |
7998

8099
For dependency coordinates (Maven, Gradle, JBang) and module-specific usage details, see the individual module READMEs linked above.
81100

@@ -111,6 +130,8 @@ examples\run-ffm.bat
111130

112131
The scripts will list the available examples and let you choose one to run:
113132

133+
- **PrintAnsi** — prints the the ANSI sequence of each key pressed
134+
- **PrintKeys** — prints the code of each key pressed
135+
- **PrintMouse** — prints mouse events
114136
- **PrintSize** — prints the current terminal dimensions
115137
- **WatchSize** — watches and prints terminal size changes in real time
116-
- **PrintKeys** — prints the code of each key pressed (Ctrl+C to exit)

examples/PrintMouse.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//DEPS org.codejive.miniterm:ansiparser:0.1.1-SNAPSHOT
2+
//DEPS org.codejive.miniterm:mousetrack:0.1.1-SNAPSHOT
3+
4+
package examples;
5+
6+
import java.io.IOException;
7+
import org.codejive.miniterm.Terminal;
8+
import org.codejive.miniterm.ansiparser.AnsiReader;
9+
import org.codejive.miniterm.mousetrack.MouseEvent;
10+
import org.codejive.miniterm.mousetrack.MouseTracking;
11+
12+
public class PrintMouse {
13+
14+
// ANSI helpers
15+
private static final String CSI = "\033[";
16+
private static final String CURSOR_UP = CSI + "A";
17+
private static final String ERASE_EOL = CSI + "K";
18+
19+
public static void main(String[] args) {
20+
try (Terminal terminal = Terminal.create()) {
21+
terminal.enableRawMode();
22+
MouseTracking.enable(terminal, MouseTracking.Protocol.ANY_MOTION);
23+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
24+
try {
25+
// Reserve 5 lines and print the initial (empty) view
26+
printView(terminal, null);
27+
28+
AnsiReader reader = new AnsiReader(() -> terminal.read(-1));
29+
String token;
30+
while ((token = reader.read()) != null) {
31+
if (token.isEmpty()) continue;
32+
if (!token.startsWith("\033") && token.charAt(0) == 3) break; // Ctrl+C
33+
if (MouseTracking.isMouseEvent(token)) {
34+
MouseEvent ev = MouseTracking.parse(token);
35+
// Move cursor back up 5 lines to overwrite the previous view
36+
terminal.write(CURSOR_UP.repeat(5));
37+
printView(terminal, ev);
38+
}
39+
}
40+
} finally {
41+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
42+
MouseTracking.disable(terminal, MouseTracking.Protocol.ANY_MOTION);
43+
}
44+
} catch (IOException e) {
45+
e.printStackTrace();
46+
}
47+
}
48+
49+
private static void printView(Terminal terminal, MouseEvent ev) throws IOException {
50+
String type = ev == null ? "-" : ev.type().name();
51+
String button = ev == null ? "-" : ev.button().name();
52+
String position = ev == null ? "-" : ev.x() + ", " + ev.y();
53+
String mods = ev == null ? "-" : modifiers(ev);
54+
55+
writeLine(terminal, "Type: " + type);
56+
writeLine(terminal, "Button: " + button);
57+
writeLine(terminal, "Position: " + position);
58+
writeLine(terminal, "Modifiers: " + mods);
59+
writeLine(terminal, "(move the mouse, click or scroll — Ctrl+C to exit)");
60+
}
61+
62+
private static void writeLine(Terminal terminal, String text) throws IOException {
63+
terminal.write(text + ERASE_EOL + "\n");
64+
}
65+
66+
private static String modifiers(MouseEvent ev) {
67+
StringBuilder sb = new StringBuilder();
68+
if (ev.shift()) sb.append("SHIFT ");
69+
if (ev.alt()) sb.append("ALT ");
70+
if (ev.ctrl()) sb.append("CTRL ");
71+
return sb.length() == 0 ? "-" : sb.toString().trim();
72+
}
73+
}

mousetrack/README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# mousetrack
2+
3+
`mousetrack` is a small (~5KB) Java 8+ terminal mouse-tracking utility, part of the [java-miniterm](../README.md) project.
4+
5+
It provides static helpers for enabling and disabling terminal mouse-event reporting protocols by writing the appropriate ANSI DEC private-mode sequences to any `Appendable`, and for detecting and parsing the escape sequences that the terminal sends back into structured `MouseEvent` objects.
6+
7+
Three wire formats are supported: legacy X10, SGR (recommended), and URXVT.
8+
For pixel-accurate coordinates, SGR can be combined with the SGR-Pixels encoding (mode 1016).
9+
10+
## Usage
11+
12+
### Enabling mouse tracking
13+
14+
Choose a [protocol](#protocols) and write the enable sequence to the terminal output. Pair an [encoding](#encodings) with it when you need coordinates beyond column/row 223 — `SGR` is the recommended choice.
15+
16+
```java
17+
import org.codejive.miniterm.mousetrack.MouseTracking;
18+
19+
MouseTracking.enable(terminal, MouseTracking.Protocol.NORMAL);
20+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
21+
```
22+
23+
Always disable tracking before exiting — ideally in a `finally` block — so the terminal is left in a clean state:
24+
25+
```java
26+
try {
27+
// … read and handle events …
28+
} finally {
29+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
30+
MouseTracking.disable(terminal, MouseTracking.Protocol.NORMAL);
31+
}
32+
```
33+
34+
### Detecting and parsing mouse events
35+
36+
After reading an escape sequence (e.g. via `AnsiReader` from the `ansiparser` module), check whether it is a mouse event and decode it:
37+
38+
```java
39+
import org.codejive.miniterm.mousetrack.MouseEvent;
40+
import org.codejive.miniterm.mousetrack.MouseTracking;
41+
42+
if (MouseTracking.isMouseEvent(seq)) {
43+
MouseEvent ev = MouseTracking.parse(seq);
44+
System.out.printf("%-8s %-12s at (%d, %d)%n",
45+
ev.type(), ev.button(), ev.x(), ev.y());
46+
}
47+
```
48+
49+
`MouseEvent` exposes:
50+
51+
| Method | Description |
52+
|---|---|
53+
| `type()` | `PRESS`, `RELEASE`, `MOVE`, `DRAG`, or `SCROLL` |
54+
| `button()` | `LEFT`, `MIDDLE`, `RIGHT`, `SCROLL_UP`, `SCROLL_DOWN`, or `NONE` |
55+
| `x()` | 1-based column |
56+
| `y()` | 1-based row |
57+
| `shift()` | Shift modifier held |
58+
| `alt()` | Alt/Meta modifier held |
59+
| `ctrl()` | Ctrl modifier held |
60+
61+
### Full example with `miniterm` and `ansiparser`
62+
63+
```java
64+
import org.codejive.miniterm.Terminal;
65+
import org.codejive.miniterm.ansiparser.AnsiReader;
66+
import org.codejive.miniterm.mousetrack.MouseEvent;
67+
import org.codejive.miniterm.mousetrack.MouseTracking;
68+
69+
try (Terminal terminal = Terminal.create()) {
70+
terminal.enableRawMode();
71+
MouseTracking.enable(terminal, MouseTracking.Protocol.BUTTON_MOTION);
72+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
73+
try {
74+
AnsiReader reader = new AnsiReader(() -> terminal.read(1000));
75+
String token;
76+
while ((token = reader.read()) != null) {
77+
if (token.isEmpty()) continue; // timeout
78+
if (!token.startsWith("\u001b") && token.charAt(0) == 3) break; // Ctrl+C
79+
if (MouseTracking.isMouseEvent(token)) {
80+
MouseEvent ev = MouseTracking.parse(token);
81+
System.out.println(ev);
82+
}
83+
}
84+
} finally {
85+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
86+
MouseTracking.disable(terminal, MouseTracking.Protocol.BUTTON_MOTION);
87+
}
88+
}
89+
```
90+
91+
## Protocols
92+
93+
| Constant | DEC mode | Reports |
94+
|---|---|---|
95+
| `Protocol.X10` | `?9` | Button-press events only |
96+
| `Protocol.NORMAL` | `?1000` | Button press and release |
97+
| `Protocol.BUTTON_MOTION` | `?1002` | Press, release, and drag (motion while a button is held) |
98+
| `Protocol.ANY_MOTION` | `?1003` | Press, release, drag, and hover (all mouse movement) |
99+
100+
`ANY_MOTION` can generate a very large number of events; use it only when needed.
101+
102+
## Encodings
103+
104+
| Constant | DEC mode | Coordinate limit | Notes |
105+
|---|---|---|---|
106+
| *(default)* || 223 cols/rows | Legacy X10 byte encoding; no mode to enable |
107+
| `Encoding.UTF8` | `?1005` | ~2047 | Deprecated by many terminals |
108+
| `Encoding.SGR` | `?1006` | Unlimited | **Recommended** — decimal fields, unambiguous release |
109+
| `Encoding.URXVT` | `?1015` | Unlimited | Decimal fields but does not identify the released button |
110+
| `Encoding.SGR_PIXELS` | `?1016` | Unlimited | Reports **pixel coordinates** instead of cell coordinates; requires `SGR` to be enabled first |
111+
112+
### SGR-Pixels (pixel-accurate coordinates)
113+
114+
`Encoding.SGR_PIXELS` (DEC mode 1016) is an extension of SGR that makes the terminal report the exact pixel position of the cursor rather than its character-cell position. Enable it together with `Encoding.SGR`:
115+
116+
```java
117+
MouseTracking.enable(terminal, MouseTracking.Protocol.NORMAL);
118+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
119+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
120+
try {
121+
// ev.x() / ev.y() are now pixel offsets from the top-left corner of the terminal window
122+
} finally {
123+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR_PIXELS);
124+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
125+
MouseTracking.disable(terminal, MouseTracking.Protocol.NORMAL);
126+
}
127+
```
128+
129+
Not all terminals support mode 1016; check the terminal's documentation before relying on it.
130+
131+
## Adding the dependency
132+
133+
### JBang
134+
135+
```java
136+
//DEPS org.codejive.miniterm:mousetrack:0.1.0
137+
```
138+
139+
### Maven
140+
141+
```xml
142+
<dependency>
143+
<groupId>org.codejive.miniterm</groupId>
144+
<artifactId>mousetrack</artifactId>
145+
<version>0.1.0</version>
146+
</dependency>
147+
```
148+
149+
### Gradle
150+
151+
```kotlin
152+
implementation("org.codejive.miniterm:mousetrack:0.1.0")
153+
```
154+
155+
## Building
156+
157+
```bash
158+
./mvnw clean install
159+
```

0 commit comments

Comments
 (0)