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..3cf4475d249 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,16 @@ 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 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..15ebf2200b6 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,12 @@ public void clearAll() { fBackend.clearAll(); } + @Override + public void enableAlternateScreen(boolean enable) { + fWriter.println("enableAlternateScreen(" + enable + ')'); //$NON-NLS-1$ + fBackend.enableAlternateScreen(enable); + } + @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..2df8e0d4e19 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 @@ -1272,9 +1272,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 +1294,13 @@ private void processDecPrivateCommand_l() { break; case 47: case 1047: - case 1048: case 1049: - // Use Normal Screen Buffer (ignored, but reset scroll region). + // Use Normal Screen Buffer, putting back what was on it. text.setScrollRegion(-1, -1); + text.enableAlternateScreen(false); + break; + case 1048: + // Restore cursor position (ignored). break; default: Logger.log("Unsupported command parameter: CSI ?" + param + 'l'); //$NON-NLS-1$ 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..3ce6beadb9e 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,15 @@ 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; + private int fNormalCursorLine; + private int fNormalCursorColumn; private boolean fVT100LineWrapping; private ScrollRegion fScrollRegion = ScrollRegion.FULL_WINDOW; @@ -99,6 +109,41 @@ 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 = 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; + 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); + } + setCursor(fNormalCursorLine, fNormalCursorColumn); + } + } + } + @Override public void clearAll() { synchronized (fTerminal) { 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..a195ea23984 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 @@ -1086,4 +1086,44 @@ 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()); + } } 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..6c399ceddf2 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,23 @@ 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")); + } }