com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lanterna Show documentation
Show all versions of lanterna Show documentation
Java library for creating text-based terminal GUIs
/*
* 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 .
*
* 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 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 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 = column -> 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 = 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
*/
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));
}
@Override
public String toString() {
return currentTextBuffer.toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy