# string_width
### A low-ish level library for building terminal UIs.
[![Package Version](https://img.shields.io/hexpm/v/string_width)](https://hex.pm/packages/string_width)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/string_width/)
```sh
gleam add string_width@3
```
`string_width` provides functions to measure the required amount of cells, and then layout strings in an ANSI-aware manner. It includes ready-to-use layout functions like [limit](./string_width.html#limit "Word wrapping and truncation") or [stack_horizontal](./string_width.html#stack_horizontal "Build column-based layouts"), as well as low-level primitives, allowing you to build your own layout algorithms.
All Gleam targets are supported, and it passes the tests of the NPM [string-width](https://www.npmjs.com/package/string-width) and [unicode-width](https://crates.io/crates/unicode-width) crate where applicable. Compared to others, a heavy focus is put on on reporting the actual width of strings in terminals instead (Check out [Internals](./internals.html) for more information.)
It is also one of the fastest options available, even including target-specific ones, while still providing you full flexibility and correctness.
### Tour
```gleam
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import string_width.{Left, Size, Top}
// we will render a simple help menu for a command-line app here.
// the help menu will automatically adjust based on the width of the terminal.
type Command {
Command(name: String, description: String)
}
const commands = [
Command(
name: "build",
description: "Build and bundle your application.\nThe generated bundle calls your apps' main function. If your main function accepts a single argument of type List(String), all additional command-line arguments will be passed there.",
),
Command(
name: "dev",
description: "Start a file watcher that automatically recompiles your app on all file changes, and then re-runs your tests. Compilation errors or test failures will be displayed in an overlay, and if everything succeeds, your window will automatically hot-reload.",
),
Command(
name: "help",
description: "Show this help text."
),
]
pub fn main() {
// get the size of the terminal, or fallback to a default size.
let term_size =
string_width.get_terminal_size()
|> result.unwrap(Size(rows: 24, columns: 80))
// how big does the left-hand side need to be?
let left_width =
list.fold(commands, 0, fn(max, cmd) {
int.max(max, string_width.line(cmd.name))
})
// add some gap between both columns and compute the size of the right side.
let gap = 4
// set a maximum width of 60 for the right column to improve readability.
let right_width = int.min(60, term_size.columns - left_width - gap)
commands
|> list.map(fn(cmd) {
[
cmd.name
|> pink
// make sure all left-hand sides are padded to left_width
|> string_width.align(left_width, Left, with: " "),
// word wrap the right-hand side such that it fits into our column.
// if the description would overflow 10 lines, truncate it.
cmd.description
|> string_width.limit(
to: Size(rows: 10, columns: right_width),
ellipsis: "…",
)
// make sure that if we used styles those would wrap properly
|> string_width.inline_styles,
]
// build the column layout;
// the name should be aligned with the top of the description
|> string_width.stack_horizontal(place: Top, gap:, with: " ")
})
// combine the help text of all commands, adding a line of space in between.
|> string_width.stack_vertical(align: Left, gap: 1, with: " ")
|> io.println
}
// string_width is agnostic to the way you add styles to your strings.
// maybe check out gleam_community_ansi!
fn pink(str: String) {
"\u{1b}[38;5;207m" <> str <> "\u{1b}[m"
}
```
Output:
<div style="
white-space:pre;
margin:var(--gap) 0;
border-radius:1px;
overflow:auto;
box-shadow:var(--shadow);
font-family:'Ubuntu Mono', SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', ui-monospace, monospace;
line-height:1.2;
background-color:var(--code-background);
padding:1em;
"><span style="color:#ff5fff">build</span> Build and bundle your application.
The generated bundle calls your apps' main function. If your
main function accepts a single argument of type
List(String), all additional command-line arguments will be
passed there.<br>
<span style="color:#ff5fff">dev</span> Start a file watcher that automatically recompiles your app
on all file changes, and then re-runs your tests.
Compilation errors or test failures will be displayed in an
overlay, and if everything succeeds, your window will
automatically hot-reload.<br>
<span style="color:#ff5fff">help</span> Show this help text.
</div>
### Measuring strings
```gleam
string_width.dimensions("hello,\n안녕하세요")
// --> string_width.Size(rows: 2, columns: 10)
string_width.line_with(
"👩👩👦👦",
string_width.new() |> string_width.mode_2027,
)
// --> 2
// word wrapping and truncation
string_width.limit(
"Lorem ipsum dolor sit amet\nIs a common placeholder string",
to: Size(rows: 3, columns: 10),
ellipsis: "..."
)
// --> "Lorem ipsum dolor\nsit amet\nIs a common place..."
// position a string inside a box
string_width.position(
"XXX",
in: Size(rows: 3, columns: 10),
align: Center,
place: Middle,
with: "."
)
// --> "..........\n...XXX....\n.........."
}
```
### Limitations
- Many terminals don't support grapheme clusters properly, so by default, the
values returned by this library try to match the behaviour of libc instead
of modern text rendering pipelines.
If you encounter a mismatch that is consistent across multiple environments,
feel free to open an issue or ping me on Discord!
- The behaviour when encountering stray variant selectors does not match other similar libraries.
### Sources
The table lookup technique used by this library is heavily based on the musl libc `wcwidth` implementation. I built updated tables using the Unicode 16.0 data, and added support for ambiguous characters. It also uses the regex of the ansi-regex npm package, and the test cases of the string-width npm package.
- **Grapheme Clusters in Terminals:** [https://mitchellh.com/writing/grapheme-clusters-in-terminals](https://mitchellh.com/writing/grapheme-clusters-in-terminals)
- **UAX #11 East Asian Width:** [https://www.unicode.org/reports/tr11/](https://www.unicode.org/reports/tr11/)
- **Terminal Unicode Core**: [https://github.com/contour-terminal/terminal-unicode-core](https://github.com/contour-terminal/terminal-unicode-core)
- **string-width:** [https://github.com/sindresorhus/string-width](https://github.com/sindresorhus/string-width)
- **musl-libc wcwidth:** [https://git.musl-libc.org/cgit/musl/tree/src/ctype/wcwidth.c](https://git.musl-libc.org/cgit/musl/tree/src/ctype/wcwidth.c)
- **reflow:** [https://github.com/muesli/reflow](https://github.com/muesli/reflow)