From 9a8dcd0961e15affbd7d763b0cf703851a0a9067 Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Tue, 17 Sep 2024 13:18:52 +0200 Subject: [PATCH] Fixed: Implement colon separated CSI parameters --- .../com/termux/terminal/TerminalEmulator.java | 74 +++++++++++++------ .../ControlSequenceIntroducerTest.java | 32 ++++++++ .../com/termux/terminal/TerminalTest.java | 39 +++++----- 3 files changed, 105 insertions(+), 40 deletions(-) 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 cbbccbb4f2..7021ef75de 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -80,8 +80,8 @@ public final class TerminalEmulator { /** Escape processing: CSI ! */ private static final int ESC_CSI_EXCLAMATION = 19; - /** 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; @@ -171,6 +171,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(); @@ -231,15 +233,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 @@ -1279,6 +1283,7 @@ private void startEscapeSequence() { mEscapeState = ESC; mArgIndex = 0; Arrays.fill(mArgs, -1); + mArgsSubParamsBitSet = 0; } private void doLinefeed() { @@ -1777,7 +1782,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) { @@ -1806,8 +1823,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; @@ -1823,11 +1840,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" @@ -1836,10 +1853,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); @@ -1853,6 +1870,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). @@ -2102,15 +2121,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') { @@ -2128,9 +2153,12 @@ 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; + } } 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..0b3ada96c3 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,34 @@ 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); + } + } + } + } 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..996cc992f7 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,55 +160,55 @@ 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"); + enterString("\033[31;;m".replace(';', separator)); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); - enterString("\033[31;m"); + enterString("\033[31;m".replace(';', separator)); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); - enterString("\033[31;;41m"); + enterString("\033[31;;41m".replace(';', separator)); 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;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,21 +217,21 @@ 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;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);