package com.termux.terminal; import java.util.Arrays; /** * A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll * history. *

* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices. */ public final class TerminalBuffer { TerminalRow[] mLines; /** The length of {@link #mLines}. */ int mTotalRows; /** The number of rows and columns visible on the screen. */ int mScreenRows, mColumns; /** The number of rows kept in history. */ private int mActiveTranscriptRows = 0; /** The index in the circular buffer where the visible screen starts. */ private int mScreenFirstRow = 0; /** * Create a transcript screen. * * @param columns the width of the screen in characters. * @param totalRows the height of the entire text area, in rows of text. * @param screenRows the height of just the screen, not including the transcript that holds lines that have scrolled off * the top of the screen. */ public TerminalBuffer(int columns, int totalRows, int screenRows) { mColumns = columns; mTotalRows = totalRows; mScreenRows = screenRows; mLines = new TerminalRow[totalRows]; blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL); } public String getTranscriptText() { return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim(); } public String getTranscriptTextWithoutJoinedLines() { return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim(); } public String getTranscriptTextWithFullLinesJoined() { return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, true, true).trim(); } public String getSelectedText(int selX1, int selY1, int selX2, int selY2) { return getSelectedText(selX1, selY1, selX2, selY2, true); } public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) { return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, false); } public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) { final StringBuilder builder = new StringBuilder(); final int columns = mColumns; if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows(); if (selY2 >= mScreenRows) selY2 = mScreenRows - 1; for (int row = selY1; row <= selY2; row++) { int x1 = (row == selY1) ? selX1 : 0; int x2; if (row == selY2) { x2 = selX2 + 1; if (x2 > columns) x2 = columns; } else { x2 = columns; } TerminalRow lineObject = mLines[externalToInternalRow(row)]; int x1Index = lineObject.findStartOfColumn(x1); int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); if (x2Index == x1Index) { // Selected the start of a wide character. x2Index = lineObject.findStartOfColumn(x2 + 1); } char[] line = lineObject.mText; int lastPrintingCharIndex = -1; int i; boolean rowLineWrap = getLineWrap(row); if (rowLineWrap && x2 == columns) { // If the line was wrapped, we shouldn't lose trailing space: lastPrintingCharIndex = x2Index - 1; } else { for (i = x1Index; i < x2Index; ++i) { char c = line[i]; if (c != ' ') lastPrintingCharIndex = i; } } int len = lastPrintingCharIndex - x1Index + 1; if (lastPrintingCharIndex != -1 && len > 0) builder.append(line, x1Index, len); boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1; if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth) && row < selY2 && row < mScreenRows - 1) builder.append('\n'); } return builder.toString(); } public String getWordAtLocation(int x, int y) { // Set y1 and y2 to the lines where the wrapped line starts and ends. // I.e. if a line that is wrapped to 3 lines starts at line 4, and this // is called with y=5, then y1 would be set to 4 and y2 would be set to 6. int y1 = y; int y2 = y; while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) { y1--; } while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) { y2++; } // Get the text for the whole wrapped line String text = getSelectedText(0, y1, mColumns, y2, true, true); // The index of x in text int textOffset = (y - y1) * mColumns + x; if (textOffset >= text.length()) { // The click was to the right of the last word on the line, so // there's no word to return return ""; } // Set x1 and x2 to the indices of the last space before x and the // first space after x in text respectively int x1 = text.lastIndexOf(' ', textOffset); int x2 = text.indexOf(' ', textOffset); if (x2 == -1) { x2 = text.length(); } if (x1 == x2) { // The click was on a space, so there's no word to return return ""; } return text.substring(x1 + 1, x2); } public int getActiveTranscriptRows() { return mActiveTranscriptRows; } public int getActiveRows() { return mActiveTranscriptRows + mScreenRows; } /** * Convert a row value from the public external coordinate system to our internal private coordinate system. * *

     * - External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
     * - Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
     *   mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
     *
     * External ↔ Internal:
     *
     * [ ...                            ]     [ ...                                     ]
     * [ -mActiveTranscriptRows         ]     [ mScreenFirstRow - mActiveTranscriptRows ]
     * [ ...                            ]     [ ...                                     ]
     * [ 0 (visible screen starts here) ]  ↔  [ mScreenFirstRow                         ]
     * [ ...                            ]     [ ...                                     ]
     * [ mScreenRows-1                  ]     [ mScreenFirstRow + mScreenRows-1         ]
     * 
* * @param externalRow a row in the external coordinate system. * @return The row corresponding to the input argument in the private coordinate system. */ public int externalToInternalRow(int externalRow) { if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows) throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows); final int internalRow = mScreenFirstRow + externalRow; return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows); } public void setLineWrap(int row) { mLines[externalToInternalRow(row)].mLineWrap = true; } public boolean getLineWrap(int row) { return mLines[externalToInternalRow(row)].mLineWrap; } public void clearLineWrap(int row) { mLines[externalToInternalRow(row)].mLineWrap = false; } /** * Resize the screen which this transcript backs. Currently, this only works if the number of columns does not * change or the rows expand (that is, it only works when shrinking the number of rows). * * @param newColumns The number of columns the screen should have. * @param newRows The number of rows the screen should have. * @param cursor An int[2] containing the (column, row) cursor location. */ public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) { // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000): if (newColumns == mColumns && newRows <= mTotalRows) { // Fast resize where just the rows changed. int shiftDownOfTopRow = mScreenRows - newRows; if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) { // Shrinking. Check if we can skip blank rows at bottom below cursor. for (int i = mScreenRows - 1; i > 0; i--) { if (cursor[1] >= i) break; int r = externalToInternalRow(i); if (mLines[r] == null || mLines[r].isBlank()) { if (--shiftDownOfTopRow == 0) break; } } } else if (shiftDownOfTopRow < 0) { // Negative shift down = expanding. Only move screen up if there is transcript to show: int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows); if (shiftDownOfTopRow != actualShift) { // The new lines revealed by the resizing are not all from the transcript. Blank the below ones. for (int i = 0; i < actualShift - shiftDownOfTopRow; i++) allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle); shiftDownOfTopRow = actualShift; } } mScreenFirstRow += shiftDownOfTopRow; mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows); mTotalRows = newTotalRows; mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow); cursor[1] -= shiftDownOfTopRow; mScreenRows = newRows; } else { // Copy away old state and update new: TerminalRow[] oldLines = mLines; mLines = new TerminalRow[newTotalRows]; for (int i = 0; i < newTotalRows; i++) mLines[i] = new TerminalRow(newColumns, currentStyle); final int oldActiveTranscriptRows = mActiveTranscriptRows; final int oldScreenFirstRow = mScreenFirstRow; final int oldScreenRows = mScreenRows; final int oldTotalRows = mTotalRows; mTotalRows = newTotalRows; mScreenRows = newRows; mActiveTranscriptRows = mScreenFirstRow = 0; mColumns = newColumns; int newCursorRow = -1; int newCursorColumn = -1; int oldCursorRow = cursor[1]; int oldCursorColumn = cursor[0]; boolean newCursorPlaced = false; int currentOutputExternalRow = 0; int currentOutputExternalColumn = 0; // Loop over every character in the initial state. // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we // keep track how many blank lines we have skipped if we later on find a non-blank line. int skippedBlankLines = 0; for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) { // Do what externalToInternalRow() does but for the old state: int internalOldRow = oldScreenFirstRow + externalOldRow; internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows); TerminalRow oldLine = oldLines[internalOldRow]; boolean cursorAtThisRow = externalOldRow == oldCursorRow; // The cursor may only be on a non-null line, which we should not skip: if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) { skippedBlankLines++; continue; } else if (skippedBlankLines > 0) { // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines. for (int i = 0; i < skippedBlankLines; i++) { if (currentOutputExternalRow == mScreenRows - 1) { scrollDownOneLine(0, mScreenRows, currentStyle); } else { currentOutputExternalRow++; } currentOutputExternalColumn = 0; } skippedBlankLines = 0; } int lastNonSpaceIndex = 0; boolean justToCursor = false; if (cursorAtThisRow || oldLine.mLineWrap) { // Take the whole line, either because of cursor on it, or if line wrapping. lastNonSpaceIndex = oldLine.getSpaceUsed(); if (cursorAtThisRow) justToCursor = true; } else { for (int i = 0; i < oldLine.getSpaceUsed(); i++) // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1; } int currentOldCol = 0; long styleAtCol = 0; for (int i = 0; i < lastNonSpaceIndex; i++) { // Note that looping over java character, not cells. char c = oldLine.mText[i]; int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; int displayWidth = WcWidth.width(codePoint); // Use the last style if this is a zero-width character: if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); // Line wrap as necessary: if (currentOutputExternalColumn + displayWidth > mColumns) { setLineWrap(currentOutputExternalRow); if (currentOutputExternalRow == mScreenRows - 1) { if (newCursorPlaced) newCursorRow--; scrollDownOneLine(0, mScreenRows, currentStyle); } else { currentOutputExternalRow++; } currentOutputExternalColumn = 0; } int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0); int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar; setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol); if (displayWidth > 0) { if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) { newCursorColumn = currentOutputExternalColumn; newCursorRow = currentOutputExternalRow; newCursorPlaced = true; } currentOldCol += displayWidth; currentOutputExternalColumn += displayWidth; if (justToCursor && newCursorPlaced) break; } } // Old row has been copied. Check if we need to insert newline if old line was not wrapping: if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) { if (currentOutputExternalRow == mScreenRows - 1) { if (newCursorPlaced) newCursorRow--; scrollDownOneLine(0, mScreenRows, currentStyle); } else { currentOutputExternalRow++; } currentOutputExternalColumn = 0; } } cursor[0] = newCursorColumn; cursor[1] = newCursorRow; } // Handle cursor scrolling off screen: if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0; } /** * Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound * into account. * * @param srcInternal The first line to be copied. * @param len The number of lines to be copied. */ private void blockCopyLinesDown(int srcInternal, int len) { if (len == 0) return; int totalRows = mTotalRows; int start = len - 1; // Save away line to be overwritten: TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows]; // Do the copy from bottom to top. for (int i = start; i >= 0; --i) mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows]; // Put back overwritten line, now above the block: mLines[(srcInternal) % totalRows] = lineToBeOverWritten; } /** * Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24). * * @param topMargin First line that is scrolled. * @param bottomMargin One line after the last line that is scrolled. * @param style the style for the newly exposed line. */ public void scrollDownOneLine(int topMargin, int bottomMargin, long style) { if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows) throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows); // Copy the fixed topMargin lines one line down so that they remain on screen in same position: blockCopyLinesDown(mScreenFirstRow, topMargin); // Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same // position: blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin); // Update the screen location in the ring buffer: mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows; // Note that the history has grown if not already full: if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++; // Blank the newly revealed line above the bottom margin: int blankRow = externalToInternalRow(bottomMargin - 1); if (mLines[blankRow] == null) { mLines[blankRow] = new TerminalRow(mColumns, style); } else { mLines[blankRow].clear(style); } } /** * Block copy characters from one position in the screen to another. The two positions can overlap. All characters * of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will * be thrown. * * @param sx source X coordinate * @param sy source Y coordinate * @param w width * @param h height * @param dx destination X coordinate * @param dy destination Y coordinate */ public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { if (w == 0) return; if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows) throw new IllegalArgumentException(); boolean copyingUp = sy > dy; for (int y = 0; y < h; y++) { int y2 = copyingUp ? y : (h - (y + 1)); TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2)); allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx); } } /** * Block set characters. All characters must be within the bounds of the screen, or else and * InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block * of characters. */ public void blockSet(int sx, int sy, int w, int h, int val, long style) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { throw new IllegalArgumentException( "Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")"); } for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) setChar(sx + x, sy + y, val, style); } public TerminalRow allocateFullLineIfNecessary(int row) { return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row]; } public void setChar(int column, int row, int codePoint, long style) { if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns) throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); row = externalToInternalRow(row); allocateFullLineIfNecessary(row).setChar(column, codePoint, style); } public long getStyleAt(int externalRow, int column) { return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); } /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left, int bottom, int right) { for (int y = top; y < bottom; y++) { TerminalRow line = mLines[externalToInternalRow(y)]; int startOfLine = (rectangular || y == top) ? left : leftMargin; int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin; for (int x = startOfLine; x < endOfLine; x++) { long currentStyle = line.getStyle(x); int foreColor = TextStyle.decodeForeColor(currentStyle); int backColor = TextStyle.decodeBackColor(currentStyle); int effect = TextStyle.decodeEffect(currentStyle); if (reverse) { // Clear out the bits to reverse and add them back in reversed: effect = (effect & ~bits) | (bits & ~effect); } else if (setOrClear) { effect |= bits; } else { effect &= ~bits; } line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect); } } } public void clearTranscript() { if (mScreenFirstRow < mActiveTranscriptRows) { Arrays.fill(mLines, mTotalRows + mScreenFirstRow - mActiveTranscriptRows, mTotalRows, null); Arrays.fill(mLines, 0, mScreenFirstRow, null); } else { Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null); } mActiveTranscriptRows = 0; } }