Skip to content

Commit

Permalink
Fixed: Implement colon separated CSI parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
fornwall committed Sep 26, 2024
1 parent 438cd73 commit cf25329
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <code>mArgs[N]</code> is a sub parameter. */
private int mArgsSubParamsBitSet = 0;

/** Holds OSC and device control arguments, which can be strings. */
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
Expand Down Expand Up @@ -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.
*
* <p>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
Expand Down Expand Up @@ -1316,6 +1320,7 @@ private void startEscapeSequence() {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
mArgsSubParamsBitSet = 0;
}

private void doLinefeed() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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"
Expand All @@ -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);
Expand All @@ -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).
Expand Down Expand Up @@ -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
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
*
* <p>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.
*
* <p>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
* <p>References:
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
* */
private void parseArg(int b) {
if (b >= '0' && b <= '9') {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -62,4 +64,48 @@ public void testCsi3J() {
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
}

/**
* See <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
*
* <pre>
* <ESC>[4:0m # no underline
* <ESC>[4:1m # straight underline
* <ESC>[4:2m # double underline
* <ESC>[4:3m # curly underline
* <ESC>[4:4m # dotted underline
* <ESC>[4:5m # dashed underline
* <ESC>[4m # straight underline (for backwards compat)
* <ESC>[24m # no underline (for backwards compat)
* </pre>
* <p>
* 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -155,55 +160,59 @@ 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);
enterString("\033[0m");
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);
Expand All @@ -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() {
Expand Down

0 comments on commit cf25329

Please sign in to comment.