Skip to content

Commit 8be5333

Browse files
committed
Fixed: Implement colon separated CSI parameters
1 parent d90e3fb commit 8be5333

File tree

3 files changed

+133
-37
lines changed

3 files changed

+133
-37
lines changed

terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java

+58-23
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public final class TerminalEmulator {
8484
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
8585
private static final int ESC_APC_ESCAPE = 21;
8686

87-
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
88-
private static final int MAX_ESCAPE_PARAMETERS = 16;
87+
/** The number of parameter arguments including colon separated sub-parameters. */
88+
private static final int MAX_ESCAPE_PARAMETERS = 32;
8989

9090
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
9191
private static final int MAX_OSC_STRING_LENGTH = 8192;
@@ -178,6 +178,8 @@ public final class TerminalEmulator {
178178
private int mArgIndex;
179179
/** Holds the arguments of the current escape sequence. */
180180
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
181+
/** 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. */
182+
private int mArgsSubParamsBitSet = 0;
181183

182184
/** Holds OSC and device control arguments, which can be strings. */
183185
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
@@ -238,15 +240,17 @@ public final class TerminalEmulator {
238240
private boolean mCursorBlinkState;
239241

240242
/**
241-
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
243+
* Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
242244
* For a 24-bit value the top byte (0xff000000) is set.
243245
*
246+
* <p>Note that the underline color is currently parsed but not yet used during rendering.
247+
*
244248
* @see TextStyle
245249
*/
246-
int mForeColor, mBackColor;
250+
int mForeColor, mBackColor, mUnderlineColor;
247251

248252
/** Current {@link TextStyle} effect. */
249-
private int mEffect;
253+
int mEffect;
250254

251255
/**
252256
* The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along
@@ -1324,6 +1328,7 @@ private void startEscapeSequence() {
13241328
mEscapeState = ESC;
13251329
mArgIndex = 0;
13261330
Arrays.fill(mArgs, -1);
1331+
mArgsSubParamsBitSet = 0;
13271332
}
13281333

13291334
private void doLinefeed() {
@@ -1808,6 +1813,11 @@ private void doCsi(int b) {
18081813
private void selectGraphicRendition() {
18091814
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
18101815
for (int i = 0; i <= mArgIndex; i++) {
1816+
// Skip leading sub parameters:
1817+
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
1818+
continue;
1819+
}
1820+
18111821
int code = getArg(i, 0, false);
18121822
if (code < 0) {
18131823
if (mArgIndex > 0) {
@@ -1827,7 +1837,19 @@ private void selectGraphicRendition() {
18271837
} else if (code == 3) {
18281838
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
18291839
} else if (code == 4) {
1830-
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1840+
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
1841+
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
1842+
i++;
1843+
if (mArgs[i] == 0) {
1844+
// No underline.
1845+
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1846+
} else {
1847+
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
1848+
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1849+
}
1850+
} else {
1851+
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1852+
}
18311853
} else if (code == 5) {
18321854
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
18331855
} else if (code == 7) {
@@ -1856,8 +1878,8 @@ private void selectGraphicRendition() {
18561878
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
18571879
} else if (code >= 30 && code <= 37) {
18581880
mForeColor = code - 30;
1859-
} else if (code == 38 || code == 48) {
1860-
// Extended set foreground(38)/background (48) color.
1881+
} else if (code == 38 || code == 48 || code == 58) {
1882+
// Extended set foreground(38)/background(48)/underline(58) color.
18611883
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
18621884
// "5;$INDEX" to set an indexed color.
18631885
if (i + 2 > mArgIndex) continue;
@@ -1873,11 +1895,11 @@ private void selectGraphicRendition() {
18731895
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
18741896
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
18751897
} else {
1876-
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
1877-
if (code == 38) {
1878-
mForeColor = argbColor;
1879-
} else {
1880-
mBackColor = argbColor;
1898+
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
1899+
switch (code) {
1900+
case 38: mForeColor = argbColor; break;
1901+
case 48: mBackColor = argbColor; break;
1902+
case 58: mUnderlineColor = argbColor; break;
18811903
}
18821904
}
18831905
i += 4; // "2;P_r;P_g;P_r"
@@ -1886,10 +1908,10 @@ private void selectGraphicRendition() {
18861908
int color = getArg(i + 2, 0, false);
18871909
i += 2; // "5;P_s"
18881910
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
1889-
if (code == 38) {
1890-
mForeColor = color;
1891-
} else {
1892-
mBackColor = color;
1911+
switch (code) {
1912+
case 38: mForeColor = color; break;
1913+
case 48: mBackColor = color; break;
1914+
case 58: mUnderlineColor = color; break;
18931915
}
18941916
} else {
18951917
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
@@ -1903,6 +1925,8 @@ private void selectGraphicRendition() {
19031925
mBackColor = code - 40;
19041926
} else if (code == 49) { // Set default background color.
19051927
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
1928+
} else if (code == 59) { // Set default underline color.
1929+
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
19061930
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
19071931
mForeColor = code - 90 + 8;
19081932
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
@@ -2152,15 +2176,21 @@ private void scrollDownOneLine() {
21522176
/**
21532177
* Process the next ASCII character of a parameter.
21542178
*
2155-
* Parameter characters modify the action or interpretation of the sequence. You can use up to
2156-
* 16 parameters per sequence. You must use the ; character to separate parameters.
2157-
* All parameters are unsigned, positive decimal integers, with the most significant
2179+
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
2180+
*
2181+
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
2182+
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
2183+
* we use a common space for parameters and sub-parameters, allowing 32 in total.
2184+
*
2185+
* <p>All parameters are unsigned, positive decimal integers, with the most significant
21582186
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
21592187
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
21602188
* or omitted parameter indicates a default value for the sequence. For most
21612189
* sequences, the default value is 1.
21622190
*
2163-
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
2191+
* <p>References:
2192+
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
2193+
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
21642194
* */
21652195
private void parseArg(int b) {
21662196
if (b >= '0' && b <= '9') {
@@ -2178,9 +2208,14 @@ private void parseArg(int b) {
21782208
mArgs[mArgIndex] = value;
21792209
}
21802210
continueSequence(mEscapeState);
2181-
} else if (b == ';') {
2182-
if (mArgIndex < mArgs.length) {
2211+
} else if (b == ';' || b == ':') {
2212+
if (mArgIndex + 1 < mArgs.length) {
21832213
mArgIndex++;
2214+
if (b == ':') {
2215+
mArgsSubParamsBitSet |= 1 << mArgIndex;
2216+
}
2217+
} else {
2218+
logError("Too many parameters when in state: " + mEscapeState);
21842219
}
21852220
continueSequence(mEscapeState);
21862221
} else {

terminal-emulator/src/test/java/com/termux/terminal/ControlSequenceIntroducerTest.java

+46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.termux.terminal;
22

3+
import java.util.List;
4+
35
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
46
public class ControlSequenceIntroducerTest extends TerminalTestCase {
57

@@ -82,4 +84,48 @@ public void testReportPixelSize() {
8284
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
8385
}
8486

87+
/**
88+
* See <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
89+
*
90+
* <pre>
91+
* <ESC>[4:0m # no underline
92+
* <ESC>[4:1m # straight underline
93+
* <ESC>[4:2m # double underline
94+
* <ESC>[4:3m # curly underline
95+
* <ESC>[4:4m # dotted underline
96+
* <ESC>[4:5m # dashed underline
97+
* <ESC>[4m # straight underline (for backwards compat)
98+
* <ESC>[24m # no underline (for backwards compat)
99+
* </pre>
100+
* <p>
101+
* We currently parse the variants, but map them to normal/no underlines as appropriate
102+
*/
103+
public void testUnderlineVariants() {
104+
for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) {
105+
for (String stop : List.of("24", "4:0")) {
106+
withTerminalSized(3, 3);
107+
enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " ");
108+
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
109+
enterString("\033[4;1m").assertLinesAre(" ", " ", " ");
110+
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
111+
enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " ");
112+
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect);
113+
}
114+
}
115+
}
116+
117+
public void testManyParameters() {
118+
StringBuilder b = new StringBuilder("\033[");
119+
for (int i = 0; i < 30; i++) {
120+
b.append("0;");
121+
}
122+
b.append("4:2");
123+
// This clearing of underline should be ignored as the parameters pass the threshold for too many parameters:
124+
b.append("4:0m");
125+
withTerminalSized(3, 3)
126+
.enterString(b.toString())
127+
.assertLinesAre(" ", " ", " ");
128+
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
129+
}
130+
85131
}

terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java

+29-14
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ public void testPaste() {
137137
}
138138

139139
public void testSelectGraphics() {
140+
selectGraphicsTestRun(';');
141+
selectGraphicsTestRun(':');
142+
}
143+
144+
public void selectGraphicsTestRun(char separator) {
140145
withTerminalSized(5, 5);
141146
enterString("\033[31m");
142147
assertEquals(mTerminal.mForeColor, 1);
@@ -155,55 +160,59 @@ public void testSelectGraphics() {
155160
// Check TerminalEmulator.parseArg()
156161
enterString("\033[31m\033[m");
157162
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
158-
enterString("\033[31m\033[;m");
163+
enterString("\033[31m\033[;m".replace(';', separator));
159164
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
160165
enterString("\033[31m\033[0m");
161166
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
162-
enterString("\033[31m\033[0;m");
167+
enterString("\033[31m\033[0;m".replace(';', separator));
163168
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
164169
enterString("\033[31;;m");
165170
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
171+
enterString("\033[31::m");
172+
assertEquals(1, mTerminal.mForeColor);
166173
enterString("\033[31;m");
167174
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
175+
enterString("\033[31:m");
176+
assertEquals(1, mTerminal.mForeColor);
168177
enterString("\033[31;;41m");
169178
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
170179
assertEquals(1, mTerminal.mBackColor);
171180
enterString("\033[0m");
172181
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
173182

174183
// 256 colors:
175-
enterString("\033[38;5;119m");
184+
enterString("\033[38;5;119m".replace(';', separator));
176185
assertEquals(119, mTerminal.mForeColor);
177186
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
178-
enterString("\033[48;5;129m");
187+
enterString("\033[48;5;129m".replace(';', separator));
179188
assertEquals(119, mTerminal.mForeColor);
180189
assertEquals(129, mTerminal.mBackColor);
181190

182191
// Invalid parameter:
183-
enterString("\033[48;8;129m");
192+
enterString("\033[48;8;129m".replace(';', separator));
184193
assertEquals(119, mTerminal.mForeColor);
185194
assertEquals(129, mTerminal.mBackColor);
186195

187196
// Multiple parameters at once:
188-
enterString("\033[38;5;178;48;5;179m");
197+
enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
189198
assertEquals(178, mTerminal.mForeColor);
190199
assertEquals(179, mTerminal.mBackColor);
191200

192201
// Omitted parameter means zero:
193-
enterString("\033[38;5;m");
202+
enterString("\033[38;5;m".replace(';', separator));
194203
assertEquals(0, mTerminal.mForeColor);
195204
assertEquals(179, mTerminal.mBackColor);
196-
enterString("\033[48;5;m");
205+
enterString("\033[48;5;m".replace(';', separator));
197206
assertEquals(0, mTerminal.mForeColor);
198207
assertEquals(0, mTerminal.mBackColor);
199208

200209
// 24 bit colors:
201210
enterString(("\033[0m")); // Reset fg and bg colors.
202-
enterString("\033[38;2;255;127;2m");
211+
enterString("\033[38;2;255;127;2m".replace(';', separator));
203212
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
204213
assertEquals(expectedForeground, mTerminal.mForeColor);
205214
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
206-
enterString("\033[48;2;1;2;254m");
215+
enterString("\033[48;2;1;2;254m".replace(';', separator));
207216
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
208217
assertEquals(expectedForeground, mTerminal.mForeColor);
209218
assertEquals(expectedBackground, mTerminal.mBackColor);
@@ -212,24 +221,30 @@ public void testSelectGraphics() {
212221
enterString(("\033[0m")); // Reset fg and bg colors.
213222
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
214223
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
215-
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
224+
enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
216225
assertEquals(expectedForeground, mTerminal.mForeColor);
217226
assertEquals(expectedBackground, mTerminal.mBackColor);
218227

219228
// 24 bit colors, invalid input:
220-
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
229+
enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
221230
assertEquals(expectedForeground, mTerminal.mForeColor);
222231
assertEquals(expectedBackground, mTerminal.mBackColor);
223232

224233
// 24 bit colors, omitted parameter means zero:
225-
enterString("\033[38;2;255;127;m");
234+
enterString("\033[38;2;255;127;m".replace(';', separator));
226235
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
227236
assertEquals(expectedForeground, mTerminal.mForeColor);
228237
assertEquals(expectedBackground, mTerminal.mBackColor);
229-
enterString("\033[38;2;123;;77m");
238+
enterString("\033[38;2;123;;77m".replace(';', separator));
230239
expectedForeground = 0xff000000 | (123 << 16) | 77;
231240
assertEquals(expectedForeground, mTerminal.mForeColor);
232241
assertEquals(expectedBackground, mTerminal.mBackColor);
242+
243+
// 24 bit colors, extra sub-parameters are skipped:
244+
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
245+
enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
246+
assertEquals(expectedForeground, mTerminal.mForeColor);
247+
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
233248
}
234249

235250
public void testBackgroundColorErase() {

0 commit comments

Comments
 (0)