From 2230854f9ffc02d820ee3550d00ddf0986252f49 Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 21:51:03 +0900 Subject: [PATCH 1/3] Terminal: take every mode in a DEC private set/reset, hide the cursor on request, and support bracketed paste DEC private mode sequences may carry several modes at once, and ncurses does exactly that when it turns the mouse on: CSI ? 1006 ; 1000 h. The emulator looked at the first parameter only and dropped the rest, so a program could end up with half of what it asked for. Every parameter is now set or reset. DECTCEM (mode 25) lets a program hide the cursor while it draws, or for good; a full screen program that hides it, such as htop, showed a cursor wandering over its screen. The request is passed to the view, which keeps its own blinking and focus handling and simply does not draw the cursor while a program has hidden it. Bracketed paste (mode 2004) marks pasted text with CSI 200~ and CSI 201~ so that a program can take it as text rather than as keys typed - a shell does not run every line of a pasted snippet, an editor does not auto-indent it. Programs that ask for it and do not get it misread multi-line pastes. An end marker inside the text is removed so the paste cannot close its own bracket early. --- .../META-INF/MANIFEST.MF | 2 +- .../control/impl/ITerminalControlForText.java | 15 ++++++++++ .../internal/emulator/VT100Emulator.java | 30 +++++++++++++++++-- .../emulator/VT100TerminalControl.java | 27 ++++++++++++++++- .../textcanvas/AbstractTextCanvasModel.java | 8 ++++- .../emulator/MockTerminalControlForText.java | 21 +++++++++++++ .../internal/emulator/VT100EmulatorTest.java | 15 ++++++++++ 7 files changed, 113 insertions(+), 5 deletions(-) diff --git a/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF b/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF index ff5a3fb72be..0c618bbd154 100644 --- a/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF +++ b/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.terminal.control; singleton:=true -Bundle-Version: 1.1.200.qualifier +Bundle-Version: 1.1.300.qualifier Bundle-Activator: org.eclipse.terminal.internal.control.impl.TerminalPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java index aca2a5690c0..66e4a82b25b 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java @@ -47,4 +47,19 @@ public interface ITerminalControlForText { */ void enableApplicationCursorKeys(boolean enable); + /** + * A program that asks for bracketed paste (CSI ?2004) wants pasted text marked + * as such, so that it can take it as text rather than as something typed. + */ + default void enableBracketedPaste(boolean enable) { + } + + /** + * Shows or hides the cursor at the program's request (DEC mode 25). + * + * @param show whether the cursor is to be drawn + */ + default void showCursor(boolean show) { + } + } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java index 659fd5cd4fd..64b0795a5b8 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java @@ -1264,18 +1264,32 @@ private void processAnsiCommand_X() { } private void processDecPrivateCommand_h() { - int param = getAnsiParameter(0); + // A program may set several modes at once: ncurses turns the mouse on with CSI ? 1006 ; 1000 h. + for (int i = 0; i <= nextAnsiParameter; i++) { + setDecPrivateMode(getAnsiParameter(i)); + } + } + + private void setDecPrivateMode(int param) { switch (param) { case 1: // Enable Application Cursor Keys (DECCKM) terminal.enableApplicationCursorKeys(true); break; + case 25: + // Show cursor (DECTCEM). + terminal.showCursor(true); + break; case 47: case 1047: case 1048: case 1049: // Use Alternate Screen Buffer (ignored). break; + case 2004: + // Bracketed paste: pasted text is wrapped so a program can tell it from typing. + terminal.enableBracketedPaste(true); + break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'h'); //$NON-NLS-1$ break; @@ -1283,12 +1297,21 @@ private void processDecPrivateCommand_h() { } private void processDecPrivateCommand_l() { - int param = getAnsiParameter(0); + for (int i = 0; i <= nextAnsiParameter; i++) { + resetDecPrivateMode(getAnsiParameter(i)); + } + } + + private void resetDecPrivateMode(int param) { switch (param) { case 1: // Enable Normal Cursor Keys (DECCKM) terminal.enableApplicationCursorKeys(false); break; + case 25: + // Hide cursor (DECTCEM). + terminal.showCursor(false); + break; case 47: case 1047: case 1048: @@ -1296,6 +1319,9 @@ private void processDecPrivateCommand_l() { // Use Normal Screen Buffer (ignored, but reset scroll region). text.setScrollRegion(-1, -1); break; + case 2004: + terminal.enableBracketedPaste(false); + break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'l'); //$NON-NLS-1$ break; diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java index e80d897f11c..e8e2f44d3c8 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java @@ -172,6 +172,9 @@ public class VT100TerminalControl implements ITerminalControlForText, ITerminalC */ private final IPropertyChangeListener fPreferenceListener = this::updatePreferences; private final IPropertyChangeListener fFontListener = this::updateFont; + private boolean fBracketedPaste; + private static final String PASTE_START = "\u001b[200~"; //$NON-NLS-1$ + private static final String PASTE_END = "\u001b[201~"; //$NON-NLS-1$ /** * Is protected by synchronize on this @@ -308,7 +311,7 @@ public boolean pasteString(String strText) { if (strText == null) { return false; } - sendString(strText); + sendString(fBracketedPaste ? bracketed(strText) : strText); return true; } @@ -569,6 +572,16 @@ public Shell getShell() { return getCtlText().getShell(); } + /** + * Marks text as pasted, so that a program takes the newlines in it as part of + * the text rather than as the user pressing return on each line. The end marker + * is taken out of the text itself, or the text could close the bracket early and + * the rest of it would arrive as if it had been typed. + */ + private static String bracketed(String text) { + return PASTE_START + text.replace(PASTE_END, "") + PASTE_END; //$NON-NLS-1$ + } + protected void sendChar(char chKey, boolean altKeyPressed) { try { int byteToSend = chKey; @@ -1288,6 +1301,11 @@ public void setState(TerminalState state) { }); } + @Override + public void enableBracketedPaste(boolean enable) { + fBracketedPaste = enable; + } + /** * @param runnable run in display thread */ @@ -1412,6 +1430,13 @@ public String getHoverSelection() { return fCtlText.getHoverSelection(); } + @Override + public void showCursor(boolean show) { + if (fPollingTextCanvasModel != null) { + fPollingTextCanvasModel.setCursorHidden(!show); + } + } + @Override public void updateTerminalDimensions() { getTerminalText().adjustTerminalDimensions(); diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java index db57118ddc5..df3b6ca358a 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java @@ -33,6 +33,7 @@ abstract public class AbstractTextCanvasModel implements ITextCanvasModel { private boolean fShowCursor; private long fCursorTime; private boolean fCursorIsEnabled; + private boolean fCursorHidden; private final ITerminalTextDataSnapshot fSnapshot; private int fLines; @@ -149,7 +150,12 @@ public int getCursorLine() { @Override public boolean isCursorOn() { - return fShowCursor && fCursorIsEnabled; + return fShowCursor && fCursorIsEnabled && !fCursorHidden; + } + + /** A program hides the cursor while it draws, or for good, with DEC mode 25. */ + public void setCursorHidden(boolean hidden) { + fCursorHidden = hidden; } /** diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java index 52e8680e2e2..e13e57333c2 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java @@ -54,6 +54,27 @@ public OutputStream getOutputStream() { } + private boolean cursorShown = true; + private boolean bracketedPaste; + + @Override + public void showCursor(boolean show) { + cursorShown = show; + } + + public boolean isCursorShown() { + return cursorShown; + } + + @Override + public void enableBracketedPaste(boolean enable) { + bracketedPaste = enable; + } + + public boolean isBracketedPaste() { + return bracketedPaste; + } + @Override public void enableApplicationCursorKeys(boolean enable) { throw new UnsupportedOperationException(); diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java index d7c5692f1a9..3783d3bbf51 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java @@ -11,6 +11,8 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.Reader; @@ -375,4 +377,17 @@ public void testMalformedStringTerminator() { () -> assertEquals(List.of("TITLE1", "TITLE2"), control.getAllTitles())); } + @Test + public void testDecPrivateModes() { + // the cursor hidden and shown again by a program + run("\u001b[?25l"); + assertFalse(control.isCursorShown()); + run("\u001b[?25h"); + assertTrue(control.isCursorShown()); + // several modes in one sequence, as ncurses sends them: every one of them counts + run("\u001b[?2004;25l"); + assertAll(() -> assertFalse(control.isCursorShown()), () -> assertFalse(control.isBracketedPaste())); + run("\u001b[?25;2004h"); + assertAll(() -> assertTrue(control.isCursorShown()), () -> assertTrue(control.isBracketedPaste())); + } } From 60bb5d30a8ed7cf1193e7c96c1159523e9b6199b Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 22:09:43 +0900 Subject: [PATCH 2/3] Terminal: report the mouse to programs that ask for it Programs that draw their own screen - htop, ncurses menus, and lately CLIs with clickable options and wheel scrolling - ask the terminal for mouse events with DEC modes 1000 (presses and releases), 1002 (also drags) and 1003 (every move), in xterm's legacy encoding or the SGR one (mode 1006). The terminal ignored the request and kept every click for its own selection, so such programs could not be clicked on or scrolled. The canvas now hands presses, releases, moves and the wheel to the control while a program has asked for them, with the alt and control keys folded into the button as xterm does. Shift keeps the mouse for the terminal, which is the convention every terminal uses to still allow a selection while a program is listening. A press that went to the program takes the terminal's selection away and moves the selection anchor to the click, so a later shift-drag starts from where the user last clicked rather than from a selection made long before. A release goes wherever its press went, whatever the keyboard state by then, so press and release always reach the same side. Mode 1004 tells the program when the terminal gains or loses focus, and mode 2026 (synchronized output) lets it mark the beginning and end of a redraw, during which the view leaves the screen alone rather than showing it half drawn. This builds on the DEC private mode parsing change (every parameter in a set/reset), since ncurses turns the mouse on with CSI ? 1006 ; 1000 h. --- .../control/impl/ITerminalControlForText.java | 35 ++++++ .../internal/emulator/VT100Emulator.java | 28 +++++ .../emulator/VT100TerminalControl.java | 112 ++++++++++++++++++ .../internal/textcanvas/GridCanvas.java | 13 +- .../textcanvas/PollingTextCanvasModel.java | 23 +++- .../internal/textcanvas/TextCanvas.java | 102 ++++++++++++++++ .../internal/emulator/AllTestSuite.java | 1 + .../emulator/MockTerminalControlForText.java | 31 +++++ .../internal/emulator/MouseReportTest.java | 44 +++++++ .../internal/emulator/VT100EmulatorTest.java | 15 +++ 10 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MouseReportTest.java diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java index 66e4a82b25b..bcf8c45512d 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/control/impl/ITerminalControlForText.java @@ -62,4 +62,39 @@ default void enableBracketedPaste(boolean enable) { default void showCursor(boolean show) { } + /** + * Turns reporting of the terminal gaining and losing focus on or off (DEC mode 1004). + * + * @param enable whether the program wants to be told + */ + default void enableFocusReporting(boolean enable) { + } + + /** + * A program that asks for mouse events wants them instead of the terminal's own + * handling of the mouse, scrolling included. + * + * @since 5.6 + */ + default void enableMouseReporting(int mode) { + } + + /** + * Says a program is drawing a screen and has not finished (DEC mode 2026), so + * that the view does not show it half drawn. + * + * @param redrawing whether a screen is being drawn + */ + default void enableSynchronizedOutput(boolean redrawing) { + } + + /** + * SGR encoding (CSI ?1006) reports coordinates as decimal numbers instead of + * single bytes, which is what lets them go past column 223. + * + * @since 5.6 + */ + default void enableSgrMouseEncoding(boolean enable) { + } + } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java index 64b0795a5b8..d1f1cb6db41 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100Emulator.java @@ -1286,10 +1286,24 @@ private void setDecPrivateMode(int param) { case 1049: // Use Alternate Screen Buffer (ignored). break; + case 2026: + terminal.enableSynchronizedOutput(true); + break; case 2004: // Bracketed paste: pasted text is wrapped so a program can tell it from typing. terminal.enableBracketedPaste(true); break; + case 1000: // report button press and release + case 1002: // also report drag + case 1003: // also report every move + terminal.enableMouseReporting(param); + break; + case 1006: + terminal.enableSgrMouseEncoding(true); + break; + case 1004: + terminal.enableFocusReporting(true); + break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'h'); //$NON-NLS-1$ break; @@ -1319,9 +1333,23 @@ private void resetDecPrivateMode(int param) { // Use Normal Screen Buffer (ignored, but reset scroll region). text.setScrollRegion(-1, -1); break; + case 2026: + terminal.enableSynchronizedOutput(false); + break; case 2004: terminal.enableBracketedPaste(false); break; + case 1000: + case 1002: + case 1003: + terminal.enableMouseReporting(0); + break; + case 1006: + terminal.enableSgrMouseEncoding(false); + break; + case 1004: + terminal.enableFocusReporting(false); + break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'l'); //$NON-NLS-1$ break; diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java index e8e2f44d3c8..e63341356b8 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java @@ -166,6 +166,10 @@ public class VT100TerminalControl implements ITerminalControlForText, ITerminalC private final EditActionAccelerators editActionAccelerators = new EditActionAccelerators(); private boolean fApplicationCursorKeys; + private int fMouseMode; + + private int fLastReportedLine, fLastReportedColumn; + private boolean fSgrMouseEncoding; /** * Listens to changes in the preferences @@ -816,6 +820,8 @@ protected void setupHelp(Composite parent, String id) { } PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, id); + getCtlText().setMouseWheelHandler(this::reportMouseWheel); + getCtlText().setMouseButtonHandler(new MouseReporter()); } @Override @@ -886,6 +892,7 @@ public void focusGained(FocusEvent event) { if (getState() == TerminalState.CONNECTED) { captureKeyEvents(true); } + reportFocus(true); IContextService contextService = PlatformUI.getWorkbench().getAdapter(IContextService.class); editContextActivation = contextService.activateContext("org.eclipse.terminal.EditContext"); //$NON-NLS-1$ @@ -895,6 +902,7 @@ public void focusGained(FocusEvent event) { public void focusLost(FocusEvent event) { // Enable all keybindings. captureKeyEvents(false); + reportFocus(false); // Restore the command context to its previous value. @@ -1415,6 +1423,103 @@ public void enableApplicationCursorKeys(boolean enable) { fApplicationCursorKeys = enable; } + @Override + public void enableMouseReporting(int mode) { + fMouseMode = mode; + fLastReportedLine = fLastReportedColumn = -1; + } + + @Override + public void enableSgrMouseEncoding(boolean enable) { + fSgrMouseEncoding = enable; + } + + /** + * A wheel notch is reported as a press of button 64 or 65, which is how a + * terminal has always told a program that the wheel moved. Without this the + * wheel only ever scrolled the terminal's own view, and a program drawing its + * own scrollable screen never heard about it. + * + * @return whether the program was told, in which case the canvas stays put + */ + private boolean reportMouseWheel(int count, int modifiers, int line, int column) { + if (fMouseMode == 0 || count == 0) { + return false; + } + int button = (count > 0 ? 64 : 65) + modifiers; // up, down + // SWT counts lines; a notch is three of them. + int notches = Math.max(1, Math.abs(count) / 3); + StringBuilder report = new StringBuilder(); + for (int i = 0; i < notches; i++) { + report.append(mouseReport(button, line, column, true)); + } + sendString(report.toString()); + return true; + } + + /** + * A press, a release and the pointer moving, told to the program the way a + * terminal has always told it, so that a program drawing its own screen can be + * clicked on. The terminal keeps the event when shift is held, which is how a + * selection is still made with the mouse while a program is listening for it. + */ + private class MouseReporter implements TextCanvas.IMouseButtonHandler { + @Override + public boolean mouseButton(int button, int modifiers, int line, int column, boolean pressed) { + if (fMouseMode == 0 || button < 1 || button > 3) { + return false; + } + sendString(mouseReport(button - 1 + modifiers, line, column, pressed)); + return true; + } + + @Override + public boolean mouseMoved(int button, int modifiers, int line, int column) { + // 1002 is only interested while a button is held, 1003 in every move. + if (fMouseMode < 1002 || (fMouseMode == 1002 && button == 0)) { + return false; + } + if (line == fLastReportedLine && column == fLastReportedColumn) { + return true; // still the same cell, and a cell is all the program is told + } + fLastReportedLine = line; + fLastReportedColumn = column; + sendString(mouseReport((button == 0 ? 3 : button - 1) + 32 + modifiers, line, column, true)); + return true; + } + } + + private boolean fFocusReporting; + + @Override + public void enableFocusReporting(boolean enable) { + fFocusReporting = enable; + } + + /** + * A program that asked for focus reporting stops its cursor blinking and holds + * off work while the terminal is not the window being typed into. + */ + private void reportFocus(boolean gained) { + if (fFocusReporting) { + sendString(gained ? "" : ""); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + private String mouseReport(int button, int line, int column, boolean pressed) { + return mouseReport(fSgrMouseEncoding, button, line, column, pressed); + } + + static String mouseReport(boolean sgr, int button, int line, int column, boolean pressed) { + if (sgr) { + return "\u001b[<" + button + ';' + column + ';' + line + (pressed ? 'M' : 'm'); //$NON-NLS-1$ + } + // The older spelling has no room past column 223 and no way to say which + // button came up, so a release is reported as button 3, meaning "some button". + return "\u001b[M" + (char) (32 + (pressed ? button : 3)) + (char) (32 + column) //$NON-NLS-1$ + + (char) (32 + line); + } + @Override public void addMouseListener(ITerminalMouseListener listener) { getCtlText().addTerminalMouseListener(listener); @@ -1437,6 +1542,13 @@ public void showCursor(boolean show) { } } + @Override + public void enableSynchronizedOutput(boolean redrawing) { + if (fPollingTextCanvasModel != null) { + fPollingTextCanvasModel.setSynchronizedOutput(redrawing); + } + } + @Override public void updateTerminalDimensions() { getTerminalText().adjustTerminalDimensions(); diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/GridCanvas.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/GridCanvas.java index c3384f57260..c5290e06596 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/GridCanvas.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/GridCanvas.java @@ -31,7 +31,7 @@ abstract public class GridCanvas extends VirtualCanvas { public GridCanvas(Composite parent, int style) { super(parent, style); addListener(SWT.MouseWheel, event -> { - if (getVerticalBar().isVisible()) { + if (!handleMouseWheel(event.x, event.y, event.count, event.stateMask) && getVerticalBar().isVisible()) { int delta = -fCellHeight * event.count; scrollYDelta(delta); } @@ -40,6 +40,17 @@ public GridCanvas(Composite parent, int style) { } + /** + * Offered the wheel before the canvas scrolls itself. + * + * @param count lines the wheel asked for, positive when scrolling up + * @param stateMask the keys held at the time + * @return whether it was taken, in which case the canvas leaves it alone + */ + protected boolean handleMouseWheel(int x, int y, int count, int stateMask) { + return false; + } + /** template method paint. * iterates over all cells in the clipping rectangle and paints them. */ diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/PollingTextCanvasModel.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/PollingTextCanvasModel.java index 5bc81c3271b..2d0fe4f29f3 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/PollingTextCanvasModel.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/PollingTextCanvasModel.java @@ -20,7 +20,26 @@ */ public class PollingTextCanvasModel extends AbstractTextCanvasModel { private static final int DEFAULT_POLL_INTERVAL = 50; + /** + * How long a program is given to finish a screen it said it was drawing. A + * program that says so and then stops must not leave the view frozen. + */ + private static final int REDRAW_GRACE = 200; int fPollInterval = -1; + private volatile long fRedrawingUntil; + + /** + * A program that draws a screen in pieces can say where a screen begins and + * ends, and while it is between the two the view leaves it alone rather than + * catching it half drawn. + */ + public void setSynchronizedOutput(boolean redrawing) { + fRedrawingUntil = redrawing ? System.currentTimeMillis() + REDRAW_GRACE : 0; + } + + private boolean mayLook() { + return fRedrawingUntil == 0 || System.currentTimeMillis() > fRedrawingUntil; + } /** * @@ -45,7 +64,9 @@ public void startPolling() { Display.getDefault().timerExec(fPollInterval, new Runnable() { @Override public void run() { - update(); + if (mayLook()) { + update(); + } Display.getDefault().timerExec(fPollInterval, this); } }); diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java index 92f924be1a7..e1fdecb6b4b 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java @@ -62,6 +62,9 @@ public class TextCanvas extends GridCanvas { private ResizeListener fResizeListener; private final List fMouseListeners; private SelectionMode fSelMode = SelectionMode.NONE; + private IMouseWheelHandler fWheelHandler; + private IMouseButtonHandler fButtonHandler; + private int fReportedButton; private enum SelectionMode { NONE, DRAG, WORD, LINE @@ -192,6 +195,16 @@ private void selectWord(Point pt) { @Override public void mouseDown(MouseEvent e) { + if (wouldReport(e) && report(e, e.button, true)) { + fReportedButton = e.button; + // The click went to the program, so whatever the terminal had + // selected is no longer what the pointer is pointing at. + fCellCanvasModel.setSelection(-1, -1, -1, -1); + fCellCanvasModel.expandHoverSelectionAt(-1, -1); + fDraggingStart = null; + return; + } + fReportedButton = 0; if (e.button == 1) { // left button fSelMode = SelectionMode.DRAG; fDraggingStart = screenPointToCell(e.x, e.y); @@ -245,6 +258,11 @@ private void selectLine(MouseEvent e) { @Override public void mouseUp(MouseEvent e) { + if (fReportedButton != 0) { + report(e, fReportedButton, false); + fReportedButton = 0; + return; + } if (e.button == 1) { // left button if (fSelMode == SelectionMode.DRAG) { updateHasSelection(e); @@ -268,6 +286,10 @@ public void mouseUp(MouseEvent e) { } }); addMouseMoveListener(e -> { + if (fDraggingStart == null && wouldReport(e) + && fButtonHandler.mouseMoved(heldButton(e.stateMask), modifiers(e.stateMask), line(e), column(e))) { + return; + } if (fDraggingStart != null) { Point curr = screenPointToCell(e.x, e.y); updateHasSelection(e); @@ -533,6 +555,86 @@ protected void repaintRange(int col, int line, int width, int height) { repaint(r); } + /** + * Hands the wheel to whoever can pass it on to the program being run. + */ + public interface IMouseWheelHandler { + /** + * @param count lines the wheel asked for, positive when scrolling up + * @param line row under the pointer, counted from one + * @param column column under the pointer, counted from one + * @return whether the program took it + */ + boolean mouseWheel(int count, int modifiers, int line, int column); + } + + /** + * Hands a button or the pointer moving to whoever can pass it on to the program + * being run. Coordinates are counted from one, from the top left of what is on + * screen, which is how a terminal has always reported them. + */ + public interface IMouseButtonHandler { + /** @return whether the program took it */ + boolean mouseButton(int button, int modifiers, int line, int column, boolean pressed); + + /** @return whether the program took it */ + boolean mouseMoved(int button, int modifiers, int line, int column); + } + + public void setMouseButtonHandler(IMouseButtonHandler handler) { + fButtonHandler = handler; + } + + /** + * Holding shift keeps the event for the terminal, which is how a selection is + * still made with the mouse while a program is listening for it. Asked of a + * press and of the pointer moving; a release is not its own decision but goes + * wherever the press it ends went, however the keyboard stands by then. + */ + private boolean wouldReport(MouseEvent e) { + return fButtonHandler != null && (e.stateMask & SWT.SHIFT) == 0 && getCellWidth() > 0 + && getCellHeight() > 0; + } + + private boolean report(MouseEvent e, int button, boolean pressed) { + return fButtonHandler.mouseButton(button, modifiers(e.stateMask), line(e), column(e), pressed); + } + + private int line(MouseEvent e) { + return Math.max(0, e.y) / getCellHeight() + 1; + } + + private int column(MouseEvent e) { + return Math.max(0, e.x) / getCellWidth() + 1; + } + + /** + * What xterm adds to a button for the keys held with it: 8 for alt, 16 for + * control. Shift is never among them, being what keeps the mouse for the + * terminal. + */ + static int modifiers(int stateMask) { + return ((stateMask & SWT.ALT) != 0 ? 8 : 0) | ((stateMask & SWT.CTRL) != 0 ? 16 : 0); + } + + private static int heldButton(int stateMask) { + return (stateMask & SWT.BUTTON1) != 0 ? 1 : (stateMask & SWT.BUTTON2) != 0 ? 2 + : (stateMask & SWT.BUTTON3) != 0 ? 3 : 0; + } + + public void setMouseWheelHandler(IMouseWheelHandler handler) { + fWheelHandler = handler; + } + + @Override + protected boolean handleMouseWheel(int x, int y, int count, int stateMask) { + if (fWheelHandler == null || getCellWidth() <= 0 || getCellHeight() <= 0) { + return false; + } + return fWheelHandler.mouseWheel(count, modifiers(stateMask), Math.max(0, y) / getCellHeight() + 1, + Math.max(0, x) / getCellWidth() + 1); + } + @Override protected void drawLine(GC gc, int line, int x, int y, int colFirst, int colLast) { fCellRenderer.drawLine(fCellCanvasModel, gc, line, x, y, colFirst, colLast); diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/AllTestSuite.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/AllTestSuite.java index b82f997997f..6f139b4066f 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/AllTestSuite.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/AllTestSuite.java @@ -21,6 +21,7 @@ */ @Suite @SelectClasses({ // + MouseReportTest.class, // VT100EmulatorBackendTest.class, // VT100EmulatorTest.class, // }) diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java index e13e57333c2..ca0bcf5a61c 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MockTerminalControlForText.java @@ -75,6 +75,37 @@ public boolean isBracketedPaste() { return bracketedPaste; } + private int mouseMode; + private boolean sgrMouse; + private boolean focusReporting; + + @Override + public void enableMouseReporting(int mode) { + mouseMode = mode; + } + + public int getMouseMode() { + return mouseMode; + } + + @Override + public void enableSgrMouseEncoding(boolean enable) { + sgrMouse = enable; + } + + public boolean isSgrMouseEncoding() { + return sgrMouse; + } + + @Override + public void enableFocusReporting(boolean enable) { + focusReporting = enable; + } + + public boolean isFocusReporting() { + return focusReporting; + } + @Override public void enableApplicationCursorKeys(boolean enable) { throw new UnsupportedOperationException(); diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MouseReportTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MouseReportTest.java new file mode 100644 index 00000000000..878cf8ef94a --- /dev/null +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/MouseReportTest.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.terminal.internal.emulator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** How a mouse event is spelled for the program, in xterm's two encodings. */ +public class MouseReportTest { + + private static final String CSI = "["; + + @Test + public void testSgrEncoding() { + // CSI < button ; column ; row M for a press, m for a release, 1-based + assertEquals(CSI + "<0;10;5M", VT100TerminalControl.mouseReport(true, 0, 5, 10, true)); + assertEquals(CSI + "<0;10;5m", VT100TerminalControl.mouseReport(true, 0, 5, 10, false)); + assertEquals(CSI + "<2;10;5M", VT100TerminalControl.mouseReport(true, 2, 5, 10, true)); + assertEquals(CSI + "<32;10;5M", VT100TerminalControl.mouseReport(true, 32, 5, 10, true)); // drag + assertEquals(CSI + "<64;10;5M", VT100TerminalControl.mouseReport(true, 64, 5, 10, true)); // wheel up + assertEquals(CSI + "<16;10;5M", VT100TerminalControl.mouseReport(true, 0 + 16, 5, 10, true)); // control + assertEquals(CSI + "<72;10;5M", VT100TerminalControl.mouseReport(true, 64 + 8, 5, 10, true)); // alt-wheel + // room for any column, which the older encoding has not + assertEquals(CSI + "<0;300;5M", VT100TerminalControl.mouseReport(true, 0, 5, 300, true)); + } + + @Test + public void testX10Encoding() { + // CSI M then button, column and row each offset by 32 + assertEquals(CSI + "M" + (char) 32 + (char) 42 + (char) 37, + VT100TerminalControl.mouseReport(false, 0, 5, 10, true)); + // a release says only that some button came up + assertEquals(CSI + "M" + (char) 35 + (char) 42 + (char) 37, + VT100TerminalControl.mouseReport(false, 0, 5, 10, false)); + } +} diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java index 3783d3bbf51..9b4d73b6602 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorTest.java @@ -390,4 +390,19 @@ public void testDecPrivateModes() { run("\u001b[?25;2004h"); assertAll(() -> assertTrue(control.isCursorShown()), () -> assertTrue(control.isBracketedPaste())); } + + @Test + public void testMouseModes() { + // how ncurses turns the mouse on: both modes in one sequence + run("\u001b[?1006;1000h"); + assertAll(() -> assertEquals(1000, control.getMouseMode()), () -> assertTrue(control.isSgrMouseEncoding())); + run("\u001b[?1002h"); + assertEquals(1002, control.getMouseMode()); + run("\u001b[?1000;1006l"); + assertAll(() -> assertEquals(0, control.getMouseMode()), () -> assertFalse(control.isSgrMouseEncoding())); + run("\u001b[?1004h"); + assertTrue(control.isFocusReporting()); + run("\u001b[?1004l"); + assertFalse(control.isFocusReporting()); + } } From 80a376201aef30ea71217b5ceae5051be1ed801b Mon Sep 17 00:00:00 2001 From: insjang Date: Thu, 3 Sep 2026 12:33:01 +0900 Subject: [PATCH 3/3] Terminal: make mouseReport public so the test bundle can call it Package-private static access does not cross an OSGi bundle boundary even within the same package name; MouseReportTest lives in a separate test bundle and failed with IllegalAccessError. --- .../terminal/internal/emulator/VT100TerminalControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java index e63341356b8..d96bc5d8416 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100TerminalControl.java @@ -1510,7 +1510,7 @@ private String mouseReport(int button, int line, int column, boolean pressed) { return mouseReport(fSgrMouseEncoding, button, line, column, pressed); } - static String mouseReport(boolean sgr, int button, int line, int column, boolean pressed) { + public static String mouseReport(boolean sgr, int button, int line, int column, boolean pressed) { if (sgr) { return "\u001b[<" + button + ';' + column + ';' + line + (pressed ? 'M' : 'm'); //$NON-NLS-1$ }