Skip to content

Commit 0415681

Browse files
fornwallagnostic-apollo
authored andcommitted
Fixed: Implement colon separated CSI parameters
1 parent 4baf12b commit 0415681

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

Lines changed: 58 additions & 23 deletions
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
@@ -1321,6 +1325,7 @@ private void startEscapeSequence() {
13211325
mEscapeState = ESC;
13221326
mArgIndex = 0;
13231327
Arrays.fill(mArgs, -1);
1328+
mArgsSubParamsBitSet = 0;
13241329
}
13251330

13261331
private void doLinefeed() {
@@ -1805,6 +1810,11 @@ private void doCsi(int b) {
18051810
private void selectGraphicRendition() {
18061811
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
18071812
for (int i = 0; i <= mArgIndex; i++) {
1813+
// Skip leading sub parameters:
1814+
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
1815+
continue;
1816+
}
1817+
18081818
int code = getArg(i, 0, false);
18091819
if (code < 0) {
18101820
if (mArgIndex > 0) {
@@ -1824,7 +1834,19 @@ private void selectGraphicRendition() {
18241834
} else if (code == 3) {
18251835
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
18261836
} else if (code == 4) {
1827-
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1837+
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
1838+
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
1839+
i++;
1840+
if (mArgs[i] == 0) {
1841+
// No underline.
1842+
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1843+
} else {
1844+
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
1845+
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1846+
}
1847+
} else {
1848+
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
1849+
}
18281850
} else if (code == 5) {
18291851
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
18301852
} else if (code == 7) {
@@ -1853,8 +1875,8 @@ private void selectGraphicRendition() {
18531875
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
18541876
} else if (code >= 30 && code <= 37) {
18551877
mForeColor = code - 30;
1856-
} else if (code == 38 || code == 48) {
1857-
// Extended set foreground(38)/background (48) color.
1878+
} else if (code == 38 || code == 48 || code == 58) {
1879+
// Extended set foreground(38)/background(48)/underline(58) color.
18581880
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
18591881
// "5;$INDEX" to set an indexed color.
18601882
if (i + 2 > mArgIndex) continue;
@@ -1870,11 +1892,11 @@ private void selectGraphicRendition() {
18701892
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
18711893
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
18721894
} else {
1873-
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
1874-
if (code == 38) {
1875-
mForeColor = argbColor;
1876-
} else {
1877-
mBackColor = argbColor;
1895+
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
1896+
switch (code) {
1897+
case 38: mForeColor = argbColor; break;
1898+
case 48: mBackColor = argbColor; break;
1899+
case 58: mUnderlineColor = argbColor; break;
18781900
}
18791901
}
18801902
i += 4; // "2;P_r;P_g;P_r"
@@ -1883,10 +1905,10 @@ private void selectGraphicRendition() {
18831905
int color = getArg(i + 2, 0, false);
18841906
i += 2; // "5;P_s"
18851907
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
1886-
if (code == 38) {
1887-
mForeColor = color;
1888-
} else {
1889-
mBackColor = color;
1908+
switch (code) {
1909+
case 38: mForeColor = color; break;
1910+
case 48: mBackColor = color; break;
1911+
case 58: mUnderlineColor = color; break;
18901912
}
18911913
} else {
18921914
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
@@ -1900,6 +1922,8 @@ private void selectGraphicRendition() {
19001922
mBackColor = code - 40;
19011923
} else if (code == 49) { // Set default background color.
19021924
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
1925+
} else if (code == 59) { // Set default underline color.
1926+
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
19031927
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
19041928
mForeColor = code - 90 + 8;
19051929
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
@@ -2149,15 +2173,21 @@ private void scrollDownOneLine() {
21492173
/**
21502174
* Process the next ASCII character of a parameter.
21512175
*
2152-
* Parameter characters modify the action or interpretation of the sequence. You can use up to
2153-
* 16 parameters per sequence. You must use the ; character to separate parameters.
2154-
* All parameters are unsigned, positive decimal integers, with the most significant
2176+
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
2177+
*
2178+
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
2179+
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
2180+
* we use a common space for parameters and sub-parameters, allowing 32 in total.
2181+
*
2182+
* <p>All parameters are unsigned, positive decimal integers, with the most significant
21552183
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
21562184
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
21572185
* or omitted parameter indicates a default value for the sequence. For most
21582186
* sequences, the default value is 1.
21592187
*
2160-
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
2188+
* <p>References:
2189+
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
2190+
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
21612191
* */
21622192
private void parseArg(int b) {
21632193
if (b >= '0' && b <= '9') {
@@ -2175,9 +2205,14 @@ private void parseArg(int b) {
21752205
mArgs[mArgIndex] = value;
21762206
}
21772207
continueSequence(mEscapeState);
2178-
} else if (b == ';') {
2179-
if (mArgIndex < mArgs.length) {
2208+
} else if (b == ';' || b == ':') {
2209+
if (mArgIndex + 1 < mArgs.length) {
21802210
mArgIndex++;
2211+
if (b == ':') {
2212+
mArgsSubParamsBitSet |= 1 << mArgIndex;
2213+
}
2214+
} else {
2215+
logError("Too many parameters when in state: " + mEscapeState);
21812216
}
21822217
continueSequence(mEscapeState);
21832218
} else {

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

Lines changed: 46 additions & 0 deletions
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

Lines changed: 29 additions & 14 deletions
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)