Outline trimming can remove trailing CJK wide characters

Open Beginner friendly
#6,735 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
62/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
cli

Research direction

The end branch of line_trim() in src/textual/_segment_tools.py passes a code-point length to split_cells(), which expects a cell count. Run the repro script first to confirm the wrong output, then check the start branch for the correct pattern. Done means trailing wide characters are kept within the cell budget, with regression tests for ASCII and one or two trailing CJK characters. Confirm the intended space-padding behavior with a maintainer before opening a PR.

Written by the indexing model from the issue text.

Description

Have you checked closed issues? Yes. I searched open and closed issues and pull requests for line_trim, CJK/wide-character trimming, outlines, and double-width truncation. #6357 also involves disappearing CJK text, but describes toast overlap and partial redraw over another widget's border; this reproduction is deterministic trimming of a widget's own outlined line.

Have you checked against the most recent version of Textual? Yes. PyPI currently lists 8.2.8. This also reproduces with Textual installed editable from current main, commit 5e5b7ef58b8c5572c13a3ff9027d756623661495.

The bug

When an outlined content line ends in double-width characters and has no trailing padding, Textual trims the end by character count rather than terminal-cell count. The cut can land inside a wide glyph and remove visible CJK text.

Complete minimal example
import asyncio

from rich.cells import cell_len
from rich.segment import Segment

from textual._segment_tools import line_trim
from textual.app import App, ComposeResult
from textual.widgets import Static


def segment_text(segments: list[Segment]) -> str:
    return "".join(segment.text for segment in segments)


for segments in (
    [Segment("ab"), Segment("中中")],
    [Segment("ab"), Segment("中")],
):
    result = segment_text(line_trim(segments, start=False, end=True))
    print(repr(result), cell_len(result))


class Repro(App[None]):
    def compose(self) -> ComposeResult:
        yield Static("xxxxxx\nab中中\nyyyyyy")


async def main() -> None:
    app = Repro()
    async with app.run_test(size=(6, 5)) as pilot:
        app.query_one(Static).styles.outline = ("round", "red")
        await pilot.pause()
        lines = [
            "".join(segment.text for segment in strip)
            for strip in app.screen._compositor.render_strips(app.screen.size)
        ]
        print(repr(lines[1]))


asyncio.run(main())

Actual output:

'ab ' 3
'ab' 2
'│b │'

Removing one terminal cell should retain five cells from ab中中 ('ab中 ') and three cells from ab中 ('ab '). In the end-to-end render, both 中 characters disappear; the corresponding content line should retain one of them ('│b中 │').

Investigation and proposed direction

The end branch of line_trim() in src/textual/_segment_tools.py currently calls:

last_segment.split_cells(len(last_segment.text) - 1)

Segment.split_cells() expects a cell offset, but len(text) counts Unicode code points. The start branch already passes a cell count. A focused correction appears to be:

last_segment.split_cells(cell_len(last_segment.text) - 1)

cell_len is already imported by the module. Regression coverage can exercise ASCII, one and two trailing wide characters, and the existing start behavior. Could @willmcgugan confirm that preserving the remaining cells with Rich's space placeholder is the intended behavior? No PR has been opened pending approval under the project's AI policy.

Environment
  • Windows
  • CPython 3.14.6
  • Textual 8.2.8 (editable from the main commit above)
  • Rich 15.0.0
  • Reproduced both with direct segment calls and headless App.run_test()

Disclosure: this report and its reproduction were prepared and verified with AI assistance (OpenCode, GPT-5.6 Sol).

Dominant language
Python
Stars
37.6k
Forks
1.4k
PR merge metrics
No merged PRs in 30d

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Textualize/textual

All issues in Textualize/textual

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.