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
    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