![JAR search and dependency download from the Maven repository](/logo.png)
com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal Maven / Gradle / Ivy
/*
* 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 .
*
* 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 dirtyTerminalCells;
private final List listeners;
private TextBuffer currentTextBuffer;
private boolean wholeBufferDirty;
private TerminalSize terminalSize;
private boolean cursorVisible;
private int backlogSize;
private final BlockingQueue inputQueue;
private final EnumSet 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
*/
public DefaultVirtualTerminal() {
this(new TerminalSize(80, 24));
}
/**
* 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 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 getDirtyCells() {
return new TreeSet(dirtyTerminalCells);
}
public synchronized TreeSet getAndResetDirtyCells() {
TreeSet 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 = new BufferLine() {
@Override
public TextCharacter getCharacterAt(int column) {
return TextCharacter.DEFAULT_CHARACTER;
}
};
ListIterator> iterator = currentTextBuffer.getLinesFrom(startRow);
for(int row = startRow; row <= endRow; row++) {
BufferLine bufferLine = emptyLine;
if(iterator.hasNext()) {
final List list = iterator.next();
bufferLine = new BufferLine() {
@Override
public TextCharacter getCharacterAt(int column) {
if(column >= list.size()) {
return TextCharacter.DEFAULT_CHARACTER;
}
return list.get(column);
}
};
}
bufferWalker.onLine(row, bufferLine);
}
}
synchronized void putCharacter(TextCharacter terminalCharacter) {
if(terminalCharacter.getCharacter() == '\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 = TerminalTextUtils.isCharDoubleWidth(terminalCharacter.getCharacter());
// 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
*/
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.
*/
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 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));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy