Skip to content

Native console apps with ANSI color wrap too early in MSYS2 UCRT64 mintty (wrap splits ESC[…m sequences) on Windows Server 2019 #411

@unsinkable2

Description

@unsinkable2

Summary

On Windows 2019 server, when running a native console program (e.g. mingw-w64-ucrt-x86_64-python) in the UCRT64 mintty shell, long lines that contain ANSI SGR color codes (ESC[...m) wrap too early.

The wrapping appears to be done on raw byte length, counting the characters in \x1b[...m as visible columns, but then the rendering treats them as zero-width formatting. This can cause the wrap to happen inside an SGR sequence, so m or 0m appears at the beginning of the next line.

The same text, when printed with MSYS bash + printf, does not show this behavior: wrapping correctly respects the visible length and treats SGR as zero-width.

So:

  • MSYS/printf → mintty: ✅ correct wrapping
  • native console app (Python) → console/pty bridge → mintty: ❌ wraps too early, splits SGR (this issue does not seem to exist on Windows 10 or 11)

Environment

OS

  • Windows Server 2019: Version 1809 OS Build 17763.7792

Terminal

  • MSYS2 UCRT64 mintty (started from the “MSYS2 UCRT64” shortcut)

MSYS2:

  • MSYSTEM=UCRT64
  • TERM=xterm-256color
  • COLUMNS=80
  • tput cols = 80

Python

$ pacman -Qi mingw-w64-ucrt-x86_64-python
Name            : mingw-w64-ucrt-x86_64-python
Version         : 3.12.12-1
Description     : A high-level scripting language (mingw-w64)
Architecture    : any
URL             : https://www.python.org/
Licenses        : spdx:PSF-2.0
Groups          : None
Provides        : mingw-w64-ucrt-x86_64-python3
                  mingw-w64-ucrt-x86_64-python3.12
Depends On      : mingw-w64-ucrt-x86_64-cc-libs  mingw-w64-ucrt-x86_64-expat
                  mingw-w64-ucrt-x86_64-bzip2  mingw-w64-ucrt-x86_64-libffi
                  mingw-w64-ucrt-x86_64-mpdecimal
                  mingw-w64-ucrt-x86_64-ncurses  mingw-w64-ucrt-x86_64-openssl
                  mingw-w64-ucrt-x86_64-sqlite3  mingw-w64-ucrt-x86_64-tcl
                  mingw-w64-ucrt-x86_64-tk  mingw-w64-ucrt-x86_64-zlib
                  mingw-w64-ucrt-x86_64-xz  mingw-w64-ucrt-x86_64-tzdata
Optional Deps   : None
Required By     : None
Optional For    : None
Conflicts With  : mingw-w64-ucrt-x86_64-python3
                  mingw-w64-ucrt-x86_64-python3.12
                  mingw-w64-ucrt-x86_64-python2<2.7.16-7
Replaces        : mingw-w64-ucrt-x86_64-python3
                  mingw-w64-ucrt-x86_64-python3.12
Installed Size  : 185.99 MiB
Packager        : CI (msys2/msys2-autobuild/55384653/18406062571)
Build Date      : Fri, 10 Oct, 2025 14:42:57
Install Date    : Wed, 3 Dec, 2025 9:47:09
Install Reason  : Explicitly installed
Install Script  : No
Validated By    : SHA-256 Sum  Signature

$ mintty --version

mintty '3.8.1' 2025-09-18_06:11 (Msys-x86_64)
© 2025 Thomas Wolff, Andy Koppe
License GPLv3+: GNU GPL version 3 or later
There is no warranty, to the extent permitted by law.

Repro 1 - bash + printf - works correctly

#!/usr/bin/env bash
# ansi_wrap_test.sh

# Report terminal width
cols=$(tput cols)
echo "cols           = $cols"

# Plain (no color) version of the line
plain='[DemoProcess1] [process1] [MainThread] [A Custom Task] [INF] Detailed step 1'
echo "plain_len      = ${#plain}"

echo
echo "Plain line:"
printf '%s\n' "$plain"
echo

# ANSI color codes
blue=$'\033[34m'
bold=$'\033[1m'
green=$'\033[32m'
reset=$'\033[0m'

# Colored version
colored="[DemoProcess1] [process1] [MainThread] ${blue}${bold}[A Custom Task]${reset} ${green}[INF]${reset} Detailed step 1"

raw_len=${#colored}

# Visible length: strip SGR sequences and measure
visible=$(printf '%s' "$colored" | sed -E 's/\x1b\[[0-9;]*m//g')
visible_len=${#visible}

echo "raw_len        = $raw_len"
echo "visible_len    = $visible_len"

echo
echo "Colored line:"
printf '%s\n' "$colored"

Example output on my machine:

cols           = 80
plain_len      = 76

Plain line:
[DemoProcess1] [process1] [MainThread] [A Custom Task] [INF] Detailed step 1

raw_len        = 98
visible_len    = 76

Colored line:
[DemoProcess1] [process1] [MainThread] [A Custom Task] [INF] Detailed step 1

Repro 2 – native Python (wraps too early / splits ESC[0m)

In the same UCRT64 mintty window, install MSYS2 UCRT Python with pacman: -S --needed mingw-w64-ucrt-x86_64-python
Create test.py:

import re
import shutil

BLUE = "\x1b[34m"
BOLD = "\x1b[1m"
GREEN = "\x1b[32m"
RESET = "\x1b[0m"

def visible_len(s: str) -> int:
    # strip SGR color codes
    return len(re.sub(r'\x1b\[[0-9;]*m', '', s))

def run_test():
    task_name = "A Custom Task"
    log_level_short = f"{GREEN}[INF]{RESET}"
    msg = "Detailed step 1"
    text = (
        "[DemoProcess1] [process1] [MainThread] "
        f"{BLUE}{BOLD}[{task_name}]{RESET} "
        f"{log_level_short} "
        f"{msg}"
    )

    max_line_length = shutil.get_terminal_size(fallback=(80, 20)).columns
    text_len = len(text)
    text_visible_len = visible_len(text)

    print(f"text_length         = {text_len}")
    print(f"text_visible_length = {text_visible_len}")
    print(f"max_line_length     = {max_line_length}")
    print(text)

if __name__ == "__main__":
    run_test()

Example output:

text_length         = 98
text_visible_length = 76
max_line_length     = 80
[DemoProcess1] [process1] [MainThread] [A Custom Task] [INF]
0m Detailed step 1

The 0m is the tail of the \x1b[0m SGR reset sequence. The wrap is happening inside the escape sequence, which suggests that the wrapping decision is based on the raw byte count (including ESC[, digits, m) instead of on visible columns.

Expected behavior

For both MSYS printf and native-console programs running under UCRT64/mintty:

  • Wrapping should be based on visible columns.
  • ANSI SGR sequences (ESC[...m) should be treated as zero-width for wrapping.
  • The line above should not wrap, since its visible length (76) is less than the terminal width (80).
  • ANSI escape sequences should not be split across lines (no stray m / 0m at line starts)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions