= 30) {
+ // getWindow().setDecorFitsSystemWindows(false);
+ // WindowInsetsController insetsController = getWindow().getInsetsController();
+ // if (insetsController != null) {
+ // insetsController.hide(WindowInsets.Type.navigationBars() | WindowInsets.Type.statusBars());
+ // insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ // }
+ // } else {
+ View decorView = getWindow().getDecorView();
+ int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ decorView.setSystemUiVisibility(flags);
+ decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ decorView.setSystemUiVisibility(flags);
+ }
+ }
+ });
+ // }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void disableImmersiveMode() {
+ // if (Build.VERSION.SDK_INT >= 30) {
+ // getWindow().setDecorFitsSystemWindows(true);
+ // WindowInsetsController insetsController = getWindow().getInsetsController();
+ // if (insetsController != null) {
+ // insetsController.show(WindowInsets.Type.navigationBars() | WindowInsets.Type.statusBars());
+ // insetsController.setSystemBarsBehavior(
+ // Build.VERSION.SDK_INT >= 31
+ // ? WindowInsetsController.BEHAVIOR_DEFAULT
+ // : WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ // );
+ // }
+ // } else {
+ View decorView = getWindow().getDecorView();
+ int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ decorView.setSystemUiVisibility(flags);
+ decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ decorView.setSystemUiVisibility(flags);
+ }
+ }
+ });
+ // }
+ }
}
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java
new file mode 100644
index 0000000000..77c75144d3
--- /dev/null
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java
@@ -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.
+ *
+ * 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 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;
+ }
+}
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
index 21d6518785..40cce97496 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
@@ -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 bitmaps;
+ public WorkingTerminalBitmap workingBitmap;
+ private boolean hasBitmaps;
+ private long bitmapLastGC;
+
/**
* Create a transcript screen.
*
@@ -35,6 +49,9 @@ public TerminalBuffer(int columns, int totalRows, int screenRows) {
mLines = new TerminalRow[totalRows];
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
+ hasBitmaps = false;
+ bitmaps = new HashMap();
+ bitmapLastGC = SystemClock.uptimeMillis();
}
public String getTranscriptText() {
@@ -401,6 +418,28 @@ public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
if (mLines[blankRow] == null) {
mLines[blankRow] = new TerminalRow(mColumns, style);
} else {
+ // find if a bitmap is completely scrolled out
+ Set used = new HashSet();
+ 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 void clearTranscript() {
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 used = new HashSet();
+ 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 keys = new HashSet(bitmaps.keySet());
+ for (Integer bn: keys) {
+ if (!used.contains(bn)) {
+ bitmaps.remove(bn);
+ }
+ }
+ bitmapLastGC = SystemClock.uptimeMillis();
+ }
}
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
index aeef393c62..92e28cf136 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
@@ -3,6 +3,7 @@
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 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 @@ private void processByte(byte byteToProcess) {
}
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 void processCodePoint(int b) {
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 void processCodePoint(int b) {
// 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 void processCodePoint(int b) {
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 void processCodePoint(int b) {
/** 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 @@ private void doDeviceControl(int b) {
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 @@ private void doDeviceControl(int b) {
mOSCOrDeviceControlArgs.appendCodePoint(b);
continueSequence(mEscapeState);
}
- }
+ }
}
private int nextTabStop(int numTabs) {
@@ -1389,6 +1523,7 @@ private void doEsc(int b) {
break;
case 'P': // Device control string
mOSCOrDeviceControlArgs.setLength(0);
+ ESC_P_escape = false;
continueSequence(ESC_P);
break;
case '[':
@@ -1402,10 +1537,15 @@ private void doEsc(int b) {
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 @@ private void doCsi(int b) {
// 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 @@ private void doCsi(int b) {
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 @@ private void selectGraphicRendition() {
}
}
+ 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 @@ private void doOsc(int b) {
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(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 @@ private void doOscEsc(int b) {
/** 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 @@ private void doOscSetTextParameters(String bellOrStringTerminator) {
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 void reset() {
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) {
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
index cbeaf52243..8d6aa46e78 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java
@@ -23,6 +23,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) {
@@ -120,6 +122,7 @@ public void clear(long style) {
Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns;
mHasNonOneWidthOrSurrogateChars = false;
+ mHasBitmap = false;
}
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
@@ -129,6 +132,10 @@ public void setChar(int columnToSet, int codePoint, long style) {
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
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java b/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java
index 173d6ae94e..7ee4b06ebc 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java
@@ -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 static int decodeEffect(long style) {
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);
+ }
+
}
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/WorkingTerminalBitmap.java b/terminal-emulator/src/main/java/com/termux/terminal/WorkingTerminalBitmap.java
new file mode 100644
index 0000000000..e9f0e2ed05
--- /dev/null
+++ b/terminal-emulator/src/main/java/com/termux/terminal/WorkingTerminalBitmap.java
@@ -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.
+ *
+ * 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< 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;
+ }
+ }
+}
diff --git a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
index 307e422694..e82720a2c9 100644
--- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
+++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java
@@ -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 void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
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 void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
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 void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
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;