The things a terminal UI does to a string that does not fit, and the one it does to a string it did not write.
Atui.Screen puts graphemes on a grid of cells, which is a harder surface to
draw text on than it looks. A line longer than the row it is in has to lose
something, and which end it loses decides whether the reader can still tell
what they are looking at: a path is identified by its last component and a
message by its first. A line of somebody else's output has to be stripped of
the escape sequences that would otherwise stop being text and start being
instructions to the terminal.
These are pure functions over strings — nothing here knows about a screen — so they compose with whatever a view is doing and are testable on their own.
Atui.Screen.truncate/2 is the neighbour of elide_right/2 that cuts
without saying it did, which is what a title wants and what a value does not.
Summary
Functions
Shortens text to width, keeping the end of it.
Shortens text to width, keeping the start of it.
The first of candidates that fits in width, or the last one.
Breaks text into chunks of exactly width characters.
Makes one line of somebody else's output safe to draw.
Breaks text into lines of at most width, on whitespace.
Functions
@spec elide_left(String.t(), non_neg_integer()) :: String.t()
Shortens text to width, keeping the end of it.
For a path, or anything else whose identifying part is at the far end: a
hundred directories that all begin /opt/production/ are told apart by what
comes after it.
@spec elide_right(String.t(), non_neg_integer()) :: String.t()
Shortens text to width, keeping the start of it.
The first of candidates that fits in width, or the last one.
How a footer says as much as there is room for: the fullest wording first, the terse one last, and the last is used even when nothing fits — the screen is then too narrow for anything to be right, and something is better than a blank.
iex> Atui.Text.first_fitting([" ↑↓ select · ⏎ open ", " ↑↓ ⏎ ", ""], 10)
" ↑↓ ⏎ "A ladder of wordings is a better answer to a narrow terminal than one wording cut short, because the thing an elided hint loses is the key it was naming.
@spec hard_wrap(String.t(), pos_integer()) :: [String.t()]
Breaks text into chunks of exactly width characters.
wrap/2 breaks on whitespace, which is right for prose and wrong for a log:
a stack trace or a JSON payload has no spaces where the break is wanted, and
losing the tail of it to an ellipsis loses the part that says what happened.
Nothing is dropped here.
Makes one line of somebody else's output safe to draw.
Anything a view did not write itself — a log line, a command's output, a file
— is not text as far as the terminal is concerned. Atui.Screen puts
graphemes in cells, and an escape sequence in a cell is not a colour, it is a
hole in the frame: one stray \e[2J from a subprocess clears the screen the
UI is drawing on, and one \e[6n makes the terminal answer into the input
stream. So the escapes come out, tabs become four spaces — a tab in a cell
moves the cursor to the next tab stop and takes the rest of the row with it —
and the remaining control characters go.
Colour is lost. That is the trade: a pane that cannot be corrupted by what it is showing.
@spec wrap(String.t(), pos_integer()) :: [String.t()]
Breaks text into lines of at most width, on whitespace.
A word longer than the width is left long rather than cut: it is a path or an id, and half of one is worse than a line that overflows and gets clipped by the screen.