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 14fb84865a..bff98b902b 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
@@ -84,8 +84,8 @@ public final class TerminalEmulator {
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
private static final int ESC_APC_ESCAPE = 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;
+ /** The number of parameter arguments including colon separated sub-parameters. */
+ private static final int MAX_ESCAPE_PARAMETERS = 32;
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
private static final int MAX_OSC_STRING_LENGTH = 8192;
@@ -175,6 +175,8 @@ public final class TerminalEmulator {
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
+ /** Holds the bit flags which arguments are sub parameters (after a colon) - bit N is set if mArgs[N]
is a sub parameter. */
+ private int mArgsSubParamsBitSet = 0;
/** Holds OSC and device control arguments, which can be strings. */
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
@@ -235,15 +237,17 @@ public final class TerminalEmulator {
private boolean mCursorBlinkState;
/**
- * Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
+ * Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
*
+ *
Note that the underline color is currently parsed but not yet used during rendering. + * * @see TextStyle */ - int mForeColor, mBackColor; + int mForeColor, mBackColor, mUnderlineColor; /** Current {@link TextStyle} effect. */ - private int mEffect; + int mEffect; /** * The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along @@ -1316,6 +1320,7 @@ private void startEscapeSequence() { mEscapeState = ESC; mArgIndex = 0; Arrays.fill(mArgs, -1); + mArgsSubParamsBitSet = 0; } private void doLinefeed() { @@ -1798,6 +1803,11 @@ private void doCsi(int b) { private void selectGraphicRendition() { if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; for (int i = 0; i <= mArgIndex; i++) { + // Skip leading sub parameters: + if ((mArgsSubParamsBitSet & (1 << i)) != 0) { + continue; + } + int code = getArg(i, 0, false); if (code < 0) { if (mArgIndex > 0) { @@ -1817,7 +1827,19 @@ private void selectGraphicRendition() { } else if (code == 3) { mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC; } else if (code == 4) { - mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) { + // Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/ + i++; + if (mArgs[i] == 0) { + // No underline. + mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + } else { + // Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/ + mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + } + } else { + mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + } } else if (code == 5) { mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK; } else if (code == 7) { @@ -1846,8 +1868,8 @@ private void selectGraphicRendition() { mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH; } else if (code >= 30 && code <= 37) { mForeColor = code - 30; - } else if (code == 38 || code == 48) { - // Extended set foreground(38)/background (48) color. + } else if (code == 38 || code == 48 || code == 58) { + // Extended set foreground(38)/background(48)/underline(58) color. // This is followed by either "2;$R;$G;$B" to set a 24-bit color or // "5;$INDEX" to set an indexed color. if (i + 2 > mArgIndex) continue; @@ -1863,11 +1885,11 @@ private void selectGraphicRendition() { if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) { finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue); } else { - int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue; - if (code == 38) { - mForeColor = argbColor; - } else { - mBackColor = argbColor; + int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue; + switch (code) { + case 38: mForeColor = argbColor; break; + case 48: mBackColor = argbColor; break; + case 58: mUnderlineColor = argbColor; break; } } i += 4; // "2;P_r;P_g;P_r" @@ -1876,10 +1898,10 @@ private void selectGraphicRendition() { int color = getArg(i + 2, 0, false); i += 2; // "5;P_s" if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) { - if (code == 38) { - mForeColor = color; - } else { - mBackColor = color; + switch (code) { + case 38: mForeColor = color; break; + case 48: mBackColor = color; break; + case 58: mUnderlineColor = color; break; } } else { if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color); @@ -1893,6 +1915,8 @@ private void selectGraphicRendition() { mBackColor = code - 40; } else if (code == 49) { // Set default background color. mBackColor = TextStyle.COLOR_INDEX_BACKGROUND; + } else if (code == 59) { // Set default underline color. + mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND; } else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes). mForeColor = code - 90 + 8; } else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes). @@ -2142,15 +2166,21 @@ private void scrollDownOneLine() { /** * Process the next ASCII character of a parameter. * - * Parameter characters modify the action or interpretation of the sequence. You can use up to - * 16 parameters per sequence. You must use the ; character to separate parameters. - * All parameters are unsigned, positive decimal integers, with the most significant + *
You must use the ; character to separate parameters and : to separate sub-parameters. + * + *
Parameter characters modify the action or interpretation of the sequence. Originally + * you can use up to 16 parameters per sequence, but following at least xterm and alacritty + * we use a common space for parameters and sub-parameters, allowing 32 in total. + * + *
All parameters are unsigned, positive decimal integers, with the most significant * digit sent first. Any parameter greater than 9999 (decimal) is set to 9999 * (decimal). If you do not specify a value, a 0 value is assumed. A 0 value * or omitted parameter indicates a default value for the sequence. For most * sequences, the default value is 1. * - * https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3 + *
References: + * VT510 Video Terminal Programmer Information: Control Sequences + * alacritty/vte: Implement colon separated CSI parameters * */ private void parseArg(int b) { if (b >= '0' && b <= '9') { @@ -2168,9 +2198,14 @@ private void parseArg(int b) { mArgs[mArgIndex] = value; } continueSequence(mEscapeState); - } else if (b == ';') { - if (mArgIndex < mArgs.length) { + } else if (b == ';' || b == ':') { + if (mArgIndex + 1 < mArgs.length) { mArgIndex++; + if (b == ':') { + mArgsSubParamsBitSet |= 1 << mArgIndex; + } + } else { + logError("Too many parameters when in state: " + mEscapeState); } continueSequence(mEscapeState); } else { diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java index 5a12edd749..21cf6addef 100644 --- a/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java +++ b/terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java @@ -1,5 +1,7 @@ package com.termux.terminal; +import java.util.List; + /** "\033[" is the Control Sequence Introducer char sequence (CSI). */ public class ControlSequenceIntroducerTest extends TerminalTestCase { @@ -62,4 +64,48 @@ public void testCsi3J() { assertEquals("y\nz", mTerminal.getScreen().getTranscriptText()); } + /** + * See Colored and styled underlines: + * + *
+ *+ *[4:0m # no underline + * [4:1m # straight underline + * [4:2m # double underline + * [4:3m # curly underline + * [4:4m # dotted underline + * [4:5m # dashed underline + * [4m # straight underline (for backwards compat) + * [24m # no underline (for backwards compat) + *
+ * We currently parse the variants, but map them to normal/no underlines as appropriate + */ + public void testUnderlineVariants() { + for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) { + for (String stop : List.of("24", "4:0")) { + withTerminalSized(3, 3); + enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " "); + assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect); + enterString("\033[4;1m").assertLinesAre(" ", " ", " "); + assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect); + enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " "); + assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect); + } + } + } + + public void testManyParameters() { + StringBuilder b = new StringBuilder("\033["); + for (int i = 0; i < 30; i++) { + b.append("0;"); + } + b.append("4:2"); + // This clearing of underline should be ignored as the parameters pass the threshold for too many parameters: + b.append("4:0m"); + withTerminalSized(3, 3) + .enterString(b.toString()) + .assertLinesAre(" ", " ", " "); + assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect); + } + } diff --git a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java index e3efa81d3f..a52d88e259 100644 --- a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java +++ b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java @@ -137,6 +137,11 @@ public void testPaste() { } public void testSelectGraphics() { + selectGraphicsTestRun(';'); + selectGraphicsTestRun(':'); + } + + public void selectGraphicsTestRun(char separator) { withTerminalSized(5, 5); enterString("\033[31m"); assertEquals(mTerminal.mForeColor, 1); @@ -155,16 +160,20 @@ public void testSelectGraphics() { // Check TerminalEmulator.parseArg() enterString("\033[31m\033[m"); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); - enterString("\033[31m\033[;m"); + enterString("\033[31m\033[;m".replace(';', separator)); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); enterString("\033[31m\033[0m"); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); - enterString("\033[31m\033[0;m"); + enterString("\033[31m\033[0;m".replace(';', separator)); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); enterString("\033[31;;m"); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); + enterString("\033[31::m"); + assertEquals(1, mTerminal.mForeColor); enterString("\033[31;m"); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); + enterString("\033[31:m"); + assertEquals(1, mTerminal.mForeColor); enterString("\033[31;;41m"); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); assertEquals(1, mTerminal.mBackColor); @@ -172,38 +181,38 @@ public void testSelectGraphics() { assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); // 256 colors: - enterString("\033[38;5;119m"); + enterString("\033[38;5;119m".replace(';', separator)); assertEquals(119, mTerminal.mForeColor); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); - enterString("\033[48;5;129m"); + enterString("\033[48;5;129m".replace(';', separator)); assertEquals(119, mTerminal.mForeColor); assertEquals(129, mTerminal.mBackColor); // Invalid parameter: - enterString("\033[48;8;129m"); + enterString("\033[48;8;129m".replace(';', separator)); assertEquals(119, mTerminal.mForeColor); assertEquals(129, mTerminal.mBackColor); // Multiple parameters at once: - enterString("\033[38;5;178;48;5;179m"); + enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator)); assertEquals(178, mTerminal.mForeColor); assertEquals(179, mTerminal.mBackColor); // Omitted parameter means zero: - enterString("\033[38;5;m"); + enterString("\033[38;5;m".replace(';', separator)); assertEquals(0, mTerminal.mForeColor); assertEquals(179, mTerminal.mBackColor); - enterString("\033[48;5;m"); + enterString("\033[48;5;m".replace(';', separator)); assertEquals(0, mTerminal.mForeColor); assertEquals(0, mTerminal.mBackColor); // 24 bit colors: enterString(("\033[0m")); // Reset fg and bg colors. - enterString("\033[38;2;255;127;2m"); + enterString("\033[38;2;255;127;2m".replace(';', separator)); int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2; assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); - enterString("\033[48;2;1;2;254m"); + enterString("\033[48;2;1;2;254m".replace(';', separator)); int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254; assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(expectedBackground, mTerminal.mBackColor); @@ -212,24 +221,30 @@ public void testSelectGraphics() { enterString(("\033[0m")); // Reset fg and bg colors. assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); - enterString("\033[38;2;255;127;2;48;2;1;2;254m"); + enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator)); assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(expectedBackground, mTerminal.mBackColor); // 24 bit colors, invalid input: - enterString("\033[38;2;300;127;2;48;2;1;300;254m"); + enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator)); assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(expectedBackground, mTerminal.mBackColor); // 24 bit colors, omitted parameter means zero: - enterString("\033[38;2;255;127;m"); + enterString("\033[38;2;255;127;m".replace(';', separator)); expectedForeground = 0xff000000 | (255 << 16) | (127 << 8); assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(expectedBackground, mTerminal.mBackColor); - enterString("\033[38;2;123;;77m"); + enterString("\033[38;2;123;;77m".replace(';', separator)); expectedForeground = 0xff000000 | (123 << 16) | 77; assertEquals(expectedForeground, mTerminal.mForeColor); assertEquals(expectedBackground, mTerminal.mBackColor); + + // 24 bit colors, extra sub-parameters are skipped: + expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2; + enterString("\033[0;38:2:255:127:2:48:2:1:2:254m"); + assertEquals(expectedForeground, mTerminal.mForeColor); + assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); } public void testBackgroundColorErase() {