Skip to content

Wrap prompt messages to the terminal width#499

Open
andrzej-pomirski-yohana wants to merge 1 commit into
tmbo:masterfrom
andrzej-pomirski-yohana:fix/398-wrap-prompt-messages-to-terminal-width
Open

Wrap prompt messages to the terminal width#499
andrzej-pomirski-yohana wants to merge 1 commit into
tmbo:masterfrom
andrzej-pomirski-yohana:fix/398-wrap-prompt-messages-to-terminal-width

Conversation

@andrzej-pomirski-yohana

@andrzej-pomirski-yohana andrzej-pomirski-yohana commented May 13, 2026

Copy link
Copy Markdown

Summary

Fixes #398 — long prompt messages stop wrapping to terminal width when the message contains a newline.

This is the questionary side of copier-org/copier#1764, which was closed pointing at #398. Copier appends "\n " to every prompt's message to push the cursor onto the next line, and the resulting newline trips the wrap-disabling code path described below.

Root cause

In prompt_toolkit/shortcuts/prompt.py, _split_multiline_prompt splits the prompt at the last newline in the message. Everything before that newline is rendered in a separate above-input Window. That window is constructed with the default wrap_lines=False; only the input-buffer window receives wrap_lines=True:

# main_input_container, abridged
HSplit([
    ConditionalContainer(
        Window(FormattedTextControl(get_prompt_text_1), dont_extend_height=True),
        Condition(has_before_fragments),
    ),
    ConditionalContainer(default_buffer_window, ...),  # wrap_lines=dyncond("wrap_lines")
    ...
])

So when a caller passes a message like "long help text\n ", prompt-toolkit puts "long help text" into the non-wrapping window and the long line overflows the terminal.

Fix

Pre-wrap each \n-separated paragraph of the message to the current terminal width before constructing the formatted text. The rendered lines are then already short enough that prompt-toolkit's missing wrap on the above-input window doesn't matter.

Two internal helpers handle this:

  • _wrap_message_to_terminal_width(message, prefix_width) in questionary/utils.py — the low-level primitive. Splits on \n, wraps each paragraph with textwrap.fill (no breaking of long words, no break-on-hyphens, preserves whitespace), returns the rewrapped string. Non-string messages (already-formatted tuples / lists) pass through unchanged so callers passing pre-styled prompts are not disturbed.
  • format_question_tokens(qmark, message) in questionary/prompts/common.py — builds the leading [("class:qmark", qmark), ("class:question", " message ")] tuple list used by every built-in prompt. Owns the prefix_width = len(qmark) + 1 arithmetic so prompt files don't have to know about it. With qmark=None it omits the qmark token and reduces to a question-only prompt.

Each prompt's get_prompt_tokens becomes a one-line call to format_question_tokens. Applied across the seven prompt types that format a user-provided message: text, select, checkbox, confirm, autocomplete, path, press_any_key_to_continue. password and rawselect delegate to text / select and inherit the fix transparently.

Terminal width source

_current_terminal_width() queries the active Application via get_app_or_none().output.get_size() first — that value updates on SIGWINCH, so subsequent re-renders pick up the new width. Falls back to shutil.get_terminal_size() outside a prompt session (e.g. unit tests), and finally to DEFAULT_TERMINAL_WIDTH (80, defined in questionary/constants.py).

Known limitation: rapid resize artifacts

This fix makes the prompt height variable with terminal width (1 line when wide, several when narrow). That exposes a pre-existing prompt-toolkit limitation around resize handling: Application._on_resize calls renderer.erase() then _request_absolute_cursor_position() then _redraw() — but the redraw fires before the async CPR response, so rapid SIGWINCH bursts can leave stale prior renders in the scrollback. The final render at any width is correct; only the intermediate frames during rapid resize are affected.

Without this fix the issue is hidden because every render is a single (cropped) line, so erase + redraw at the same height stays clean.

The proper fix for the resize race is upstream in prompt-toolkit's renderer / CPR handling; this PR doesn't attempt it.

Tests

17 new unit tests.

tests/test_utils.py (14 tests on _wrap_message_to_terminal_width and _current_terminal_width):

  • Short string returned unchanged
  • Long string wraps at terminal width
  • Existing newlines preserved as paragraph breaks
  • Long paragraph followed by \n wraps (regression for Newline \n in message breaks line wrap in terminal #398)
  • prefix_width reserves space on the first line
  • Non-string messages pass through (regression for the FormattedText tuple case caught early in development)
  • Empty string passes through
  • Single very long word with no breakable whitespace returns unchanged (textwrap with break_long_words=False)
  • Pathologically narrow terminal (1 col) returns unchanged
  • _current_terminal_width falls back to shutil on no-app, then to DEFAULT_TERMINAL_WIDTH on OSError
  • _current_terminal_width prefers the active app's size over shutil
  • _current_terminal_width falls through to shutil when the app reports columns=0
  • _current_terminal_width returns DEFAULT_TERMINAL_WIDTH when shutil reports columns=0

tests/prompts/test_common.py (3 tests on format_question_tokens):

  • Builds the [qmark, question] token pair with the expected style classes
  • Omits the qmark token when qmark=None
  • Passes non-string messages through into the format string (preserving pre-existing behavior)

All 180 tests pass (163 pre-existing + 17 new).

Contributor checklist

  • make test — 180 tests pass
  • make lint — pre-commit hooks pass (autoflake, black, isort, flake8)
  • make types — mypy clean, no issues in 19 source files

Manual verification

End-to-end test with copier (which is what surfaced the bug originally):

  1. Created a venv with copier 9.15.1 + this branch installed editable.
  2. Ran copier copy against a template with long help strings in a 60-col terminal.
  3. Before the fix: help text cropped at terminal edge.
  4. After the fix: help text wraps cleanly across multiple lines, each ≤ 60 cols.

@andrzej-pomirski-yohana andrzej-pomirski-yohana force-pushed the fix/398-wrap-prompt-messages-to-terminal-width branch 8 times, most recently from 5642cfb to 9e0ee77 Compare May 13, 2026 22:14
When a prompt message contains a newline, prompt-toolkit splits the
message at the last newline (in _split_multiline_prompt) and renders
everything before that newline in a Window whose wrap_lines is unset.
Only the input buffer window honours wrap_lines, so long help text from
callers like Copier — which appends a newline plus space to push the
cursor onto the next line — gets truncated at the terminal edge instead
of wrapping. See tmbo#398 and copier-org/copier#1764.

Pre-wrap each newline-separated paragraph of the message to the current
terminal width before constructing the formatted text. The rendered
lines are then already short enough that prompt-toolkit's missing wrap
on the above-input window doesn't matter.

The terminal width is queried from the running Application via
get_app_or_none().output.get_size() so SIGWINCH resizes pick up the new
width on the next render. Falls back to shutil.get_terminal_size outside
a prompt session (tests, etc.), and finally to 80.

A helper format_question_tokens in prompts/common.py owns the
prefix-width arithmetic and the qmark + question token assembly, so the
seven prompt types that format a user-provided message (text, select,
checkbox, confirm, autocomplete, path, press_any_key_to_continue) share
one call site instead of each duplicating the wrap and formatting
logic. password and rawselect delegate to text and select and inherit
the fix transparently. The low-level wrap function stays in utils.py
for callers building custom prompts on top of questionary's primitives.

Non-string messages (already-formatted tuples and lists) pass through
unchanged so callers passing pre-styled prompts are not disturbed.
@andrzej-pomirski-yohana andrzej-pomirski-yohana force-pushed the fix/398-wrap-prompt-messages-to-terminal-width branch from 9e0ee77 to bcb5e9c Compare May 13, 2026 22:18
@andrzej-pomirski-yohana andrzej-pomirski-yohana marked this pull request as ready for review May 13, 2026 22:20
@andrzej-pomirski-yohana

Copy link
Copy Markdown
Author

Hi, this change was Claude Code-aided, but it was not fully AI generated, I did read all the code and make fixes to it. Hopefully it passes your quality bar

@kiancross kiancross changed the title Wrap prompt messages to the terminal width (fixes #398) Wrap prompt messages to the terminal width May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Newline \n in message breaks line wrap in terminal

1 participant