From a3e108c54b584a0e9a99928fd01f3ff75222c41e Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 21:21:34 +0900 Subject: [PATCH 1/4] Terminal: implement the alternate screen buffer (DEC mode 1049) Full screen programs - vi, less, htop, and lately CLIs that draw their own UI - ask for the alternate screen with CSI ? 1049 h and give it back with CSI ? 1049 l. The emulator accepted the sequences and ignored them, so such a program drew over the shell's scrollback and left its last screen behind when it exited. Switching to the alternate screen now saves the normal buffer and the cursor, clears the screen and caps the buffer at the screen height, so that scrolling drops the top line instead of growing history the alternate screen is not supposed to have. Switching back restores the saved buffer and cursor, lifts the cap again, and brings the buffer to the current width and at least the screen height, since the window may have been resized in the meantime: a narrower buffer made every write past its old margin throw, a shorter one put the top of the screen above its first line. The restored buffer tells its snapshots so the view redraws instead of keeping the program's last screen. Modes 47 and 1047 are treated the same; 1048 (save/restore cursor alone) stays ignored. Asking for the screen one is already on is a no-op, as programs do ask twice. --- .../META-INF/MANIFEST.MF | 2 +- .../emulator/IVT100EmulatorBackend.java | 10 +++++ .../emulator/VT100BackendTraceDecorator.java | 6 +++ .../internal/emulator/VT100Emulator.java | 14 ++++-- .../emulator/VT100EmulatorBackend.java | 45 +++++++++++++++++++ .../internal/model/TerminalTextData.java | 7 +++ .../emulator/VT100EmulatorBackendTest.java | 40 +++++++++++++++++ .../internal/emulator/VT100EmulatorTest.java | 22 +++++++++ 8 files changed, 141 insertions(+), 5 deletions(-) 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")); + } } From 0924176741bdaef0dc075165cb2e9de0e8299f78 Mon Sep 17 00:00:00 2001 From: insjang Date: Mon, 28 Sep 2026 16:43:00 +0900 Subject: [PATCH 2/4] Terminal: keep the alternate screen history-free across resizes The alternate screen caps its buffer at the screen height so that nothing scrolls into history. The cap was set once, on entry, so after the window shrank the buffer grew back to the old height and left history behind. It now follows the screen height on every resize. The cursor of the normal screen was saved as a row of the screen. When the window grew meanwhile, the same row pointed into older history on return, and the next output overwrote it. It is now saved as a line of the normal buffer and turned back into a row of the screen as it is on return. Found by review of #2899. --- .../emulator/VT100EmulatorBackend.java | 10 ++++++-- .../emulator/VT100EmulatorBackendTest.java | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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 3ce6beadb9e..9b6df7a1e51 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 @@ -100,6 +100,7 @@ int getHeight() { */ 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; @@ -119,7 +120,7 @@ public void enableAlternateScreen(boolean enable) { if (enable) { fNormalScreen = new TerminalTextDataStore(); fNormalScreen.copy(fTerminal); - fNormalCursorLine = fCursorLine; + fNormalCursorLine = toAbsoluteLine(fCursorLine); fNormalCursorColumn = fCursorColumn; fNormalMaxHeight = fTerminal.getMaxHeight(); // clearAll leaves the buffer the size of the screen, which is what the @@ -139,7 +140,8 @@ public void enableAlternateScreen(boolean enable) { if (fTerminal.getHeight() < fLines || fTerminal.getWidth() != fColumns) { fTerminal.setDimensions(Math.max(fTerminal.getHeight(), fLines), fColumns); } - setCursor(fNormalCursorLine, fNormalCursorColumn); + // back to the line the cursor was on, wherever it is on the screen now + setCursor(fNormalCursorLine - (fTerminal.getHeight() - fLines), fNormalCursorColumn); } } } @@ -183,6 +185,10 @@ public void setDimensions(int lines, int cols) { fColumns = cols; // make the terminal at least as high as we need lines fTerminal.setDimensions(newLines, fColumns); + if (fNormalScreen != null) { + // the alternate screen keeps no history at its new height either + fTerminal.setMaxHeight(fLines); + } // compute relative cursor line cl = acl - (newLines - fLines); setCursor(cl, cc); 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 a195ea23984..4cbcfae655d 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 @@ -1125,5 +1125,28 @@ public void testAlternateScreenBuffer() { 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() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(100); + vt100.setDimensions(5, 10); + vt100.enableAlternateScreen(true); + vt100.setCursor(0, 0); + vt100.setDimensions(3, 10); + assertEquals(3, term.getMaxHeight()); + for (int i = 0; i < 6; i++) { + vt100.appendString("x"); + vt100.processNewline(); + } + // scrolling drops the top line instead of growing back to the old height + assertEquals(3, term.getHeight()); + vt100.enableAlternateScreen(false); + assertEquals(100, term.getMaxHeight()); } } From 84b7af6a9fe3620a97ac57aeab1ddfb504b741a1 Mon Sep 17 00:00:00 2001 From: insjang Date: Mon, 28 Sep 2026 17:25:08 +0900 Subject: [PATCH 3/4] Terminal: resize the alternate screen without a cap below its height The previous commit lowered the cap to the new screen height while the buffer could still be higher. TerminalTextDataFastScroll, the buffer the terminal actually uses, rejects that with an IllegalArgumentException, so shrinking the window while a full screen program was running threw from the resize handler. TerminalTextDataStore, used by the tests, accepts it, which is how it went unnoticed. The alternate screen now keeps its buffer exactly as high as the screen: on a shorter screen the lines above the cursor are dropped, as many as it takes to keep the cursor on the screen, and the cap follows only after. The test runs on both buffers. --- .../emulator/VT100EmulatorBackend.java | 20 +++++++-- .../emulator/VT100EmulatorBackendTest.java | 41 ++++++++++++------- 2 files changed, 42 insertions(+), 19 deletions(-) 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 9b6df7a1e51..c9d7d6f8af6 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 @@ -169,6 +169,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; @@ -185,10 +201,6 @@ public void setDimensions(int lines, int cols) { fColumns = cols; // make the terminal at least as high as we need lines fTerminal.setDimensions(newLines, fColumns); - if (fNormalScreen != null) { - // the alternate screen keeps no history at its new height either - fTerminal.setMaxHeight(fLines); - } // compute relative cursor line cl = acl - (newLines - fLines); setCursor(cl, cc); 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 4cbcfae655d..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; @@ -1132,21 +1133,31 @@ public void testAlternateScreenBuffer() { @Test public void testAlternateScreenShrunkKeepsNoHistory() { - ITerminalTextData term = makeITerminalTextData(); - IVT100EmulatorBackend vt100 = makeBakend(term); - term.setMaxHeight(100); - vt100.setDimensions(5, 10); - vt100.enableAlternateScreen(true); - vt100.setCursor(0, 0); - vt100.setDimensions(3, 10); - assertEquals(3, term.getMaxHeight()); - for (int i = 0; i < 6; i++) { - vt100.appendString("x"); - vt100.processNewline(); + // 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()); } - // scrolling drops the top line instead of growing back to the old height - assertEquals(3, term.getHeight()); - vt100.enableAlternateScreen(false); - assertEquals(100, term.getMaxHeight()); } } From 381accb75fb72dd475f56ff4b11274efe11c41c5 Mon Sep 17 00:00:00 2001 From: insjang Date: Mon, 28 Sep 2026 17:53:42 +0900 Subject: [PATCH 4/4] Terminal: leave the alternate screen on reset, keep its cap on preference changes Three ways the alternate screen state could leak, found by review: - resetState(), which runs before every connection, did not leave the alternate screen. A connection that ended while a full screen program had the screen left the next one on the capped buffer, without history, and a later CSI ? 1049 l brought back the old screen. Reset now leaves the alternate screen, putting the normal one back. - CSI ? 1049 l reset the scroll region before finding out whether the alternate screen was showing at all, so a repeated request cleared the margins of the normal screen. The backend now resets them when it actually switches back. - setBufferLineLimit(), called on every preference event, set the model's maximum height directly. On the alternate screen that lifted the cap and history accumulated, and on return the limit from before overwrote the new one. The limit now goes through the backend, which keeps it for the normal screen while the alternate one is showing. The tests run on the terminal's real buffer and fail without the change. --- .../emulator/IVT100EmulatorBackend.java | 9 ++++ .../emulator/VT100BackendTraceDecorator.java | 6 +++ .../internal/emulator/VT100Emulator.java | 14 +++++- .../emulator/VT100EmulatorBackend.java | 20 ++++++++ .../emulator/VT100TerminalControl.java | 11 +---- .../internal/emulator/VT100EmulatorTest.java | 47 +++++++++++++++++++ 6 files changed, 96 insertions(+), 11 deletions(-) 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 3cf4475d249..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 @@ -35,6 +35,15 @@ public interface IVT100EmulatorBackend { */ 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 15ebf2200b6..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 @@ -44,6 +44,12 @@ public void enableAlternateScreen(boolean enable) { 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 2df8e0d4e19..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(); @@ -1295,8 +1302,8 @@ private void processDecPrivateCommand_l() { case 47: case 1047: case 1049: - // Use Normal Screen Buffer, putting back what was on it. - 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: @@ -1570,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 c9d7d6f8af6..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 @@ -132,6 +132,8 @@ public void enableAlternateScreen(boolean enable) { } 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 @@ -146,6 +148,24 @@ public void enableAlternateScreen(boolean enable) { } } + @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) { 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/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 6c399ceddf2..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 @@ -397,4 +397,51 @@ public void testAlternateScreen() { 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()); + } }