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 Oct 21, 2024
1 parent cc05960 commit d325985
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,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.
Expand Down Expand Up @@ -276,6 +274,8 @@ public final class TerminalEmulator {
* 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.
Expand Down Expand Up @@ -335,14 +335,16 @@ public final class TerminalEmulator {
* Current foreground and background 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 @@ -1406,6 +1408,7 @@ private void startEscapeSequence() {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
mArgsSubParamsBitSet = 0;
}

private void doLinefeed() {
Expand Down Expand Up @@ -1900,6 +1903,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 @@ -1919,7 +1927,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 @@ -1948,8 +1968,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 @@ -1964,11 +1984,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 @@ -1977,10 +1997,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) Log.w(LOG_TAG, "Invalid color index: " + color);
Expand All @@ -1994,6 +2014,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 @@ -2246,16 +2268,22 @@ private void scrollDownOneLine() {

/**
* Process the next ASCII character of a parameter.
* <p>
* 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.
* <p>
* 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 @@ -2273,9 +2301,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 @@ -82,4 +84,48 @@ public void testReportPixelSize() {
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
}

/**
* 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 @@ -143,6 +143,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 @@ -161,55 +166,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");
assertEquals(0, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
enterString("\033[48;5;m");
assertEquals(0, mTerminal.mForeColor);
assertEquals(0, mTerminal.mBackColor);
// Omitted parameter means zero:
enterString("\033[38;5;m".replace(';', separator));
assertEquals(0, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
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 @@ -218,24 +227,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".replace(';', separator));
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
enterString("\033[38;2;123;;77m".replace(';', separator));
expectedForeground = 0xff000000 | (123 << 16) | 77;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);

// 24 bit colors, omitted parameter means zero:
enterString("\033[38;2;255;127;m");
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
enterString("\033[38;2;123;;77m");
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 d325985

Please sign in to comment.