Skip to content

Cursor drift after auto-wrap when first char of wrapped line is a space #2071

@herrschmidt

Description

@herrschmidt

prompt_toolkit Bug Report: Cursor drift after auto-wrap with leading space

Summary

When typing in a TextArea (or BufferControl) with wrap_lines=True and
multiline=False, the cursor visually drifts to the right when a space
character becomes the first character of an auto-wrapped visual line. The
drift equals approximately the number of wraps (e.g., 4 wraps → 4 characters
of drift). Navigating back to the line with arrow keys corrects the cursor
position.

Environment

  • prompt_toolkit version: 3.0.x (shipped with Hermes Agent, May 2026)
  • Python version: 3.11
  • OS: Linux (Ubuntu 24.04)
  • Terminal: SSH from Windows (PowerShell, Termius)
  • TERM: xterm-256color
  • Application: Hermes Agent (hermes --tui)

Steps to Reproduce

  1. Open a prompt_toolkit application with a TextArea that has:
    • multiline=False (single line input)
    • wrap_lines=True
    • A prompt (e.g., BeforeInput)
  2. Type a long line of text without spaces until it auto-wraps to the next
    visual line
  3. Continue typing — the first character typed on the new visual line, if it
    is a space (because the text logically has a space at that point), causes
    the cursor to visually jump ~1 character to the right per wrap
  4. The cursor now appears 4+ characters away from where text is actually
    inserted

Additional Observations

  • Adding spaces BEFORE the auto-wrap point (pushing the wrap point further
    right) prevents the drift
  • With full_screen=False, the drift is ~1 character per wrap (4 wraps = 4
    chars)
  • With full_screen=True, the drift becomes a full line offset
  • The drift is corrected when the user navigates to the line with arrow keys
    (cursor position is recalculated)

Suspected Root Cause

In BufferControl._create_get_processed_line_func, the translate_rowcol
function computes cursor position as:

Point(x=get_processed_line(row).source_to_display(col), y=row)

The y coordinate is always the buffer row (row), which is 0 for a single
line. For wrapped content, the cursor may be on a different visual row, but
y doesn't account for wrapping. The Window._copy_body renders wrapped
content correctly, but the UIContent.cursor_position uses the unadjusted Y.

Additionally, BeforeInput.apply_transformation uses fragment_list_len
for the source_to_display shift, which counts characters rather than
display width. Multi-width characters (emoji, CJK) are miscounted.

Related Code

  • prompt_toolkit/layout/containers.pyWindow._copy_body wraps text at
    line 2016+ but doesn't feed wrapping info back to cursor position
  • prompt_toolkit/layout/controls.pyBufferControl._create_get_processed_line_func
    line 773: translate_rowcol returns y=row
  • prompt_toolkit/layout/processors.pyBeforeInput line 507:
    shift_position = fragment_list_len(fragments_before) should be
    fragment_list_width(fragments_before)

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