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/emulator/IVT100EmulatorBackend.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/IVT100EmulatorBackend.java index 08ad52bd45c..fb337b1cd9e 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/IVT100EmulatorBackend.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/IVT100EmulatorBackend.java @@ -25,6 +25,25 @@ public interface IVT100EmulatorBackend { */ void clearAll(); + /** + * Switches between the screen a shell writes to, which keeps its scrollback, and + * the one a full screen program is given, which has none. Leaving the alternate + * screen puts back exactly what was on the normal one, so a program that takes + * over the display does not cost the user the history behind it. + * + * @param enable whether to show the alternate screen + */ + void enableAlternateScreen(boolean enable); + + /** + * Sets how many lines the normal screen keeps, history included. While the + * alternate screen is showing, the limit is kept for the normal screen and + * applies once it is back, since the alternate screen keeps no history. + * + * @param bufferLineLimit the maximum number of lines, ignored when not positive + */ + void setBufferLineLimit(int bufferLineLimit); + /** * Sets the Dimensions of the addressable scroll space of the screen.... * Keeps the cursor position relative to the bottom of the screen! diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100BackendTraceDecorator.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100BackendTraceDecorator.java index a67cbff2f48..8129b1b4415 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100BackendTraceDecorator.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100BackendTraceDecorator.java @@ -38,6 +38,18 @@ public void clearAll() { fBackend.clearAll(); } + @Override + public void enableAlternateScreen(boolean enable) { + fWriter.println("enableAlternateScreen(" + enable + ')'); //$NON-NLS-1$ + fBackend.enableAlternateScreen(enable); + } + + @Override + public void setBufferLineLimit(int bufferLineLimit) { + fWriter.println("setBufferLineLimit(" + bufferLineLimit + ')'); //$NON-NLS-1$ + fBackend.setBufferLineLimit(bufferLineLimit); + } + @Override public void deleteCharacters(int n) { fWriter.println("deleteCharacters(" + n + ")"); //$NON-NLS-1$ //$NON-NLS-2$ 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..6c0ba46d402 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 @@ -197,6 +197,13 @@ public void setInputStreamReader(Reader reader) { fReader = reader; } + /** + * @see IVT100EmulatorBackend#setBufferLineLimit(int) + */ + public void setBufferLineLimit(int bufferLineLimit) { + text.setBufferLineLimit(bufferLineLimit); + } + public void setDimensions(int lines, int cols) { text.setDimensions(lines, cols); ITerminalConnector telnetConnection = getConnector(); @@ -1272,9 +1279,12 @@ private void processDecPrivateCommand_h() { break; case 47: case 1047: - case 1048: case 1049: - // Use Alternate Screen Buffer (ignored). + // Use Alternate Screen Buffer. + text.enableAlternateScreen(true); + break; + case 1048: + // Save cursor position (ignored). break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'h'); //$NON-NLS-1$ @@ -1291,10 +1301,13 @@ private void processDecPrivateCommand_l() { break; case 47: case 1047: - case 1048: case 1049: - // Use Normal Screen Buffer (ignored, but reset scroll region). - text.setScrollRegion(-1, -1); + // Use Normal Screen Buffer, putting back what was on it. Leaving also resets + // the margins, which a repeated request must not do to the normal screen. + text.enableAlternateScreen(false); + break; + case 1048: + // Restore cursor position (ignored). break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'l'); //$NON-NLS-1$ @@ -1564,6 +1577,9 @@ public void resetState() { text.setStyle(text.getDefaultStyle()); text.setScrollRegion(-1, -1); text.setInsertMode(false); + // a new connection starts on the normal screen, even if the last one ended + // while a full screen program had the alternate one + text.enableAlternateScreen(false); } // public OutputStream getOutputStream() { diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java index 4dfc1f1b560..eae2b3810bd 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java @@ -15,6 +15,7 @@ package org.eclipse.terminal.internal.emulator; import org.eclipse.terminal.model.ITerminalTextData; +import org.eclipse.terminal.internal.model.TerminalTextDataStore; import org.eclipse.terminal.model.TerminalStyle; /** @@ -92,6 +93,16 @@ int getHeight() { int fLines; int fColumns; final private ITerminalTextData fTerminal; + + /** + * What the normal screen held while the alternate one is showing, and null the + * rest of the time, which is also how we know which screen we are on. + */ + private ITerminalTextData fNormalScreen; + private int fNormalMaxHeight; + /** in the normal buffer, not on the screen: the screen may be resized meanwhile */ + private int fNormalCursorLine; + private int fNormalCursorColumn; private boolean fVT100LineWrapping; private ScrollRegion fScrollRegion = ScrollRegion.FULL_WINDOW; @@ -99,6 +110,62 @@ public VT100EmulatorBackend(ITerminalTextData terminal) { fTerminal = terminal; } + @Override + public void enableAlternateScreen(boolean enable) { + synchronized (fTerminal) { + if (enable == (fNormalScreen != null)) { + // Already on the screen being asked for. Programs do ask twice. + return; + } + if (enable) { + fNormalScreen = new TerminalTextDataStore(); + fNormalScreen.copy(fTerminal); + fNormalCursorLine = toAbsoluteLine(fCursorLine); + fNormalCursorColumn = fCursorColumn; + fNormalMaxHeight = fTerminal.getMaxHeight(); + // clearAll leaves the buffer the size of the screen, which is what the + // alternate screen is: no history to scroll back through. Capping the + // buffer there keeps it so: scrolling drops the top line instead of + // growing the buffer, as a program on this screen expects. + clearAll(); + fTerminal.setMaxHeight(fLines); + } else { + fTerminal.copy(fNormalScreen); + fNormalScreen = null; + // the margins were the full screen program's, not the shell's + fScrollRegion = ScrollRegion.FULL_WINDOW; + fTerminal.setMaxHeight(Math.max(fNormalMaxHeight, fTerminal.getHeight())); + // The window may have been resized while the program had the screen, and + // the buffer put back is the one from before. Narrower, and every write + // past its margin throws; shorter than the screen, and the top of the + // screen sits above its first line, so every line number comes out negative. + if (fTerminal.getHeight() < fLines || fTerminal.getWidth() != fColumns) { + fTerminal.setDimensions(Math.max(fTerminal.getHeight(), fLines), fColumns); + } + // back to the line the cursor was on, wherever it is on the screen now + setCursor(fNormalCursorLine - (fTerminal.getHeight() - fLines), fNormalCursorColumn); + } + } + } + + @Override + public void setBufferLineLimit(int bufferLineLimit) { + if (bufferLineLimit <= 0) { + return; + } + synchronized (fTerminal) { + if (fNormalScreen != null) { + // the alternate screen keeps its cap; the normal screen gets the limit back + fNormalMaxHeight = bufferLineLimit; + return; + } + if (fTerminal.getHeight() > bufferLineLimit) { + fTerminal.setDimensions(bufferLineLimit, fTerminal.getWidth()); + } + fTerminal.setMaxHeight(bufferLineLimit); + } + } + @Override public void clearAll() { synchronized (fTerminal) { @@ -122,6 +189,22 @@ public void setDimensions(int lines, int cols) { // relative cursor line int cl = getCursorLine(); int cc = getCursorColumn(); + if (fNormalScreen != null) { + // The alternate screen has no history, so its buffer stays exactly as + // high as the screen. On a shorter screen the lines above the cursor go + // first, as many as it takes to keep the cursor on it; the cap follows + // once the buffer is no higher than it, which the buffer requires. + int drop = Math.max(0, cl + 1 - lines); + if (drop > 0) { + fTerminal.scroll(0, fTerminal.getHeight(), -drop); + } + fLines = lines; + fColumns = cols; + fTerminal.setDimensions(lines, cols); + fTerminal.setMaxHeight(lines); + setCursor(cl - drop, cc); + return; + } int height = fTerminal.getHeight(); // absolute cursor line int acl = cl + height - fLines; 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..0f3b09570a3 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 @@ -1341,15 +1341,8 @@ public int getBufferLineLimit() { @Override public void setBufferLineLimit(int bufferLineLimit) { - if (bufferLineLimit <= 0) { - return; - } - synchronized (fTerminalModel) { - if (fTerminalModel.getHeight() > bufferLineLimit) { - fTerminalModel.setDimensions(bufferLineLimit, fTerminalModel.getWidth()); - } - fTerminalModel.setMaxHeight(bufferLineLimit); - } + // through the emulator, which knows whether the alternate screen is showing + fTerminalText.setBufferLineLimit(bufferLineLimit); } @Override diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java index baa3b620df8..78990827baf 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java @@ -262,9 +262,16 @@ public void addLine() { @Override public void copy(ITerminalTextData source) { + int height = getHeight(), width = getWidth(); fData.copy(source); fCursorLine = source.getCursorLine(); fCursorColumn = source.getCursorColumn(); + // Every line and the size of the buffer with them. Without saying so the view + // keeps drawing what it last saw, which is the screen a program left behind. + sendLinesChangedToSnapshot(0, Math.max(height, getHeight())); + if (height != getHeight() || width != getWidth()) { + sendDimensionsChanged(); + } } @Override diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java index aecbc640616..f0a28bb1dac 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.eclipse.terminal.internal.model.TerminalTextData; import org.eclipse.terminal.internal.model.TerminalTextDataStore; import org.eclipse.terminal.internal.model.TerminalTextTestHelper; import org.eclipse.terminal.model.ITerminalTextData; @@ -1086,4 +1087,77 @@ public void testNewlineInTopAnchoredScrollRegionCurrentlyDiscardsTopLine() { assertNull(term.getChars(3)); assertEquals("4444", new String(term.getChars(4))); // footer below the region is untouched } + + @Test + public void testAlternateScreenBuffer() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(100); + vt100.setDimensions(3, 10); + vt100.setCursor(0, 0); + vt100.appendString("one\r\n"); + for (int i = 0; i < 5; i++) { + vt100.appendString("more"); + vt100.processNewline(); + vt100.setCursorColumn(0); + } + vt100.appendString("last"); + int historyHeight = term.getHeight(); + assertTrue(historyHeight > 3); + + vt100.enableAlternateScreen(true); + // no history to scroll back through, and none accumulates: scrolling drops the top line + assertEquals(3, term.getHeight()); + assertEquals(3, term.getMaxHeight()); + vt100.appendString("alt"); + vt100.setCursor(2, 0); + vt100.processNewline(); + vt100.processNewline(); + assertEquals(3, term.getHeight()); + assertNull(term.getChars(0)); + + // resized while the program had the screen + vt100.setDimensions(5, 7); + vt100.enableAlternateScreen(false); + // the buffer put back is as wide as the screen is now and at least as tall + assertEquals(7, term.getWidth()); + assertTrue(term.getHeight() >= 5); + assertEquals(100, term.getMaxHeight()); + assertEquals("last", new String(term.getChars(term.getHeight() - 1), 0, 4)); + vt100.appendString("x"); // a write past the old margin used to throw + assertEquals(5, vt100.getCursorColumn()); + // the cursor is back on the line it left, now the bottom of a taller screen + assertEquals(4, vt100.getCursorLine()); + assertEquals("lastx", new String(term.getChars(term.getHeight() - 1), 0, 5)); + } + + @Test + public void testAlternateScreenShrunkKeepsNoHistory() { + // also on the buffer the terminal actually uses, which rejects a cap below its height + for (ITerminalTextData term : new ITerminalTextData[] { makeITerminalTextData(), new TerminalTextData() }) { + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(100); + vt100.setDimensions(5, 10); + vt100.enableAlternateScreen(true); + vt100.setCursor(4, 0); + vt100.appendString("bottom"); + vt100.setDimensions(3, 10); + assertEquals(3, term.getHeight()); + assertEquals(3, term.getMaxHeight()); + // the lines above the cursor went, the cursor and its line stayed + assertEquals(2, vt100.getCursorLine()); + assertEquals("bottom", new String(term.getChars(2), 0, 6)); + for (int i = 0; i < 6; i++) { + vt100.processNewline(); + vt100.appendString("x"); + } + // scrolling drops the top line instead of growing back to the old height + assertEquals(3, term.getHeight()); + vt100.setDimensions(6, 10); + assertEquals(6, term.getHeight()); + assertEquals(6, term.getMaxHeight()); + vt100.enableAlternateScreen(false); + assertEquals(100, term.getMaxHeight()); + } + } } 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..1a3f85d8753 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 @@ -57,6 +57,9 @@ private static String SCROLL_REGION(int startRow, int endRow) { * Set the cursor position to line/column. Note that this is the logical * line and column, so 1, 1 is the top left. */ + private static final String ALTERNATE_SCREEN_ON = "\u001b[?1049h"; + private static final String ALTERNATE_SCREEN_OFF = "\u001b[?1049l"; + private static String CURSOR_POSITION(int line, int column) { return "\033[" + line + ";" + column + "H"; } @@ -375,4 +378,70 @@ public void testMalformedStringTerminator() { () -> assertEquals(List.of("TITLE1", "TITLE2"), control.getAllTitles())); } + @Test + public void testAlternateScreen() { + data.setMaxHeight(1000); + run("Hello 1\r\nHello 2"); + assertAll(() -> assertCursorLocation(1, 7), () -> assertTextEquals("Hello 1", "Hello 2")); + // a full screen program takes the screen: it starts blank and no history is kept + run(ALTERNATE_SCREEN_ON); + assertAll(() -> assertCursorLocation(0, 0), () -> assertTextEquals("")); + assertEquals(WINDOW_LINES, data.getHeight()); + run("Full screen"); + assertTextEquals("Full screen"); + run(ALTERNATE_SCREEN_ON); // programs do ask twice + assertTextEquals("Full screen"); + // and gives it back with what was there before, cursor included + run(ALTERNATE_SCREEN_OFF); + assertAll(() -> assertCursorLocation(1, 7), () -> assertTextEquals("Hello 1", "Hello 2")); + run(ALTERNATE_SCREEN_OFF); // harmless twice as well + assertAll(() -> assertCursorLocation(1, 7), () -> assertTextEquals("Hello 1", "Hello 2")); + } + + @Test + public void testResetStateLeavesAlternateScreen() { + data.setMaxHeight(1000); + run("Hello 1\r\nHello 2"); + run(ALTERNATE_SCREEN_ON); + run("Full screen"); + // the connection ends while the program has the screen; the next one starts + // on the normal screen, with its history back + emulator.resetState(); + assertAll(() -> assertTextEquals("Hello 1", "Hello 2"), () -> assertEquals(1000, data.getMaxHeight())); + // a late request to leave does not bring back the old program's screen + run(ALTERNATE_SCREEN_OFF); + assertTextEquals("Hello 1", "Hello 2"); + for (int i = 0; i < WINDOW_LINES; i++) { + run("\r\nmore"); + } + assertEquals(WINDOW_LINES + 2, data.getHeight()); // history accumulates again + } + + @Test + public void testRepeatedAlternateScreenOffKeepsMargins() { + run("A\r\nB\r\nC"); + run(SCROLL_REGION(1, 2)); + run(ALTERNATE_SCREEN_OFF); // already on the normal screen: must change nothing + run(CURSOR_POSITION(2, 1), "\n"); + // the newline on the bottom margin scrolls rows 1 and 2 only + assertTextEquals("B", "", "C"); + } + + @Test + public void testBufferLineLimitWhileAlternateScreen() { + data.setMaxHeight(1000); + emulator.setBufferLineLimit(300); + assertEquals(300, data.getMaxHeight()); + run(ALTERNATE_SCREEN_ON); + // a preference change must not give the alternate screen a history + emulator.setBufferLineLimit(500); + assertEquals(WINDOW_LINES, data.getMaxHeight()); + for (int i = 0; i < 2 * WINDOW_LINES; i++) { + run("line\r\n"); + } + assertEquals(WINDOW_LINES, data.getHeight()); + // and the new limit is the normal screen's once it is back + run(ALTERNATE_SCREEN_OFF); + assertEquals(500, data.getMaxHeight()); + } }