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;