From 3a21ba7b65799f3a6100f8c67a4e4fa841f6627d Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 14 Apr 2026 11:28:08 -0400 Subject: [PATCH] support CBT and CHT control sequences in the emulator and terminal model When a terminal advertises itself as `xterm`, it is expected to support the `CBT` (Cursor Backward Tab). However, JediTerm's emulator did not support this control sequence, which caused some applications to behave incorrectly when running in JediTerm. This commit adds support for the `CBT` control sequence in the `JediEmulator` class, as well as the corresponding `CHT` (Cursor Forward Tab) control sequence. The `Terminal` interface is updated to include methods for finding the previous and next tab stops, and the `JediTerminal` class implements these methods using its existing tabulator functionality. Fixes: https://github.com/JetBrains/jediterm/issues/328 --- core/src/com/jediterm/terminal/Terminal.java | 4 +++ .../terminal/emulator/JediEmulator.java | 31 +++++++++++++++++++ .../jediterm/terminal/model/JediTerminal.java | 10 ++++++ core/tests/src/com/jediterm/EmulatorTest.java | 22 +++++++++++++ 4 files changed, 67 insertions(+) diff --git a/core/src/com/jediterm/terminal/Terminal.java b/core/src/com/jediterm/terminal/Terminal.java index c2d326c97..3a47b75df 100644 --- a/core/src/com/jediterm/terminal/Terminal.java +++ b/core/src/com/jediterm/terminal/Terminal.java @@ -148,6 +148,10 @@ public interface Terminal { void setTabStopAtCursor(); + int previousTab(int position); + + int nextTab(int position); + void writeUnwrappedString(String string); void setTerminalOutput(@Nullable TerminalOutputStream terminalOutput); diff --git a/core/src/com/jediterm/terminal/emulator/JediEmulator.java b/core/src/com/jediterm/terminal/emulator/JediEmulator.java index 2caf447ca..ab5428fe4 100644 --- a/core/src/com/jediterm/terminal/emulator/JediEmulator.java +++ b/core/src/com/jediterm/terminal/emulator/JediEmulator.java @@ -466,6 +466,8 @@ private boolean processControlSequence(ControlSequence args) { case 'f': case 'H': //CUP return cursorPosition(args); + case 'I': //CHT + return cursorForwardTab(args.getArg(0, 1)); case 'J': //DECSED return eraseInDisplay(args); case 'K': //EL @@ -482,6 +484,8 @@ private boolean processControlSequence(ControlSequence args) { return scrollUp(args); case 'T': //SD return scrollDown(args); + case 'Z': //CBT + return cursorBackwardTab(args.getArg(0, 1)); case 'c': //Send Device Attributes (Primary DA) if (args.startsWithMoreMark()) { //Send Device Attributes (Secondary DA) return sendSecondaryDeviceAttributes(args); @@ -1177,4 +1181,31 @@ private void setModeEnabled(final TerminalMode mode, final boolean enabled) { public void setMouseMode(MouseMode mouseMode) { myTerminal.setMouseMode(mouseMode); } + + private boolean cursorBackwardTab(int count) { + if (count == 0) count = 1; + int curX = myTerminal.getCursorX() - 1; + for (int i = 0; i < count && curX > 0; i++) { + curX = myTerminal.previousTab(curX); + if (curX < 0) { + curX = 0; + } + } + myTerminal.cursorPosition(curX + 1, myTerminal.getCursorY()); + return true; + } + + private boolean cursorForwardTab(int count) { + if (count == 0) count = 1; + int curX = myTerminal.getCursorX() - 1; + int width = myTerminal.getTerminalWidth(); + for (int i = 0; i < count && curX < width - 1; i++) { + curX = myTerminal.nextTab(curX); + if (curX >= width) { + curX = width - 1; + } + } + myTerminal.cursorPosition(curX + 1, myTerminal.getCursorY()); + return true; + } } diff --git a/core/src/com/jediterm/terminal/model/JediTerminal.java b/core/src/com/jediterm/terminal/model/JediTerminal.java index 360f091e5..8d58c295e 100644 --- a/core/src/com/jediterm/terminal/model/JediTerminal.java +++ b/core/src/com/jediterm/terminal/model/JediTerminal.java @@ -364,6 +364,16 @@ public void horizontalTab() { myDisplay.setCursor(myCursorX, myCursorY); } + @Override + public int previousTab(int position) { + return myTabulator.previousTab(position); + } + + @Override + public int nextTab(int position) { + return myTabulator.nextTab(position); + } + @Override public void eraseInDisplay(final int arg) { // ED (Erase in Display) https://vt100.net/docs/vt510-rm/ED.html diff --git a/core/tests/src/com/jediterm/EmulatorTest.java b/core/tests/src/com/jediterm/EmulatorTest.java index cd6bcaea4..798176dd0 100644 --- a/core/tests/src/com/jediterm/EmulatorTest.java +++ b/core/tests/src/com/jediterm/EmulatorTest.java @@ -249,6 +249,28 @@ public void testNoScrollWhenOutsideScrollRegion() throws IOException { )); } + public void testCursorBackwardTab() throws IOException { + TestSession session = new TestSession(24, 3); + session.process("\u001b[1;1H"); // move cursor to column 1 + session.process("\t"); // tab to column 9 + session.process("\t"); // tab to column 17 + session.assertCursorPosition(17, 1); + session.process("\u001b[Z"); // CBT: back one tab stop + session.assertCursorPosition(9, 1); + session.process("\u001b[2Z"); // CBT: back two tab stops + session.assertCursorPosition(1, 1); + } + + public void testCursorForwardTab() throws IOException { + TestSession session = new TestSession(24, 3); + session.process("\u001b[1;1H"); // move cursor to column 1 + session.assertCursorPosition(1, 1); + session.process("\u001b[I"); // CHT: forward one tab stop + session.assertCursorPosition(9, 1); + session.process("\u001b[2I"); // CHT: forward two tab stops + session.assertCursorPosition(24, 1); // clamped at terminal width (last column) + } + private void assertScreenLines(@NotNull TestSession session, @NotNull List expectedScreenLines) { Assert.assertEquals(expectedScreenLines, TerminalLinesUtilKt.getLineTexts(session.getTerminalTextBuffer().getScreenLinesStorage())); }