package com.jediterm.terminal.display;
import com.google.common.collect.Maps;
import com.jediterm.terminal.*;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Buffer for storing styled text data.
* Stores only text that fit into one screen XxY, but has scrollBuffer to save history lines and textBuffer to restore
* screen after resize. ScrollBuffer stores all lines before the first line currently shown on the screen. TextBuffer
* stores lines that are shown currently on the screen and they have there(in TextBuffer) their initial length (even if
* it doesn't fit to screen width).
* <p/>
* Also handles screen damage (TODO: write about it).
*/
public class BackBuffer implements StyledTextConsumer {
private static final Logger LOG = Logger.getLogger(BackBuffer.class);
private static final char EMPTY_CHAR = ' '; // (char) 0x0;
private char[] myBuf;
private TextStyle[] myStyleBuf;
private BitSet myDamage;
@NotNull
private final StyleState myStyleState;
private LinesBuffer myScrollBuffer = new LinesBuffer();
private LinesBuffer myTextBuffer = new LinesBuffer();
private int myWidth;
private int myHeight;
private final Lock myLock = new ReentrantLock();
private LinesBuffer myTextBufferBackup; // to store textBuffer after switching to alternate buffer
private LinesBuffer myScrollBufferBackup;
private boolean myAlternateBuffer = false;
private boolean myUsingAlternateBuffer = false;
public BackBuffer(final int width, final int height, @NotNull StyleState styleState) {
myStyleState = styleState;
myWidth = width;
myHeight = height;
allocateBuffers();
}
private void allocateBuffers() {
myBuf = new char[myWidth * myHeight];
Arrays.fill(myBuf, EMPTY_CHAR);
myStyleBuf = new TextStyle[myWidth * myHeight];
Arrays.fill(myStyleBuf, TextStyle.EMPTY);
myDamage = new BitSet(myWidth * myHeight);
}
public Dimension resize(@NotNull final Dimension pendingResize,
@NotNull final RequestOrigin origin,
final int cursorY,
@NotNull JediTerminal.ResizeHandler resizeHandler,
@Nullable TerminalSelection mySelection) {
final char[] oldBuf = myBuf;
final TextStyle[] oldStyleBuf = myStyleBuf;
final int oldHeight = myHeight;
final int oldWidth = myWidth;
final int newWidth = pendingResize.width;
final int newHeight = pendingResize.height;
final int scrollLinesCountOld = myScrollBuffer.getLineCount();
final int textLinesCountOld = myTextBuffer.getLineCount();
boolean textBufferUpdated = false;
if (newHeight < cursorY) {
//we need to move lines from text buffer to the scroll buffer
int count = cursorY - newHeight;
if (!myAlternateBuffer) {
myTextBuffer.moveTopLinesTo(count, myScrollBuffer);
}
if (mySelection != null) {
mySelection.shiftY(-count);
}
}
else if (newHeight > cursorY && myScrollBuffer.getLineCount() > 0) {
//we need to move lines from scroll buffer to the text buffer
if (!myAlternateBuffer) {
myScrollBuffer.moveBottomLinesTo(newHeight - cursorY, myTextBuffer);
textBufferUpdated = true;
}
if (mySelection != null) {
mySelection.shiftY(newHeight - cursorY);
}
}
myWidth = newWidth;
myHeight = newHeight;
allocateBuffers();
final int copyWidth = Math.min(oldWidth, myWidth);
final int copyHeight = Math.min(oldHeight, myHeight);
final int oldStart;
final int start;
if (myHeight > oldHeight) {
oldStart = 0;
start = Math.min(myHeight - copyHeight, scrollLinesCountOld);
}
else {
oldStart = Math.max(0, cursorY - myHeight);
start = 0;
}
// copying lines...
for (int i = 0; i < copyHeight; i++) {
System.arraycopy(oldBuf, (oldStart + i) * oldWidth, myBuf, (start + i) * myWidth, copyWidth);
System.arraycopy(oldStyleBuf, (oldStart + i) * oldWidth, myStyleBuf, (start + i) * myWidth, copyWidth);
}
if (!myAlternateBuffer && (myWidth > oldWidth || textBufferUpdated)) {
//we need to fill new space with data from the text buffer
resetFromTextBuffer();
}
if (myTextBuffer.getLineCount() >= myHeight) {
myTextBuffer.moveTopLinesTo(myTextBuffer.getLineCount() - myHeight, myScrollBuffer);
}
int myCursorY = cursorY + (myTextBuffer.getLineCount() - textLinesCountOld);
myDamage.set(0, myWidth * myHeight - 1, true);
resizeHandler.sizeUpdated(myWidth, myHeight, myCursorY);
return pendingResize;
}
private void resetFromTextBuffer() {
clearArea();
myTextBuffer.processLines(0, getTextBufferLinesCount(), this, 0);
}
private void clearArea() {
clearArea(0, 0, myWidth, myHeight);
}
private void clearArea(final int leftX, final int topY, final int rightX, final int bottomY) {
clearArea(leftX, topY, rightX, bottomY, createEmptyStyleWithCurrentColor());
}
private void clearArea(final int leftX, final int topY, final int rightX, final int bottomY, @NotNull TextStyle textStyle) {
if (topY > bottomY) {
LOG.error("Attempt to clear upside down area: top:" + topY + " > bottom:" + bottomY);
return;
}
for (int y = topY; y < bottomY; y++) {
if (y > myHeight - 1 || y < 0) {
LOG.error("attempt to clear line " + y + "\n" +
"args were x1:" + leftX + " y1:" + topY + " x2:"
+ rightX + " y2:" + bottomY);
}
else if (leftX > rightX) {
LOG.error("Attempt to clear backwards area: left:" + leftX + " > right:" + rightX);
}
else {
Arrays.fill(myBuf,
y * myWidth + leftX,
y * myWidth + rightX,
EMPTY_CHAR);
Arrays.fill(myStyleBuf,
y * myWidth + leftX,
y * myWidth + rightX,
textStyle
);
myDamage.set(y * myWidth + leftX,
y * myWidth + rightX,
true);
}
}
}
private TextStyle createEmptyStyleWithCurrentColor() {
return myStyleState.getCurrent().createEmptyWithColors();
}
public void deleteCharacters(final int x, final int y, final int count) {
if (y > myHeight - 1 || y < 0) {
LOG.error("attempt to delete in line " + y + "\n" +
"args were x:" + x + " count:" + count);
}
else if (count < 0) {
LOG.error("Attempt to delete negative chars number: count:" + count);
}
else if (count == 0) { //nothing to do
return;
}
else {
int to = y * myWidth + x;
int from = to + count;
int remain = myWidth - x - count;
LOG.debug("About to delete " + count + " chars on line " + y + ", starting from " + x +
" (from : " + from + " to : " + to + " remain : " + remain + ")");
System.arraycopy(myBuf, from, myBuf, to, remain);
Arrays.fill(myBuf, to + remain, (y + 1) * myWidth, EMPTY_CHAR);
System.arraycopy(myStyleBuf, from, myStyleBuf, to, remain);
Arrays.fill(myStyleBuf, to + remain, (y + 1) * myWidth, createEmptyStyleWithCurrentColor());
myTextBuffer.deleteCharacters(x, y, count);
myDamage.set(to, (y + 1) * myWidth, true);
}
}
public void insertBlankCharacters(final int x, final int y, final int count) {
if (y > myHeight - 1 || y < 0) {
LOG.error("attempt to insert blank chars in line " + y + "\n" +
"args were x:" + x + " count:" + count);
}
else if (count < 0) {
LOG.error("Attempt to insert negative blank chars number: count:" + count);
}
else if (count == 0) { //nothing to do
return;
}
else {
int from = y * myWidth + x;
int to = from + count;
int remain = myWidth - x - count;
LOG.debug("About to insert " + count + " blank chars on line " + y + ", starting from " + x +
" (from : " + from + " to : " + to + " remain : " + remain + ")");
System.arraycopy(myBuf, from, myBuf, to, remain);
Arrays.fill(myBuf, from, to, EMPTY_CHAR);
System.arraycopy(myStyleBuf, from, myStyleBuf, to, remain);
Arrays.fill(myStyleBuf, from, to, createEmptyStyleWithCurrentColor());
myTextBuffer.insertBlankCharacters(x, y, count, myWidth);
myDamage.set(from, (y + 1) * myWidth, true);
}
}
public void writeBytes(final int x, final int y, final char[] bytes, final int start, final int len) {
final int adjY = y - 1;
if (adjY >= myHeight || adjY < 0) {
if (LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder("Attempt to draw line ")
.append(adjY).append(" at (").append(x).append(",")
.append(y).append(")");
CharacterUtils.appendBuf(sb, bytes, start, len);
LOG.debug(sb);
}
return;
}
TextStyle style = myStyleState.getCurrent();
for (int i = 0; i < len; i++) {
final int location = adjY * myWidth + x + i;
myBuf[location] = bytes[start + i]; // Arraycopy does not convert
myStyleBuf[location] = style;
}
myTextBuffer.writeString(x, adjY, new String(bytes, start, len), style); //TODO: make write bytes method
myDamage.set(adjY * myWidth + x, adjY * myWidth + x + len);
}
public void writeString(final int x, final int y, @NotNull final String str) {
writeString(x, y, str, myStyleState.getCurrent());
}
private void writeString(int x, int y, @NotNull String str, @NotNull TextStyle style) {
if (writeToBackBuffer(x, y, str, style)) return;
myTextBuffer.writeString(x, y - 1, str, style);
}
private boolean writeToBackBuffer(int x, int y, @NotNull String str, @NotNull TextStyle style) {
final int adjY = y - 1;
if (adjY >= myHeight || adjY < 0) {
LOG.debug("Attempt to draw line out of bounds: " + adjY + " at (" + x + "," + y + ")");
return true;
}
str.getChars(0, str.length(), myBuf, adjY * myWidth + x);
for (int i = 0; i < str.length(); i++) {
final int location = adjY * myWidth + x + i;
myStyleBuf[location] = style;
}
myDamage.set(adjY * myWidth + x, adjY * myWidth + x + str.length());
return false;
}
public void scrollArea(final int scrollRegionTop, final int dy, int scrollRegionBottom) {
if (dy == 0) {
return;
}
if (dy > 0) {
insertLines(scrollRegionTop - 1, dy, scrollRegionBottom);
}
else {
LinesBuffer removed = deleteLines(scrollRegionTop - 1, -dy, scrollRegionBottom);
if (scrollRegionTop == 1) {
removed.moveTopLinesTo(removed.getLineCount(), myScrollBuffer);
}
}
}
private void moveLinesUp(int y, int dy, int lastLine) {
if (dy >= 0) {
LOG.error("dy should be negative");
}
for (int line = y; line < lastLine; line++) {
if (line >= myHeight) {
// this is not necessary an error; simply skip it
LOG.debug("Attempt to scroll line from below bottom of screen: " + line);
continue;
}
if (line + dy < 0) {
// this is not necessary an error; simply skip it
LOG.debug("Attempt to scroll to line off top of screen: " + (line + dy));
continue;
}
System.arraycopy(myBuf, line * myWidth, myBuf, (line + dy) * myWidth, myWidth);
System.arraycopy(myStyleBuf, line * myWidth, myStyleBuf, (line + dy) * myWidth, myWidth);
myDamage.set((line + dy) * myWidth, (line + dy + 1) * myWidth);
}
}
private void moveLinesDown(int y, int dy, int lastLine) {
if (dy <= 0) {
LOG.error("dy should be positive");
}
for (int line = lastLine - dy; line >= y; line--) {
if (line < 0) {
// this is not necessary an error; simply skip it
LOG.debug("Attempt to scroll line from above top of screen: " + line);
continue;
}
if (line + dy + 1 > myHeight) {
// this is not necessary an error; simply skip it
LOG.debug("Attempt to scroll line off bottom of screen: " + (line + dy));
continue;
}
System.arraycopy(myBuf, line * myWidth, myBuf, (line + dy) * myWidth, myWidth);
System.arraycopy(myStyleBuf, line * myWidth, myStyleBuf, (line + dy) * myWidth, myWidth);
myDamage.set((line + dy) * myWidth, (line + dy + 1) * myWidth);
}
}
public String getStyleLines() {
int count = 0;
Map<Integer, Integer> hashMap = Maps.newHashMap();
myLock.lock();
try {
final StringBuilder sb = new StringBuilder();
for (int row = 0; row < myHeight; row++) {
for (int col = 0; col < myWidth; col++) {
final TextStyle style = myStyleBuf[row * myWidth + col];
int styleNum = style == null ? 0 : style.getId();
if (!hashMap.containsKey(styleNum)) {
hashMap.put(styleNum, count++);
}
sb.append(String.format("%02d ", hashMap.get(styleNum)));
}
sb.append("\n");
}
return sb.toString();
}
finally {
myLock.unlock();
}
}
public TerminalLine getLine(int index) {
if (index >= 0) {
if (index >= getHeight()) {
LOG.error("Attempt to get line out of bounds: " + index + " >= " + getHeight());
return TerminalLine.createEmpty();
}
return myTextBuffer.getLine(index);
}
else {
if (index < - myScrollBuffer.getLineCount()) {
LOG.error("Attempt to get line out of bounds: " + index + " < " + -myScrollBuffer.getLineCount());
return TerminalLine.createEmpty();
}
return myScrollBuffer.getLine(getScrollBufferLinesCount() + index);
}
}
public String getLines() {
myLock.lock();
try {
final StringBuilder sb = new StringBuilder();
for (int row = 0; row < myHeight; row++) {
sb.append(myBuf, row * myWidth, myWidth);
sb.append('\n');
}
return sb.toString();
}
finally {
myLock.unlock();
}
}
public String getDamageLines() {
myLock.lock();
try {
final StringBuilder sb = new StringBuilder();
for (int row = 0; row < myHeight; row++) {
for (int col = 0; col < myWidth; col++) {
boolean isDamaged = myDamage.get(row * myWidth + col);
sb.append(isDamaged ? 'X' : '-');
}
sb.append("\n");
}
return sb.toString();
}
finally {
myLock.unlock();
}
}
public void resetDamage() {
myLock.lock();
try {
myDamage.clear();
}
finally {
myLock.unlock();
}
}
public void processTextBufferLines(final int yStart, final int yCount, @NotNull final StyledTextConsumer consumer, int startRow) {
myTextBuffer.processLines(yStart - startRow, Math.min(yCount, myTextBuffer.getLineCount()), consumer, startRow);
}
public void processTextBufferLines(final int yStart, final int yCount, @NotNull final StyledTextConsumer consumer) {
myTextBuffer.processLines(yStart - getTextBufferLinesCount(), Math.min(yCount, myTextBuffer.getLineCount()), consumer);
}
public void processBufferRows(final int startRow, final int height, final StyledTextConsumer consumer) {
processBufferCells(0, startRow, myWidth, height, consumer);
}
public void processBufferCells(final int startCol,
final int startRow,
final int width,
final int height,
final StyledTextConsumer consumer) {
final int endRow = startRow + height;
final int endCol = startCol + width;
myLock.lock();
try {
for (int row = startRow; row < endRow; row++) {
processBufferRow(row, startCol, endCol, consumer);
}
}
finally {
myLock.unlock();
}
}
public void processBufferRow(int row, StyledTextConsumer consumer) {
processBufferRow(row, 0, myWidth, consumer);
}
public void processBufferRow(int row, int startCol, int endCol, StyledTextConsumer consumer) {
TextStyle lastStyle = null;
int beginRun = startCol;
for (int col = startCol; col < endCol; col++) {
final int location = row * myWidth + col;
if (location < 0 || location >= myStyleBuf.length) {
throw new IllegalStateException("Can't pump a char at " + row + "x" + col);
}
final TextStyle cellStyle = myStyleBuf[location];
if (lastStyle == null) {
//begin line
lastStyle = cellStyle;
}
else if (!cellStyle.equals(lastStyle)) {
//start of new run
consumer.consume(beginRun, row, lastStyle, new CharBuffer(myBuf, row * myWidth + beginRun, col - beginRun), 0);
beginRun = col;
lastStyle = cellStyle;
}
}
//end row
if (endCol == startCol) { // no run occurred : retrieve text style
final int location = row * myWidth + startCol;
if (location < 0 || location >= myStyleBuf.length) {
throw new IllegalStateException("Can't pump a char at " + row + "x" + startCol);
}
lastStyle = myStyleBuf[location];
}
if (lastStyle == null) {
LOG.error("Style is null for run supposed to be from " + beginRun + " to " + endCol + " on row " + row);
}
else {
consumer.consume(beginRun, row, lastStyle, new CharBuffer(myBuf, row * myWidth + beginRun, endCol - beginRun), 0);
}
}
/**
* Cell is a styled block of text
*
* @param consumer
*/
public void processDamagedCells(final StyledTextConsumer consumer) {
final int startRow = 0;
final int endRow = myHeight;
final int startCol = 0;
final int endCol = myWidth;
myLock.lock();
try {
for (int row = startRow; row < endRow; row++) {
TextStyle lastStyle = null;
int beginRun = startCol;
for (int col = startCol; col < endCol; col++) {
final int location = row * myWidth + col;
if (location < 0 || location > myStyleBuf.length) {
LOG.error("Requested out of bounds runs: pumpFromDamage");
continue;
}
final TextStyle cellStyle = myStyleBuf[location];
final boolean isDamaged = myDamage.get(location);
if (!isDamaged) {
if (lastStyle != null) { //flush previous run
flushStyledText(consumer, row, lastStyle, beginRun, col);
}
lastStyle = null;
}
else {
if (lastStyle == null) {
//begin a new run
beginRun = col;
lastStyle = cellStyle;
}
else if (!cellStyle.equals(lastStyle)) {
//flush prev run and start of a new one
flushStyledText(consumer, row, lastStyle, beginRun, col);
beginRun = col;
lastStyle = cellStyle;
}
}
}
//flush the last run
if (lastStyle != null) {
flushStyledText(consumer, row, lastStyle, beginRun, endCol);
}
}
}
finally {
myLock.unlock();
}
}
private void flushStyledText(StyledTextConsumer consumer, int row, TextStyle lastStyle, int beginRun, int col) {
consumer.consume(beginRun, row, lastStyle, new CharBuffer(myBuf, row * myWidth + beginRun, col - beginRun), 0);
}
public boolean hasDamage() {
return myDamage.nextSetBit(0) != -1;
}
public void lock() {
myLock.lock();
}
public void unlock() {
myLock.unlock();
}
public boolean tryLock() {
return myLock.tryLock();
}
private String getLineTrimTrailing(int row) {
StringBuilder sb = new StringBuilder();
sb.append(myBuf, row * myWidth, myWidth);
return Util.trimTrailing(sb.toString());
}
@Override
public void consume(int x, int y, @NotNull TextStyle style, @NotNull CharBuffer characters, int startRow) {
int len = Math.min(myWidth - x, characters.getLength());
if (len > 0) {
writeToBackBuffer(x, y - startRow + 1, new String(characters.getBuf(), characters.getStart(), len), style);
}
}
public int getWidth() {
return myWidth;
}
public int getHeight() {
return myHeight;
}
public int getScrollBufferLinesCount() {
return myScrollBuffer.getLineCount();
}
public int getTextBufferLinesCount() {
return myTextBuffer.getLineCount();
}
public String getTextBufferLines() {
return myTextBuffer.getLines();
}
public boolean checkTextBufferIsValid(int row) {
return myTextBuffer.getLineText(row).startsWith(getLineTrimTrailing(row));//in a row back buffer is always a prefix of text buffer
}
public char getCharAt(int x, int y) {
return myBuf[x + myWidth * y];
}
public char getBuffersCharAt(int x, int y) {
String lineText = getLine(y).getText();
return x < lineText.length() ? lineText.charAt(x) : EMPTY_CHAR;
}
public TextStyle getStyleAt(int x, int y) {
return myStyleBuf[x + myWidth * y];
}
public void useAlternateBuffer(boolean enabled) {
myAlternateBuffer = enabled;
if (enabled) {
if (!myUsingAlternateBuffer) {
myTextBufferBackup = myTextBuffer;
myScrollBufferBackup = myScrollBuffer;
myTextBuffer = new LinesBuffer();
myScrollBuffer = new LinesBuffer();
clearArea();
myUsingAlternateBuffer = true;
}
}
else {
if (myUsingAlternateBuffer) {
myTextBuffer = myTextBufferBackup;
myScrollBuffer = myScrollBufferBackup;
resetFromTextBuffer();
myUsingAlternateBuffer = false;
}
}
}
public LinesBuffer getScrollBuffer() {
return myScrollBuffer;
}
public void insertLines(int y, int count, int scrollRegionBottom) {
moveLinesDown(y, count, scrollRegionBottom - 1);
clearArea(0, y, myWidth, Math.min(y + count, scrollRegionBottom));
myTextBuffer.insertLines(y, count, scrollRegionBottom - 1);
}
// returns deleted lines
public LinesBuffer deleteLines(int y, int count, int scrollRegionBottom) {
moveLinesUp(y + count, -count, scrollRegionBottom);
clearArea(0, Math.max(y, scrollRegionBottom - count), myWidth, scrollRegionBottom);
return myTextBuffer.deleteLines(y, count, scrollRegionBottom - 1);
}
public void clearLines(int startRow, int endRow) {
TextStyle style = createEmptyStyleWithCurrentColor();
myTextBuffer.clearLines(startRow, endRow);
clearArea(0, startRow, myWidth, endRow, style);
}
public void eraseCharacters(int leftX, int rightX, int y) {
TextStyle style = createEmptyStyleWithCurrentColor();
if (y >= 0) {
clearArea(leftX, y, rightX, y + 1, style);
myTextBuffer.clearArea(leftX, y, rightX, y + 1, style);
}
else {
LOG.error("Attempt to erase characters in line: " + y);
}
}
public void clearAll() {
clearArea();
myScrollBuffer.clearAll();
}
}