WT doesn't support U+2068 and U+2069 characters #21110

Closed
opened 2026-01-31 07:33:20 +00:00 by claunia · 17 comments
Owner

Originally created by @KexyBiscuit on GitHub (Jan 19, 2024).

Windows Terminal version

1.20.10161.0

Windows build number

10.0.26020.1000

Other Software

AOSC OS on WSL

Steps to reproduce

  1. Install AOSC OS on WSL from Microsoft Store
  2. Finish the OOBE
  3. Run sudo oma refresh

Expected Behavior

The output is fully human readable.

Actual Behavior

Several unreadable characters appear, specifically U+2068 and U+2069.

These characters are for Explicit Directional Isolates. Microsoft's doc also mentions these characters. It seems that Windows Terminal doesn't support them, and just print the characters out.

image

Originally created by @KexyBiscuit on GitHub (Jan 19, 2024). ### Windows Terminal version 1.20.10161.0 ### Windows build number 10.0.26020.1000 ### Other Software AOSC OS on WSL ### Steps to reproduce 1. Install AOSC OS on WSL from [Microsoft Store](https://www.microsoft.com/store/productId/9NMDF21NV65Z?ocid=pdpshare) 2. Finish the OOBE 3. Run `sudo oma refresh` ### Expected Behavior The output is fully human readable. ### Actual Behavior Several unreadable characters appear, specifically U+2068 and U+2069. These characters are for [Explicit Directional Isolates](https://www.unicode.org/reports/tr9/#Explicit_Directional_Isolates). [Microsoft's doc](https://learn.microsoft.com/en-us/globalization/fonts-layout/text-directionality#explicit-formatting-characters) also mentions these characters. It seems that Windows Terminal doesn't support them, and just print the characters out. ![image](https://github.com/microsoft/terminal/assets/8013154/922e7eba-b4d0-4622-9550-508a7b736588)
Author
Owner

@zadjii-msft commented on GitHub (Jan 19, 2024):

@lhecker you're more familiar with unicode than I - this is basically a dup of #538, yea?

@zadjii-msft commented on GitHub (Jan 19, 2024): @lhecker you're more familiar with unicode than I - this is basically a dup of #538, yea?
Author
Owner

@KexyBiscuit commented on GitHub (Jan 19, 2024):

Just added the screenshot, I think it's partially a duplicate, but what I care the most is to improve readability, not to actually support that Unicode feature.

@KexyBiscuit commented on GitHub (Jan 19, 2024): Just added the screenshot, I think it's partially a duplicate, but what I care the most is to improve readability, not to actually support that Unicode feature.
Author
Owner

@lhecker commented on GitHub (Jan 19, 2024):

I believe we can consider this part of #538 even if automatic directionality has not been mentioned yet there. The reason this occurs is because we don't yet analyze the input text for directional markers at all. I'll leave this issue open though, because I want to suppress rendering these glyphs in a hotfix first. Afterwards we can continue tracking the overall issue in #538.

BTW: I see that the numeric parameter for OMA's packages-can-be-upgrade message is also wrapped in a FSI/PDI pair. It probably shouldn't do that, since the number is part of the sentence and some text rendering approaches look a bit weird if you change the directionality of text more than once per paragraph.

@lhecker commented on GitHub (Jan 19, 2024): I believe we can consider this part of #538 even if automatic directionality has not been mentioned yet there. The reason this occurs is because we don't yet analyze the input text for directional markers at all. I'll leave this issue open though, because I want to suppress rendering these glyphs in a hotfix first. Afterwards we can continue tracking the overall issue in #538. BTW: I see that the numeric parameter for OMA's `packages-can-be-upgrade` message is also wrapped in a FSI/PDI pair. It probably shouldn't do that, since the number is part of the sentence and some text rendering approaches look a bit weird if you change the directionality of text more than once per paragraph.
Author
Owner

@adalinesimonian commented on GitHub (May 22, 2025):

Re: @lhecker

It's continuing to rear its head a year later. Just got a user report for gdvm from a user that's using Windows running into this character spam on the terminal.

I'll leave this issue open though, because I want to suppress rendering these glyphs in a hotfix first. Afterwards we can continue tracking the overall issue in #538.

I think suppressing the characters temporarily is the best way forward given the scope of work for proper RTL support.

Is there any appetite or capacity for the team to tackle this issue near-term/soon? I would presume this would be a pretty straightfoward and quick fix for someone familiar with the codebase. Or is this something where you could use some help/contributions from the community?

@adalinesimonian commented on GitHub (May 22, 2025): Re: @lhecker It's continuing to rear its head a year later. Just got a user report for gdvm from a user that's using Windows running into this character spam on the terminal. > I'll leave this issue open though, because I want to suppress rendering these glyphs in a hotfix first. Afterwards we can continue tracking the overall issue in [#538](https://github.com/microsoft/terminal/issues/538). I think suppressing the characters temporarily is the best way forward given the scope of work for proper RTL support. Is there any appetite or capacity for the team to tackle this issue near-term/soon? I would presume this would be a pretty straightfoward and quick fix for someone familiar with the codebase. Or is this something where you could use some help/contributions from the community?
Author
Owner

@lhecker commented on GitHub (May 22, 2025):

I think I would need help with contributions.

I'm currently and for the foreseeable time unable to work on this. https://github.com/microsoft/edit launched now and I need to maintain it (together with DHowett), and bug fixing for Windows Terminal (if I get to do it) also consumes quite a lot of time unfortunately.

@lhecker commented on GitHub (May 22, 2025): I think I would need help with contributions. I'm currently and for the foreseeable time unable to work on this. https://github.com/microsoft/edit launched now and I need to maintain it (together with DHowett), and bug fixing for Windows Terminal (if I get to do it) also consumes quite a lot of time unfortunately.
Author
Owner

@adalinesimonian commented on GitHub (May 22, 2025):

No problem. I can take a look if you'd like. Any pointers on where I should start digging in?

@adalinesimonian commented on GitHub (May 22, 2025): No problem. I can take a look if you'd like. Any pointers on where I should start digging in?
Author
Owner

@lhecker commented on GitHub (May 22, 2025):

There's two approaches to this.

  • Either we filter the offending characters here directly from the incoming text stream: a093ca3d54/src/renderer/atlas/AtlasEngine.cpp (L802)
  • Or we look up the offending glyphs (similar to replacementCharacterFontFace in the same file) and filter them out of the row.glyphIndices, etc., arrays (also same file)

I think option 1 would be easier. This would require us to simply filter out the offending characters in the _api.bufferLine string and offending offsets in _api.bufferLineColumn. (Both have to be kept in sync! Each character in _api.bufferLine maps to a value in _api.bufferLineColumn.) I suppose this requires writing a custom replacement loop, but that should only be moderately difficult at most.

You can test your renderer changes in conhost directly (the old console), if you'd like. It builds a lot faster. To do so set Host.EXE as your startup project and create a UseDx DWORD value inside the HKEY_CURRENT_USER\Console key and set it to 1. This will enable the use of the AtlasEngine inside conhost. You'll notice that it works because the font size will appear a lot larger suddenly.

@lhecker commented on GitHub (May 22, 2025): There's two approaches to this. * Either we filter the offending characters here directly from the incoming text stream: https://github.com/microsoft/terminal/blob/a093ca3d54ff00d79f00dc4dbd0e4f63ad96aa46/src/renderer/atlas/AtlasEngine.cpp#L802 * Or we look up the offending glyphs (similar to `replacementCharacterFontFace` in the same file) and filter them out of the `row.glyphIndices`, etc., arrays (also same file) I think option 1 would be easier. This would require us to simply filter out the offending characters in the `_api.bufferLine` string and offending offsets in `_api.bufferLineColumn`. (Both have to be kept in sync! Each character in `_api.bufferLine` maps to a value in `_api.bufferLineColumn`.) I suppose this requires writing a custom replacement loop, but that should only be moderately difficult at most. You can test your renderer changes in conhost directly (the old console), if you'd like. It builds a lot faster. To do so set `Host.EXE` as your startup project and create a `UseDx` DWORD value inside the `HKEY_CURRENT_USER\Console` key and set it to `1`. This will enable the use of the AtlasEngine inside conhost. You'll notice that it works because the font size will appear a lot larger suddenly.
Author
Owner

@adalinesimonian commented on GitHub (May 22, 2025):

Great tips, thanks. I will give it a look.

@adalinesimonian commented on GitHub (May 22, 2025): Great tips, thanks. I will give it a look.
Author
Owner

@j4james commented on GitHub (May 22, 2025):

Would this not make more sense to handle higher up, like in the state machine? Because these are really more like application-level controls which are no-op in the terminal (NUL, SOH, STX, etc.). I would expect them to be filtered out up front so they don't occupy space in the buffer.

Like in the example from the gdvm issue. You assumedly want that that rendered as

Succcessfully set 4.4.1-stable (C#) as the default Godot version.

and not

Succcessfully set  4.4.1-stable (C#)  as the default Godot version.
@j4james commented on GitHub (May 22, 2025): Would this not make more sense to handle higher up, like in the state machine? Because these are really more like application-level controls which are no-op in the terminal (NUL, SOH, STX, etc.). I would expect them to be filtered out up front so they don't occupy space in the buffer. Like in the example from the [gdvm issue](https://github.com/adalinesimonian/gdvm/issues/35). You assumedly want that that rendered as ``` Succcessfully set 4.4.1-stable (C#) as the default Godot version. ``` and not ``` Succcessfully set 4.4.1-stable (C#) as the default Godot version. ```
Author
Owner

@lhecker commented on GitHub (May 23, 2025):

At least long-term, I do think we have to store these in the buffer so that we can apply these markers during rendering. I'm not super certain if DirectWrite can actually do it already, but if I had designed it, it would support them. Because of this, I think that the handling of BiDi markers belongs in the rendering code which would also be responsible for any other BiDi mapping anyway, even if DirectWrite doesn't support them.

It's just that we should store these zero-width markers as zero-width...

What do you think?

@lhecker commented on GitHub (May 23, 2025): At least long-term, I do think we have to store these in the buffer so that we can apply these markers during rendering. I'm not super certain if DirectWrite can actually do it already, but if I had designed it, it would support them. Because of this, I think that the handling of BiDi markers belongs in the rendering code which would also be responsible for any other BiDi mapping anyway, even if DirectWrite doesn't support them. It's just that we should store these zero-width markers as zero-width... What do you think?
Author
Owner

@adalinesimonian commented on GitHub (May 23, 2025):

If I was following what @j4james was putting down, this would be the fix that would just elide these characters from the buffers, correct?

Screenshot

This is working (above is a dev build with patch, below is the release version from the store):

Screenshot

However, if you prefer the approach above (i.e. what you, @lhecker, originally suggested) then I can take a stab at it. Just took some time to get a dev environment working. I gave up on getting it building in VS Code and just installed VS 2022; looked like the build instructions were out of date.

@adalinesimonian commented on GitHub (May 23, 2025): If I was following what @j4james was putting down, this would be the fix that would just elide these characters from the buffers, correct? ![Screenshot](https://github.com/user-attachments/assets/1c669bda-d33f-4118-8e2c-c1acdfdca689) This is working (above is a dev build with patch, below is the release version from the store): ![Screenshot](https://github.com/user-attachments/assets/36162ee0-304a-4113-b0b1-45050681c989) However, if you prefer the approach above (i.e. what you, @lhecker, originally suggested) then I can take a stab at it. Just took some time to get a dev environment working. I gave up on getting it building in VS Code and just installed VS 2022; looked like the build instructions were out of date.
Author
Owner

@lhecker commented on GitHub (May 23, 2025):

Ah, right! I forgot that you could just make the change in PaintBufferLine. Nice work! Personally, I'm fine with that approach, but I'd like to wait for @j4james's response as he's super knowledgeable, particularly about the parts he mentioned. 🙂

I gave up on getting it building in VS Code and just installed VS 2022; looked like the build instructions were out of date.

I apologize for that. As you probably know, staffing in the industry is... "tight" at the moment. We're branching out a little right now to do a couple new projects, which further limits our throughput.
(Our team recently released Windows Command Palette and Microsoft Edit (and one more that's not announced yet)!)

Edit: Apropos, and that's definitely not the right moment to say this, but if you, @j4james, could ever check out https://github.com/microsoft/edit, I'd be happy to receive any feedback you may have. 🙂

@lhecker commented on GitHub (May 23, 2025): Ah, right! I forgot that you could just make the change in `PaintBufferLine`. Nice work! Personally, I'm fine with that approach, but I'd like to wait for @j4james's response as he's super knowledgeable, particularly about the parts he mentioned. 🙂 > I gave up on getting it building in VS Code and just installed VS 2022; looked like the build instructions were out of date. I apologize for that. As you probably know, staffing in the industry is... "tight" at the moment. We're branching out a little right now to do a couple new projects, which further limits our throughput. (Our team recently released Windows Command Palette and Microsoft Edit (and one more that's not announced yet)!) Edit: Apropos, and that's definitely not the right moment to say this, but if you, @j4james, could ever check out https://github.com/microsoft/edit, I'd be happy to receive any feedback you may have. 🙂
Author
Owner

@adalinesimonian commented on GitHub (May 23, 2025):

I'm working on moving from Seattle to Norway right now, I definitely have first-hand accounts of the difficulties in the industry you mention. So no problem at all, I appreciate you being communicative even with the constraints on your time.

I opened #18942; I felt it may be appropriate to have a PR for commentary on the actual technical implementation. Happy to make any revisions as requested.

@adalinesimonian commented on GitHub (May 23, 2025): I'm working on moving from Seattle to Norway right now, I definitely have first-hand accounts of the difficulties in the industry you mention. So no problem at all, I appreciate you being communicative even with the constraints on your time. I opened #18942; I felt it may be appropriate to have a PR for commentary on the actual technical implementation. Happy to make any revisions as requested.
Author
Owner

@j4james commented on GitHub (May 23, 2025):

If you've got something working, don't let me hold you up. I just thought it would be easier not writing these characters to the buffer at all, because the gdvm screenshot gave the impression that they're taking up space, which we assumedly don't want. However, that seems to have been fixed (mostly) in the current preview build, so maybe that's not a problem anymore, or at least something that can be addressed separately.

if you, @j4james, could ever check out https://github.com/microsoft/edit, I'd be happy to receive any feedback you may have.

@lhecker I have already seen that, and I think it's fantastic! I haven't looked at the code, because I know absolutely nothing about rust, but my first thought was that I would love to try and add bidi support to it (assuming I had the time), because with a decent bidi-aware editor, you'll immediately see how essential it is that the terminal doesn't screw with the ordering of characters (and why I don't think we need the renderer having anything to do with these direction markers).

For example, this is what it looks like editing some Hebrew text on a terminal that tries to be clever with its handling of RTL characters. It starts off looking great, with the Hebrew characters in the correct order, but as soon as you try editing something, it all starts to fall apart.

Image

Rendering RTL characters in the correct order should be fairly easy to handle in the editor itself. But once the terminal starts messing with things, there's nothing an app can do to recover from that.

@j4james commented on GitHub (May 23, 2025): If you've got something working, don't let me hold you up. I just thought it would be easier not writing these characters to the buffer at all, because the gdvm screenshot gave the impression that they're taking up space, which we assumedly don't want. However, that seems to have been fixed (mostly) in the current preview build, so maybe that's not a problem anymore, or at least something that can be addressed separately. > if you, @j4james, could ever check out https://github.com/microsoft/edit, I'd be happy to receive any feedback you may have. @lhecker I have already seen that, and I think it's fantastic! I haven't looked at the code, because I know absolutely nothing about rust, but my first thought was that I would love to try and add bidi support to it (assuming I had the time), because with a decent bidi-aware editor, you'll immediately see how essential it is that the terminal doesn't screw with the ordering of characters (and why I don't think we need the renderer having anything to do with these direction markers). For example, this is what it looks like editing some Hebrew text on a terminal that tries to be clever with its handling of RTL characters. It starts off looking great, with the Hebrew characters in the correct order, but as soon as you try editing something, it all starts to fall apart. ![Image](https://github.com/user-attachments/assets/690f90c5-3df9-4b8f-9b77-e6c908501700) Rendering RTL characters in the correct order should be fairly easy to handle in the editor itself. But once the terminal starts messing with things, there's nothing an app can do to recover from that.
Author
Owner

@adalinesimonian commented on GitHub (May 24, 2025):

If you've got something working, don't let me hold you up. I just thought it would be easier not writing these characters to the buffer at all, because the gdvm screenshot gave the impression that they're taking up space, which we assumedly don't want. However, that seems to have been fixed (mostly) in the current preview build, so maybe that's not a problem anymore, or at least something that can be addressed separately.

When you first managed the state machine I thought you meant the state of the renderer in abstract terms, but then I found later the actual state machine you were referring to (I haven't touched this repo before). From my own testing, these characters are being treated as zero-width characters, so simply skipping rendering is having the desired effect.

The only thing is, as I mentioned in #18942, that I don't see a place to be able to put a reasonable test for this behaviour. Whereas, if the implementation actually got rid of the characters from the state machine, then I presume this draft of a possible test would work:

Example FSI/PDI state machine test
TEST_METHOD(TestFSIPDINotRendered)
{
    class CaptureDispatch : public TermDispatch
    {
    public:
        std::wstring combined;
        void Print(const wchar_t wchPrintable) override
        {
            combined.push_back(wchPrintable);
        }
        void PrintString(const std::wstring_view string) override
        {
            combined.append(string);
        }
    };

    // Create state machine with a dispatch that captures output.
    std::unique_ptr<ITermDispatch> dispatchUp(new CaptureDispatch());
    auto capture = static_cast<CaptureDispatch*>(dispatchUp.get());
    auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatchUp));
    StateMachine mach(std::move(engine));

    // Feed characters A, FSI (0x2068), B, PDI (0x2069), C.
    mach.ProcessCharacter(L'A');
    mach.ProcessCharacter(wchar_t(0x2068));
    mach.ProcessCharacter(L'B');
    mach.ProcessCharacter(wchar_t(0x2069));
    mach.ProcessCharacter(L'C');

    // Characters should be printed except FSI/PDI.
    VERIFY_ARE_EQUAL(std::wstring(L"ABC"), capture->combined);
}

With the current implementation this test would fail as the dispatch would pick up the FSI and PDI characters.

If you think we should just go ahead with the current implementation, maybe one of y'all could give the PR a look and permit the tests to run? If we really should exclude these chars from the state machine, though, I'm happy to update the implementation. In either case, I'm happy to help in whatever way I can to get a fix for this long-standing issue pushed out.

@adalinesimonian commented on GitHub (May 24, 2025): > If you've got something working, don't let me hold you up. I just thought it would be easier not writing these characters to the buffer at all, because the gdvm screenshot gave the impression that they're taking up space, which we assumedly don't want. However, that seems to have been fixed (mostly) in the current preview build, so maybe that's not a problem anymore, or at least something that can be addressed separately. When you first managed the state machine I thought you meant the state of the renderer in abstract terms, but then I found later the actual state machine you were referring to (I haven't touched this repo before). From my own testing, these characters are being treated as zero-width characters, so simply skipping rendering is having the desired effect. The only thing is, as I mentioned in #18942, that I don't see a place to be able to put a reasonable test for this behaviour. Whereas, if the implementation actually got rid of the characters from the state machine, then I presume this draft of a possible test would work: <details> <summary>Example FSI/PDI state machine test</summary> ```cpp TEST_METHOD(TestFSIPDINotRendered) { class CaptureDispatch : public TermDispatch { public: std::wstring combined; void Print(const wchar_t wchPrintable) override { combined.push_back(wchPrintable); } void PrintString(const std::wstring_view string) override { combined.append(string); } }; // Create state machine with a dispatch that captures output. std::unique_ptr<ITermDispatch> dispatchUp(new CaptureDispatch()); auto capture = static_cast<CaptureDispatch*>(dispatchUp.get()); auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatchUp)); StateMachine mach(std::move(engine)); // Feed characters A, FSI (0x2068), B, PDI (0x2069), C. mach.ProcessCharacter(L'A'); mach.ProcessCharacter(wchar_t(0x2068)); mach.ProcessCharacter(L'B'); mach.ProcessCharacter(wchar_t(0x2069)); mach.ProcessCharacter(L'C'); // Characters should be printed except FSI/PDI. VERIFY_ARE_EQUAL(std::wstring(L"ABC"), capture->combined); } ``` </details> With the current implementation this test would fail as the dispatch would pick up the FSI and PDI characters. If you think we should just go ahead with the current implementation, maybe one of y'all could give the PR a look and permit the tests to run? If we really should exclude these chars from the state machine, though, I'm happy to update the implementation. In either case, I'm happy to help in whatever way I can to get a fix for this long-standing issue pushed out.
Author
Owner

@j4james commented on GitHub (May 24, 2025):

From my own testing, these characters are being treated as zero-width characters, so simply skipping rendering is having the desired effect.

The problem is that they aren't always treated as zero-width. If you output them at the start of a line, they'll occupy one column. Although this isn't just a problem with the bidi isolates - this is a general issue with all zero-width characters as far as I can tell.

For example, in the test case below (a zero-width space followed by an asterisk), I'd expect the asterisk to appear in the leftmost column, but it actually ends up in column 2.

echo -e '\U200B*'

This is not necessarily something I'd expect you to fix, but your PR actually makes the situation worse in some ways, because the buffer and renderer will now be out of sync.

As a test, you can draw a line down the middle of the screen with something like this:

echo -e '\e[124;1;40;;40$x'

Then output U+2068 at the start of a line like this:

echo -e '\e[10H\U2068'

With your PR applied, notice how it makes the | on line 10 shift left by one column. This is definitely not expected behavior.

@j4james commented on GitHub (May 24, 2025): > From my own testing, these characters are being treated as zero-width characters, so simply skipping rendering is having the desired effect. The problem is that they aren't always treated as zero-width. If you output them at the start of a line, they'll occupy one column. Although this isn't just a problem with the bidi isolates - this is a general issue with all zero-width characters as far as I can tell. For example, in the test case below (a zero-width space followed by an asterisk), I'd expect the asterisk to appear in the leftmost column, but it actually ends up in column 2. ``` echo -e '\U200B*' ``` This is not necessarily something I'd expect you to fix, but your PR actually makes the situation worse in some ways, because the buffer and renderer will now be out of sync. As a test, you can draw a line down the middle of the screen with something like this: ``` echo -e '\e[124;1;40;;40$x' ``` Then output U+2068 at the start of a line like this: ``` echo -e '\e[10H\U2068' ``` With your PR applied, notice how it makes the `|` on line 10 shift left by one column. This is definitely not expected behavior.
Author
Owner

@adalinesimonian commented on GitHub (May 24, 2025):

The problem is that they aren't always treated as zero-width. If you output them at the start of a line, they'll occupy one column. Although this isn't just a problem with the bidi isolates - this is a general issue with all zero-width characters as far as I can tell.

For example, in the test case below (a zero-width space followed by an asterisk), I'd expect the asterisk to appear in the leftmost column, but it actually ends up in column 2.

With your PR applied, notice how it makes the | on line 10 shift left by one column. This is definitely not expected behavior.

This is a great explanation, thank you! I don't want a fix for this to cause more issues than it resolves; just pushed a new implementation to the PR that moves logic into the state machine. It also adds a test, which is now possible.

@adalinesimonian commented on GitHub (May 24, 2025): > The problem is that they aren't always treated as zero-width. If you output them at the start of a line, they'll occupy one column. Although this isn't just a problem with the bidi isolates - this is a general issue with all zero-width characters as far as I can tell. > > For example, in the test case below (a zero-width space followed by an asterisk), I'd expect the asterisk to appear in the leftmost column, but it actually ends up in column 2. > > With your PR applied, notice how it makes the `|` on line 10 shift left by one column. This is definitely not expected behavior. This is a great explanation, thank you! I don't want a fix for this to cause more issues than it resolves; just pushed [a new implementation](https://github.com/microsoft/terminal/pull/18942/commits/5a9b861b71d4cfb1efefb134ad2db3c678bf9bb7) to the PR that moves logic into the state machine. It also adds a test, which is now possible.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#21110