Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1292,12 +1292,22 @@ 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:
// Use Alternate Screen Buffer, the cursor staying where it is.
Expand All @@ -1313,19 +1323,32 @@ private void processDecPrivateCommand_h() {
// Save cursor, as ESC 7 does.
saveCursor();
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;
}
}

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:
// Use Normal Screen Buffer, putting back what was on it, the cursor
Expand All @@ -1343,6 +1366,9 @@ private void processDecPrivateCommand_l() {
// Restore cursor, as ESC 8 does.
restoreCursor();
break;
case 2004:
terminal.enableBracketedPaste(false);
break;
default:
Logger.log("Unsupported command parameter: CSI ?" + param + 'l'); //$NON-NLS-1$
break;
Expand Down Expand Up @@ -1616,6 +1642,10 @@ public void resetState() {
text.setStyle(text.getDefaultStyle());
text.setScrollRegion(-1, -1);
text.setInsertMode(false);
// the modes a program asked for end with its connection: a program that
// hid the cursor and was cut off must not leave the next shell without one
terminal.showCursor(true);
terminal.enableBracketedPaste(false);
}

// public OutputStream getOutputStream() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -308,7 +311,7 @@ public boolean pasteString(String strText) {
if (strText == null) {
return false;
}
sendString(strText);
sendString(pasteText(strText, fBracketedPaste));
return true;
}

Expand Down Expand Up @@ -569,6 +572,30 @@ public Shell getShell() {
return getCtlText().getShell();
}

/**
* Pasted text as it goes to the program: each line break as the Enter key sends
* it, a CR, as Windows Terminal (CR LF to CR) and tmux (LF to CR) paste. A
* Windows clipboard's CR LF would reach the shell as two breaks, and readline
* then draws two rows for every line: a paste taller than the screen pushes the
* prompt off it, and the redraw on the next key lands where the prompt no
* longer is. With bracketed paste on, the text is marked as pasted.
*/
public static String pasteText(String text, boolean bracketedPaste) {
String lines = text.replace("\r\n", "\r").replace('\n', '\r'); //$NON-NLS-1$ //$NON-NLS-2$
return bracketedPaste ? bracketed(lines) : lines;
}

/**
* 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 text cannot
* end the paste early: every ESC and C1 CSI goes, as xterm does, so no end marker
* can be left over or put together from the pieces around one taken out
* ("ESC[20ESC[201~1~").
*/
private static String bracketed(String text) {
return PASTE_START + text.replace("\u001b", "").replace("\u009b", "") + PASTE_END; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
}

protected void sendChar(char chKey, boolean altKeyPressed) {
try {
int byteToSend = chKey;
Expand Down Expand Up @@ -1288,6 +1315,11 @@ public void setState(TerminalState state) {
});
}

@Override
public void enableBracketedPaste(boolean enable) {
fBracketedPaste = enable;
}

/**
* @param runnable run in display thread
*/
Expand Down Expand Up @@ -1407,6 +1439,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ abstract public class AbstractTextCanvasModel implements ITextCanvasModel {
private boolean fShowCursor;
private long fCursorTime;
private boolean fCursorIsEnabled;
// set by the emulator thread, read when painting
private volatile boolean fCursorHidden;
private final ITerminalTextDataSnapshot fSnapshot;
private int fLines;

Expand Down Expand Up @@ -150,7 +152,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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*******************************************************************************
* Copyright (c) 2026 Eclipse contributors and others.
*
* 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.view.ui.internal;

/**
* Where to cut the input written to a process in pieces.
*/
public final class InputChunks {

private InputChunks() {
}

/**
* How much of the data to write next: at most {@code max} bytes from {@code from},
* but never ending inside a UTF-8 character or an escape sequence. Between two
* writes there is a pause, and ConPTY has passed the pieces of a sequence split
* across reads on as plain text (microsoft/terminal#16352): the end marker of a
* bracketed paste cut as "ESC[20" and "1~" reached the shell as the text "1~",
* and the shell then took everything typed after it as part of the paste.
*
* @return the number of bytes, at least 1
*/
public static int writeLength(byte[] data, int from, int max) {
int end = from + max;
if (end >= data.length) {
return data.length - from;
}
while (end > from && (data[end] & 0xC0) == 0x80) {
end--; // a UTF-8 continuation byte: the next write starts at its character
}
// an escape sequence that would not be complete in this write moves to the next
for (int i = end - 1; i >= Math.max(from, end - 32); i--) {
if (data[i] == 0x1b) {
if (!isComplete(data, i, end)) {
end = i;
}
break;
}
}
return end > from ? end - from : Math.min(max, data.length - from);
}

/** whether the escape sequence at {@code esc} ends before {@code end} */
private static boolean isComplete(byte[] data, int esc, int end) {
if (esc + 1 >= end) {
return false;
}
byte type = data[esc + 1];
if (type == '[') {
// CSI: parameters and intermediates up to a final byte
for (int i = esc + 2; i < end; i++) {
if (data[i] >= 0x40 && data[i] <= 0x7e) {
return true;
}
}
return false;
}
if (type == 'O') {
return esc + 2 < end; // SS3, as an arrow key in application mode
}
return true; // ESC and one character (Alt with a key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.eclipse.osgi.util.NLS;
import org.eclipse.terminal.connector.ITerminalControl;
import org.eclipse.terminal.view.core.ILineSeparatorConstants;
import org.eclipse.terminal.view.ui.internal.InputChunks;
import org.eclipse.terminal.view.ui.internal.Messages;
import org.eclipse.ui.services.IDisposable;

Expand Down Expand Up @@ -248,7 +249,8 @@ protected void writeStream() {
int written = 0;
byte[] buf = new byte[1000];
while (written < data.length) {
int len = Math.min(buf.length, data.length - written);
// not cutting a character or an escape sequence
int len = InputChunks.writeLength(data, written, buf.length);
System.arraycopy(data, written, buf, 0, len);
// Write the data to the stream
stream.write(buf, 0, len);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
// a full reset (ESC c) turns them off; nothing to record here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

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;
Expand Down Expand Up @@ -562,4 +563,40 @@ public void testRedrawingOnTheAlternateScreenKeepsTheHistory() {
run("\u001b[2J");
assertEquals(height + 2, data.getHeight());
}

@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 testResetStateEndsDecPrivateModes() {
run("\u001b[?25l\u001b[?2004h");
assertAll(() -> assertFalse(control.isCursorShown()), () -> assertTrue(control.isBracketedPaste()));
// the connection ends while the program has them set; the next one starts clean
emulator.resetState();
assertAll(() -> assertTrue(control.isCursorShown()), () -> assertFalse(control.isBracketedPaste()));
}

@Test
public void testPasteText() {
// each line break goes as a CR, as the Enter key sends it: a Windows CR LF is one break
assertEquals("a\rb\rc\rd", VT100TerminalControl.pasteText("a\r\nb\nc\rd", false));
String start = "\033[200~", end = "\033[201~";
assertEquals(start + "a\rb" + end, VT100TerminalControl.pasteText("a\r\nb", true));
// the text cannot end the paste early: every ESC and C1 CSI goes
assertEquals(start + "a[201~b" + end, VT100TerminalControl.pasteText("a\033[201~b", true));
// not even with a marker put together from the pieces left after taking one out
assertEquals(start + "[20[201~1~x" + end, VT100TerminalControl.pasteText("\033[20\033[201~1~x", true));
assertEquals(start + "201~x" + end, VT100TerminalControl.pasteText("\u009b201~x", true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
org.eclipse.terminal.internal.textcanvas.TextCanvasModelTest.class, //
org.eclipse.terminal.internal.connector.TerminalConnectorTest.class, //
org.eclipse.terminal.internal.connector.TerminalToRemoteInjectionOutputStreamTest.class, //
org.eclipse.terminal.view.ui.tests.InputChunksTest.class, //
org.eclipse.terminal.view.ui.tests.TerminalsViewReorderTest.class, //
})
public class AutomatedTestSuite {
Expand Down
Loading
Loading