Repository: Textualize/rich (python)

Issue:
[BUG] ANSI sequences parsed incorrectly: \x1b(B\x1b[m
- [x] I've checked [docs](https://rich.readthedocs.io/en/latest/introduction.html) and [closed issues](https://github.com/Textualize/rich/issues?q=is%3Aissue+is%3Aclosed) for possible solutions.
- [x] I can't find my issue in the [FAQ](https://github.com/Textualize/rich/blob/master/FAQ.md).

**Describe the bug**

Some ANSI sequences (which I didn't know about) are not correctly parsed.
Here is a text example with such ANSI sequences:

```
src/failprint/capture.py:57: \x1b[1m\x1b[31merror:\x1b(B\x1b[m Property \x1b(B\x1b[m\x1b[1m"buffer"\x1b(B\x1b[m defined in \x1b(B\x1b[m\x1b[1m"TextIOWrapper"\x1b(B\x1b[m is read-only  \x1b(B\x1b[m\x1b[33m[misc]\x1b(B\x1b[m
src/failprint/capture.py:57: \x1b[1m\x1b[31merror:\x1b(B\x1b[m Incompatible types in assignment (expression has type \x1b(B\x1b[m\x1b[1m"_BytesBuffer"\x1b(B\x1b[m, variable has type \x1b(B\x1b[m\x1b[1m"BinaryIO"\x1b(B\x1b[m)  \x1b(B\x1b[m\x1b[33m[assignment]\x1b(B\x1b[m
src/failprint/capture.py:77: \x1b[1m\x1b[31merror:\x1b(B\x1b[m Argument 1 to \x1b(B\x1b[m\x1b[1m"StringIO"\x1b(B\x1b[m has incompatible type \x1b(B\x1b[m\x1b[1m"StringIO"\x1b(B\x1b[m; expected \x1b(B\x1b[m\x1b[1m"Optional[str]"\x1b(B\x1b[m  \x1b(B\x1b[m\x1b[33m[arg-type]\x1b(B\x1b[m
src/failprint/runners.py:221: \x1b[1m\x1b[31merror:\x1b(B\x1b[m Incompatible return value type (got \x1b(B\x1b[m\x1b[1m"Union[str, int, None]"\x1b(B\x1b[m, expected \x1b(B\x1b[m\x1b[1m"int"\x1b(B\x1b[m)  \x1b(B\x1b[m\x1b[33m[return-value]\x1b(B\x1b[m
\x1b[1m\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m
```

This is the (ANSI colored) output of mypy.
Other libraries like `ansimarkup` are able to print it.

```python
from ansimarkup import ansiprint
ansiprint('\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m\n')
```

```python
from rich.text import Text
from rich import print as richprint
richprint(Text.from_ansi('\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m\n'))
```

![richansi](https://user-images.githubusercontent.com/3999221/206017227-0d275ae4-667b-4fb3-a82f-96ef255b543d.png)

Note the trailing `B` in the above screenshot.

**Platform**
<details>
<summary>Click to expand</summary>

What platform (Win/Linux/Mac) are you running on? What terminal software are you using?

I may ask you to copy and paste the output of the following commands. It may save some time if you do it now.

If you're using Rich in a terminal:

```
python -m rich.diagnose
pip freeze | grep rich
```

```
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=191 ColorSystem.TRUECOLOR>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'truecolor'                                                   │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 19                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=191, height=19),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=191,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=19,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=191, height=19)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 191                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm-256color',      │
│     'COLORTERM': 'truecolor',      │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': 'vscode',      │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JUPYTER_COLUMNS': None,       │
│     'JUPYTER_LINES': None,         │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"
rich==12.6.0
```

</details>


Pull request description:
ANSI: nF-escape symbol "(" decoding issue
## Type of changes

- [x] Bug fix

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description
Resolves #2688, by skipping past the current token when a `(` nF-escape control symbol is encountered.

I don't really know whether this is a sensible approach, but it seems worth sharing the attempt (I'm not aware of other work-in-progress to resolve the issue).

Todo:

- [x] More test coverage

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1897c96c..019a81ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Use `Console(stderr=True)` in `rich.traceback.install` to support io redirection.
 - Fixes superfluous spaces in html output https://github.com/Textualize/rich/issues/2832
 - Fixed duplicate output in Jupyter https://github.com/Textualize/rich/pulls/2804
+- Filter ANSI character-encoding-change codes in `Text.from_ansi` parser
 - Fixes traceback failing when a frame filename is unreadable https://github.com/Textualize/rich/issues/2821
 
 ### Added
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 51b79b2a..6db56775 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -64,3 +64,4 @@ The following people have contributed to the development of Rich:
 - [Zhe Huang](https://github.com/onlyacat)
 - [Ke Sun](https://github.com/ksun212)
 - [Qiming Xu](https://github.com/xqm32)
+- [James Addison](https://github.com/jayaddison)
diff --git a/rich/ansi.py b/rich/ansi.py
index 92ef5194..66365e65 100644
--- a/rich/ansi.py
+++ b/rich/ansi.py
@@ -43,6 +43,9 @@ def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:
         if start > position:
             yield _AnsiToken(ansi_text[position:start])
         if sgr:
+            if sgr == "(":
+                position = end + 1
+                continue
             if sgr.endswith("m"):
                 yield _AnsiToken("", sgr[1:-1], osc)
         else:


Test patch:
diff --git a/tests/test_ansi.py b/tests/test_ansi.py
index 1a21765c..ae87cde1 100644
--- a/tests/test_ansi.py
+++ b/tests/test_ansi.py
@@ -1,3 +1,5 @@
+import pytest
+
 from rich.ansi import AnsiDecoder
 from rich.console import Console
 from rich.style import Style
@@ -45,3 +47,24 @@ def test_decode_example():
     print(repr(result))
     expected = "\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:\x1b[0m In function '\x1b[1mmain\x1b[0m':\n\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:3:5:\x1b[0m \x1b[1;35mwarning: \x1b[0munused variable '\x1b[1ma\x1b[0m' \n[\x1b[1;35m-Wunused-variable\x1b[0m]\n    3 | int \x1b[1;35ma\x1b[0m=1;\n      |     \x1b[1;35m^\x1b[0m\n"
     assert result == expected
+
+
+@pytest.mark.parametrize(
+    "ansi_bytes, expected_text",
+    [
+        # https://github.com/Textualize/rich/issues/2688
+        (
+            b"\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m\n",
+            "Found 4 errors in 2 files (checked 18 source files)",
+        ),
+        # https://mail.python.org/pipermail/python-list/2007-December/424756.html
+        (b"Hallo", "Hallo"),
+        (b"\x1b(BHallo", "Hallo"),
+        (b"\x1b(JHallo", "Hallo"),
+        (b"\x1b(BHal\x1b(Jlo", "Hallo"),
+    ],
+)
+def test_decode_issue_2688(ansi_bytes, expected_text):
+    text = Text.from_ansi(ansi_bytes.decode())
+
+    assert str(text) == expected_text