Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 43 additions & 18 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1862,24 +1862,49 @@ def test_no_newline(self):
output, exit_code = self.run_repl(commands)
self.assertEqual(exit_code, 0)

# Define escape sequences that don't affect cursor position or visual output
bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste
application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys
application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad
insert_character = r'\x1b\[(?:1)?@(?=[ -~])' # Insert exactly 1 char (safe form)
cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor
cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking
device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses

safe_escapes = re.compile(
f'{bracketed_paste_mode}|'
f'{application_cursor_keys}|'
f'{application_keypad_mode}|'
f'{insert_character}|'
f'{cursor_visibility}|'
f'{cursor_blinking}|'
f'{device_attributes}'
)
# Build patterns for escape sequences that don't affect cursor position
# or visual output. Use terminfo to get platform-specific sequences,
# falling back to hard-coded patterns for capabilities not in terminfo.
from _pyrepl.terminfo import TermInfo
ti = TermInfo(os.environ.get("TERM", ""))

safe_patterns = []

# smkx/rmkx - application cursor keys and keypad mode
smkx = ti.get("smkx")
rmkx = ti.get("rmkx")
if smkx:
safe_patterns.append(re.escape(smkx.decode("ascii")))
if rmkx:
safe_patterns.append(re.escape(rmkx.decode("ascii")))
if not smkx and not rmkx:
safe_patterns.append(r'\x1b\[\?1[hl]') # application cursor keys
safe_patterns.append(r'\x1b[=>]') # application keypad mode

# ich1 - insert character (only safe form that inserts exactly 1 char)
ich1 = ti.get("ich1")
if ich1:
safe_patterns.append(re.escape(ich1.decode("ascii")) + r'(?=[ -~])')
else:
safe_patterns.append(r'\x1b\[(?:1)?@(?=[ -~])')

# civis/cnorm - cursor visibility (may include cursor blinking control)
civis = ti.get("civis")
cnorm = ti.get("cnorm")
if civis:
safe_patterns.append(re.escape(civis.decode("ascii")))
if cnorm:
safe_patterns.append(re.escape(cnorm.decode("ascii")))
if not civis and not cnorm:
safe_patterns.append(r'\x1b\[\?25[hl]') # cursor visibility
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking

# Modern extensions not in standard terminfo - always use patterns
safe_patterns.append(r'\x1b\[\?2004[hl]') # bracketed paste mode
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking (may be separate)
safe_patterns.append(r'\x1b\[\?[01]c') # device attributes

safe_escapes = re.compile('|'.join(safe_patterns))
cleaned_output = safe_escapes.sub('', output)
self.assertIn(expected_output_sequence, cleaned_output)

Expand Down
Loading