Skip to content
Open
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ classifiers = [
"Topic :: Terminals :: Terminal Emulators/X Terminals",
]
dependencies = [
"wcwidth",
"wcwidth>=0.3.5,<1",
]

[project.urls]
Expand Down
24 changes: 1 addition & 23 deletions pyte/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from functools import lru_cache
from typing import TYPE_CHECKING, NamedTuple, TypeVar

from wcwidth import wcwidth as _wcwidth, wcswidth as _wcswidth # type: ignore[import-untyped]
from wcwidth import wcwidth as _wcwidth, wcswidth as _wcswidth, iter_graphemes as grapheme_clusters

from . import (
charsets as cs,
Expand Down Expand Up @@ -120,28 +120,6 @@ def __init__(self, x: int, y: int, attrs: Char = Char(" ")) -> None:
self.hidden = False


def grapheme_clusters(text: str) -> "Generator[str, None, None]":
"""Yield grapheme clusters from *text*."""
cluster = ""
for char in text:
if not cluster:
cluster = char
continue
if (
cluster.endswith("\u200d")
or unicodedata.combining(char)
or char == "\u200d"
or 0xFE00 <= ord(char) <= 0xFE0F
or 0x1F3FB <= ord(char) <= 0x1F3FF
):
cluster += char
else:
yield cluster
cluster = char
if cluster:
yield cluster


class StaticDefaultDict(dict[KT, VT]):
"""A :func:`dict` with a static default value.

Expand Down
85 changes: 85 additions & 0 deletions tests/test_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,91 @@ def test_display_complex_emoji():
assert screen.display == [emoji + "a "]


def test_vs16_heart_emoji_40_in_80_columns():
screen = pyte.Screen(80, 1)
# RED HEART + VARIATION SELECTOR-16: ❤️ (wide)
heart_vs16 = '\u2764\ufe0f'
screen.draw(heart_vs16 * 40)
assert screen.cursor.x == 80
assert screen.buffer[0][0].data == heart_vs16
assert screen.buffer[0][1].data == ''


def test_heart_bare_is_narrow():
screen = pyte.Screen(80, 1)
# RED HEART (text presentation): ❤ (narrow)
heart_bare = '\u2764'
screen.draw(heart_bare * 80)
assert screen.cursor.x == 80


def test_zwj_emoji_40_in_80_columns():
screen = pyte.Screen(80, 1)
# MAN + ZWJ + WOMAN + ZWJ + GIRL (family): 👨‍👩‍👧
family = '\U0001F468\u200D\U0001F469\u200D\U0001F467'
screen.draw(family * 40)
assert screen.cursor.x == 80
assert screen.buffer[0][0].data == family
assert screen.buffer[0][1].data == ''


def test_flag_emoji_40_in_80_columns():
screen = pyte.Screen(80, 1)
# REGIONAL INDICATOR U + REGIONAL INDICATOR S (US flag): 🇺🇸
flag_us = '\U0001F1FA\U0001F1F8'
screen.draw(flag_us * 40)
assert screen.cursor.x == 80
assert screen.buffer[0][0].data == flag_us
assert screen.buffer[0][1].data == ''


def test_mixed_emoji_widths():
screen = pyte.Screen(10, 1)
# ❤ (width 1) + ❤️ (width 2) + 🇺🇸 (width 2) + 'a' (width 1) = 6
text = '\u2764' + '\u2764\ufe0f' + '\U0001F1FA\U0001F1F8' + 'a'
screen.draw(text)
assert screen.cursor.x == 6


@pytest.mark.parametrize(("grapheme", "expected_width"), [
('\U0001F44B\U0001F3FB', 2),
('\U0001F469\U0001F3FB\u200D\U0001F4BB', 2),
('\U0001F9D1\U0001F3FB\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F9D1\U0001F3FD', 2),
('\u26F9\U0001F3FB\u200D\u2640\uFE0F', 2),
], ids=['wave_skin', 'woman_technologist', 'kiss_couple', 'person_ball_female'])
def test_wide_grapheme_cluster(grapheme, expected_width):
"""Wide grapheme clusters occupy expected width and store as single unit."""
screen = pyte.Screen(10, 1)
screen.draw(grapheme)
assert screen.cursor.x == expected_width
assert screen.buffer[0][0].data == grapheme
assert screen.buffer[0][1].data == ''


@pytest.mark.parametrize(("grapheme",), [
('cafe\u0301',),
('ok\u1100\u1161ok',),
('ok\uAC00\u11A8ok',),
], ids=['combining_accent', 'hangul_lv', 'hangul_lvt'])
def test_grapheme_cluster_in_context(grapheme):
"""Grapheme clusters with combining/jamo characters stored correctly."""
screen = pyte.Screen(10, 1)
screen.draw(grapheme)
assert screen.buffer[0][2].data in ('\u1100\u1161', '\uAC00\u11A8', 'f')
if grapheme.startswith('cafe'):
assert screen.buffer[0][3].data == 'e\u0301'


def test_regional_indicator_graphemes():
"""Regional indicator pairs form flags; odd RI stays separate."""
screen = pyte.Screen(10, 1)
# four unicode RI's make two flags, 4 total cells
screen.draw('\U0001F1FA\U0001F1F8\U0001F1E6\U0001F1FA')
assert screen.cursor.x == 4
assert screen.buffer[0][0].data == '\U0001F1FA\U0001F1F8'
assert screen.buffer[0][2].data == '\U0001F1E6\U0001F1FA'


def test_carriage_return():
screen = pyte.Screen(3, 3)
screen.cursor.x = 2
Expand Down