/*
 * This file is part of lanterna (http://code.google.com/p/lanterna/).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.screen;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.Scrollable;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.terminal.Terminal;
import com.googlecode.lanterna.terminal.TerminalResizeListener;

import java.io.IOException;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Map;
import java.util.TreeMap;

This is the default concrete implementation of the Screen interface, a buffered layer sitting on top of a Terminal. If you want to get started with the Screen layer, this is probably the class you want to use. Remember to start the screen before you can use it and stop it when you are done with it. This will place the terminal in private mode during the screen operations and leave private mode afterwards.
Author:martin
/** * This is the default concrete implementation of the Screen interface, a buffered layer sitting on top of a Terminal. * If you want to get started with the Screen layer, this is probably the class you want to use. Remember to start the * screen before you can use it and stop it when you are done with it. This will place the terminal in private mode * during the screen operations and leave private mode afterwards. * @author martin */
public class TerminalScreen extends AbstractScreen { private final Terminal terminal; private boolean isStarted; private boolean fullRedrawHint; private ScrollHint scrollHint;
Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially blank. The default character used for unused space (the newly initialized state of the screen and new areas after expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.

Before you can display the content of this buffered screen to the real underlying terminal, you must call the startScreen() method. This will ask the terminal to enter private mode (which is required for Screens to work properly). Similarly, when you are done, you should call stopScreen() which will exit private mode.

Params:
  • terminal – Terminal object to create the DefaultScreen on top of
Throws:
  • IOException – If there was an underlying I/O error when querying the size of the terminal
/** * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially * blank. The default character used for unused space (the newly initialized state of the screen and new areas after * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color. * <p> * Before you can display the content of this buffered screen to the real underlying terminal, you must call the * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode. * * @param terminal Terminal object to create the DefaultScreen on top of * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal */
public TerminalScreen(Terminal terminal) throws IOException { this(terminal, DEFAULT_CHARACTER); }
Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially blank. The default character used for unused space (the newly initialized state of the screen and new areas after expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.

Before you can display the content of this buffered screen to the real underlying terminal, you must call the startScreen() method. This will ask the terminal to enter private mode (which is required for Screens to work properly). Similarly, when you are done, you should call stopScreen() which will exit private mode.

Params:
  • terminal – Terminal object to create the DefaultScreen on top of.
  • defaultCharacter – What character to use for the initial state of the screen and expanded areas
Throws:
  • IOException – If there was an underlying I/O error when querying the size of the terminal
/** * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially * blank. The default character used for unused space (the newly initialized state of the screen and new areas after * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color. * <p> * Before you can display the content of this buffered screen to the real underlying terminal, you must call the * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode. * * @param terminal Terminal object to create the DefaultScreen on top of. * @param defaultCharacter What character to use for the initial state of the screen and expanded areas * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal */
public TerminalScreen(Terminal terminal, TextCharacter defaultCharacter) throws IOException { super(terminal.getTerminalSize(), defaultCharacter); this.terminal = terminal; this.terminal.addResizeListener(new TerminalScreenResizeListener()); this.isStarted = false; this.fullRedrawHint = true; } @Override public synchronized void startScreen() throws IOException { if(isStarted) { return; } isStarted = true; getTerminal().enterPrivateMode(); getTerminal().getTerminalSize(); getTerminal().clearScreen(); this.fullRedrawHint = true; TerminalPosition cursorPosition = getCursorPosition(); if(cursorPosition != null) { getTerminal().setCursorVisible(true); getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow()); } else { getTerminal().setCursorVisible(false); } } @Override public void stopScreen() throws IOException { stopScreen(true); } public synchronized void stopScreen(boolean flushInput) throws IOException { if(!isStarted) { return; } if (flushInput) { //Drain the input queue KeyStroke keyStroke; do { keyStroke = pollInput(); } while(keyStroke != null && keyStroke.getKeyType() != KeyType.EOF); } getTerminal().exitPrivateMode(); isStarted = false; } @Override public synchronized void refresh(RefreshType refreshType) throws IOException { if(!isStarted) { return; } if((refreshType == RefreshType.AUTOMATIC && fullRedrawHint) || refreshType == RefreshType.COMPLETE) { refreshFull(); fullRedrawHint = false; } else if(refreshType == RefreshType.AUTOMATIC && (scrollHint == null || scrollHint == ScrollHint.INVALID)) { double threshold = getTerminalSize().getRows() * getTerminalSize().getColumns() * 0.75; if(getBackBuffer().isVeryDifferent(getFrontBuffer(), (int) threshold)) { refreshFull(); } else { refreshByDelta(); } } else { refreshByDelta(); } getBackBuffer().copyTo(getFrontBuffer()); TerminalPosition cursorPosition = getCursorPosition(); if(cursorPosition != null) { getTerminal().setCursorVisible(true); //If we are trying to move the cursor to the padding of a CJK character, put it on the actual character instead if(cursorPosition.getColumn() > 0 && TerminalTextUtils.isCharCJK(getFrontBuffer().getCharacterAt(cursorPosition.withRelativeColumn(-1)).getCharacter())) { getTerminal().setCursorPosition(cursorPosition.getColumn() - 1, cursorPosition.getRow()); } else { getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow()); } } else { getTerminal().setCursorVisible(false); } getTerminal().flush(); } private void useScrollHint() throws IOException { if (scrollHint == null) { return; } try { if (scrollHint == ScrollHint.INVALID) { return; } Terminal term = getTerminal(); if (term instanceof Scrollable) { // just try and see if it cares: scrollHint.applyTo( (Scrollable)term ); // if that didn't throw, then update front buffer: scrollHint.applyTo( getFrontBuffer() ); } } catch (UnsupportedOperationException uoe) { /* ignore */ } finally { scrollHint = null; } } private void refreshByDelta() throws IOException { Map<TerminalPosition, TextCharacter> updateMap = new TreeMap<TerminalPosition, TextCharacter>(new ScreenPointComparator()); TerminalSize terminalSize = getTerminalSize(); useScrollHint(); for(int y = 0; y < terminalSize.getRows(); y++) { for(int x = 0; x < terminalSize.getColumns(); x++) { TextCharacter backBufferCharacter = getBackBuffer().getCharacterAt(x, y); TextCharacter frontBufferCharacter = getFrontBuffer().getCharacterAt(x, y); if(!backBufferCharacter.equals(frontBufferCharacter)) { updateMap.put(new TerminalPosition(x, y), backBufferCharacter); } if(TerminalTextUtils.isCharCJK(backBufferCharacter.getCharacter())) { x++; //Skip the trailing padding } else if (TerminalTextUtils.isCharCJK(frontBufferCharacter.getCharacter())) { if (x+1 < terminalSize.getColumns()) { updateMap.put(new TerminalPosition(x+1, y), frontBufferCharacter.withCharacter(' ')); } } } } if(updateMap.isEmpty()) { return; } TerminalPosition currentPosition = updateMap.keySet().iterator().next(); getTerminal().setCursorPosition(currentPosition.getColumn(), currentPosition.getRow()); TextCharacter firstScreenCharacterToUpdate = updateMap.values().iterator().next(); EnumSet<SGR> currentSGR = firstScreenCharacterToUpdate.getModifiers(); getTerminal().resetColorAndSGR(); for(SGR sgr: currentSGR) { getTerminal().enableSGR(sgr); } TextColor currentForegroundColor = firstScreenCharacterToUpdate.getForegroundColor(); TextColor currentBackgroundColor = firstScreenCharacterToUpdate.getBackgroundColor(); getTerminal().setForegroundColor(currentForegroundColor); getTerminal().setBackgroundColor(currentBackgroundColor); for(TerminalPosition position: updateMap.keySet()) { if(!position.equals(currentPosition)) { getTerminal().setCursorPosition(position.getColumn(), position.getRow()); currentPosition = position; } TextCharacter newCharacter = updateMap.get(position); if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) { getTerminal().setForegroundColor(newCharacter.getForegroundColor()); currentForegroundColor = newCharacter.getForegroundColor(); } if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) { getTerminal().setBackgroundColor(newCharacter.getBackgroundColor()); currentBackgroundColor = newCharacter.getBackgroundColor(); } for(SGR sgr: SGR.values()) { if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) { getTerminal().disableSGR(sgr); currentSGR.remove(sgr); } else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) { getTerminal().enableSGR(sgr); currentSGR.add(sgr); } } getTerminal().putCharacter(newCharacter.getCharacter()); if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) { //CJK characters advances two columns currentPosition = currentPosition.withRelativeColumn(2); } else { //Normal characters advances one column currentPosition = currentPosition.withRelativeColumn(1); } } } private void refreshFull() throws IOException { getTerminal().setForegroundColor(TextColor.ANSI.DEFAULT); getTerminal().setBackgroundColor(TextColor.ANSI.DEFAULT); getTerminal().clearScreen(); getTerminal().resetColorAndSGR(); scrollHint = null; // discard any scroll hint for full refresh EnumSet<SGR> currentSGR = EnumSet.noneOf(SGR.class); TextColor currentForegroundColor = TextColor.ANSI.DEFAULT; TextColor currentBackgroundColor = TextColor.ANSI.DEFAULT; for(int y = 0; y < getTerminalSize().getRows(); y++) { getTerminal().setCursorPosition(0, y); int currentColumn = 0; for(int x = 0; x < getTerminalSize().getColumns(); x++) { TextCharacter newCharacter = getBackBuffer().getCharacterAt(x, y); if(newCharacter.equals(DEFAULT_CHARACTER)) { continue; } if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) { getTerminal().setForegroundColor(newCharacter.getForegroundColor()); currentForegroundColor = newCharacter.getForegroundColor(); } if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) { getTerminal().setBackgroundColor(newCharacter.getBackgroundColor()); currentBackgroundColor = newCharacter.getBackgroundColor(); } for(SGR sgr: SGR.values()) { if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) { getTerminal().disableSGR(sgr); currentSGR.remove(sgr); } else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) { getTerminal().enableSGR(sgr); currentSGR.add(sgr); } } if(currentColumn != x) { getTerminal().setCursorPosition(x, y); currentColumn = x; } getTerminal().putCharacter(newCharacter.getCharacter()); if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) { //CJK characters take up two columns currentColumn += 2; x++; } else { //Normal characters take up one column currentColumn += 1; } } } }
Returns the underlying Terminal interface that this Screen is using.

Be aware: directly modifying the underlying terminal will most likely result in unexpected behaviour if you then go on and try to interact with the Screen. The Screen's back-buffer/front-buffer will not know about the operations you are going on the Terminal and won't be able to properly generate a refresh unless you enforce a Screen.RefreshType.COMPLETE, at which the entire terminal area will be repainted according to the back-buffer of the Screen.

Returns:Underlying terminal used by the screen
/** * Returns the underlying {@code Terminal} interface that this Screen is using. * <p> * <b>Be aware:</b> directly modifying the underlying terminal will most likely result in unexpected behaviour if * you then go on and try to interact with the Screen. The Screen's back-buffer/front-buffer will not know about * the operations you are going on the Terminal and won't be able to properly generate a refresh unless you enforce * a {@code Screen.RefreshType.COMPLETE}, at which the entire terminal area will be repainted according to the * back-buffer of the {@code Screen}. * @return Underlying terminal used by the screen */
@SuppressWarnings("WeakerAccess") public Terminal getTerminal() { return terminal; } @Override public KeyStroke readInput() throws IOException { return terminal.readInput(); } @Override public KeyStroke pollInput() throws IOException { return terminal.pollInput(); } @Override public synchronized void clear() { super.clear(); fullRedrawHint = true; scrollHint = ScrollHint.INVALID; } @Override public synchronized TerminalSize doResizeIfNecessary() { TerminalSize newSize = super.doResizeIfNecessary(); if(newSize != null) { fullRedrawHint = true; } return newSize; }
Perform the scrolling and save scroll-range and distance in order to be able to optimize Terminal-update later.
/** * Perform the scrolling and save scroll-range and distance in order * to be able to optimize Terminal-update later. */
@Override public void scrollLines(int firstLine, int lastLine, int distance) { // just ignore certain kinds of garbage: if (distance == 0 || firstLine > lastLine) { return; } super.scrollLines(firstLine, lastLine, distance); // Save scroll hint for next refresh: ScrollHint newHint = new ScrollHint(firstLine,lastLine,distance); if (scrollHint == null) { // no scroll hint yet: use the new one: scrollHint = newHint; } else //noinspection StatementWithEmptyBody if (scrollHint == ScrollHint.INVALID) { // scroll ranges already inconsistent since latest refresh! // leave at INVALID } else if (scrollHint.matches(newHint)) { // same range: just accumulate distance: scrollHint.distance += newHint.distance; } else { // different scroll range: no scroll-optimization for next refresh this.scrollHint = ScrollHint.INVALID; } } private class TerminalScreenResizeListener implements TerminalResizeListener { @Override public void onResized(Terminal terminal, TerminalSize newSize) { addResizeRequest(newSize); } } private static class ScreenPointComparator implements Comparator<TerminalPosition> { @Override public int compare(TerminalPosition o1, TerminalPosition o2) { if(o1.getRow() == o2.getRow()) { if(o1.getColumn() == o2.getColumn()) { return 0; } else { return new Integer(o1.getColumn()).compareTo(o2.getColumn()); } } else { return new Integer(o1.getRow()).compareTo(o2.getRow()); } } } private static class ScrollHint { public static final ScrollHint INVALID = new ScrollHint(-1,-1,0); public final int firstLine; public final int lastLine; public int distance; public ScrollHint(int firstLine, int lastLine, int distance) { this.firstLine = firstLine; this.lastLine = lastLine; this.distance = distance; } public boolean matches(ScrollHint other) { return this.firstLine == other.firstLine && this.lastLine == other.lastLine; } public void applyTo( Scrollable scr ) throws IOException { scr.scrollLines(firstLine, lastLine, distance); } } }