How Rich Handles Windows Console Differences Between Legacy Console and Windows Terminal

Rich automatically detects Virtual Terminal (VT) support via the Win32 API and switches between standard ANSI escape sequences for Windows Terminal and a specialized Win32 API rendering pipeline for legacy consoles.

The Textualize/rich library provides seamless cross-platform terminal rendering, but Windows presents unique architectural challenges between the classic Win32 console and modern terminal emulators. Understanding how Rich handles Windows console differences ensures consistent styled output whether your users run legacy PowerShell or the latest Windows Terminal.

Detecting Console Capabilities at Runtime

Querying the Win32 API for VT Support

Rich determines terminal capabilities through rich._windows.get_windows_console_features(), which interrogates the console mode using the Windows GetConsoleMode API on the standard output handle. The detection specifically checks for the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag (value 4) to determine if the environment supports ANSI escape sequences.


# rich/_windows.py (excerpt)

def get_windows_console_features() -> WindowsConsoleFeatures:
    handle = GetStdHandle()
    console_mode = GetConsoleMode(handle)
    vt = bool(console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
    truecolor = vt and (win_version.major > 10 or
                        (win_version.major == 10 and win_version.build >= 15063))
    return WindowsConsoleFeatures(vt=vt, truecolor=truecolor)

Source: rich/_windows.py

Windows Version and True Color Detection

Beyond VT detection, Rich evaluates the Windows version to determine true-color support. True-color (24-bit color) is only enabled when VT processing is available and the system runs Windows 10 build 15063 or later, or Windows 11.

Legacy Windows Console Mode

When ENABLE_VIRTUAL_TERMINAL_PROCESSING is absent, Rich activates legacy mode through the detect_legacy_windows() helper invoked during Console.__init__ in rich/console.py.

The legacy_windows Flag and Width Adjustment

Setting self.legacy_windows = True triggers specific behavioral changes in the rendering pipeline. Most notably, Rich reduces the console width by exactly one column because the legacy console treats the final column as a trigger for automatic scrolling.


# rich/console.py (excerpt)

self.legacy_windows: bool = (
    (detect_legacy_windows() and not self.is_jupyter)
    if legacy_windows is None
    else legacy_windows
)
if self.legacy_windows:
    # Width is one column narrower because the legacy console

    # treats the last column specially.

    width = int(columns) - self.legacy_windows

Source: rich/console.py

Win32 API Rendering Pipeline

Instead of emitting ANSI sequences, Rich diverts output to rich._windows_renderer.legacy_windows_render(). This function iterates over Segment objects and translates them into Win32 API calls via the LegacyWindowsTerm class defined in rich/_win32_console.py.


# rich/_windows_renderer.py

def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None:
    for text, style, control in buffer:
        if not control:
            term.write_styled(text, style) if style else term.write_text(text)
        else:
            # translate ControlCodes into Win32 API calls

            ...

Source: rich/_windows_renderer.py

The LegacyWindowsTerm class utilizes native functions like WriteConsole and SetConsoleTextAttribute to apply colors and styles, while cursor movement operations use SetConsoleCursorPosition.

Windows Terminal and Modern VT Mode

Standard ANSI Escape Sequence Rendering

When VT processing is detected (vt=True), Console.legacy_windows remains False and Rich follows its standard rendering path. The console builds Segment objects containing raw ANSI escape codes and writes them directly to sys.stdout without Win32 API intermediation.


# rich/console.py (excerpt, around line 2060)

use_legacy_windows_render = False
if self.legacy_windows:
    use_legacy_windows_render = True

if use_legacy_windows_render:
    from rich._windows_renderer import legacy_windows_render
    legacy_windows_render(buffer, LegacyWindowsTerm(self.file))
else:
    # Normal ANSI‑escape rendering

    self.file.write(...escape sequences...)

Source: rich/console.py

Full Unicode and True Color Support

Modern terminals receive full-featured output including 24-bit true-color (when the Windows version permits), Unicode box-drawing characters, and emoji support. Components like Tree, ProgressBar, and Box consult options.legacy_windows to bypass these features in legacy environments, falling back to ASCII equivalents.

Working with Console Modes in Practice

Detecting the Mode at Runtime

You can inspect the legacy_windows attribute to determine which rendering path Rich selected:

from rich.console import Console

console = Console()
if console.legacy_windows:
    console.print("[bold red]Running in legacy Windows console[/]")
else:
    console.print("[bold green]Running in a VT‑enabled terminal[/]")

Forcing Legacy Mode for Testing

To test fallback behavior on a VT-capable terminal, explicitly enable legacy mode:


# Force legacy mode even on a VT‑capable terminal

console = Console(legacy_windows=True)

# The table will fall back to ASCII box characters and no true‑color.

console.print(
    "[table]\n"
    "Header 1 | Header 2\n"
    "--------+--------\n"
    "value 1 | value 2\n"
    "[/table]"
)

Observing Feature Differences

Features automatically adapt their output based on the console mode. Progress bars use ASCII blocks in legacy mode but switch to Unicode characters and smoother animations in VT mode:

from rich.progress import Progress

# Progress bar will use ASCII blocks in legacy mode,

# but Unicode/ANSI colors in VT mode.

with Progress() as progress:
    task = progress.add_task("Processing...", total=100)
    for i in range(100):
        progress.update(task, advance=1)

Direct Low-Level Legacy Rendering

For advanced scenarios, you can bypass the standard console write path and invoke the legacy renderer directly:

from rich.console import Console
from rich.segment import Segment
from rich._windows_renderer import legacy_windows_render
from rich._win32_console import LegacyWindowsTerm

console = Console()
buffer = [Segment("Hello ", style=None), Segment("World!", style="bold red")]
term = LegacyWindowsTerm(console.file)

# Bypass the normal console write path

legacy_windows_render(buffer, term)

Summary

  • Automatic Detection: Rich queries GetConsoleMode via rich._windows.get_windows_console_features() to check for ENABLE_VIRTUAL_TERMINAL_PROCESSAL_PROCESSING (value 4).
  • Dual-Path Rendering: The legacy_windows boolean flag in rich/console.py routes output either through standard ANSI sequences or the Win32 API-based legacy_windows_render() function.
  • Width Compensation: Legacy mode automatically reduces console width by one column to prevent unwanted scrolling behavior in the classic console.
  • Feature Fallback: When legacy_windows is True, Rich substitutes Unicode box drawing and true-color with ASCII equivalents and 16-color palette limits.
  • Zero Configuration: The library requires no manual intervention; it detects capabilities during Console initialization and selects the appropriate renderer.

Frequently Asked Questions

How does Rich detect if it is running in Windows Terminal versus the legacy console?

Rich calls get_windows_console_features() from rich/_windows.py, which uses the Win32 API function GetConsoleMode to inspect the standard output handle. If the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag (value 4) is present, Rich assumes VT support and disables legacy mode. If the flag is absent, detect_legacy_windows() returns True, triggering the Win32 API rendering path.

Why does Rich reduce the console width by one column in legacy mode?

The classic Windows console treats the final column as a special trigger for automatic scrolling when characters are written there. To prevent premature scrolling and layout corruption, rich/console.py subtracts one from the detected column count when self.legacy_windows is True (self.width = columns - self.legacy_windows).

Can I force Rich to use legacy rendering even on Windows Terminal?

Yes. Instantiate Console with the legacy_windows=True parameter to override automatic detection. This forces Rich to use legacy_windows_render() from rich/_windows_renderer.py and the LegacyWindowsTerm class, which relies solely on Win32 API calls rather than ANSI escape sequences.

What specific Win32 API functions does Rich use for legacy rendering?

The LegacyWindowsTerm class in rich/_win32_console.py wraps several native functions: WriteConsole for text output, SetConsoleTextAttribute for applying color and style attributes, and SetConsoleCursorPosition for cursor movement. These replace the ANSI escape codes used in modern terminals.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →