/*
 * 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;

import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.graphics.AbstractTextGraphics;
import com.googlecode.lanterna.TextCharacter;
import com.googlecode.lanterna.graphics.TextGraphics;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

This is the terminal's implementation of TextGraphics. Upon creation it takes a snapshot for the terminal's size, so that it won't require to do an expensive lookup on every call to getSize(), but this also means that it can go stale quickly if the terminal is resized. You should try to use the object quickly and then let it be GC:ed. It will not pick up on terminal resize! Also, the state of the Terminal after an operation performed by this TextGraphics implementation is undefined and you should probably re-initialize colors and modifiers.

Any write operation that results in an IOException will be wrapped by a RuntimeException since the TextGraphics interface doesn't allow throwing IOException
/** * This is the terminal's implementation of TextGraphics. Upon creation it takes a snapshot for the terminal's size, so * that it won't require to do an expensive lookup on every call to {@code getSize()}, but this also means that it can * go stale quickly if the terminal is resized. You should try to use the object quickly and then let it be GC:ed. It * will not pick up on terminal resize! Also, the state of the Terminal after an operation performed by this * TextGraphics implementation is undefined and you should probably re-initialize colors and modifiers. * <p/> * Any write operation that results in an IOException will be wrapped by a RuntimeException since the TextGraphics * interface doesn't allow throwing IOException */
class TerminalTextGraphics extends AbstractTextGraphics { private final Terminal terminal; private final TerminalSize terminalSize; private final Map<TerminalPosition, TextCharacter> writeHistory; private AtomicInteger manageCallStackSize; private TextCharacter lastCharacter; private TerminalPosition lastPosition; TerminalTextGraphics(Terminal terminal) throws IOException { this.terminal = terminal; this.terminalSize = terminal.getTerminalSize(); this.manageCallStackSize = new AtomicInteger(0); this.writeHistory = new HashMap<>(); this.lastCharacter = null; this.lastPosition = null; } @Override public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { return setCharacter(new TerminalPosition(columnIndex, rowIndex), textCharacter); } @Override public synchronized TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) { try { if(manageCallStackSize.get() > 0) { if(lastCharacter == null || !lastCharacter.equals(textCharacter)) { applyGraphicState(textCharacter); lastCharacter = textCharacter; } if(lastPosition == null || !lastPosition.equals(position)) { terminal.setCursorPosition(position.getColumn(), position.getRow()); lastPosition = position; } } else { terminal.setCursorPosition(position.getColumn(), position.getRow()); applyGraphicState(textCharacter); } terminal.putString(textCharacter.getCharacterString()); if(manageCallStackSize.get() > 0) { lastPosition = position.withRelativeColumn(1); } writeHistory.put(position, textCharacter); } catch(IOException e) { throw new RuntimeException(e); } return this; } @Override public TextCharacter getCharacter(int column, int row) { return getCharacter(new TerminalPosition(column, row)); } @Override public synchronized TextCharacter getCharacter(TerminalPosition position) { return writeHistory.get(position); } private void applyGraphicState(TextCharacter textCharacter) throws IOException { terminal.resetColorAndSGR(); terminal.setForegroundColor(textCharacter.getForegroundColor()); terminal.setBackgroundColor(textCharacter.getBackgroundColor()); for(SGR sgr: textCharacter.getModifiers()) { terminal.enableSGR(sgr); } } @Override public TerminalSize getSize() { return terminalSize; } @Override public synchronized TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) { try { enterAtomic(); super.drawLine(fromPoint, toPoint, character); return this; } finally { leaveAtomic(); } } @Override public synchronized TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { try { enterAtomic(); super.drawTriangle(p1, p2, p3, character); return this; } finally { leaveAtomic(); } } @Override public synchronized TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { try { enterAtomic(); super.fillTriangle(p1, p2, p3, character); return this; } finally { leaveAtomic(); } } @Override public synchronized TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { try { enterAtomic(); super.fillRectangle(topLeft, size, character); return this; } finally { leaveAtomic(); } } @Override public synchronized TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { try { enterAtomic(); super.drawRectangle(topLeft, size, character); return this; } finally { leaveAtomic(); } } @Override public synchronized TextGraphics putString(int column, int row, String string) { try { enterAtomic(); return super.putString(column, row, string); } finally { leaveAtomic(); } }
It's tricky with this implementation because we can't rely on any state in between two calls to setCharacter since the caller might modify the terminal's state outside of this writer. However, many calls inside TextGraphics will indeed make multiple calls in setCharacter where we know that the state won't change (actually, we can't be 100% sure since the caller might create a separate thread and maliciously write directly to the terminal while call one of the draw/fill/put methods in here). We could just set the state before writing every single character but that would be inefficient. Rather, we keep a counter of if we are inside an 'atomic' (meaning we know multiple calls to setCharacter will have the same state). Some drawing methods call other drawing methods internally for their implementation so that's why this is implemented with an integer value instead of a boolean; when the counter reaches zero we remove the memory of what state the terminal is in.
/** * It's tricky with this implementation because we can't rely on any state in between two calls to setCharacter * since the caller might modify the terminal's state outside of this writer. However, many calls inside * TextGraphics will indeed make multiple calls in setCharacter where we know that the state won't change (actually, * we can't be 100% sure since the caller might create a separate thread and maliciously write directly to the * terminal while call one of the draw/fill/put methods in here). We could just set the state before writing every * single character but that would be inefficient. Rather, we keep a counter of if we are inside an 'atomic' * (meaning we know multiple calls to setCharacter will have the same state). Some drawing methods call other * drawing methods internally for their implementation so that's why this is implemented with an integer value * instead of a boolean; when the counter reaches zero we remove the memory of what state the terminal is in. */
private void enterAtomic() { manageCallStackSize.incrementAndGet(); } private void leaveAtomic() { if(manageCallStackSize.decrementAndGet() == 0) { lastPosition = null; lastCharacter = null; } } }