DEC double-height ANSI sometimes not rendered #20379

Closed
opened 2026-01-31 07:11:57 +00:00 by claunia · 7 comments
Owner

Originally created by @ClaireCJS on GitHub (Aug 16, 2023).

Windows Terminal version

1.17.11461.0

Windows build number

Windows 10 [Version 10.0.19045.3208]

Other Software

TCC.exe (take command command line)

Steps to reproduce

I know I'm a bit strange in my ways, but I pipe my copy and move commands through a postprocessor ("copy-move-post.py") in python to make them more visually appealing and more "screamy" because of the way my attention works as well as my aesthetic tastes.

The file copies/moves end up with a summary that I repurpose into double-height (DEC) ansi. Really helps when watching stuff happen from across the room.

Sometimes this just....... doesn't render. I don't know why.

I feel like it's something I'm doing, but I also feel like this shouldn't happen regardless of what I'm doing. So I feel like it's a dual-ended bug.

Here's an example:

image

Oops, I deleted a FLAC I wanted to keep. I will undelete it to show what proper output looks like:

image

So yeah, normally it renders just fine.

But sometimes it doesn't.

I'll provide the code in comments.

Expected Behavior

I would expect it to render properly, like this:
image

Actual Behavior

It renders improperly, like this:
image

Originally created by @ClaireCJS on GitHub (Aug 16, 2023). ### Windows Terminal version 1.17.11461.0 ### Windows build number Windows 10 [Version 10.0.19045.3208] ### Other Software TCC.exe (take command command line) ### Steps to reproduce I know I'm a bit strange in my ways, but I pipe my copy and move commands through a postprocessor ("copy-move-post.py") in python to make them more visually appealing and more "screamy" because of the way my attention works as well as my aesthetic tastes. The file copies/moves end up with a summary that I repurpose into double-height (DEC) ansi. Really helps when watching stuff happen from across the room. Sometimes this just....... doesn't render. I don't know why. I feel like it's something I'm doing, but I also feel like this shouldn't happen regardless of what I'm doing. So I feel like it's a dual-ended bug. Here's an example: ![image](https://github.com/microsoft/terminal/assets/789591/b13c2ca5-245e-4985-8737-cb98eac0040e) Oops, I deleted a FLAC I wanted to keep. I will undelete it to show what proper output looks like: ![image](https://github.com/microsoft/terminal/assets/789591/d3f13c8a-ef84-437f-9ecc-148e0db4f011) So yeah, normally it renders just fine. But sometimes it doesn't. I'll provide the code in comments. ### Expected Behavior I would expect it to render properly, like this: ![image](https://github.com/microsoft/terminal/assets/789591/623a4468-7c49-4de1-a1ed-f592256d2cfe) ### Actual Behavior It renders improperly, like this: ![image](https://github.com/microsoft/terminal/assets/789591/db6ec0db-7253-4b2a-bc80-885229ff921e)
claunia added the Issue-QuestionIssue-BugResolution-Duplicate labels 2026-01-31 07:11:57 +00:00
Author
Owner

@ClaireCJS commented on GitHub (Aug 16, 2023):

Here's my copy-move-post.py

Basically, i alias my move/copy/mv/cp commands to pipe stdout and stderr to copy-move-post.py


import random
import sys
import re
import io
from colorama import init
init(autoreset=False)




sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')


FOOTERS = [
           "files copied", "file copied",
           "files moved" , "file moved" ,
           "dirs copied" , "dir copied" ,
           "dirs moved"  , "dir moved"  ,
          ]

MIN_RGB_VALUE_FG = 64; MAX_RGB_VALUE_FG = 255
MIN_RGB_VALUE_BG = 16; MAX_RGB_VALUE_BG = 64

def get_random_color(bg=False):
    if bg: min_rgb_value = MIN_RGB_VALUE_BG; max_rgb_value = MAX_RGB_VALUE_BG
    else:  min_rgb_value = MIN_RGB_VALUE_FG; max_rgb_value = MAX_RGB_VALUE_FG
    return random.randint(min_rgb_value,max_rgb_value), \
           random.randint(min_rgb_value,max_rgb_value), \
           random.randint(min_rgb_value,max_rgb_value)

def enclose_numbers(line): return re.sub(r'(\d+)', r'\033[21m\1\033[24m', line)                                             # ansi-stylize numbers - italics + double-underline

def print_line(line_buffer, r, g, b, additional_beginning_ansi=""):
    double  = False                                                                                                         # double height or not?
    summary = False                                                                                                         # copy/mv summary line?
    if any(substring in line_buffer for substring in FOOTERS):
        line_buffer = enclose_numbers(line_buffer)
        double      = True
        summary     = True
    line = f'\033[93m'                                                                                                      # i like my arrow yellow
    if   any(substring in line_buffer for substring in ["Y/N/A/R"]):     line += f'❓❓ '                                     # emojis at beginning of prompty  lines
    if   any(substring in line_buffer for substring in ["=>","->"]):     line += f'⭢︋📂 '                                    # emojis at beginning of filename lines
    elif any(substring in line_buffer for substring in ["TCC: (Sys)"]):
        double = True;                                                   line += f'🛑🛑\033[6m\033[3m\033[4m\033[7m'        # emojis at beginning of error    lines during copying
    elif any(substring in line_buffer for substring in ["Deleting "]):   line += f'👻⛔'

    elif summary:                                                        line += f'✔️'                                      # emojis at beginning of summary  lines of how many files copied
    else:                                                                line += f'  '                                      # normal lines get prefixed with this
    line += f'\033[38;2;{r};{g};{b}m{additional_beginning_ansi}{line_buffer.rstrip()}\033[0m\n'                             # print line in our random-RGB color
    line = line.replace("=>>","↪️") #.replace("=>","⭢︋")
    if not double: sys.stdout.write(line)                                                                                   # normal height line
    else:          sys.stdout.write(f'\033#3{line}\033#4{line}')                                                            # double height line


################################################################################################################################################################

line_buffer = ""
in_prompt = False
additional_beginning_ansi = ""
r, g, b = get_random_color()
while True:                                                                                                                         # It's tempting to process things line-by-line, but due to prompts and such, we must process things char-by-char
    char = sys.stdin.read(1)
    if not char: break
    sys.stdout.flush()
    line_buffer += char
    if char == '?' and not in_prompt:
        in_prompt = True
        bgr, bgg, bgb = get_random_color(bg=True)                                                                                   # Reset for the next line
        r  ,   g,   b = get_random_color()                                                                                          # Reset for the next line
        #ys.stdout.write(f'\033[38;2;{r};{g};{b}m{additional_beginning_ansi}❓❓   \033[6m{line_buffer}\033[0m ') #\033[1C
        #ys.stdout.write(f'\033[48;2;{r};{g};{b}m{additional_beginning_ansi}❓❓   \033[6m{line_buffer}\033[0m ') #\033[1C
        sys.stdout.write(f'\033[48;2;{bgr};{bgg};{bgb}m\033[38;2;{r};{g};{b}m{additional_beginning_ansi}❓❓   \033[6m{line_buffer} \033[0m') #\033[0m #\033[1C
        #moved to end of loop: sys.stdout.flush()                                                                                   # Flush the output buffer to display the prompt immediately
        line_buffer = ""
    elif in_prompt and char == '\n':
        in_prompt = False
        sys.stdout.write(f'\033[1D{line_buffer.rstrip()}\033[0m\n')
        sys.stdout.flush()
        line_buffer = ""
    elif char == '\n':
        if any(substring in line_buffer for substring in FOOTERS): additional_beginning_ansi += "\033[6m"                           # make it blink
        print_line(line_buffer, r, g, b, additional_beginning_ansi)
        line_buffer               = ""                                                                                              # Reset for the next line
        additional_beginning_ansi = ""                                                                                              # Reset for the next line
        r, g, b = get_random_color()                                                                                                # Reset for the next line
    sys.stdout.flush()

@ClaireCJS commented on GitHub (Aug 16, 2023): Here's my copy-move-post.py Basically, i alias my move/copy/mv/cp commands to pipe stdout and stderr to copy-move-post.py ``` import random import sys import re import io from colorama import init init(autoreset=False) sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') FOOTERS = [ "files copied", "file copied", "files moved" , "file moved" , "dirs copied" , "dir copied" , "dirs moved" , "dir moved" , ] MIN_RGB_VALUE_FG = 64; MAX_RGB_VALUE_FG = 255 MIN_RGB_VALUE_BG = 16; MAX_RGB_VALUE_BG = 64 def get_random_color(bg=False): if bg: min_rgb_value = MIN_RGB_VALUE_BG; max_rgb_value = MAX_RGB_VALUE_BG else: min_rgb_value = MIN_RGB_VALUE_FG; max_rgb_value = MAX_RGB_VALUE_FG return random.randint(min_rgb_value,max_rgb_value), \ random.randint(min_rgb_value,max_rgb_value), \ random.randint(min_rgb_value,max_rgb_value) def enclose_numbers(line): return re.sub(r'(\d+)', r'\033[21m\1\033[24m', line) # ansi-stylize numbers - italics + double-underline def print_line(line_buffer, r, g, b, additional_beginning_ansi=""): double = False # double height or not? summary = False # copy/mv summary line? if any(substring in line_buffer for substring in FOOTERS): line_buffer = enclose_numbers(line_buffer) double = True summary = True line = f'\033[93m' # i like my arrow yellow if any(substring in line_buffer for substring in ["Y/N/A/R"]): line += f'❓❓ ' # emojis at beginning of prompty lines if any(substring in line_buffer for substring in ["=>","->"]): line += f'⭢︋📂 ' # emojis at beginning of filename lines elif any(substring in line_buffer for substring in ["TCC: (Sys)"]): double = True; line += f'🛑🛑\033[6m\033[3m\033[4m\033[7m' # emojis at beginning of error lines during copying elif any(substring in line_buffer for substring in ["Deleting "]): line += f'👻⛔' elif summary: line += f'✔️' # emojis at beginning of summary lines of how many files copied else: line += f' ' # normal lines get prefixed with this line += f'\033[38;2;{r};{g};{b}m{additional_beginning_ansi}{line_buffer.rstrip()}\033[0m\n' # print line in our random-RGB color line = line.replace("=>>","↪️") #.replace("=>","⭢︋") if not double: sys.stdout.write(line) # normal height line else: sys.stdout.write(f'\033#3{line}\033#4{line}') # double height line ################################################################################################################################################################ line_buffer = "" in_prompt = False additional_beginning_ansi = "" r, g, b = get_random_color() while True: # It's tempting to process things line-by-line, but due to prompts and such, we must process things char-by-char char = sys.stdin.read(1) if not char: break sys.stdout.flush() line_buffer += char if char == '?' and not in_prompt: in_prompt = True bgr, bgg, bgb = get_random_color(bg=True) # Reset for the next line r , g, b = get_random_color() # Reset for the next line #ys.stdout.write(f'\033[38;2;{r};{g};{b}m{additional_beginning_ansi}❓❓ \033[6m{line_buffer}\033[0m ') #\033[1C #ys.stdout.write(f'\033[48;2;{r};{g};{b}m{additional_beginning_ansi}❓❓ \033[6m{line_buffer}\033[0m ') #\033[1C sys.stdout.write(f'\033[48;2;{bgr};{bgg};{bgb}m\033[38;2;{r};{g};{b}m{additional_beginning_ansi}❓❓ \033[6m{line_buffer} \033[0m') #\033[0m #\033[1C #moved to end of loop: sys.stdout.flush() # Flush the output buffer to display the prompt immediately line_buffer = "" elif in_prompt and char == '\n': in_prompt = False sys.stdout.write(f'\033[1D{line_buffer.rstrip()}\033[0m\n') sys.stdout.flush() line_buffer = "" elif char == '\n': if any(substring in line_buffer for substring in FOOTERS): additional_beginning_ansi += "\033[6m" # make it blink print_line(line_buffer, r, g, b, additional_beginning_ansi) line_buffer = "" # Reset for the next line additional_beginning_ansi = "" # Reset for the next line r, g, b = get_random_color() # Reset for the next line sys.stdout.flush() ```
Author
Owner

@DHowett commented on GitHub (Aug 16, 2023):

Interesting, thanks for sharing!

image

This always means that somebody turned off ENABLE_VIRTUAL_TERMINAL_PROCESSING using SetConsoleMode on the output handle. This often happens when multiple processes are attached to the same console: one terminates and tries to restore the console mode (often directly, by opening the special handle CONOUT$ regardless of redirection (!)) before the other is done printing text in the mode that it had requested.

This will largely be solved by making the console mode "per-process" inheritable state, but we're not quite prepared for that today.

@DHowett commented on GitHub (Aug 16, 2023): Interesting, thanks for sharing! <img width="77" alt="image" src="https://github.com/microsoft/terminal/assets/189190/64fa3106-35c1-4e0a-bc01-4f5907be6385"> This always means that somebody turned off `ENABLE_VIRTUAL_TERMINAL_PROCESSING` using `SetConsoleMode` on the output handle. This often happens when multiple processes are attached to the same console: one terminates and tries to restore the console mode (often directly, by opening the special handle `CONOUT$` regardless of redirection (!)) before the other is done printing text in the mode that it had requested. This will largely be solved by making the console mode "per-process" inheritable state, but we're not quite prepared for that today.
Author
Owner

@ClaireCJS commented on GitHub (Aug 16, 2023):

Interesting, thanks for sharing!

But of course :)

This always means that somebody turned off ENABLE_VIRTUAL_TERMINAL_PROCESSING using SetConsoleMode on the output handle.

Well, "somebody" seems like a strong word, haha :)

I mean, it happens sometimes, and it doesn't happen other times, but this is just me piping through Python. I suppose the colorama autoreset option could have some interplay?

This often happens when multiple processes are attached to the same console: one terminates and tries to restore the console mode (often directly, by opening the special handle CONOUT$ regardless of redirection (!)) before the other is done printing text in the mode that it had requested.

Is that what happens with piping? That sounds suspiciously like what happens with piping. My command finishes before the postprocessor finishes -- it would have to by definition.

Hmm, very compelling.

This will largely be solved by making the console mode "per-process" inheritable state, but we're not quite prepared for that today.

Any idea how I can fix it within the context of my usage here? Because it only happens sometimes. So it's a timing issue I think?

@ClaireCJS commented on GitHub (Aug 16, 2023): > Interesting, thanks for sharing! But of course :) > This always means that somebody turned off `ENABLE_VIRTUAL_TERMINAL_PROCESSING` using `SetConsoleMode` on the output handle. Well, "somebody" seems like a strong word, haha :) I mean, it happens sometimes, and it doesn't happen other times, but this is just me piping through Python. I suppose the colorama autoreset option could have some interplay? >This often happens when multiple processes are attached to the same console: one terminates and tries to restore the console mode (often directly, by opening the special handle `CONOUT$` regardless of redirection (!)) before the other is done printing text in the mode that it had requested. Is that what happens with piping? That sounds suspiciously like what happens with piping. My command finishes before the postprocessor finishes -- it would have to by definition. Hmm, very compelling. > This will largely be solved by making the console mode "per-process" inheritable state, but we're not quite prepared for that today. Any idea how I can fix it within the context of my usage here? Because it only happens sometimes. So it's a timing issue I think?
Author
Owner

@zadjii-msft commented on GitHub (Aug 22, 2023):

So, usually I'd drop this at the start of all the python scripts I run: https://github.com/zadjii/win-py-vt/blob/master/enable.py

but since this seems to be a timing thing, I'd just sprinkle in enable_vt_support before wherever you output VT and it isn't working 🤷‍♂️

ninja: this might be worthy of just moving to a Discussion thread

@zadjii-msft commented on GitHub (Aug 22, 2023): So, usually I'd drop this at the start of all the python scripts I run: https://github.com/zadjii/win-py-vt/blob/master/enable.py but since this seems to be a timing thing, I'd just sprinkle in `enable_vt_support` before wherever you output VT and it isn't working 🤷‍♂️ ninja: this might be worthy of just moving to a Discussion thread
Author
Owner

@ClaireCJS commented on GitHub (Aug 25, 2023):

So, usually I'd drop this at the start of all the python scripts I run: https://github.com/zadjii/win-py-vt/blob/master/enable.py

but since this seems to be a timing thing, I'd just sprinkle in enable_vt_support before wherever you output VT and it isn't working 🤷‍♂️

OMG Thank you very much for this arcane voodoo! I'll have no way of knowing if it fixed things other than if the problem doesn't seem to manifest over the next few weeks.

I wonder if anyone else is piping their move/copy commands to a postprocessing script like weird-ol'-me.

ninja: this might be worthy of just moving to a Discussion thread

@ClaireCJS commented on GitHub (Aug 25, 2023): > So, usually I'd drop this at the start of all the python scripts I run: https://github.com/zadjii/win-py-vt/blob/master/enable.py > > but since this seems to be a timing thing, I'd just sprinkle in `enable_vt_support` before wherever you output VT and it isn't working 🤷‍♂️ OMG Thank you very much for this arcane voodoo! I'll have no way of knowing if it fixed things other than if the problem doesn't seem to manifest over the next few weeks. I wonder if anyone else is piping their move/copy commands to a postprocessing script like weird-ol'-me. > > ninja: this might be worthy of just moving to a Discussion thread
Author
Owner

@lhecker commented on GitHub (Aug 30, 2023):

I'm glad it seems better now! I'll mark this issue as a /duplicate of #4954, as fixing #4954 would fix your issue permanently and in a more robust manner.

@lhecker commented on GitHub (Aug 30, 2023): I'm glad it seems better now! I'll mark this issue as a /duplicate of #4954, as fixing #4954 would fix your issue permanently and in a more robust manner.
Author
Owner

@microsoft-github-policy-service[bot] commented on GitHub (Aug 30, 2023):

Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!

@microsoft-github-policy-service[bot] commented on GitHub (Aug 30, 2023): Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#20379