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..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 @@ -47,4 +47,54 @@ 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) { + } + + /** + * 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 659fd5cd4fd..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 @@ -1264,18 +1264,46 @@ 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 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; @@ -1283,12 +1311,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 +1333,23 @@ private void processDecPrivateCommand_l() { // 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 e80d897f11c..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 @@ -166,12 +166,19 @@ 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 */ 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 +315,7 @@ public boolean pasteString(String strText) { if (strText == null) { return false; } - sendString(strText); + sendString(fBracketedPaste ? bracketed(strText) : strText); return true; } @@ -569,6 +576,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; @@ -803,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 @@ -873,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$ @@ -882,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. @@ -1288,6 +1309,11 @@ public void setState(TerminalState state) { }); } + @Override + public void enableBracketedPaste(boolean enable) { + fBracketedPaste = enable; + } + /** * @param runnable run in display thread */ @@ -1397,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); + } + + 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$ + } + // 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); @@ -1412,6 +1535,20 @@ public String getHoverSelection() { return fCtlText.getHoverSelection(); } + @Override + public void showCursor(boolean show) { + if (fPollingTextCanvasModel != null) { + fPollingTextCanvasModel.setCursorHidden(!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/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/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 52e8680e2e2..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 @@ -54,6 +54,58 @@ 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; + } + + 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 d7c5692f1a9..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 @@ -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,32 @@ 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())); + } + + @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()); + } }