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
77 changes: 77 additions & 0 deletions twinkle-text/src/main/java/org/codejive/twinkle/ansi/Ansi.java
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,82 @@ public static String linkEnd() {
return OSC + HYPERLINK + ";" + OSC_END;
}

/**
* Returns the ANSI escape sequence to enable SGR extended mouse mode tracking. This should be
* combined with one of the mouse tracking modes (e.g., mouseTrackingEnable()). SGR mode
* provides a more reliable encoding that supports coordinates beyond 223.
*
* @return the ANSI escape sequence to enable SGR extended mouse mode
*/
public static String mouseSgrModeEnable() {
return CSI + MOUSE_SGR_EXT_MODE_ENABLE;
}

/**
* Returns the ANSI escape sequence to disable SGR extended mouse mode tracking.
*
* @return the ANSI escape sequence to disable SGR extended mouse mode
*/
public static String mouseSgrModeDisable() {
return CSI + MOUSE_SGR_EXT_MODE_DISABLE;
}

/**
* Returns the ANSI escape sequence to enable basic mouse tracking. This will report button
* press and release events.
*
* @return the ANSI escape sequence to enable basic mouse tracking
*/
public static String mouseTrackingEnable() {
return CSI + MOUSE_BUTTON_TRACKING_ENABLE;
}

/**
* Returns the ANSI escape sequence to disable basic mouse tracking.
*
* @return the ANSI escape sequence to disable basic mouse tracking
*/
public static String mouseTrackingDisable() {
return CSI + MOUSE_BUTTON_TRACKING_DISABLE;
}

/**
* Returns the ANSI escape sequence to enable button event tracking. This will report button
* press, release, and drag events.
*
* @return the ANSI escape sequence to enable button event tracking
*/
public static String mouseButtonTrackingEnable() {
return CSI + MOUSE_BUTTON_AND_DRAG_TRACKING_ENABLE;
}

/**
* Returns the ANSI escape sequence to disable button event tracking.
*
* @return the ANSI escape sequence to disable button event tracking
*/
public static String mouseButtonTrackingDisable() {
return CSI + MOUSE_BUTTON_AND_DRAG_TRACKING_DISABLE;
}

/**
* Returns the ANSI escape sequence to enable any event tracking. This will report all mouse
* events, including movement without buttons pressed.
*
* @return the ANSI escape sequence to enable any event tracking
*/
public static String mouseAnyEventTrackingEnable() {
return CSI + MOUSE_ANY_EVENT_TRACKING_ENABLE;
}

/**
* Returns the ANSI escape sequence to disable any event tracking.
*
* @return the ANSI escape sequence to disable any event tracking
*/
public static String mouseAnyEventTrackingDisable() {
return CSI + MOUSE_ANY_EVENT_TRACKING_DISABLE;
}

private Ansi() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,36 @@ public class Constants {
public static final String LINE_WRAP_OFF = "=7l";

public static final String HYPERLINK = "8;";

// Mouse tracking modes
public static final String MOUSE_BUTTON_TRACKING_ENABLE =
"?1000h"; // Enable basic mouse tracking
public static final String MOUSE_BUTTON_TRACKING_DISABLE =
"?1000l"; // Disable basic mouse tracking
public static final String MOUSE_BUTTON_AND_DRAG_TRACKING_ENABLE =
"?1002h"; // Enable button event and drag tracking
public static final String MOUSE_BUTTON_AND_DRAG_TRACKING_DISABLE =
"?1002l"; // Disable button event and drag tracking
public static final String MOUSE_ANY_EVENT_TRACKING_ENABLE =
"?1003h"; // Enable any event tracking
public static final String MOUSE_ANY_EVENT_TRACKING_DISABLE =
"?1003l"; // Disable any event tracking
public static final String MOUSE_SGR_EXT_MODE_ENABLE = "?1006h"; // Enable SGR extended mode
public static final String MOUSE_SGR_EXT_MODE_DISABLE = "?1006l"; // Disable SGR extended mode

// Mouse button codes (for SGR mode)
public static final int MOUSE_BUTTON_LEFT = 0;
public static final int MOUSE_BUTTON_MIDDLE = 1;
public static final int MOUSE_BUTTON_RIGHT = 2;
public static final int MOUSE_BUTTON_RELEASE = 3;
public static final int MOUSE_SCROLL_UP = 64;
public static final int MOUSE_SCROLL_DOWN = 65;

// Mouse modifier flags
public static final int MOUSE_MODIFIER_SHIFT = 4;
public static final int MOUSE_MODIFIER_ALT = 8;
public static final int MOUSE_MODIFIER_CTRL = 16;

// X10 mouse encoding
public static final int MOUSE_X10_OFFSET = 32; // Offset for X10 encoding (button, x, y)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.codejive.twinkle.ansi.util;

import static org.codejive.twinkle.ansi.Constants.*;

import org.jspecify.annotations.NonNull;

/**
* A decoder for ANSI mouse event escape sequences. This class supports both SGR (Select Graphic
* Rendition) extended mouse protocol and X10 mouse protocol events.
*
* <p>SGR mouse events have the format:
*
* <ul>
* <li>{@code ESC[<Cb;Cx;CyM} for button press and drag events
* <li>{@code ESC[<Cb;Cx;Cym} for button release events
* </ul>
*
* <p>Where:
*
* <ul>
* <li>Cb = button code with modifiers (Shift=4, Alt=8, Ctrl=16)
* <li>Cx = x coordinate (1-based in protocol, converted to 0-based)
* <li>Cy = y coordinate (1-based in protocol, converted to 0-based)
* </ul>
*
* <p>X10 mouse events have the format:
*
* <ul>
* <li>{@code ESC[M<Cb><Cx><Cy>} where each parameter is a single byte
* </ul>
*
* <p>Where:
*
* <ul>
* <li>Cb = button code + 32 with modifiers (Shift=4, Alt=8, Ctrl=16)
* <li>Cx = x coordinate + 32 (0-based)
* <li>Cy = y coordinate + 32 (0-based)
* </ul>
*/
public class MouseDecoder {
private final @NonNull MouseEvent handler;

private static final int DRAG_FLAG = 32;
private static final int MOVE_FLAG = 35; // Button code for move events

/**
* Creates a new MouseDecoder with the specified event handler.
*
* @param handler the MouseEvent handler that will receive callbacks for decoded mouse events
*/
public MouseDecoder(@NonNull MouseEvent handler) {
this.handler = handler;
}

/**
* Attempts to decode the given escape sequence as a mouse event.
*
* <p>If the sequence is a valid SGR or X10 mouse event, this method will call the appropriate
* callback on the MouseEvent handler and return true. If the sequence is not a mouse event or
* is malformed, it returns false without calling the handler.
*
* @param sequence the complete ANSI escape sequence to analyze
* @return true if the sequence was a mouse event and was successfully decoded, false otherwise
*/
public boolean accept(String sequence) {
if (sequence == null || sequence.length() < 6) {
return false;
}

// Check for SGR format: ESC[<...M or ESC[<...m
if (sequence.startsWith("\u001B[<")) {
return decodeSgr(sequence);
}

// Check for X10 format: ESC[M followed by exactly 3 bytes
if (sequence.startsWith("\u001B[M") && sequence.length() == 6) {
return decodeX10(sequence);
}

return false;
}

/**
* Decodes an SGR extended mouse event sequence.
*
* @param sequence the SGR mouse sequence
* @return true if successfully decoded, false otherwise
*/
private boolean decodeSgr(String sequence) {
// Check the final character (M for press/drag, m for release)
char finalChar = sequence.charAt(sequence.length() - 1);
if (finalChar != 'M' && finalChar != 'm') {
return false;
}

boolean isRelease = (finalChar == 'm');

// Extract the parameters: button, x, y
String params = sequence.substring(3, sequence.length() - 1);
String[] parts = params.split(";");

if (parts.length != 3) {
return false;
}

try {
int buttonCode = Integer.parseInt(parts[0]);
int x = Integer.parseInt(parts[1]) - 1; // Convert from 1-based to 0-based
int y = Integer.parseInt(parts[2]) - 1; // Convert from 1-based to 0-based

dispatchMouseEvent(buttonCode, x, y, isRelease);
return true;
} catch (NumberFormatException e) {
// Malformed sequence
return false;
}
}

/**
* Decodes an X10 mouse event sequence.
*
* @param sequence the X10 mouse sequence (ESC[M followed by 3 bytes)
* @return true if successfully decoded, false otherwise
*/
private boolean decodeX10(String sequence) {
// Extract the three bytes: button, x, y
int buttonCode = sequence.charAt(3) - MOUSE_X10_OFFSET;
int x = sequence.charAt(4) - MOUSE_X10_OFFSET;
int y = sequence.charAt(5) - MOUSE_X10_OFFSET;

// X10 uses button bits 0-1 to encode which button, with 3 meaning release
int buttonBits = buttonCode & 3;
boolean isRelease = (buttonBits == 3);

dispatchMouseEvent(buttonCode, x, y, isRelease);
return true;
}

/**
* Dispatches a mouse event to the appropriate handler method based on the button code and
* release flag.
*
* @param buttonCode the raw button code with modifiers
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param isRelease true if this is a button release event
*/
private void dispatchMouseEvent(int buttonCode, int x, int y, boolean isRelease) {
// Extract modifiers
boolean shift = (buttonCode & MOUSE_MODIFIER_SHIFT) != 0;
boolean alt = (buttonCode & MOUSE_MODIFIER_ALT) != 0;
boolean ctrl = (buttonCode & MOUSE_MODIFIER_CTRL) != 0;

// Remove modifiers to get the base button code
int baseButton =
buttonCode & ~(MOUSE_MODIFIER_SHIFT | MOUSE_MODIFIER_ALT | MOUSE_MODIFIER_CTRL);

// Handle different event types
if (isRelease) {
// Button release event
handler.onButtonRelease(baseButton, x, y, shift, alt, ctrl);
} else {
// Check if it's a drag/move event (bit 5 set, value 32)
if ((baseButton & DRAG_FLAG) != 0) {
int dragButton = baseButton & ~DRAG_FLAG;
// Check if it's a move event (no button pressed)
if (dragButton == MOVE_FLAG - DRAG_FLAG) {
handler.onMove(x, y, shift, alt, ctrl);
} else {
handler.onDrag(dragButton, x, y, shift, alt, ctrl);
}
} else if (baseButton >= MOUSE_SCROLL_UP && baseButton <= MOUSE_SCROLL_DOWN) {
// Scroll event
handler.onScroll(baseButton, x, y, shift, alt, ctrl);
} else {
// Button press event
handler.onButtonPress(baseButton, x, y, shift, alt, ctrl);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.codejive.twinkle.ansi.util;

/**
* Interface for handling mouse events decoded from ANSI escape sequences. Implementations can be
* passed to {@link MouseDecoder} to receive callbacks for different types of mouse events.
*/
public interface MouseEvent {
/**
* Called when a mouse button is pressed.
*
* @param button the button code (e.g., MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
* MOUSE_BUTTON_MIDDLE)
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param shift true if the Shift key was held during the event
* @param alt true if the Alt key was held during the event
* @param ctrl true if the Ctrl key was held during the event
*/
void onButtonPress(int button, int x, int y, boolean shift, boolean alt, boolean ctrl);

/**
* Called when a mouse button is released.
*
* @param button the button code (e.g., MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
* MOUSE_BUTTON_MIDDLE)
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param shift true if the Shift key was held during the event
* @param alt true if the Alt key was held during the event
* @param ctrl true if the Ctrl key was held during the event
*/
void onButtonRelease(int button, int x, int y, boolean shift, boolean alt, boolean ctrl);

/**
* Called when the mouse is moved while a button is held down (drag event).
*
* @param button the button code (e.g., MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
* MOUSE_BUTTON_MIDDLE)
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param shift true if the Shift key was held during the event
* @param alt true if the Alt key was held during the event
* @param ctrl true if the Ctrl key was held during the event
*/
void onDrag(int button, int x, int y, boolean shift, boolean alt, boolean ctrl);

/**
* Called when the mouse wheel is scrolled.
*
* @param direction the scroll direction (MOUSE_SCROLL_UP or MOUSE_SCROLL_DOWN)
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param shift true if the Shift key was held during the event
* @param alt true if the Alt key was held during the event
* @param ctrl true if the Ctrl key was held during the event
*/
void onScroll(int direction, int x, int y, boolean shift, boolean alt, boolean ctrl);

/**
* Called when the mouse is moved without any buttons held down (requires any event tracking).
*
* @param x the x coordinate (0-based)
* @param y the y coordinate (0-based)
* @param shift true if the Shift key was held during the event
* @param alt true if the Alt key was held during the event
* @param ctrl true if the Ctrl key was held during the event
*/
void onMove(int x, int y, boolean shift, boolean alt, boolean ctrl);
}
Loading
Loading