/*
 * This file is part of lanterna (https://github.com/mabe02/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.terminal.virtual;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.screen.TabBehaviour;
import com.googlecode.lanterna.terminal.AbstractTerminal;

import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class DefaultVirtualTerminal extends AbstractTerminal implements VirtualTerminal {
    private final TextBuffer regularTextBuffer;
    private final TextBuffer privateModeTextBuffer;
    private final TreeSet<TerminalPosition> dirtyTerminalCells;
    private final List<VirtualTerminalListener> listeners;

    private TextBuffer currentTextBuffer;
    private boolean wholeBufferDirty;

    private TerminalSize terminalSize;
    private boolean cursorVisible;
    private int backlogSize;

    private final BlockingQueue<KeyStroke> inputQueue;
    private final EnumSet<SGR> activeModifiers;
    private TextColor activeForegroundColor;
    private TextColor activeBackgroundColor;

    // Global coordinates, i.e. relative to the top-left corner of the full buffer
    private TerminalPosition cursorPosition;

    // Used when switching back from private mode, to restore the earlier cursor position
    private TerminalPosition savedCursorPosition;


    
Creates a new virtual terminal with an initial size set
/** * Creates a new virtual terminal with an initial size set */
public DefaultVirtualTerminal() { this(new TerminalSize(80, 24)); }
Creates a new virtual terminal with an initial size set
Params:
  • initialTerminalSize – Starting size of the virtual terminal
/** * Creates a new virtual terminal with an initial size set * @param initialTerminalSize Starting size of the virtual terminal */
public DefaultVirtualTerminal(TerminalSize initialTerminalSize) { this.regularTextBuffer = new TextBuffer(); this.privateModeTextBuffer = new TextBuffer(); this.dirtyTerminalCells = new TreeSet<>(); this.listeners = new ArrayList<>(); // Terminal state this.inputQueue = new LinkedBlockingQueue<>(); this.activeModifiers = EnumSet.noneOf(SGR.class); this.activeForegroundColor = TextColor.ANSI.DEFAULT; this.activeBackgroundColor = TextColor.ANSI.DEFAULT; // Start with regular mode this.currentTextBuffer = regularTextBuffer; this.wholeBufferDirty = false; this.terminalSize = initialTerminalSize; this.cursorVisible = true; this.cursorPosition = TerminalPosition.TOP_LEFT_CORNER; this.savedCursorPosition = TerminalPosition.TOP_LEFT_CORNER; this.backlogSize = 1000; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Terminal interface methods (and related) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public synchronized TerminalSize getTerminalSize() { return terminalSize; } @Override public synchronized void setTerminalSize(TerminalSize newSize) { this.terminalSize = newSize; trimBufferBacklog(); correctCursor(); for(VirtualTerminalListener listener: listeners) { listener.onResized(this, terminalSize); } super.onResized(newSize.getColumns(), newSize.getRows()); } @Override public synchronized void enterPrivateMode() { currentTextBuffer = privateModeTextBuffer; savedCursorPosition = getCursorBufferPosition(); setCursorPosition(TerminalPosition.TOP_LEFT_CORNER); setWholeBufferDirty(); } @Override public synchronized void exitPrivateMode() { currentTextBuffer = regularTextBuffer; cursorPosition = savedCursorPosition; setWholeBufferDirty(); } @Override public synchronized void clearScreen() { currentTextBuffer.clear(); setWholeBufferDirty(); setCursorPosition(TerminalPosition.TOP_LEFT_CORNER); } @Override public synchronized void setCursorPosition(int x, int y) { setCursorPosition(cursorPosition.withColumn(x).withRow(y)); } @Override public synchronized void setCursorPosition(TerminalPosition cursorPosition) { if(terminalSize.getRows() < getBufferLineCount()) { cursorPosition = cursorPosition.withRelativeRow(getBufferLineCount() - terminalSize.getRows()); } this.cursorPosition = cursorPosition; correctCursor(); } @Override public synchronized TerminalPosition getCursorPosition() { if(getBufferLineCount() <= terminalSize.getRows()) { return getCursorBufferPosition(); } else { return cursorPosition.withRelativeRow(-(getBufferLineCount() - terminalSize.getRows())); } } @Override public synchronized TerminalPosition getCursorBufferPosition() { return cursorPosition; } @Override public synchronized void setCursorVisible(boolean visible) { this.cursorVisible = visible; } @Override public synchronized void putCharacter(char c) { if(c == '\n') { moveCursorToNextLine(); } else if(TerminalTextUtils.isPrintableCharacter(c)) { putCharacter(new TextCharacter(c, activeForegroundColor, activeBackgroundColor, activeModifiers)); } } @Override public synchronized void putString(String string) { for (TextCharacter textCharacter: TextCharacter.fromString(string, activeForegroundColor, activeBackgroundColor, activeModifiers)) { putCharacter(textCharacter); } } @Override public synchronized void enableSGR(SGR sgr) { activeModifiers.add(sgr); } @Override public synchronized void disableSGR(SGR sgr) { activeModifiers.remove(sgr); } @Override public synchronized void resetColorAndSGR() { this.activeModifiers.clear(); this.activeForegroundColor = TextColor.ANSI.DEFAULT; this.activeBackgroundColor = TextColor.ANSI.DEFAULT; } @Override public synchronized void setForegroundColor(TextColor color) { this.activeForegroundColor = color; } @Override public synchronized void setBackgroundColor(TextColor color) { this.activeBackgroundColor = color; } @Override public synchronized byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) { return getClass().getName().getBytes(); } @Override public synchronized void bell() { for(VirtualTerminalListener listener: listeners) { listener.onBell(); } } @Override public synchronized void flush() { for(VirtualTerminalListener listener: listeners) { listener.onFlush(); } } @Override public void close() { for(VirtualTerminalListener listener: listeners) { listener.onClose(); } } @Override public synchronized KeyStroke pollInput() { return inputQueue.poll(); } @Override public synchronized KeyStroke readInput() { try { return inputQueue.take(); } catch(InterruptedException e) { throw new RuntimeException("Unexpected interrupt", e); } } @Override public TextGraphics newTextGraphics() { return new VirtualTerminalTextGraphics(this); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // VirtualTerminal specific methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public synchronized void addVirtualTerminalListener(VirtualTerminalListener listener) { if(listener != null) { listeners.add(listener); } } @Override public synchronized void removeVirtualTerminalListener(VirtualTerminalListener listener) { listeners.remove(listener); } @Override public synchronized void setBacklogSize(int backlogSize) { this.backlogSize = backlogSize; } @Override public synchronized boolean isCursorVisible() { return cursorVisible; } @Override public void addInput(KeyStroke keyStroke) { inputQueue.add(keyStroke); } public synchronized TreeSet<TerminalPosition> getDirtyCells() { return new TreeSet<>(dirtyTerminalCells); } public synchronized TreeSet<TerminalPosition> getAndResetDirtyCells() { TreeSet<TerminalPosition> copy = new TreeSet<>(dirtyTerminalCells); dirtyTerminalCells.clear(); return copy; } public synchronized boolean isWholeBufferDirtyThenReset() { boolean copy = wholeBufferDirty; wholeBufferDirty = false; return copy; } @Override public synchronized TextCharacter getCharacter(TerminalPosition position) { return getCharacter(position.getColumn(), position.getRow()); } @Override public synchronized TextCharacter getCharacter(int column, int row) { if(terminalSize.getRows() < currentTextBuffer.getLineCount()) { row += currentTextBuffer.getLineCount() - terminalSize.getRows(); } return getBufferCharacter(column, row); } @Override public TextCharacter getBufferCharacter(int column, int row) { return currentTextBuffer.getCharacter(row, column); } @Override public TextCharacter getBufferCharacter(TerminalPosition position) { return getBufferCharacter(position.getColumn(), position.getRow()); } @Override public synchronized int getBufferLineCount() { return currentTextBuffer.getLineCount(); } @Override public synchronized void forEachLine(int startRow, int endRow, BufferWalker bufferWalker) { final BufferLine emptyLine = column -> TextCharacter.DEFAULT_CHARACTER; ListIterator<List<TextCharacter>> iterator = currentTextBuffer.getLinesFrom(startRow); for(int row = startRow; row <= endRow; row++) { BufferLine bufferLine = emptyLine; if(iterator.hasNext()) { final List<TextCharacter> list = iterator.next(); bufferLine = column -> { if(column >= list.size()) { return TextCharacter.DEFAULT_CHARACTER; } return list.get(column); }; } bufferWalker.onLine(row, bufferLine); } } synchronized void putCharacter(TextCharacter terminalCharacter) { if(terminalCharacter.is('\t')) { int nrOfSpaces = TabBehaviour.ALIGN_TO_COLUMN_4.getTabReplacement(cursorPosition.getColumn()).length(); for(int i = 0; i < nrOfSpaces && cursorPosition.getColumn() < terminalSize.getColumns() - 1; i++) { putCharacter(terminalCharacter.withCharacter(' ')); } } else { boolean doubleWidth = terminalCharacter.isDoubleWidth(); // If we're at the last column and the user tries to print a double-width character, reset the cell and move // to the next line if(cursorPosition.getColumn() == terminalSize.getColumns() - 1 && doubleWidth) { currentTextBuffer.setCharacter(cursorPosition.getRow(), cursorPosition.getColumn(), TextCharacter.DEFAULT_CHARACTER); moveCursorToNextLine(); } if(cursorPosition.getColumn() == terminalSize.getColumns()) { moveCursorToNextLine(); } // Update the buffer int i = currentTextBuffer.setCharacter(cursorPosition.getRow(), cursorPosition.getColumn(), terminalCharacter); if(!wholeBufferDirty) { dirtyTerminalCells.add(new TerminalPosition(cursorPosition.getColumn(), cursorPosition.getRow())); if(i == 1) { dirtyTerminalCells.add(new TerminalPosition(cursorPosition.getColumn() + 1, cursorPosition.getRow())); } else if(i == 2) { dirtyTerminalCells.add(new TerminalPosition(cursorPosition.getColumn() - 1, cursorPosition.getRow())); } if(dirtyTerminalCells.size() > (terminalSize.getColumns() * terminalSize.getRows() * 0.9)) { setWholeBufferDirty(); } } //Advance cursor cursorPosition = cursorPosition.withRelativeColumn(doubleWidth ? 2 : 1); if(cursorPosition.getColumn() > terminalSize.getColumns()) { moveCursorToNextLine(); } } }
Moves the text cursor to the first column of the next line and trims the backlog of necessary
/** * Moves the text cursor to the first column of the next line and trims the backlog of necessary */
private void moveCursorToNextLine() { cursorPosition = cursorPosition.withColumn(0).withRelativeRow(1); if(cursorPosition.getRow() >= currentTextBuffer.getLineCount()) { currentTextBuffer.newLine(); } trimBufferBacklog(); correctCursor(); }
Marks the whole buffer as dirty so every cell is considered in need to repainting. This is used by methods such as clear and bell that will affect all content at once.
/** * Marks the whole buffer as dirty so every cell is considered in need to repainting. This is used by methods such * as clear and bell that will affect all content at once. */
private void setWholeBufferDirty() { wholeBufferDirty = true; dirtyTerminalCells.clear(); } private void trimBufferBacklog() { // Now see if we need to discard lines from the backlog int bufferBacklogSize = backlogSize; if(currentTextBuffer == privateModeTextBuffer) { bufferBacklogSize = 0; } int trimBacklogRows = currentTextBuffer.getLineCount() - (bufferBacklogSize + terminalSize.getRows()); if(trimBacklogRows > 0) { currentTextBuffer.removeTopLines(trimBacklogRows); // Adjust cursor position cursorPosition = cursorPosition.withRelativeRow(-trimBacklogRows); correctCursor(); if(!wholeBufferDirty) { // Adjust all "dirty" positions TreeSet<TerminalPosition> newDirtySet = new TreeSet<>(); for(TerminalPosition dirtyPosition: dirtyTerminalCells) { TerminalPosition adjustedPosition = dirtyPosition.withRelativeRow(-trimBacklogRows); if(adjustedPosition.getRow() >= 0) { newDirtySet.add(adjustedPosition); } } dirtyTerminalCells.clear(); dirtyTerminalCells.addAll(newDirtySet); } } } private void correctCursor() { this.cursorPosition = cursorPosition.withColumn(Math.min(cursorPosition.getColumn(), terminalSize.getColumns() - 1)); this.cursorPosition = cursorPosition.withRow(Math.min(cursorPosition.getRow(), Math.max(terminalSize.getRows(), getBufferLineCount()) - 1)); this.cursorPosition = new TerminalPosition( Math.max(cursorPosition.getColumn(), 0), Math.max(cursorPosition.getRow(), 0)); } @Override public String toString() { return currentTextBuffer.toString(); } }