Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 3 additions & 2 deletions tests/test_clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
('\x1b[1m\U0001F468\u200D\U0001F469\u200D\U0001F467\x1b[0m',
'\U0001F468\u200D\U0001F469\u200D\U0001F467'),
('\x1b', '\x1b'),
('a\x1bb', 'a\x1bb'),
('a\x1bb', 'a'),
('\x1b[', ''),
('text\x1b[mmore', 'textmore'),
]
Expand Down Expand Up @@ -148,7 +148,7 @@ def test_clip_sequences_between_chars():
assert clip('a\x1b[31mb\x1b[0mc', 1, 2) == '\x1b[31mb\x1b[0m'


def test_clip_sequences_lone_esc():
def test_clip_sequences_fs_escape():
assert clip('a\x1bb', 0, 2) == 'a\x1bb'


Expand Down Expand Up @@ -236,6 +236,7 @@ def test_clip_tab_with_sequences():
('abc\rde', 0, 5, 'abc\rde'),
('\a\b\rHello', 0, 5, '\a\b\rHello'),
('ab\x01\x02cd', 0, 4, 'ab\x01\x02cd'),
('ab\x1b\x00cd', 0, 4, 'ab\x1b\x00cd'),
]


Expand Down
10 changes: 8 additions & 2 deletions tests/test_textwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ def test_wrap_unicode(benchmark, text, w, expected):
# Sequences in long word breaking - red starts after 'x', continues across lines
('x\x1b[31mabcdefghij\x1b[0m', 3,
['x\x1b[31mab\x1b[0m', '\x1b[31mcde\x1b[0m', '\x1b[31mfgh\x1b[0m', '\x1b[31mij\x1b[0m']),
# Lone ESC - not a valid SGR sequence, stays with preceding text
('abc\x1bdefghij', 3, ['abc\x1b', 'def', 'ghi', 'j']),
# Fs sequence (ESC d) - zero-width, stays with preceding text
('abc\x1bdefghij', 3, ['abc\x1bd', 'efg', 'hij']),
]

SEQUENCE_CASES_NO_PROPAGATE = [
Expand Down Expand Up @@ -521,3 +521,9 @@ def test_wrap_replace_whitespace_false_newlines_zero_width():
"""Newlines have zero display width, so more text fits per line than stdlib."""
assert wrap('hello\nworld foo\nbar', 10, replace_whitespace=False) == [
'hello\nworld', 'foo\nbar']


def test_wrap_bare_esc():
"""Bare ESC not part of a recognized sequence is treated as zero-width."""
assert wrap('ab\x1bcd ef', 5) == ['ab\x1bcd', 'ef']
assert wrap('ab\x1b\x00cdef', 3) == ['ab\x1b\x00c', 'def']
92 changes: 89 additions & 3 deletions tests/test_width.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# local
import wcwidth
from wcwidth.escape_sequences import ZERO_WIDTH_PATTERN
from wcwidth.escape_sequences import ZERO_WIDTH_PATTERN, INDETERMINATE_EFFECT_SEQUENCE

BASIC_WIDTH_CASES = [
('', 0, 'empty'),
Expand All @@ -29,7 +29,7 @@ def test_width_basic(text, expected, name):
('\x1b[31mred\x1b[0m', 3, 'SGR_sequence'),
('hello\x80world', 10, 'C1_control'),
('\x1b', 0, 'lone_ESC'),
('a\x1bb', 2, 'lone_ESC_between'),
('a\x1bb', 1, 'fs_sequence_between'),
]


Expand Down Expand Up @@ -65,7 +65,7 @@ def test_width_control_codes_strict_raises(text, name):
('\x1b[31mred\x1b[0m', 3, 'SGR_sequence'),
('a\x1b[2Cb', 4, 'cursor_right'),
('\x1b', 0, 'lone_ESC'),
('a\x1bb', 2, 'lone_ESC_between'),
('a\x1bb', 1, 'fs_sequence_between'),
('\x1b!', 1, 'ESC_unrecognized'),
]

Expand All @@ -88,6 +88,7 @@ def test_width_control_codes_strict_allowed(text, expected, name):
('\x1b[1X', 'erase_chars'),
('\x1b[1S', 'parm_index'),
('\x1b[1T', 'parm_rindex'),
('\x1bc', 'full_reset'),
]


Expand Down Expand Up @@ -449,3 +450,88 @@ def test_fitzpatrick_modifier_standalone_width():
"""Standalone Fitzpatrick modifier, however, is wide character in width()."""
result = wcwidth.width('\U0001F3FB')
assert result == 2


FS_SEQUENCE_CASES = [
('\x1bc', 'ris'),
('\x1bl', 'memory_lock'),
('\x1bm', 'memory_unlock'),
('\x1bn', 'ls2'),
('\x1bo', 'ls3'),
('\x1b|', 'ls3r'),
('\x1b}', 'ls2r'),
('\x1b~', 'ls1r'),
]


@pytest.mark.parametrize('seq,name', FS_SEQUENCE_CASES)
def test_fs_sequences_matched(seq, name):
"""Fs (independent function) sequences are matched as zero-width."""
segments = list(wcwidth.iter_sequences(seq))
assert segments == [(seq, True)]
assert wcwidth.width(seq) == 0


FP_SEQUENCE_CASES = [
('\x1b7', 'decsc'),
('\x1b8', 'decrc'),
('\x1b=', 'deckpam'),
('\x1b>', 'deckpnm'),
('\x1b0', 'fp_0'),
('\x1b1', 'fp_1'),
('\x1b9', 'fp_9'),
]


@pytest.mark.parametrize('seq,name', FP_SEQUENCE_CASES)
def test_fp_sequences_matched(seq, name):
"""Fp (private use) sequences are matched as zero-width."""
segments = list(wcwidth.iter_sequences(seq))
assert segments == [(seq, True)]
assert wcwidth.width(seq) == 0


NF_SEQUENCE_CASES = [
('\x1b F', 's7c1t'),
('\x1b G', 's8c1t'),
('\x1b#3', 'decdhl_top'),
('\x1b#4', 'decdhl_bottom'),
('\x1b#5', 'decswl'),
('\x1b#6', 'decdwl'),
('\x1b#8', 'decaln'),
('\x1b%G', 'utf8_designate'),
('\x1b%@', 'iso2022_return'),
]


@pytest.mark.parametrize('seq,name', NF_SEQUENCE_CASES)
def test_nf_sequences_matched(seq, name):
"""NF (multi-byte) escape sequences are matched as zero-width."""
segments = list(wcwidth.iter_sequences(seq))
assert segments == [(seq, True)]
assert wcwidth.width(seq) == 0


def test_fs_sequence_embedded_in_text():
"""Fs sequence surrounded by text is correctly segmented."""
segments = list(wcwidth.iter_sequences('abc\x1bcdef'))
assert segments == [('abc', False), ('\x1bc', True), ('def', False)]
assert wcwidth.width('abc\x1bcdef') == 6


def test_nf_sequence_embedded_in_text():
"""NF sequence surrounded by text is correctly segmented."""
segments = list(wcwidth.iter_sequences('abc\x1b#8def'))
assert segments == [('abc', False), ('\x1b#8', True), ('def', False)]
assert wcwidth.width('abc\x1b#8def') == 6


def test_screen_title_sequences():
"""Screen/tmux title sequence ESC k hello ST."""
segments = list(wcwidth.iter_sequences('\x1bkhello\x1b\\'))
assert segments[0] == ('\x1bk', True)


def test_ris_indeterminate():
"""RIS (ESC c) is flagged as indeterminate effect."""
assert INDETERMINATE_EFFECT_SEQUENCE.match('\x1bc')
13 changes: 9 additions & 4 deletions wcwidth/escape_sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
r'\x1bP[^\x1b\x07]*(?:\x07|\x1b\\)|'
# PM sequences
r'\x1b\^[^\x1b\x07]*(?:\x07|\x1b\\)|'
# Character set designation
# Character set designation (subset of nF, handled separately for clarity)
r'\x1b[()].|'
# Fe sequences
# nF sequences: ESC + one or more intermediate bytes (0x20-0x2F) + final byte (0x30-0x7E)
r'\x1b[\x20-\x2f]+[\x30-\x7e]|'
# Fe sequences (C1 controls)
r'\x1b[\x40-\x5f]|'
# Fp sequences
r'\x1b[78=>g]'
# Fp sequences (private use)
r'\x1b[\x30-\x3f]|'
# Fs sequences (independent functions)
r'\x1b[\x60-\x7e]'
)

# Cursor right movement: CSI [n] C, parameter may be parsed by width()
Expand Down Expand Up @@ -65,5 +69,6 @@
r'\x1b8', # restore_cursor
r'\x1bD', # scroll_forward (index)
r'\x1bM', # scroll_reverse (reverse index)
r'\x1bc', # full_reset (RIS)
))
)
Loading