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())); }