From 232ab8b859a8dbb00c0a5602d24e99731b40d40a Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Fri, 2 Sep 2022 12:23:28 +0300 Subject: [PATCH 01/14] Add sixel support: - In TerminalEmulator, interpret sixel sequences, and send them to TerminalBuffer for constructing a bitmap. - Sixel sequences may be longer than 8192 characters, so break them in natural places ($,-,#), rather than collecting all in the buffer. - The bitmap is sliced to character cell sized slices, and each the the style attribute is used to store which bitmap slice is displayed in place of this character. - In TerminalRenderer the style is interpreted, and drawn using drawBitmap, instead of drawText. Support iTerm inline image protocol (OSC 1337): - Using the same bitmap display infrastructure introduced for sixels. - Collects the image data outside of the OSC buffer. - Ignoring some parameters. Small emulator changes: - Also eat APC sequences, not echoing to screen. - Fix `CSI 14 t` to give actual size - Add `CSI 16 t` - Add `4` (sixel) to device attributes --- .../com/termux/terminal/TerminalBuffer.java | 237 ++++++++++++++ .../com/termux/terminal/TerminalEmulator.java | 307 +++++++++++++++++- .../java/com/termux/terminal/TerminalRow.java | 7 + .../java/com/termux/terminal/TextStyle.java | 6 + .../com/termux/view/TerminalRenderer.java | 24 +- 5 files changed, 567 insertions(+), 14 deletions(-) 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..590205c15d 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,14 @@ 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 @@ -20,6 +28,203 @@ public final class TerminalBuffer { /** The index in the circular buffer where the visible screen starts. */ private int mScreenFirstRow = 0; + final private int MAX_SIXELS = 1024; + private Bitmap sixelBitmap[]; + private int sixelCellW[]; + private int sixelCellH[]; + private int sixelWidth[]; + private int sixelHeight[]; + private int sixelNum = -1; + private int sixelX; + private int sixelY; + private int[] sixelColorMap; + private int sixelColor; + private long sixelLastGC; + private boolean sixelHasBitmaps = false; + final private int sixelInitialColorMap[] = {0xFF000000, 0xFF3333CC, 0xFFCC2323, 0xFF33CC33, 0xFFCC33CC, 0xFF33CCCC, 0xFFCCCC33, 0xFF777777, + 0xFF444444, 0xFF565699, 0xFF994444, 0xFF569956, 0xFF995699, 0xFF569999, 0xFF999956, 0xFFCCCCCC}; + + private 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 = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + newbm.setPixels(pixels, 0, bm.getWidth(), 0, 0, bm.getWidth(), bm.getHeight()); + return newbm; + } + + public Bitmap getSixelBitmap(int codePoint, long style) { + int sn = (int)(style & 0xffff0000) >> 16 ; + return sixelBitmap[sn]; + } + + public Rect getSixelRect(int codePoint, long style ) { + int sn = (int)(style & 0xffff0000) >> 16 ; + int x = (int)((style >> 48) & 0xfff); + int y = (int)((style >> 32) & 0xfff); + Rect r = new Rect(x * sixelCellW[sn], y * sixelCellH[sn], (x+1) * sixelCellW[sn], (y+1) * sixelCellH[sn]); + return r; + } + + public void sixelStart(int width, int height) { + sixelNum++; + if (sixelNum >= MAX_SIXELS) { + sixelNum = 0; + } + sixelBitmap[sixelNum] = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888); + sixelBitmap[sixelNum].eraseColor(0); + sixelWidth[sixelNum] = 0; + sixelHeight[sixelNum] = 0; + sixelX = 0; + sixelY = 0; + sixelColorMap = new int[256]; + for (int i=0; i<16; i++) { + sixelColorMap[i] = sixelInitialColorMap[i]; + } + } + + public void sixelChar(int c, int rep) { + if (c == '$') { + sixelX = 0; + return; + } + if (c == '-') { + sixelX = 0; + sixelY += 6; + return; + } + if (sixelBitmap[sixelNum].getWidth() < sixelX + rep) { + sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelX + rep + 100, sixelBitmap[sixelNum].getHeight()); + } + if (sixelBitmap[sixelNum].getHeight() < sixelY + 6) { + // Very unlikely to resize both at the same time + sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelBitmap[sixelNum].getWidth(), sixelY + 100); + } + while (rep-- > 0) { + if (c >= '?' && c <= '~') { + int b = c - '?'; + for (int i = 0 ; i < 6 ; i++) { + if ((b & (1< sixelWidth[sixelNum]) { + sixelWidth[sixelNum] = sixelX; + } + if (sixelY + 6 > sixelHeight[sixelNum]) { + sixelHeight[sixelNum] = sixelY + 6; + } + } + } + } + + public void sixelSetColor(int col) { + if (col >= 0 && col < 256) { + sixelColor = sixelColorMap[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); + sixelColor = 0xff000000 + (red << 16) + (green << 8) + blue; + sixelColorMap[col] = sixelColor; + } + } + + public int sixelEnd(int Y, int X, int cellW, int cellH) { + sixelCellW[sixelNum] = cellW; + sixelCellH[sixelNum] = cellH; + int w = Math.min(mColumns - X,(sixelWidth[sixelNum] + cellW - 1) / cellW); + int h = (sixelHeight[sixelNum] + cellH - 1) / cellH; + int s = 0; + for (int i=0; i 0 || height > 0) { + if (aspect) { + double wFactor = 9999.0; + double hFactor = 9999.0; + if (width > 0) { + wFactor = (double)width / bm.getWidth(); + } + if (height > 0) { + hFactor = (double)height / bm.getHeight(); + } + double factor = Math.min(wFactor, hFactor); + bm = Bitmap.createScaledBitmap(bm, (int)(factor * bm.getWidth()), (int)(factor * bm.getHeight()), true); + } else { + if (height <= 0) { + height = bm.getHeight(); + } + if (width <= 0) { + width = bm.getWidth(); + } + bm = Bitmap.createScaledBitmap(bm, width, height, true); + } + if (bm == null) { + return new int[] {0,0}; + } + } + sixelNum++; + if (sixelNum >= MAX_SIXELS) { + sixelNum = 0; + } + sixelBitmap[sixelNum] = bm; + sixelWidth[sixelNum] = sixelBitmap[sixelNum].getWidth(); + sixelHeight[sixelNum] = sixelBitmap[sixelNum].getHeight(); + if ((sixelWidth[sixelNum] % cellW) != 0 || (sixelHeight[sixelNum] % cellH) != 0) { + sixelBitmap[sixelNum] = resizeBitmap(bm, ((sixelWidth[sixelNum]-1) / cellW) * cellW + cellW, ((sixelHeight[sixelNum]-1) / cellH) * cellH + cellH); + } + int lines = sixelEnd(Y, X, cellW, cellH); + return new int[] {lines, (sixelWidth[sixelNum] + cellW - 1) / cellW}; + } + + public void sixelGC(int timeDelta) { + if (!sixelHasBitmaps || sixelLastGC + timeDelta > SystemClock.uptimeMillis()) { + return; + } + Set bitmaps = 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.decodeBitmap(st)) { + bitmaps.add((int)(st >> 16) & 0xffff); + } + } + } + } + for (int bm = 0; bm < MAX_SIXELS; bm++) { + if (bm != sixelNum && sixelBitmap[bm] != null && !bitmaps.contains(bm)) { + sixelBitmap[bm] = null; + } + } + sixelLastGC = SystemClock.uptimeMillis(); + } + /** * Create a transcript screen. * @@ -35,6 +240,12 @@ public TerminalBuffer(int columns, int totalRows, int screenRows) { mLines = new TerminalRow[totalRows]; blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL); + sixelBitmap = new Bitmap[MAX_SIXELS]; + sixelCellW = new int[MAX_SIXELS]; + sixelCellH = new int[MAX_SIXELS]; + sixelWidth = new int[MAX_SIXELS]; + sixelHeight = new int[MAX_SIXELS]; + sixelLastGC = SystemClock.uptimeMillis(); } public String getTranscriptText() { @@ -401,6 +612,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 bitmaps = new HashSet(); + if(mLines[blankRow].mHasBitmap) { + for (int column = 0; column < mColumns; column++) { + final long st = mLines[blankRow].getStyle(column); + if (TextStyle.decodeBitmap(st)) { + bitmaps.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.decodeBitmap(st)) { + bitmaps.remove((int)(st >> 16) & 0xffff); + } + } + } + for(Integer bm: bitmaps) { + sixelBitmap[bm] = null; + } + } mLines[blankRow].clear(style); } } @@ -492,6 +725,10 @@ public void clearTranscript() { Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null); } mActiveTranscriptRows = 0; + for(int i = 0; i < MAX_SIXELS; i++) { + sixelBitmap[i] = null; + } + sixelHasBitmaps = false; } } 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..155baa7b76 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,13 +568,15 @@ private void processByte(byte byteToProcess) { } public void processCodePoint(int b) { + mScreen.sixelGC(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). @@ -587,10 +604,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(!ESC_P_sixel) { + // Ignore CR/LF inside sixels + doLinefeed(); + } break; case 13: // Carriage return (CR, \r). - setCursorCol(mLeftMargin); + if(!ESC_P_sixel) { + // Ignore CR/LF inside sixels + 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 +633,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 +837,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 +922,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 +1033,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 +1137,7 @@ private void doDeviceControl(int b) { mOSCOrDeviceControlArgs.appendCodePoint(b); continueSequence(mEscapeState); } - } + } } private int nextTabStop(int numTabs) { @@ -1389,6 +1520,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 +1534,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 +1765,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 +1852,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 +2007,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 +2044,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; } } @@ -2035,6 +2217,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 (ESC_OSC_colon >= 0 && mOSCOrDeviceControlArgs.length() > ESC_OSC_colon) { + while (mOSCOrDeviceControlArgs.length() - ESC_OSC_colon < 4) { + mOSCOrDeviceControlArgs.append('='); + } + 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); + } + if (ESC_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; 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..0cae974ae9 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.decodeBitmap(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..de36d20ff4 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,8 @@ public static int decodeEffect(long style) { return (int) (style & 0b11111111111); } + public static boolean decodeBitmap(long style) { + return (style & 0x8000) != 0; + } + } 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..debeea6f1c 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.decodeBitmap(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; + 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 From 35a7bb851c9abf81ad8e3245ab69e31b3b1bdf1e Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Sun, 4 Sep 2022 22:10:33 +0300 Subject: [PATCH 02/14] `\007` is bell, only if not ending APC or OSC. Add missing {} that change the logic. --- .../main/java/com/termux/terminal/TerminalEmulator.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 155baa7b76..40b6211c2c 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -575,9 +575,12 @@ public void processCodePoint(int b) { 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 if (mEscapeState == ESC_APC) - doApc(b); + else { + if (mEscapeState == ESC_APC) { + doApc(b); + } mSession.onBell(); + } break; case 8: // Backspace (BS, ^H). if (mLeftMargin == mCursorCol) { From 92dac00cbaad799bc6f712ea1deaaae28d4867db Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Tue, 6 Sep 2022 21:14:51 +0300 Subject: [PATCH 03/14] Better handling of Out of Memory errors in bitmap allocations. - For iterm2 images - catch the error, and cancel the image. - For sixels - if it happens when resizing the bitmap, than ignore drawing outside of the current image. --- .../com/termux/terminal/TerminalBuffer.java | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) 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 590205c15d..c5b326e20a 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -48,7 +48,9 @@ private 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 = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - newbm.setPixels(pixels, 0, bm.getWidth(), 0, 0, bm.getWidth(), bm.getHeight()); + 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; } @@ -70,7 +72,12 @@ public void sixelStart(int width, int height) { if (sixelNum >= MAX_SIXELS) { sixelNum = 0; } - sixelBitmap[sixelNum] = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888); + try { + sixelBitmap[sixelNum] = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError e) { + sixelBitmap[sixelNum] = null; + sixelNum--; + } sixelBitmap[sixelNum].eraseColor(0); sixelWidth[sixelNum] = 0; sixelHeight[sixelNum] = 0; @@ -83,6 +90,9 @@ public void sixelStart(int width, int height) { } public void sixelChar(int c, int rep) { + if (sixelBitmap[sixelNum] == null) { + return; + } if (c == '$') { sixelX = 0; return; @@ -93,11 +103,23 @@ public void sixelChar(int c, int rep) { return; } if (sixelBitmap[sixelNum].getWidth() < sixelX + rep) { - sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelX + rep + 100, sixelBitmap[sixelNum].getHeight()); + try { + sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelX + rep + 100, sixelBitmap[sixelNum].getHeight()); + } catch(OutOfMemoryError e) { + } } if (sixelBitmap[sixelNum].getHeight() < sixelY + 6) { // Very unlikely to resize both at the same time - sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelBitmap[sixelNum].getWidth(), sixelY + 100); + try { + sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelBitmap[sixelNum].getWidth(), sixelY + 100); + } catch(OutOfMemoryError e) { + } + } + if (sixelX + rep > sixelBitmap[sixelNum].getWidth()) { + rep = sixelBitmap[sixelNum].getWidth() - sixelX; + } + if ( sixelY + 5 > sixelBitmap[sixelNum].getHeight()) { + return; } while (rep-- > 0) { if (c >= '?' && c <= '~') { @@ -135,6 +157,9 @@ public void sixelSetColor(int col, int r, int g, int b) { } public int sixelEnd(int Y, int X, int cellW, int cellH) { + if (sixelBitmap[sixelNum] == null) { + return 0; + } sixelCellW[sixelNum] = cellW; sixelCellH[sixelNum] = cellH; int w = Math.min(mColumns - X,(sixelWidth[sixelNum] + cellW - 1) / cellW); @@ -158,7 +183,11 @@ public int sixelEnd(int Y, int X, int cellW, int cellH) { } public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int width, int height, boolean aspect) { - Bitmap bm = BitmapFactory.decodeByteArray(image, 0, image.length); + Bitmap bm = null; + try { + bm = BitmapFactory.decodeByteArray(image, 0, image.length); + } catch(OutOfMemoryError e) { + } if (bm == null) { return new int[] {0,0}; } @@ -174,7 +203,11 @@ public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int widt hFactor = (double)height / bm.getHeight(); } double factor = Math.min(wFactor, hFactor); - bm = Bitmap.createScaledBitmap(bm, (int)(factor * bm.getWidth()), (int)(factor * bm.getHeight()), true); + try { + bm = Bitmap.createScaledBitmap(bm, (int)(factor * bm.getWidth()), (int)(factor * bm.getHeight()), true); + } catch(OutOfMemoryError e) { + bm = null; + } } else { if (height <= 0) { height = bm.getHeight(); @@ -182,7 +215,11 @@ public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int widt if (width <= 0) { width = bm.getWidth(); } - bm = Bitmap.createScaledBitmap(bm, width, height, true); + try { + bm = Bitmap.createScaledBitmap(bm, width, height, true); + } catch(OutOfMemoryError e) { + bm = null; + } } if (bm == null) { return new int[] {0,0}; @@ -195,11 +232,19 @@ public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int widt sixelBitmap[sixelNum] = bm; sixelWidth[sixelNum] = sixelBitmap[sixelNum].getWidth(); sixelHeight[sixelNum] = sixelBitmap[sixelNum].getHeight(); - if ((sixelWidth[sixelNum] % cellW) != 0 || (sixelHeight[sixelNum] % cellH) != 0) { - sixelBitmap[sixelNum] = resizeBitmap(bm, ((sixelWidth[sixelNum]-1) / cellW) * cellW + cellW, ((sixelHeight[sixelNum]-1) / cellH) * cellH + cellH); + if (sixelWidth[sixelNum] > cellW * mColumns || (sixelWidth[sixelNum] % cellW) != 0 || (sixelHeight[sixelNum] % cellH) != 0) { + try { + sixelBitmap[sixelNum] = resizeBitmap(bm, Math.min(cellW * mColumns, ((sixelWidth[sixelNum]-1) / cellW) * cellW + cellW), + ((sixelHeight[sixelNum]-1) / cellH) * cellH + cellH); + } catch(OutOfMemoryError e) { + sixelBitmap[sixelNum] = null; + sixelNum--; + return new int[] {0,0}; + } } int lines = sixelEnd(Y, X, cellW, cellH); return new int[] {lines, (sixelWidth[sixelNum] + cellW - 1) / cellW}; + } public void sixelGC(int timeDelta) { From 6b46ae7052db57d7aaaf5d724de12ab68693766b Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Wed, 7 Sep 2022 16:16:23 +0300 Subject: [PATCH 04/14] Refactor bitmap code - Move working bitmap code (drawing current bitmap) to WorkingTerminalBitmap class. - Move bitmap handling code to TerminalBitmap class. --- .../com/termux/terminal/TerminalBitmap.java | 180 +++++++++ .../com/termux/terminal/TerminalBuffer.java | 356 +++++------------- .../com/termux/terminal/TerminalEmulator.java | 10 +- .../java/com/termux/terminal/TerminalRow.java | 2 +- .../java/com/termux/terminal/TextStyle.java | 18 +- .../terminal/WorkingTerminalBitmap.java | 110 ++++++ .../com/termux/view/TerminalRenderer.java | 2 +- 7 files changed, 415 insertions(+), 263 deletions(-) create mode 100644 terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java create mode 100644 terminal-emulator/src/main/java/com/termux/terminal/WorkingTerminalBitmap.java 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..e639b6b9cc --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java @@ -0,0 +1,180 @@ +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"); + } + 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 c5b326e20a..8a8506c052 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.HashMap; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -28,247 +29,10 @@ public final class TerminalBuffer { /** The index in the circular buffer where the visible screen starts. */ private int mScreenFirstRow = 0; - final private int MAX_SIXELS = 1024; - private Bitmap sixelBitmap[]; - private int sixelCellW[]; - private int sixelCellH[]; - private int sixelWidth[]; - private int sixelHeight[]; - private int sixelNum = -1; - private int sixelX; - private int sixelY; - private int[] sixelColorMap; - private int sixelColor; - private long sixelLastGC; - private boolean sixelHasBitmaps = false; - final private int sixelInitialColorMap[] = {0xFF000000, 0xFF3333CC, 0xFFCC2323, 0xFF33CC33, 0xFFCC33CC, 0xFF33CCCC, 0xFFCCCC33, 0xFF777777, - 0xFF444444, 0xFF565699, 0xFF994444, 0xFF569956, 0xFF995699, 0xFF569999, 0xFF999956, 0xFFCCCCCC}; - - private 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 = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - 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; - } - - public Bitmap getSixelBitmap(int codePoint, long style) { - int sn = (int)(style & 0xffff0000) >> 16 ; - return sixelBitmap[sn]; - } - - public Rect getSixelRect(int codePoint, long style ) { - int sn = (int)(style & 0xffff0000) >> 16 ; - int x = (int)((style >> 48) & 0xfff); - int y = (int)((style >> 32) & 0xfff); - Rect r = new Rect(x * sixelCellW[sn], y * sixelCellH[sn], (x+1) * sixelCellW[sn], (y+1) * sixelCellH[sn]); - return r; - } - - public void sixelStart(int width, int height) { - sixelNum++; - if (sixelNum >= MAX_SIXELS) { - sixelNum = 0; - } - try { - sixelBitmap[sixelNum] = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888); - } catch (OutOfMemoryError e) { - sixelBitmap[sixelNum] = null; - sixelNum--; - } - sixelBitmap[sixelNum].eraseColor(0); - sixelWidth[sixelNum] = 0; - sixelHeight[sixelNum] = 0; - sixelX = 0; - sixelY = 0; - sixelColorMap = new int[256]; - for (int i=0; i<16; i++) { - sixelColorMap[i] = sixelInitialColorMap[i]; - } - } - - public void sixelChar(int c, int rep) { - if (sixelBitmap[sixelNum] == null) { - return; - } - if (c == '$') { - sixelX = 0; - return; - } - if (c == '-') { - sixelX = 0; - sixelY += 6; - return; - } - if (sixelBitmap[sixelNum].getWidth() < sixelX + rep) { - try { - sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelX + rep + 100, sixelBitmap[sixelNum].getHeight()); - } catch(OutOfMemoryError e) { - } - } - if (sixelBitmap[sixelNum].getHeight() < sixelY + 6) { - // Very unlikely to resize both at the same time - try { - sixelBitmap[sixelNum] = resizeBitmap(sixelBitmap[sixelNum], sixelBitmap[sixelNum].getWidth(), sixelY + 100); - } catch(OutOfMemoryError e) { - } - } - if (sixelX + rep > sixelBitmap[sixelNum].getWidth()) { - rep = sixelBitmap[sixelNum].getWidth() - sixelX; - } - if ( sixelY + 5 > sixelBitmap[sixelNum].getHeight()) { - return; - } - while (rep-- > 0) { - if (c >= '?' && c <= '~') { - int b = c - '?'; - for (int i = 0 ; i < 6 ; i++) { - if ((b & (1< sixelWidth[sixelNum]) { - sixelWidth[sixelNum] = sixelX; - } - if (sixelY + 6 > sixelHeight[sixelNum]) { - sixelHeight[sixelNum] = sixelY + 6; - } - } - } - } - - public void sixelSetColor(int col) { - if (col >= 0 && col < 256) { - sixelColor = sixelColorMap[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); - sixelColor = 0xff000000 + (red << 16) + (green << 8) + blue; - sixelColorMap[col] = sixelColor; - } - } - - public int sixelEnd(int Y, int X, int cellW, int cellH) { - if (sixelBitmap[sixelNum] == null) { - return 0; - } - sixelCellW[sixelNum] = cellW; - sixelCellH[sixelNum] = cellH; - int w = Math.min(mColumns - X,(sixelWidth[sixelNum] + cellW - 1) / cellW); - int h = (sixelHeight[sixelNum] + cellH - 1) / cellH; - int s = 0; - for (int i=0; i 0 || height > 0) { - if (aspect) { - double wFactor = 9999.0; - double hFactor = 9999.0; - if (width > 0) { - wFactor = (double)width / bm.getWidth(); - } - if (height > 0) { - hFactor = (double)height / bm.getHeight(); - } - double factor = Math.min(wFactor, hFactor); - try { - bm = Bitmap.createScaledBitmap(bm, (int)(factor * bm.getWidth()), (int)(factor * bm.getHeight()), true); - } catch(OutOfMemoryError e) { - bm = null; - } - } else { - if (height <= 0) { - height = bm.getHeight(); - } - if (width <= 0) { - width = bm.getWidth(); - } - try { - bm = Bitmap.createScaledBitmap(bm, width, height, true); - } catch(OutOfMemoryError e) { - bm = null; - } - } - if (bm == null) { - return new int[] {0,0}; - } - } - sixelNum++; - if (sixelNum >= MAX_SIXELS) { - sixelNum = 0; - } - sixelBitmap[sixelNum] = bm; - sixelWidth[sixelNum] = sixelBitmap[sixelNum].getWidth(); - sixelHeight[sixelNum] = sixelBitmap[sixelNum].getHeight(); - if (sixelWidth[sixelNum] > cellW * mColumns || (sixelWidth[sixelNum] % cellW) != 0 || (sixelHeight[sixelNum] % cellH) != 0) { - try { - sixelBitmap[sixelNum] = resizeBitmap(bm, Math.min(cellW * mColumns, ((sixelWidth[sixelNum]-1) / cellW) * cellW + cellW), - ((sixelHeight[sixelNum]-1) / cellH) * cellH + cellH); - } catch(OutOfMemoryError e) { - sixelBitmap[sixelNum] = null; - sixelNum--; - return new int[] {0,0}; - } - } - int lines = sixelEnd(Y, X, cellW, cellH); - return new int[] {lines, (sixelWidth[sixelNum] + cellW - 1) / cellW}; - - } - - public void sixelGC(int timeDelta) { - if (!sixelHasBitmaps || sixelLastGC + timeDelta > SystemClock.uptimeMillis()) { - return; - } - Set bitmaps = 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.decodeBitmap(st)) { - bitmaps.add((int)(st >> 16) & 0xffff); - } - } - } - } - for (int bm = 0; bm < MAX_SIXELS; bm++) { - if (bm != sixelNum && sixelBitmap[bm] != null && !bitmaps.contains(bm)) { - sixelBitmap[bm] = null; - } - } - sixelLastGC = SystemClock.uptimeMillis(); - } + public HashMap bitmaps; + public WorkingTerminalBitmap workingBitmap; + private boolean hasBitmaps; + private long bitmapLastGC; /** * Create a transcript screen. @@ -285,12 +49,9 @@ public TerminalBuffer(int columns, int totalRows, int screenRows) { mLines = new TerminalRow[totalRows]; blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL); - sixelBitmap = new Bitmap[MAX_SIXELS]; - sixelCellW = new int[MAX_SIXELS]; - sixelCellH = new int[MAX_SIXELS]; - sixelWidth = new int[MAX_SIXELS]; - sixelHeight = new int[MAX_SIXELS]; - sixelLastGC = SystemClock.uptimeMillis(); + hasBitmaps = false; + bitmaps = new HashMap(); + bitmapLastGC = SystemClock.uptimeMillis(); } public String getTranscriptText() { @@ -658,25 +419,25 @@ public void scrollDownOneLine(int topMargin, int bottomMargin, long style) { mLines[blankRow] = new TerminalRow(mColumns, style); } else { // find if a bitmap is completely scrolled out - Set bitmaps = new HashSet(); + Set used = new HashSet(); if(mLines[blankRow].mHasBitmap) { for (int column = 0; column < mColumns; column++) { final long st = mLines[blankRow].getStyle(column); - if (TextStyle.decodeBitmap(st)) { - bitmaps.add((int)(st >> 16) & 0xffff); + 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.decodeBitmap(st)) { - bitmaps.remove((int)(st >> 16) & 0xffff); + if (TextStyle.isBitmap(st)) { + used.remove((int)(st >> 16) & 0xffff); } } } - for(Integer bm: bitmaps) { - sixelBitmap[bm] = null; + for(Integer bm: used) { + bitmaps.remove(bm); } } mLines[blankRow].clear(style); @@ -770,10 +531,91 @@ public void clearTranscript() { Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null); } mActiveTranscriptRows = 0; - for(int i = 0; i < MAX_SIXELS; i++) { - sixelBitmap[i] = null; + 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; } - sixelHasBitmaps = false; + 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); + } + } + } + } + for (Integer bn: bitmaps.keySet()) { + 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 40b6211c2c..fc17363a3e 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -568,7 +568,7 @@ private void processByte(byte byteToProcess) { } public void processCodePoint(int b) { - mScreen.sixelGC(300000); + mScreen.bitmapGC(300000); switch (b) { case 0: // Null character (NUL, ^@). Do nothing. break; @@ -607,13 +607,13 @@ public void processCodePoint(int b) { case 10: // Line feed (LF, \n). case 11: // Vertical tab (VT, \v). case 12: // Form feed (FF, \f). - if(!ESC_P_sixel) { + if(mEscapeState != ESC_P || !ESC_P_sixel) { // Ignore CR/LF inside sixels doLinefeed(); } break; case 13: // Carriage return (CR, \r). - if(!ESC_P_sixel) { + if(mEscapeState != ESC_P || !ESC_P_sixel) { // Ignore CR/LF inside sixels setCursorCol(mLeftMargin); } @@ -2739,6 +2739,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 0cae974ae9..8d6aa46e78 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java @@ -132,7 +132,7 @@ public void setChar(int columnToSet, int codePoint, long style) { mStyle[columnToSet] = style; - if (!mHasBitmap && TextStyle.decodeBitmap(style)) { + if (!mHasBitmap && TextStyle.isBitmap(style)) { mHasBitmap = true; } 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 de36d20ff4..7ee4b06ebc 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TextStyle.java @@ -89,8 +89,24 @@ public static int decodeEffect(long style) { return (int) (style & 0b11111111111); } - public static boolean decodeBitmap(long style) { + 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 debeea6f1c..34e764b0a2 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -103,7 +103,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, 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.decodeBitmap(style)) { + if (TextStyle.isBitmap(style)) { Bitmap bm = mEmulator.getScreen().getSixelBitmap(codePoint, style); if (bm != null) { float left = column * mFontWidth; From 1ce120edc5a891036e93bab893e1a93734704bb4 Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Thu, 8 Sep 2022 11:34:17 +0300 Subject: [PATCH 05/14] Copy the set of keys from the map before iterating. To avoid removing elements from the map while iterating over it. This should solve the Concurrent Modification Exception in the bitmap garbage collection. --- .../src/main/java/com/termux/terminal/TerminalBuffer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8a8506c052..40cce97496 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -611,7 +611,8 @@ public void bitmapGC(int timeDelta) { } } } - for (Integer bn: bitmaps.keySet()) { + Set keys = new HashSet(bitmaps.keySet()); + for (Integer bn: keys) { if (!used.contains(bn)) { bitmaps.remove(bn); } From a25a55e693151b901bd359b467e822a6fb327207 Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Sat, 17 Sep 2022 16:16:44 +0300 Subject: [PATCH 06/14] Handle another failure path Avoid crash when BitmapFactory cannot decode image --- .../src/main/java/com/termux/terminal/TerminalBitmap.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java index e639b6b9cc..77c75144d3 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java @@ -77,6 +77,13 @@ public TerminalBitmap(int num, byte[] image, int Y, int X, int cellW, int cellH, 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) { From 8eb080ad0551ec8e1a0c636923c659e859a951fd Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Sat, 17 Sep 2022 17:30:50 +0300 Subject: [PATCH 07/14] Ignore line feeds (and similar control characters) inside base64 data --- .../com/termux/terminal/TerminalEmulator.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 fc17363a3e..92e28cf136 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -607,14 +607,14 @@ public void processCodePoint(int b) { case 10: // Line feed (LF, \n). case 11: // Vertical tab (VT, \v). case 12: // Form feed (FF, \f). - if(mEscapeState != ESC_P || !ESC_P_sixel) { - // Ignore CR/LF inside sixels + 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). - if(mEscapeState != ESC_P || !ESC_P_sixel) { - // Ignore CR/LF inside sixels + if((mEscapeState != ESC_P || !ESC_P_sixel) && ESC_OSC_colon <= 0) { + // Ignore CR/LF inside sixels or iterm2 data setCursorCol(mLeftMargin); } break; @@ -2085,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++) { @@ -2282,21 +2284,21 @@ private void doOscSetTextParameters(String bellOrStringTerminator) { finishSequence(); return; } - if (ESC_OSC_colon >= 0 && mOSCOrDeviceControlArgs.length() > ESC_OSC_colon) { - while (mOSCOrDeviceControlArgs.length() - ESC_OSC_colon < 4) { + if (osc_colon >= 0 && mOSCOrDeviceControlArgs.length() > osc_colon) { + while (mOSCOrDeviceControlArgs.length() - osc_colon < 4) { mOSCOrDeviceControlArgs.append('='); } try { - byte[] decoded = Base64.decode(mOSCOrDeviceControlArgs.substring(ESC_OSC_colon), 0); + 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(ESC_OSC_colon); + mOSCOrDeviceControlArgs.setLength(osc_colon); } - if (ESC_OSC_colon >= 0) { + 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(); From 329d5cbb42df10fab15e7747d2ee473278e4ad27 Mon Sep 17 00:00:00 2001 From: Matan Ziv-Av Date: Sun, 18 Sep 2022 16:49:12 +0300 Subject: [PATCH 08/14] Start first text run after the last column of bitmap, instead on the last column. This creates a zero length text run, so skip it. --- .../src/main/java/com/termux/view/TerminalRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 34e764b0a2..e82720a2c9 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -115,7 +115,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, measuredWidthForRun = 0.f; lastRunStyle = 0; lastRunInsideCursor = false; - lastRunStartColumn = column; + lastRunStartColumn = column + 1; lastRunStartIndex = currentCharIndex; lastRunFontWidthMismatch = false; currentCharIndex += charsForCodePoint; @@ -134,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; From c9a4b52e6962f1366067d0590a2ef1268e71743e Mon Sep 17 00:00:00 2001 From: Matt Tew Date: Tue, 2 Jan 2024 10:03:06 +0800 Subject: [PATCH 09/14] Dex immersive mode --- app/src/main/AndroidManifest.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4e95702bc3..a4946ce6a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,7 @@ android:label="@string/application_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" + android:resizeableActivity="true" android:supportsRtl="false" android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar" tools:targetApi="m"> @@ -73,6 +74,11 @@ + + + Date: Tue, 2 Jan 2024 11:23:36 +0800 Subject: [PATCH 10/14] Update AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4946ce6a3..868a6bf66a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,11 @@ + + + Date: Tue, 2 Jan 2024 11:37:32 +0800 Subject: [PATCH 11/14] Update AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 868a6bf66a..9cc309f2bf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,6 +78,9 @@ + + @@ -97,6 +100,9 @@ + + From f7ad05d30fdacc81e7f106d88fe4b6d0c523d6a3 Mon Sep 17 00:00:00 2001 From: Matt Tew Date: Wed, 17 Jan 2024 10:28:30 +0800 Subject: [PATCH 12/14] Add immersive view --- .../java/com/termux/app/TermuxActivity.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 308d1f0b2b..ada7279d0c 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -22,6 +22,8 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.autofill.AutofillManager; +import android.view.WindowInsets; +import android.view.WindowInsetsController; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ListView; @@ -1021,4 +1023,74 @@ public static Intent newInstance(@NonNull final Context context) { return intent; } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + if (mProperties.isUsingFullScreen) { + enableImmersiveMode(); + } else { + disableImmersiveMode(); + } + } + } + + @SuppressWarnings("deprecation") + private void enableImmersiveMode() { + if (Build.VERSION.SDK_INT >= 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); + } + } + }); + } + } } From 83efab3d6cf066eabb3794808f7e0b2ba2bde22a Mon Sep 17 00:00:00 2001 From: Matt Tew Date: Wed, 17 Jan 2024 10:43:52 +0800 Subject: [PATCH 13/14] Remove SDK 30+ code --- .../java/com/termux/app/TermuxActivity.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index ada7279d0c..b08fa9969e 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -22,8 +22,8 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.autofill.AutofillManager; -import android.view.WindowInsets; -import android.view.WindowInsetsController; +// import android.view.WindowInsets; +// import android.view.WindowInsetsController; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ListView; @@ -1037,14 +1037,14 @@ public void onWindowFocusChanged(boolean hasFocus) { @SuppressWarnings("deprecation") private void enableImmersiveMode() { - if (Build.VERSION.SDK_INT >= 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 { + // if (Build.VERSION.SDK_INT >= 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 @@ -1061,23 +1061,23 @@ public void onSystemUiVisibilityChange(int visibility) { } } }); - } + // } } @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 { + // 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 @@ -1091,6 +1091,6 @@ public void onSystemUiVisibilityChange(int visibility) { } } }); - } + // } } } From 782f0c75c8f90dbbc12b778306b0f4c7e6a5f76c Mon Sep 17 00:00:00 2001 From: Matt Tew Date: Wed, 17 Jan 2024 10:52:12 +0800 Subject: [PATCH 14/14] isUsingFullScreen is a method --- app/src/main/java/com/termux/app/TermuxActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index b08fa9969e..1cb8b82ac4 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -1027,7 +1027,7 @@ public static Intent newInstance(@NonNull final Context context) { public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { - if (mProperties.isUsingFullScreen) { + if (mProperties.isUsingFullScreen()) { enableImmersiveMode(); } else { disableImmersiveMode();