1
0
mirror of https://github.com/termux/termux-app synced 2024-06-18 07:07:08 +00:00
This commit is contained in:
Matan Ziv-Av 2024-04-20 19:52:30 +03:00 committed by GitHub
commit 7d50facaef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 778 additions and 15 deletions

View File

@ -0,0 +1,187 @@
package com.termux.terminal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.os.SystemClock;
/**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history.
* <p>
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
*/
public class TerminalBitmap {
public Bitmap bitmap;
public int cellWidth;
public int cellHeight;
public int scrollLines;
public int[] cursorDelta;
private static final String LOG_TAG = "TerminalBitmap";
public TerminalBitmap(int num, WorkingTerminalBitmap sixel, int Y, int X, int cellW, int cellH, TerminalBuffer screen) {
Bitmap bm = sixel.bitmap;
bm = resizeBitmapConstraints(bm, sixel.width, sixel.height, cellW, cellH, screen.mColumns - X);
addBitmap(num, bm, Y, X, cellW, cellH, screen);
}
public TerminalBitmap(int num, byte[] image, int Y, int X, int cellW, int cellH, int width, int height, boolean aspect, TerminalBuffer screen) {
Bitmap bm = null;
int imageHeight;
int imageWidth;
int newWidth = width;
int newHeight = height;
if (height > 0 || width > 0) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try {
BitmapFactory.decodeByteArray(image, 0, image.length, options);
} catch (Exception e) {
Logger.logWarn(null, LOG_TAG, "Cannot decode image");
}
imageHeight = options.outHeight;
imageWidth = options.outWidth;
if (aspect) {
double wFactor = 9999.0;
double hFactor = 9999.0;
if (width > 0) {
wFactor = (double)width / imageWidth;
}
if (height > 0) {
hFactor = (double)height / imageHeight;
}
double factor = Math.min(wFactor, hFactor);
newWidth = (int)(factor * imageWidth);
newHeight = (int)(factor * imageHeight);
} else {
if (height <= 0) {
newHeight = imageHeight;
}
if (width <= 0) {
newWidth = imageWidth;
}
}
int scaleFactor = 1;
while (imageHeight >= 2 * newHeight * scaleFactor && imageWidth >= 2 * newWidth * scaleFactor) {
scaleFactor = scaleFactor * 2;
}
BitmapFactory.Options scaleOptions = new BitmapFactory.Options();
scaleOptions.inSampleSize = scaleFactor;
try {
bm = BitmapFactory.decodeByteArray(image, 0, image.length, scaleOptions);
} catch (Exception e) {
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot decode image");
bitmap = null;
return;
}
if (bm == null) {
Logger.logWarn(null, LOG_TAG, "Could not decode image");
bitmap = null;
return;
}
int maxWidth = (screen.mColumns - X) * cellW;
if (newWidth > maxWidth) {
int cropWidth = bm.getWidth() * maxWidth / newWidth;
try {
bm = Bitmap.createBitmap(bm, 0, 0, cropWidth, bm.getHeight());
newWidth = maxWidth;
} catch(OutOfMemoryError e) {
// This is just a memory optimization. If it fails,
// continue (and probably fail later).
}
}
try {
bm = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true);
} catch(OutOfMemoryError e) {
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot rescale image");
bm = null;
}
} else {
try {
bm = BitmapFactory.decodeByteArray(image, 0, image.length);
} catch (Exception e) {
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot decode image");
}
}
if (bm == null) {
Logger.logWarn(null, LOG_TAG, "Cannot decode image");
bitmap = null;
return;
}
bm = resizeBitmapConstraints(bm, bm.getWidth(), bm.getHeight(), cellW, cellH, screen.mColumns - X);
addBitmap(num, bm, Y, X, cellW, cellH, screen);
cursorDelta = new int[] {scrollLines, (bitmap.getWidth() + cellW - 1) / cellW};
}
private void addBitmap(int num, Bitmap bm, int Y, int X, int cellW, int cellH, TerminalBuffer screen) {
if (bm == null) {
bitmap = null;
return;
}
int width = bm.getWidth();
int height = bm.getHeight();
cellWidth = cellW;
cellHeight = cellH;
int w = Math.min(screen.mColumns - X, (width + cellW - 1) / cellW);
int h = (height + cellH - 1) / cellH;
int s = 0;
for (int i=0; i<h; i++) {
if (Y+i-s == screen.mScreenRows) {
screen.scrollDownOneLine(0, screen.mScreenRows, TextStyle.NORMAL);
s++;
}
for (int j=0; j<w ; j++) {
screen.setChar(X+j, Y+i-s, '+', TextStyle.encodeBitmap(num, j, i));
}
}
if (w * cellW < width) {
try {
bm = Bitmap.createBitmap(bm, 0, 0, w * cellW, height);
} catch(OutOfMemoryError e) {
// Image cannot be cropped to only visible part due to out of memory.
// This causes memory waste.
}
}
bitmap = bm;
scrollLines = h - s;
}
static public Bitmap resizeBitmap(Bitmap bm, int w, int h) {
int[] pixels = new int[bm.getAllocationByteCount()];
bm.getPixels(pixels, 0, bm.getWidth(), 0, 0, bm.getWidth(), bm.getHeight());
Bitmap newbm;
try {
newbm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
} catch(OutOfMemoryError e) {
// Only a minor display glitch in this case
return bm;
}
int newWidth = Math.min(bm.getWidth(), w);
int newHeight = Math.min(bm.getHeight(), h);
newbm.setPixels(pixels, 0, bm.getWidth(), 0, 0, newWidth, newHeight);
return newbm;
}
static public Bitmap resizeBitmapConstraints(Bitmap bm, int w, int h, int cellW, int cellH, int Columns) {
// Width and height must be multiples of the cell width and height
// Bitmap should not extend beyonf screen width
if (w > cellW * Columns || (w % cellW) != 0 || (h % cellH) != 0) {
int newW = Math.min(cellW * Columns, ((w - 1) / cellW) * cellW + cellW);
int newH = ((h - 1) / cellH) * cellH + cellH;
try {
bm = resizeBitmap(bm, newW, newH);
} catch(OutOfMemoryError e) {
// Only a minor display glitch in this case
}
}
return bm;
}
}

View File

@ -1,6 +1,15 @@
package com.termux.terminal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.HashMap;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.os.SystemClock;
/**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
@ -20,6 +29,11 @@ public final class TerminalBuffer {
/** The index in the circular buffer where the visible screen starts. */
private int mScreenFirstRow = 0;
public HashMap<Integer,TerminalBitmap> bitmaps;
public WorkingTerminalBitmap workingBitmap;
private boolean hasBitmaps;
private long bitmapLastGC;
/**
* Create a transcript screen.
*
@ -35,6 +49,9 @@ public final class TerminalBuffer {
mLines = new TerminalRow[totalRows];
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
hasBitmaps = false;
bitmaps = new HashMap<Integer,TerminalBitmap>();
bitmapLastGC = SystemClock.uptimeMillis();
}
public String getTranscriptText() {
@ -401,6 +418,28 @@ public final class TerminalBuffer {
if (mLines[blankRow] == null) {
mLines[blankRow] = new TerminalRow(mColumns, style);
} else {
// find if a bitmap is completely scrolled out
Set<Integer> used = new HashSet<Integer>();
if(mLines[blankRow].mHasBitmap) {
for (int column = 0; column < mColumns; column++) {
final long st = mLines[blankRow].getStyle(column);
if (TextStyle.isBitmap(st)) {
used.add((int)(st >> 16) & 0xffff);
}
}
TerminalRow nextLine = mLines[(blankRow + 1) % mTotalRows];
if(nextLine.mHasBitmap) {
for (int column = 0; column < mColumns; column++) {
final long st = nextLine.getStyle(column);
if (TextStyle.isBitmap(st)) {
used.remove((int)(st >> 16) & 0xffff);
}
}
}
for(Integer bm: used) {
bitmaps.remove(bm);
}
}
mLines[blankRow].clear(style);
}
}
@ -492,6 +531,92 @@ public final class TerminalBuffer {
Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null);
}
mActiveTranscriptRows = 0;
bitmaps.clear();
hasBitmaps = false;
}
public Bitmap getSixelBitmap(int codePoint, long style) {
return bitmaps.get(TextStyle.bitmapNum(style)).bitmap;
}
public Rect getSixelRect(int codePoint, long style ) {
TerminalBitmap bm = bitmaps.get(TextStyle.bitmapNum(style));
int x = TextStyle.bitmapX(style);
int y = TextStyle.bitmapY(style);
Rect r = new Rect(x * bm.cellWidth, y * bm.cellHeight, (x+1) * bm.cellWidth, (y+1) * bm.cellHeight);
return r;
}
public void sixelStart(int width, int height) {
workingBitmap = new WorkingTerminalBitmap(width, height);
}
public void sixelChar(int c, int rep) {
workingBitmap.sixelChar(c, rep);
}
public void sixelSetColor(int col) {
workingBitmap.sixelSetColor(col);
}
public void sixelSetColor(int col, int r, int g, int b) {
workingBitmap.sixelSetColor(col, r, g, b);
}
private int findFreeBitmap() {
int i = 0;
while (bitmaps.containsKey(i)) {
i++;
}
return i;
}
public int sixelEnd(int Y, int X, int cellW, int cellH) {
int num = findFreeBitmap();
bitmaps.put(num, new TerminalBitmap(num, workingBitmap, Y, X, cellW, cellH, this));
workingBitmap = null;
if (bitmaps.get(num).bitmap == null) {
bitmaps.remove(num);
return 0;
}
hasBitmaps = true;
bitmapGC(30000);
return bitmaps.get(num).scrollLines;
}
public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int width, int height, boolean aspect) {
int num = findFreeBitmap();
bitmaps.put(num, new TerminalBitmap(num, image, Y, X, cellW, cellH, width, height, aspect, this));
if (bitmaps.get(num).bitmap == null) {
bitmaps.remove(num);
return new int[] {0,0};
}
hasBitmaps = true;
bitmapGC(30000);
return bitmaps.get(num).cursorDelta;
}
public void bitmapGC(int timeDelta) {
if (!hasBitmaps || bitmapLastGC + timeDelta > SystemClock.uptimeMillis()) {
return;
}
Set<Integer> used = new HashSet<Integer>();
for (int line = 0; line < mLines.length; line++) {
if(mLines[line] != null && mLines[line].mHasBitmap) {
for (int column = 0; column < mColumns; column++) {
final long st = mLines[line].getStyle(column);
if (TextStyle.isBitmap(st)) {
used.add((int)(st >> 16) & 0xffff);
}
}
}
}
Set<Integer> keys = new HashSet<Integer>(bitmaps.keySet());
for (Integer bn: keys) {
if (!used.contains(bn)) {
bitmaps.remove(bn);
}
}
bitmapLastGC = SystemClock.uptimeMillis();
}
}

View File

@ -3,6 +3,7 @@ package com.termux.terminal;
import android.util.Base64;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
@ -79,6 +80,9 @@ public final class TerminalEmulator {
private static final int ESC_CSI_SINGLE_QUOTE = 18;
/** Escape processing: CSI ! */
private static final int ESC_CSI_EXCLAMATION = 19;
/** Escape processing: APC */
private static final int ESC_APC = 20;
private static final int ESC_APC_ESC = 21;
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
private static final int MAX_ESCAPE_PARAMETERS = 16;
@ -188,6 +192,10 @@ public final class TerminalEmulator {
/** The current state of the escape sequence state machine. One of the ESC_* constants. */
private int mEscapeState;
private boolean ESC_P_escape = false;
private boolean ESC_P_sixel = false;
private ArrayList<Byte> ESC_OSC_data;
private int ESC_OSC_colon = 0;
private final SavedScreenState mSavedStateMain = new SavedScreenState();
private final SavedScreenState mSavedStateAlt = new SavedScreenState();
@ -263,6 +271,13 @@ public final class TerminalEmulator {
private static final String LOG_TAG = "TerminalEmulator";
private int cellW = 12, cellH = 12;
public void setCellSize(int w, int h) {
cellW = w;
cellH = h;
}
private boolean isDecsetInternalBitSet(int bit) {
return (mCurrentDecSetFlags & bit) != 0;
}
@ -553,14 +568,19 @@ public final class TerminalEmulator {
}
public void processCodePoint(int b) {
mScreen.bitmapGC(300000);
switch (b) {
case 0: // Null character (NUL, ^@). Do nothing.
break;
case 7: // Bell (BEL, ^G, \a). If in an OSC sequence, BEL may terminate a string; otherwise signal bell.
if (mEscapeState == ESC_OSC)
doOsc(b);
else
else {
if (mEscapeState == ESC_APC) {
doApc(b);
}
mSession.onBell();
}
break;
case 8: // Backspace (BS, ^H).
if (mLeftMargin == mCursorCol) {
@ -587,10 +607,16 @@ public final class TerminalEmulator {
case 10: // Line feed (LF, \n).
case 11: // Vertical tab (VT, \v).
case 12: // Form feed (FF, \f).
doLinefeed();
if((mEscapeState != ESC_P || !ESC_P_sixel) && ESC_OSC_colon <= 0) {
// Ignore CR/LF inside sixels or iterm2 data
doLinefeed();
}
break;
case 13: // Carriage return (CR, \r).
setCursorCol(mLeftMargin);
if((mEscapeState != ESC_P || !ESC_P_sixel) && ESC_OSC_colon <= 0) {
// Ignore CR/LF inside sixels or iterm2 data
setCursorCol(mLeftMargin);
}
break;
case 14: // Shift Out (Ctrl-N, SO) Switch to Alternate Character Set. This invokes the G1 character set.
mUseLineDrawingUsesG0 = false;
@ -610,9 +636,14 @@ public final class TerminalEmulator {
// Starts an escape sequence unless we're parsing a string
if (mEscapeState == ESC_P) {
// XXX: Ignore escape when reading device control sequence, since it may be part of string terminator.
ESC_P_escape = true;
return;
} else if (mEscapeState != ESC_OSC) {
startEscapeSequence();
if (mEscapeState != ESC_APC) {
startEscapeSequence();
} else {
doApc(b);
}
} else {
doOsc(b);
}
@ -809,6 +840,12 @@ public final class TerminalEmulator {
break;
case ESC_PERCENT:
break;
case ESC_APC:
doApc(b);
break;
case ESC_APC_ESC:
doApcEsc(b);
break;
case ESC_OSC:
doOsc(b);
break;
@ -888,8 +925,17 @@ public final class TerminalEmulator {
/** When in {@link #ESC_P} ("device control") sequence. */
private void doDeviceControl(int b) {
switch (b) {
case (byte) '\\': // End of ESC \ string Terminator
boolean firstSixel = false;
if (!ESC_P_sixel && (b=='$' || b=='-' || b=='#')) {
//Check if sixel sequence that needs breaking
String dcs = mOSCOrDeviceControlArgs.toString();
if (dcs.matches("[0-9;]*q.*")) {
firstSixel = true;
}
}
if (firstSixel || (ESC_P_escape && b == '\\') || (ESC_P_sixel && (b=='$' || b=='-' || b=='#')))
// ESC \ terminates OSC
// Sixel sequences may be very long. '$' and '!' are natural for breaking the sequence.
{
String dcs = mOSCOrDeviceControlArgs.toString();
// DCS $ q P t ST. Request Status String (DECRQSS)
@ -990,14 +1036,102 @@ public final class TerminalEmulator {
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
}
}
} else if (ESC_P_sixel || dcs.matches("[0-9;]*q.*")) {
int pos = 0;
if (!ESC_P_sixel) {
ESC_P_sixel = true;
mScreen.sixelStart(100, 100);
while (dcs.codePointAt(pos) != 'q') {
pos++;
}
pos++;
}
if (b=='$' || b=='-') {
// Add to string
dcs = dcs + (char)b;
}
int rep = 1;
while (pos < dcs.length()) {
if (dcs.codePointAt(pos) == '"') {
pos++;
int args[]={0,0,0,0};
int arg = 0;
while (pos < dcs.length() && ((dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') || dcs.codePointAt(pos) == ';')) {
if (dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
args[arg] = args[arg] * 10 + dcs.codePointAt(pos) - '0';
} else {
arg++;
if (arg > 3) {
break;
}
}
pos++;
}
if (pos == dcs.length()) {
break;
}
} else if (dcs.codePointAt(pos) == '#') {
int col = 0;
pos++;
while (pos < dcs.length() && dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
col = col * 10 + dcs.codePointAt(pos++) - '0';
}
if (pos == dcs.length() || dcs.codePointAt(pos) != ';') {
mScreen.sixelSetColor(col);
} else {
pos++;
int args[]={0,0,0,0};
int arg = 0;
while (pos < dcs.length() && ((dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') || dcs.codePointAt(pos) == ';')) {
if (dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
args[arg] = args[arg] * 10 + dcs.codePointAt(pos) - '0';
} else {
arg++;
if (arg > 3) {
break;
}
}
pos++;
}
if (args[0] == 2) {
mScreen.sixelSetColor(col, args[1], args[2], args[3]);
}
}
} else if (dcs.codePointAt(pos) == '!') {
rep = 0;
pos++;
while (pos < dcs.length() && dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
rep = rep * 10 + dcs.codePointAt(pos++) - '0';
}
} else if (dcs.codePointAt(pos) == '$' || dcs.codePointAt(pos) == '-' || (dcs.codePointAt(pos) >= '?' && dcs.codePointAt(pos) <= '~')) {
mScreen.sixelChar(dcs.codePointAt(pos++), rep);
rep = 1;
} else {
pos++;
}
}
if (b == '\\') {
ESC_P_sixel = false;
int n = mScreen.sixelEnd(mCursorRow, mCursorCol, cellW, cellH);
for(;n>0;n--) {
doLinefeed();
}
} else {
mOSCOrDeviceControlArgs.setLength(0);
if (b=='#') {
mOSCOrDeviceControlArgs.appendCodePoint(b);
}
// Do not finish sequence
continueSequence(mEscapeState);
return;
}
} else {
if (LOG_ESCAPE_SEQUENCES)
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
}
finishSequence();
}
break;
default:
} else {
ESC_P_escape = false;
if (mOSCOrDeviceControlArgs.length() > MAX_OSC_STRING_LENGTH) {
// Too long.
mOSCOrDeviceControlArgs.setLength(0);
@ -1006,7 +1140,7 @@ public final class TerminalEmulator {
mOSCOrDeviceControlArgs.appendCodePoint(b);
continueSequence(mEscapeState);
}
}
}
}
private int nextTabStop(int numTabs) {
@ -1389,6 +1523,7 @@ public final class TerminalEmulator {
break;
case 'P': // Device control string
mOSCOrDeviceControlArgs.setLength(0);
ESC_P_escape = false;
continueSequence(ESC_P);
break;
case '[':
@ -1402,10 +1537,15 @@ public final class TerminalEmulator {
case ']': // OSC
mOSCOrDeviceControlArgs.setLength(0);
continueSequence(ESC_OSC);
ESC_OSC_colon = -1;
break;
case '>': // DECKPNM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
break;
case '_': // APC
mOSCOrDeviceControlArgs.setLength(0);
continueSequence(ESC_APC);
break;
default:
unknownSequence(b);
break;
@ -1628,7 +1768,7 @@ public final class TerminalEmulator {
// The important part that may still be used by some (tmux stores this value but does not currently use it)
// is the first response parameter identifying the terminal service class, where we send 64 for "vt420".
// This is followed by a list of attributes which is probably unused by applications. Send like xterm.
if (getArg0(0) == 0) mSession.write("\033[?64;1;2;6;9;15;18;21;22c");
if (getArg0(0) == 0) mSession.write("\033[?64;1;2;4;6;9;15;18;21;22c");
break;
case 'd': // ESC [ Pn d - Vert Position Absolute
setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1);
@ -1715,8 +1855,10 @@ public final class TerminalEmulator {
mSession.write("\033[3;0;0t");
break;
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
// We just report characters time 12 here.
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * cellH, mColumns * cellW));
break;
case 16: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", cellH, cellW));
break;
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
@ -1868,6 +2010,33 @@ public final class TerminalEmulator {
}
}
private void doApc(int b) {
switch (b) {
case 7: // Bell.
break;
case 27: // Escape.
continueSequence(ESC_APC_ESC);
break;
default:
collectOSCArgs(b);
continueSequence(ESC_OSC);
}
}
private void doApcEsc(int b) {
switch (b) {
case '\\':
break;
default:
// The ESC character was not followed by a \, so insert the ESC and
// the current character in arg buffer.
collectOSCArgs(27);
collectOSCArgs(b);
continueSequence(ESC_APC);
break;
}
}
private void doOsc(int b) {
switch (b) {
case 7: // Bell.
@ -1878,6 +2047,22 @@ public final class TerminalEmulator {
break;
default:
collectOSCArgs(b);
if (ESC_OSC_colon == -1 && b == ':') {
// Collect base64 data for OSC 1337
ESC_OSC_colon = mOSCOrDeviceControlArgs.length();
ESC_OSC_data = new ArrayList<Byte>(65536);
} else if (ESC_OSC_colon >= 0 && mOSCOrDeviceControlArgs.length() - ESC_OSC_colon == 4) {
try {
byte[] decoded = Base64.decode(mOSCOrDeviceControlArgs.substring(ESC_OSC_colon), 0);
for (int i = 0 ; i < decoded.length; i++) {
ESC_OSC_data.add(decoded[i]);
}
} catch(Exception e) {
// Ignore non-Base64 data.
}
mOSCOrDeviceControlArgs.setLength(ESC_OSC_colon);
}
break;
}
}
@ -1900,6 +2085,8 @@ public final class TerminalEmulator {
/** An Operating System Controls (OSC) Set Text Parameters. May come here from BEL or ST. */
private void doOscSetTextParameters(String bellOrStringTerminator) {
int value = -1;
int osc_colon = ESC_OSC_colon;
ESC_OSC_colon = -1;
String textParameter = "";
// Extract initial $value from initial "$value;..." string.
for (int mOSCArgTokenizerIndex = 0; mOSCArgTokenizerIndex < mOSCOrDeviceControlArgs.length(); mOSCArgTokenizerIndex++) {
@ -2035,6 +2222,105 @@ public final class TerminalEmulator {
break;
case 119: // Reset highlight color.
break;
case 1337: // iTerm extemsions
if (textParameter.startsWith("File=")) {
int pos = 5;
boolean inline = false;
boolean aspect = true;
int width = -1;
int height = -1;
while (pos < textParameter.length()) {
int eqpos = textParameter.indexOf('=', pos);
if (eqpos == -1) {
break;
}
int semicolonpos = textParameter.indexOf(';', eqpos);
if (semicolonpos == -1) {
semicolonpos = textParameter.length() - 1;
}
String k = textParameter.substring(pos, eqpos);
String v = textParameter.substring(eqpos + 1, semicolonpos);
pos = semicolonpos + 1;
if (k.equalsIgnoreCase("inline")) {
inline = v.equals("1");
}
if (k.equalsIgnoreCase("preserveAspectRatio")) {
aspect = ! v.equals("0");
}
if (k.equalsIgnoreCase("width")) {
double factor = cellW;
int div = 1;
int e = v.length();
if (v.endsWith("px")) {
factor = 1;
e -= 2;
} else if (v.endsWith("%")) {
factor = 0.01 * cellW * mColumns;
e -= 1;
}
try {
width = (int)(factor * Integer.parseInt(v.substring(0,e)));
} catch(Exception ex) {
}
}
if (k.equalsIgnoreCase("height")) {
double factor = cellH;
int div = 1;
int e = v.length();
if (v.endsWith("px")) {
factor = 1;
e -= 2;
} else if (v.endsWith("%")) {
factor = 0.01 * cellH * mRows;
e -= 1;
}
try {
height = (int)(factor * Integer.parseInt(v.substring(0,e)));
} catch(Exception ex) {
}
}
}
if (!inline) {
finishSequence();
return;
}
if (osc_colon >= 0 && mOSCOrDeviceControlArgs.length() > osc_colon) {
while (mOSCOrDeviceControlArgs.length() - osc_colon < 4) {
mOSCOrDeviceControlArgs.append('=');
}
try {
byte[] decoded = Base64.decode(mOSCOrDeviceControlArgs.substring(osc_colon), 0);
for (int i = 0 ; i < decoded.length; i++) {
ESC_OSC_data.add(decoded[i]);
}
} catch(Exception e) {
// Ignore non-Base64 data.
}
mOSCOrDeviceControlArgs.setLength(osc_colon);
}
if (osc_colon >= 0) {
byte[] result = new byte[ESC_OSC_data.size()];
for(int i = 0; i < ESC_OSC_data.size(); i++) {
result[i] = ESC_OSC_data.get(i).byteValue();
}
int[] res = mScreen.addImage(result, mCursorRow, mCursorCol, cellW, cellH, width, height, aspect);
int col = res[1] + mCursorCol;
if (col < mColumns -1) {
res[0] -= 1;
} else {
col = 0;
}
for(;res[0] > 0; res[0]--) {
doLinefeed();
}
mCursorCol = col;
ESC_OSC_data.clear();
} else {
}
} else if (textParameter.startsWith("ReportCellSize")) {
mSession.write(String.format(Locale.US, "\0331337;ReportCellSize=%d;%d\007", cellH, cellW));
}
break;
default:
unknownParameter(value);
break;
@ -2455,6 +2741,10 @@ public final class TerminalEmulator {
mColors.reset();
mSession.onColorsChanged();
ESC_P_escape = false;
ESC_P_sixel = false;
ESC_OSC_colon = -1;
}
public String getSelectedText(int x1, int y1, int x2, int y2) {

View File

@ -49,6 +49,8 @@ public final class TerminalRow {
final long[] mStyle;
/** If this row might contain chars with width != 1, used for deactivating fast path */
boolean mHasNonOneWidthOrSurrogateChars;
/** If this row has a bitmap. Used for performace only */
public boolean mHasBitmap;
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
public TerminalRow(int columns, long style) {
@ -146,6 +148,7 @@ public final class TerminalRow {
Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns;
mHasNonOneWidthOrSurrogateChars = false;
mHasBitmap = false;
}
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
@ -155,6 +158,10 @@ public final class TerminalRow {
mStyle[columnToSet] = style;
if (!mHasBitmap && TextStyle.isBitmap(style)) {
mHasBitmap = true;
}
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
// Fast path when we don't have any chars with width != 1

View File

@ -35,6 +35,8 @@ public final class TextStyle {
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
/** If true (24-bit) color is used for the cell for foreground. */
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
/** If true, character represents a bitmap slice, not text. */
public final static int BITMAP = 1 << 15;
public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257;
@ -87,4 +89,24 @@ public final class TextStyle {
return (int) (style & 0b11111111111);
}
public static long encodeBitmap(int num, int X, int Y) {
return ((long)num << 16) | ((long)Y << 32) | ((long)X << 48) | BITMAP;
}
public static boolean isBitmap(long style) {
return (style & 0x8000) != 0;
}
public static int bitmapNum(long style) {
return (int)(style & 0xffff0000) >> 16;
}
public static int bitmapX(long style) {
return (int)((style >> 48) & 0xfff);
}
public static int bitmapY(long style) {
return (int)((style >> 32) & 0xfff);
}
}

View File

@ -0,0 +1,110 @@
package com.termux.terminal;
import android.graphics.Bitmap;
/**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history.
* <p>
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
*/
public final class WorkingTerminalBitmap {
final private int sixelInitialColorMap[] = {0xFF000000, 0xFF3333CC, 0xFFCC2323, 0xFF33CC33, 0xFFCC33CC, 0xFF33CCCC, 0xFFCCCC33, 0xFF777777,
0xFF444444, 0xFF565699, 0xFF994444, 0xFF569956, 0xFF995699, 0xFF569999, 0xFF999956, 0xFFCCCCCC};
private int[] colorMap;
private int curX;
private int curY;
private int color;
public int width;
public int height;
public Bitmap bitmap;
private static final String LOG_TAG = "WorkingTerminalBitmap";
public WorkingTerminalBitmap(int w, int h) {
try {
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel ignored");
bitmap = null;
}
bitmap.eraseColor(0);
width = 0;
height = 0;
curX = 0;
curY = 0;
colorMap = new int[256];
for (int i=0; i<16; i++) {
colorMap[i] = sixelInitialColorMap[i];
}
color = colorMap[0];
}
public void sixelChar(int c, int rep) {
if (bitmap == null) {
return;
}
if (c == '$') {
curX = 0;
return;
}
if (c == '-') {
curX = 0;
curY += 6;
return;
}
if (bitmap.getWidth() < curX + rep) {
try {
bitmap = TerminalBitmap.resizeBitmap(bitmap, curX + rep + 100, bitmap.getHeight());
} catch(OutOfMemoryError e) {
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel truncated");
}
}
if (bitmap.getHeight() < curY + 6) {
// Very unlikely to resize both at the same time
try {
bitmap = TerminalBitmap.resizeBitmap(bitmap, bitmap.getWidth(), curY + 100);
} catch(OutOfMemoryError e) {
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel truncated");
}
}
if (curX + rep > bitmap.getWidth()) {
rep = bitmap.getWidth() - curX;
}
if ( curY + 6 > bitmap.getHeight()) {
return;
}
if (rep > 0 && c >= '?' && c <= '~') {
int b = c - '?';
if (curY + 6 > height) {
height = curY + 6;
}
while (rep-- > 0) {
for (int i = 0 ; i < 6 ; i++) {
if ((b & (1<<i)) != 0) {
bitmap.setPixel(curX, curY + i, color);
}
}
curX += 1;
if (curX > width) {
width = curX;
}
}
}
}
public void sixelSetColor(int col) {
if (col >= 0 && col < 256) {
color = colorMap[col];
}
}
public void sixelSetColor(int col, int r, int g, int b) {
if (col >= 0 && col < 256) {
int red = Math.min(255, r*255/100);
int green = Math.min(255, g*255/100);
int blue = Math.min(255, b*255/100);
color = 0xff000000 + (red << 16) + (green << 8) + blue;
colorMap[col] = color;
}
}
}

View File

@ -1,8 +1,11 @@
package com.termux.view;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import com.termux.terminal.TerminalBuffer;
@ -65,6 +68,7 @@ public final class TerminalRenderer {
final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors;
final int cursorShape = mEmulator.getCursorStyle();
mEmulator.setCellSize((int)mFontWidth, (int)mFontLineSpacing);
if (reverseVideo)
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
@ -98,10 +102,28 @@ public final class TerminalRenderer {
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final long style = lineObject.getStyle(column);
if (TextStyle.isBitmap(style)) {
Bitmap bm = mEmulator.getScreen().getSixelBitmap(codePoint, style);
if (bm != null) {
float left = column * mFontWidth;
float top = heightOffset - mFontLineSpacing;
RectF r = new RectF(left, top, left + mFontWidth, top + mFontLineSpacing);
canvas.drawBitmap(mEmulator.getScreen().getSixelBitmap(codePoint, style), mEmulator.getScreen().getSixelRect(codePoint, style), r, null);
}
column += 1;
measuredWidthForRun = 0.f;
lastRunStyle = 0;
lastRunInsideCursor = false;
lastRunStartColumn = column + 1;
lastRunStartIndex = currentCharIndex;
lastRunFontWidthMismatch = false;
currentCharIndex += charsForCodePoint;
continue;
}
final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
final boolean insideSelection = column >= selx1 && column <= selx2;
final long style = lineObject.getStyle(column);
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
@ -112,7 +134,7 @@ public final class TerminalRenderer {
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || fontWidthMismatch || lastRunFontWidthMismatch) {
if (column == 0) {
if (column == 0 || column == lastRunStartColumn) {
// Skip first column as there is nothing to draw, just record the current style.
} else {
final int columnWidthSinceLastRun = column - lastRunStartColumn;