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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions