displaytext

Grapheme-aware display-cell text boundaries for terminal UIs.

unicode
terminal
tui
grapheme
text
moon add moonbit-community/displaytext@0.1.5
Download zip
Version
0.1.5
License
Apache-2.0
Last updated
2 months ago
Downloads
17K
README

#displaytext

Grapheme-aware display-cell text boundaries for terminal UIs.

displaytext combines Unicode grapheme cluster boundaries with unicodewidth display-width rules. TUI authors can split plain text into hard lines, move through each line by safe textual positions, map between textual positions and terminal columns, create zero-copy views, and truncate without cutting through a display unit.

#Installation

> moon add moonbit-community/displaytext

#API

#DisplayText(s : String, cjk? : Bool = false) -> DisplayText

Parses s as one terminal display text run.

  • s: text that the caller already wants to measure and navigate as one run
  • cjk: when true, ambiguous-width characters are treated as wide

This constructor does not split hard line breaks. If s contains \n, \r\n, or \r, those characters remain part of the returned DisplayText. Use split_lines for arbitrary multiline text.

#split_lines(s : @string.View, cjk? : Bool = false) -> Array[DisplayText]

Splits plain text into hard lines and parses each line into terminal display boundaries.

  • s: plain text that may contain hard line breaks
  • cjk: when true, ambiguous-width characters are treated as wide

Hard line breaks are \n, \r\n, and \r. They are not included in returned lines. Empty lines are preserved, including the trailing empty line after a final line break.

Each returned DisplayText exposes:

  • total display width with width()
  • legal textual boundaries with start(), end(), next(), and prev()
  • conversion from textual boundaries to columns with display_position()
  • conversion from display columns to textual boundaries with textual_position_at_or_before() and textual_position_at_or_after()
  • zero-copy text views with view()
  • terminal-cell truncation with truncate()

#Usage

///|
test {
let lines = @displaytext.split_lines("a你好b\nnext")
let line = lines[0]

// Whole-line display width.
assert_eq(line.width(), 6)

// Move by legal textual positions, not UTF-16 code units.
let after_a = line.next(line.start()).unwrap()
let after_ni = line.next(after_a).unwrap()
assert_eq(line.display_position(after_ni).column(), 3)

// Convert a display column inside a wide character back to text boundaries.
let middle = @displaytext.DisplayPosition::new(column=2)
assert_eq(
line.view(line.start(), line.textual_position_at_or_before(middle)),
"a",
)
assert_eq(
line.view(line.start(), line.textual_position_at_or_after(middle)),
"a你",
)

// Truncate without cutting through a display unit.
assert_eq(line.truncate(4), "a你…")
}

#Consumer-Owned Wrapping

Soft wrapping is a layout policy owned by the application. A TUI can maintain its own used display columns and ask a DisplayText for the legal textual boundary that fits the remaining width.

///|
test {
let line = @displaytext.DisplayText::DisplayText("ab你好")
let remaining = @displaytext.DisplayPosition::new(column=3)
let end = line.textual_position_at_or_before(remaining)

assert_eq(line.view(line.start(), end), "ab")
}

#Concepts

DisplayText is one run of display-position state. Values created by split_lines never contain \n, \r\n, or \r; values created by DisplayText::new contain exactly the text the caller passed in.

TextualPosition is a legal boundary in the original line. Values are produced by DisplayText; callers cannot construct arbitrary positions inside a display unit.

DisplayPosition is a zero-based terminal display column. Construct it with DisplayPosition::new(column=...); negative columns are clamped to zero.

#Scope

This module provides terminal display-cell boundaries for plain text. It does not own viewport policy, soft wrapping, rich-text styling, bidi reordering, font shaping, or pixel measurement.

#Testing

> moon test

#License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

#
DisplayPosition

type DisplayPosition derive(Eq,
Debug
)

A display-column position in a single DisplayText.

#
DisplayPosition::column

fn DisplayPosition::column(self : DisplayPosition) -> Int

The zero-based terminal display column.

#
DisplayPosition::new

fn DisplayPosition::new(column~ : Int) -> DisplayPosition

Build a single-line display position. Negative columns are clamped to zero.

#
DisplayText

pub struct DisplayText {
// private fields
} derive(
Debug
)
#alias(new)
fn DisplayText::DisplayText(text : String, cjk? : Bool) -> DisplayText

A single text run parsed into terminal display units.
impl Eq for DisplayText

#
DisplayText::char_offset

fn DisplayText::char_offset(self : DisplayText, position : TextualPosition) -> Int

The UTF-16 code-unit offset of a legal textual boundary in the underlying text (the same unit as String::length).

This is the textual analogue of display_position: where that projects a boundary onto display columns, this projects it onto raw string offsets. It is the inverse of textual_position_at_char.

#
DisplayText::display_position

fn DisplayText::display_position(self : DisplayText, position : TextualPosition) -> DisplayPosition

Convert a legal textual boundary to its display-column position.

#
DisplayText::end

The final legal textual boundary in this text run.

#
DisplayText::next

fn DisplayText::next(self : DisplayText, position : TextualPosition) -> TextualPosition?

The next legal textual boundary after position, if any.

#
DisplayText::prev

fn DisplayText::prev(self : DisplayText, position : TextualPosition) -> TextualPosition?

The previous legal textual boundary before position, if any.

#
DisplayText::start

The first legal textual boundary in this text run.

#
DisplayText::text

fn DisplayText::text(self : DisplayText) -> String

Return the original text owned by this display text run.

#
DisplayText::textual_position_at_char

fn DisplayText::textual_position_at_char(self : DisplayText, offset~ : Int) -> TextualPosition

The nearest legal textual boundary at or before offset, a UTF-16 code-unit offset into the underlying text (the same unit as String::length).

When offset lands inside a display unit — for example midway through a non-additive grapheme run like an Arabic lam-alef ligature, where no legal boundary exists — it snaps back to the start of that unit. This is the char analogue of textual_position_at_or_before, and the inverse of char_offset. Out-of-range offsets are clamped to [0, text length].

#
DisplayText::textual_position_at_or_after

fn DisplayText::textual_position_at_or_after(self : DisplayText, position : DisplayPosition) -> TextualPosition

Return the nearest legal textual boundary at or after position.

#
DisplayText::textual_position_at_or_before

fn DisplayText::textual_position_at_or_before(self : DisplayText, position : DisplayPosition) -> TextualPosition

Return the nearest legal textual boundary at or before position.

#
DisplayText::truncate

fn DisplayText::truncate(self : DisplayText, width : Int, suffix? : StringView) -> String

Truncate this text run to width display cells, appending suffix when content is dropped.

#
DisplayText::view

fn DisplayText::view(self : DisplayText, start : TextualPosition, end : TextualPosition) -> StringView

View between two legal textual boundaries.

#
DisplayText::width

fn DisplayText::width(self : DisplayText) -> Int

Total display width of this text run in terminal cells.

#
TextualPosition

type TextualPosition derive(Eq,
Debug
)

A legal boundary in a DisplayText.

Values are produced by DisplayText methods. They are intentionally opaque so callers cannot accidentally point into the middle of a display unit.

#
split_lines

fn split_lines(text : StringView, cjk? : Bool) -> Array[DisplayText]

Split text into hard-line display text runs.

Hard line breaks (\n, \r\n, and \r) are not included in the returned lines. Empty lines are preserved, including the trailing empty line after a final line break. Each returned DisplayText exposes safe textual boundaries and display columns for TUI editing, truncation, and hit testing.

Source Files