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 @@ -128,7 +128,12 @@ public class VT100Emulator implements ControlListener {
private final int[] savedCursorColumn = new int[2];

/** whether a program asked for the alternate screen (47, 1047 or 1049) */
private boolean alternateScreen;
private volatile boolean alternateScreen;

/** @return whether a program shows the alternate screen, as full screen programs do */
public boolean isAlternateScreen() {
return alternateScreen;
}

/**
* This field holds an array of StringBuffer objects, each of which is one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ private record XtermKey(int number, char last) {
Map.entry(SWT.ARROW_UP, new XtermKey(1, 'A')), Map.entry(SWT.ARROW_DOWN, new XtermKey(1, 'B')),
Map.entry(SWT.ARROW_RIGHT, new XtermKey(1, 'C')), Map.entry(SWT.ARROW_LEFT, new XtermKey(1, 'D')),
Map.entry(SWT.HOME, new XtermKey(1, 'H')), Map.entry(SWT.END, new XtermKey(1, 'F')),
Map.entry(SWT.INSERT, new XtermKey(2, '~')),
Map.entry(SWT.INSERT, new XtermKey(2, '~')), Map.entry((int) SWT.DEL, new XtermKey(3, '~')),
Map.entry(SWT.PAGE_UP, new XtermKey(5, '~')), Map.entry(SWT.PAGE_DOWN, new XtermKey(6, '~')),
Map.entry(SWT.F1, new XtermKey(1, 'P')), Map.entry(SWT.F2, new XtermKey(1, 'Q')),
Map.entry(SWT.F3, new XtermKey(1, 'R')), Map.entry(SWT.F4, new XtermKey(1, 'S')),
Expand Down Expand Up @@ -656,6 +656,44 @@ 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$
}

/**
* What Ctrl alone with a key sends where SWT gives the key's own character: Space
* (NUL, the mark in Emacs), / (undo in Emacs) and the digits as xterm spells them,
* Ctrl+6 being vim's alternate file. Windows hands Ctrl+2 over as '2', so its NUL
* comes from here too.
*
* @param character the character SWT reports
* @return the control character, or the character itself
*/
public static char ctrlCharacter(char character) {
return switch (character) {
case ' ', '2' -> '\u0000';
case '3' -> '\u001b';
case '4' -> '\u001c';
case '5' -> '\u001d';
case '6' -> '\u001e';
case '/', '7' -> '\u001f';
case '8' -> '\u007f';
default -> character;
};
}

/**
* SWT reports AltGr as Ctrl+Alt, with the character AltGr types. A real Ctrl+Alt
* gives a control character instead, which keeps its Alt as in xterm: ESC before
* it, so Ctrl+Alt+V reaches Codex as itself.
*
* @param stateMask the modifiers of the key event
* @param character the character SWT reports
* @return whether the character goes out with ESC before it
*/
public static boolean altSendsEscape(int stateMask, char character) {
if ((stateMask & SWT.ALT) == 0) {
return false;
}
return (stateMask & SWT.CTRL) == 0 || character < '\u0020' || character == '\u007f';
}

protected void sendChar(char chKey, boolean altKeyPressed) {
try {
int byteToSend = chKey;
Expand Down Expand Up @@ -895,6 +933,12 @@ protected void setupListeners() {
fFocusListener = new TerminalFocusListener();

getCtlText().addKeyListener(fKeyHandler);
// Shift+Tab is a key for the program (back tab, see keyPressed), not a move of the focus out of the terminal
getCtlText().addTraverseListener(e -> {
if (e.detail == SWT.TRAVERSE_TAB_PREVIOUS && (e.stateMask & SWT.MODIFIER_MASK) == SWT.SHIFT) {
e.doit = false;
}
});
getCtlText().addFocusListener(fFocusListener);

}
Expand Down Expand Up @@ -1063,7 +1107,7 @@ public void keyPressed(KeyEvent event) {
// Control key is _not_ also pressed. This works around a bug in SWT where,
// on European keyboards, the AltGr key being pressed appears to us as Control
// + Alt being pressed simultaneously.
boolean altKeyPressed = (event.stateMask & SWT.ALT) != 0 && !ctrlKeyPressed;
boolean altKeyPressed = altSendsEscape(event.stateMask, character);

//if (!isConnected()) {
if (fState == TerminalState.CLOSED) {
Expand All @@ -1079,9 +1123,10 @@ public void keyPressed(KeyEvent event) {
return;
}

// Manage the Del key
if (event.keyCode == 0x000007f) {
sendString("\u001b[3~"); //$NON-NLS-1$
// Manage the Del key: with a modifier as xterm spells it, Ctrl+Delete deleting a word in Codex
if (event.keyCode == SWT.DEL) {
String modified = modifiedSpecialKey(SWT.DEL, modifierKeys);
sendString(modified != null ? modified : "\u001b[3~"); //$NON-NLS-1$
return;
}

Expand Down Expand Up @@ -1260,8 +1305,10 @@ public void keyPressed(KeyEvent event) {
}

// a key Eclipse has a binding for stays with it, as Alt+Right (Quick Access)
// and Alt+Up (Maximize) do in this terminal
if (escSeq == null && !hasKeyBinding(accelerator)) {
// and Alt+Up (Maximize) do in this terminal; on the alternate screen the
// arrows go to the full screen program, Codex and Claude Code using Alt+Up
boolean arrow = event.keyCode >= SWT.ARROW_UP && event.keyCode <= SWT.ARROW_RIGHT;
if (escSeq == null && (arrow && fTerminalText.isAlternateScreen() || !hasKeyBinding(accelerator))) {
escSeq = modifiedSpecialKey(event.keyCode, modifierKeys);
}

Expand All @@ -1281,24 +1328,20 @@ public void keyPressed(KeyEvent event) {
Logger.log("stateMask = " + event.stateMask); //$NON-NLS-1$

if (onlyCtrlKeyPressed) {
switch (character) {
case ' ':
// Send a NUL character -- many terminal emulators send NUL when
// Control-Space is pressed. This is used to set the mark in Emacs.
character = '\u0000';
break;
case '/':
// Ctrl+/ is undo in emacs
character = '\u001f';
break;
}
character = ctrlCharacter(character);
}

// see javadoc on convertBackspace for details
if (convertBackspace && !ctrlKeyPressed && character == '\b') {
character = 0x7f;
}

// Shift+Tab sends back tab (CSI Z), as xterm and Windows Terminal do: Claude Code switches modes with it
if (character == '\t' && modifierKeys == SWT.SHIFT) {
sendString("\u001b[Z"); //$NON-NLS-1$
return;
}

//TODO: At this point, Ctrl+M sends the same as Ctrl+Shift+M .
//This is undesired. Fixing this here might make the special Ctrl+Shift+C
//handling unnecessary further up.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,9 @@ public void testModifiedSpecialKeys() {
// xterm's spelling: CSI number ; modifier final, with 2 shift, 3 alt, 5 control and their sums
Map<Integer, String> keys = Map.ofEntries(Map.entry(SWT.ARROW_UP, "1;%dA"), Map.entry(SWT.ARROW_DOWN, "1;%dB"),
Map.entry(SWT.ARROW_RIGHT, "1;%dC"), Map.entry(SWT.ARROW_LEFT, "1;%dD"), Map.entry(SWT.HOME, "1;%dH"),
Map.entry(SWT.END, "1;%dF"), Map.entry(SWT.INSERT, "2;%d~"), Map.entry(SWT.PAGE_UP, "5;%d~"),
Map.entry(SWT.PAGE_DOWN, "6;%d~"), Map.entry(SWT.F1, "1;%dP"), Map.entry(SWT.F2, "1;%dQ"),
Map.entry(SWT.END, "1;%dF"), Map.entry(SWT.INSERT, "2;%d~"), Map.entry((int) SWT.DEL, "3;%d~"),
Map.entry(SWT.PAGE_UP, "5;%d~"), Map.entry(SWT.PAGE_DOWN, "6;%d~"), Map.entry(SWT.F1, "1;%dP"),
Map.entry(SWT.F2, "1;%dQ"),
Map.entry(SWT.F3, "1;%dR"), Map.entry(SWT.F4, "1;%dS"), Map.entry(SWT.F5, "15;%d~"),
Map.entry(SWT.F6, "17;%d~"), Map.entry(SWT.F7, "18;%d~"), Map.entry(SWT.F8, "19;%d~"),
Map.entry(SWT.F9, "20;%d~"), Map.entry(SWT.F10, "21;%d~"), Map.entry(SWT.F11, "23;%d~"),
Expand All @@ -617,6 +618,39 @@ public void testModifiedSpecialKeys() {
assertEquals(null, VT100TerminalControl.modifiedSpecialKey(SWT.ARROW_LEFT, SWT.COMMAND | SWT.SHIFT));
}

@Test
public void testCtrlCharacter() {
// xterm: Ctrl+2 to Ctrl+8 are NUL, ESC, FS, GS, RS, US and DEL
assertEquals("\u0000\u001b\u001c\u001d\u001e\u001f\u007f",
"2345678".chars().map(c -> VT100TerminalControl.ctrlCharacter((char) c))
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString());
assertEquals('\u0000', VT100TerminalControl.ctrlCharacter(' '));
assertEquals('\u001f', VT100TerminalControl.ctrlCharacter('/'));
assertEquals('1', VT100TerminalControl.ctrlCharacter('1'));
assertEquals('\u0016', VT100TerminalControl.ctrlCharacter('\u0016'));
}

@Test
public void testAltSendsEscape() {
assertTrue(VT100TerminalControl.altSendsEscape(SWT.ALT, 'f'));
assertTrue(VT100TerminalControl.altSendsEscape(SWT.ALT | SWT.SHIFT, 'F'));
// a real Ctrl+Alt+V gives a control character and keeps its Alt
assertTrue(VT100TerminalControl.altSendsEscape(SWT.CTRL | SWT.ALT, '\u0016'));
// AltGr+Q on a German keyboard: Ctrl+Alt with the '@' it types, no ESC
assertFalse(VT100TerminalControl.altSendsEscape(SWT.CTRL | SWT.ALT, '@'));
assertFalse(VT100TerminalControl.altSendsEscape(SWT.CTRL, '\u0016'));
assertFalse(VT100TerminalControl.altSendsEscape(0, 'f'));
}

@Test
public void testAlternateScreenIsReported() {
assertFalse(emulator.isAlternateScreen());
run("\u001b[?1049h");
assertTrue(emulator.isAlternateScreen());
run("\u001b[?1049l");
assertFalse(emulator.isAlternateScreen());
}

@Test
public void testDecPrivateModes() {
// the cursor hidden and shown again by a program
Expand Down
Loading