string_width

The with variants of all functions additionally work with custom options.

Tip: Hover the links for short summaries!

Measure

line[_with] dimensions[_with], get_terminal_size

Layout & Positioning

limit[_with], soft_wrap[_with], align[_with], tabs_to_spaces[_with]

stack_horizontal[_with], stack_vertical[_with], padding[_with], position[_with], scroll[_with]

ANSI escape sequence helpers

inline_styles, strip_ansi, is_ansi_component

Options Builder

new, ambiguous_as_wide, count_ansi_codes, mode_2027, mode_2027_ext, mode_wcwidth, at_tab_offset, with_tab_width

Advanced

fold[_with], fold_raw, fold_raw_pieces

Types

Control the text or box alignment on the horizontal axis.

pub type Alignment {
  Left
  Center
  Right
}

Constructors

  • Left
  • Center
  • Right

Options to change the default behaviour of the functions in this library. If you are unsure what to do here, the defaults should work great!

pub opaque type Options

The measured pieces of a string, passed to you by fold and fold_with

pub type Piece {
  Piece(piece: String, row: Int, column: Int, width: Int)
}

Constructors

  • Piece(piece: String, row: Int, column: Int, width: Int)

Control the box alignment on the vertical axis.

pub type Placement {
  Top
  Middle
  Bottom
}

Constructors

  • Top
  • Middle
  • Bottom

A custom type making it easy to differentiate the width (columns) and height (rows).

pub type Size {
  Size(rows: Int, columns: Int)
}

Constructors

  • Size(rows: Int, columns: Int)

Functions

pub fn align(
  str: String,
  to max_width: Int,
  align alignment: Alignment,
  with space: String,
) -> String

Align each line horizontally, using a spacer character or string as a filler. Each line of the return value will be at least max_width wide.

If a line is already longer than the max width, it will not be changed. If the space strings width does not evenly divide the missing amount of columns, the extra spacer will overflow the max width.

Examples

align("hello", to: 10, align: Left, with: " ")
// --> "hello     "

align("12345", to: 8, align: Right, with: "0")
// --> "00012345"

align(" Welcome ", to: 20, align: Center, with: "==")
// --> "====== Welcome ======" // (len = 21)

align("Trans\nrights\nare\nhuman\nrights", to: 7, align: Right, with: " ")
// --> "  Trans\n rights\n    are\n  human\n rights"
pub fn align_with(
  str: String,
  to max_width: Int,
  align alignment: Alignment,
  using options: Options,
  with space: String,
) -> String

Like align, bu use custom options for measure.

pub fn ambiguous_as_wide(options: Options) -> Options

Some characters are marked by Unicode as “ambiguous”, meaning they may occupy 1 or 2 cells, depending on the context, current language, selected font, surrounding text, and more.

Unicode recommends treating these characters as narrow by default, but you can change this behaviour using this option.

pub fn at_tab_offset(
  options: Options,
  tab_offset: Int,
) -> Options

Define an offset for tab stop calculations. (Default: 0)

If the text you print doesn’t start at the first column, but is instead indented somehow, you can set this option to this number to make sure tab stops are correctly calculated.

pub fn count_ansi_codes(options: Options) -> Options

Do not ignore ansi escape sequences, and count them as regular characters.

You can enable this option as an optimisation if you are sure that your string doesn’t contain any ansi escape codes.

pub fn dimensions(str: String) -> Size

The required number of rows and columns to display the given text.

The number of rows is equal to the number of lines, while the number of columns represents the maximum line width.

Examples

dimensions("안녕하세요")
// --> Size(rows: 1, columns: 10)

dimensions("hello,\n안녕하세요")
// --> Size(rows: 2, columns: 10)
pub fn dimensions_with(str: String, options: Options) -> Size

Like dimensions, but use custom options.

pub fn fold(
  over str: String,
  from state: a,
  with fun: fn(a, Piece) -> a,
) -> a

A higher-level fold that keeps track of the position inside of the string.

Handles tabs and newlines, and always passes full grapheme clusters, regardless of options. Concatenating the graphemes produces the original string.

Intended to be used as a basis for custom layout algorithms.

Example

fold("hi 😊", from: [], with: list.prepend)
|> list.reverse
// --> [Piece("h", 0, 0, 1), Piece("i", 0, 1, 1), Piece(" ", 0, 2, 1), Piece("😊", 0, 3, 2)]
pub fn fold_raw(
  over string: String,
  using options: Options,
  from state: a,
  with fun: fn(a, String, Int) -> a,
) -> a

A low-level fold that iterate over the measured components of a string. Components are either graphemes or codepoints depending on the mode option, or other undivisible sequences, like ANSI escape codes.

This function does not by itself keep track of any additional state, and doesn’t for example handle newlines or tabs like fold would. If you know that your algorithm is safe and won’t break up graphemes (or maybe this is acceptable), you can use this fold variant instead as a performance optimisation.

Concatenating all components is guaranteed to produce the original string.

Examples

// A slower string_width.line implementation that doesn't handle tabs
fold_raw("hello", new(), from: 0, with: fn(so_far, _chr, width) { width + so_far })
// --> 5
// truncate a string after 50 characters, but keep all ansi sequences.
let options = new()
use #(total, acc), chr, width <- fold_raw(input, options, from: #(0, ""))
case total >= 50 {
  True -> case is_ansi_component(chr, options) {
    True -> #(total, acc <> chr)
    False -> #(total, acc)
  }
  False -> #(total + width, acc <> chr)
}
pub fn fold_raw_pieces(
  over string: String,
  using options: Options,
  from state: a,
  with fun: fn(a, Piece) -> a,
) -> a

A version of fold_raw that does keep track of some state for you to handle tabs, and count the current row/column, passing Piece values to you instead.

Example

fold_raw_pieces("hi!", using: new(), from: "", with: list.prepend)
|> list.reverse
// --> [Piece("h", 0, 0, 1), Piece("i", 0, 1, 1), Piece("!", 0, 2, 1)]
pub fn fold_with(
  over str: String,
  using options: Options,
  from state: a,
  with fun: fn(a, Piece) -> a,
) -> a

A higher-level fold that keeps track of the position inside of the string. Like fold, but using custom options to measure the pieces.

Handles tabs and newlines, and always passes full grapheme clusters, regardless of options. Concatenating the graphemes produces the original string.

Intended to be used as a basis for custom layout algorithms.

pub fn get_terminal_size() -> Result(Size, Nil)

Get the size of the terminal, if available.

Checks the built-in methods for the target, as well as the LINES and COLUMNS environment variables.

pub fn inline_styles(str: String) -> String

Make sure SGR ansi codes (these are the ones you’d use for colors and styling!) never wrap around a line. This makes it save to use other layout functions on the resulting string, without affecting the styling anymore.

It does so by collecting all SGR sequences since the last reset, and re-applying them on each line break.

Warning: This function assumes it is safe to emit reset commands, and that no ambient styling is in effect (that is, the terminal would be in its default state when printing the resulting string!)

Examples

// "\u{1b}[31m": red text color
// "\u{1b}[m": reset/normal mode
inline_styles("\u{1b}[31mhello\nworld")
// --> "\u{1b}[31mhello" <> "\u{1b}[m\n" <> "\u{1b}[31mworld" <> "\u{1b}[m"
pub fn is_ansi_component(str: String, options: Options) -> Bool

Returns true if a given component string recieved in fold is an ANSI escape sequence.

pub fn limit(
  str: String,
  to max_size: Size,
  ellipsis ellipsis: String,
) -> String

Limit the dimensions of a string, either by wrapping on white space characters or soft hyphens, or by truncating the last line and appending an ellipsis.

The lines of the resulting string are left aligned and are at most columns wide. If a single word is longer than the maximum allowed line width, the word is broken into 2 pieces at the grapheme level. If the string gets truncated, this function will keep collecting ANSI sequences to make sure all formatting is preserved.

A commonly used ellipsis character is "…", also known as U+2026 HORIZONTAL ELLIPSIS.

Examples

limit("Hello World", Size(rows: 1, columns: 10), ellipsis: "...")
// --> "Hello W..."

limit("Hello World", Size(rows: 2, columns: 10), ellipsis: "...")
// --> "Hello\nWorld"
pub fn limit_with(
  str: String,
  to max_size: Size,
  using options: Options,
  ellipsis ellipsis: String,
) -> String

Like limit, but also customise the options used for measuring.

pub fn line(str: String) -> Int

Get the number of columns required to print a line in a terminal.

If multiple lines are given, the length of the longest line is returned.

Examples

line("äöüè")
// --> 4

line("안녕하세요")
// --> 10

line("👩‍👩‍👦‍👦")
// --> 8

line("\u{1B}[31mhello\u{1B}[39m")
// --> 5
pub fn line_with(str: String, options: Options) -> Int

Like line, but use custom options.

Example

let options =
  new()
  |> mode_2027

line_with("👩‍👩‍👦‍👦", options)
// --> 2
pub fn mode_2027(options: Options) -> Options

Measure grapheme clusters instead of individual code points.

Most terminal emulators do not handle grapheme clusters well and will instead show their decomposition. To make sure a given string always fits even on those terminals, the functions in this package will copy this behaviour as well.

If you enable this option, the returned numbers will more accurately represent the width of a string on a website or in an editor.

NOTE: Grapheme support in terminals is highly experimental and subject to ongoing discussion! When working on CLIs or TUIs, you do not want to use this option most of the time.

See also Grapheme Clusters and Terminal Emulators for a better explanation on how terminals behave.

pub fn mode_2027_ext(options: Options) -> Options

Measure grapheme clusters instead of individual code points, and always treat grapheme clusters with more than one non-modifier character as wide.

This is a custom extension to Unicode Core. Many single grapheme clusters would currently still render as wide glyphs in many contexts, since font shaping would not be able to collapse them into a single narrow “ligature”.

This mode is an additional heuristic that tries to handle more such cases.

See mode_2027 for more explanation on the implications.

pub fn mode_wcwidth(options: Options) -> Options

The default mode and the mode suitable for most terminals.

Measures individual code points, similar to the libc wcwidth function that almost all of these terminals use.

Note that except for fold_raw, this library will still always process grapheme clusters as a unit.

pub fn new() -> Options

Start building up new options.

pub fn padding(
  str: String,
  top top: Int,
  right right: Int,
  bottom bottom: Int,
  left left: Int,
  with space: String,
) -> String

Add some padding around the box of a string, filling empty space and the padded area with the provided character.

Padding is given as a number of rows/columns. Values are given clockwise, following the CSS shorthand property.

Ansi sequences will always be kept. If the space strings’ width does not evenly divide the missing area in the computed outer size of the string, the extra spacer will the resulting lines may not all be equally long.

Tip: Provide negative values to drop columns or rows!

Examples

padding("o", 1, 1, 1, 1, with: "x")
// --> "xxx\nxox\nxxx"

padding("hello", 0, 0, 0, right: -1, with: "")
// --> "hell"

padding("Trans\nrights!", top: 2, right: 5, bottom: 1, left: 1, with: "-")
// --> "-------------\n-------------\n-Trans-------\n-rights!-----\n-------------"
pub fn padding_with(
  str: String,
  top top: Int,
  right right: Int,
  bottom bottom: Int,
  left left: Int,
  using options: Options,
  with space: String,
) -> String

Like padding, but use custom options for measuring.

pub fn position(
  str: String,
  in bounding_box: Size,
  align alignment: Alignment,
  place placement: Placement,
  with space: String,
) -> String

Position the string area inside a bigger box without changing text alignment. The box will be filled with the space character.

Tip: If you only want to align a string on one axis, you can provide 0 for the orthogonal one! Extra characters and lines will be kept.

If the space strings’ width does not evenly divide the missing amount of columns, the extra spacer will overflow the max width.

Examples

position("X", in: Size(3, 3), align: Center, place: Middle, with: "o")
// --> "ooo\noXo\nooo"

// text still left-aligned, but moved to the right by 1 space
position("Trans\nrights\nare\nhuman\nrights", Size(0, 7), Right, Top, " ")
// --> " Trans \n rights\n are   \n human \n rights"
pub fn position_with(
  str: String,
  in bounding_box: Size,
  horizontal alignment: Alignment,
  vertical placement: Placement,
  using options: Options,
  with space: String,
) -> String

Like position, but use custom options to measure each line.

pub fn scroll(
  str: String,
  top top: Int,
  left left: Int,
  view viewport_size: Size,
  with space: String,
) -> String

Cut out a viewort at the given origin #(top, left) from a string. This is similar to “scrolling” a web page.

You can scroll past the filled area of the string in any direction. Empty space is filled using the space string. If the space string does not evenly fill the empty area, some lines may be longer than the given viewport.

All ANSI sequences in the entire string will be kept, even if they are outside of the visible viewport.

Example

scroll("wibble", top: 0, left: 2, view: Size(1, 2), with: " ")
// --> "bb"

"Hello\nHow are you today?\nDid you know:\nLucy says trans rights!"
|> scroll(top: 2, left: 10, view: Size(3, 5), with: "-")
// --> "ow:--\ntrans\n-----"
pub fn scroll_with(
  str: String,
  top top: Int,
  left left: Int,
  view viewport_size: Size,
  using options: Options,
  with space: String,
) -> String

Like scroll, but provide custom options for measuring.

pub fn soft_wrap(
  str: String,
  to max_size: Size,
  ellipsis ellipsis: String,
  to_indent to_indent: fn(String) -> String,
) -> String

Limit the dimensions of a string, either by wrapping on white space characters or soft hyphens while using a custom indentation function, or by truncating the last line and appending an ellipsis.

This function gives a little bit more control over what happens on soft line breaks than limit and allows you to inspect the line printed so far to figure out the indentation for all soft-wrapped lines. The to_indent callback is called with the line so far, and should return an indentation string (usually just the proper amount of spaces) that is prepended to all the following soft-wrapped lines.

Behaves like limit otherwise.

Examples

fn to_indent(str) {
  case str {
    // if it is a list, indent with 2 spaces
    "- " <> _ -> "  "
    // if it is a blockquote, prepend another blockquote marker
    "> " <> _ -> "> "
    // else, don't indent (like limit)
    _ -> ""
  }
}

// Girly Girl Productions - 10 drunk cigarettes
let lyrics = "
> Getting girls rich, yeah, that's a part of my plan.
> And I could name 10 things us girls need, before we ever need a man:

- One new vape, two lines of coke
- Three drinks from the bar, four more lines of coke
- Five Guys fries, six hits from my blunt
- Seven more lines of coke, eight pairs of shoes
- Nine BB belts, and 10 drunk cigarettes.
"

lyrics
|> soft_wrap(to: Size(rows: 24, columns: 34), ellipsis: "...", to_indent:)
|> io.println

Output:

> Getting girls rich, yeah, that's
> a part of my plan.
> And I could name 10 things us
> girls need, before we ever need
> a man:

- One new vape, two lines of coke
- Three drinks from the bar, four
  more lines of coke
- Five Guys fries, six hits from
  my blunt
- Seven more lines of coke, eight
  pairs of shoes
- Nine BB belts, and 10 drunk
  cigarettes.

Note: This is not meant to allow for arbitrary text formatting, but as a quick way to indicate soft-wrapping in the printed text. As shown in the example, it can be used to do some very light formatting, but you should almost always parse the string first, and print it in a way that makes sense for your text format, for example using the awesome glam package!

pub fn soft_wrap_with(
  str: String,
  to max_size: Size,
  using options: Options,
  ellipsis ellipsis: String,
  to_indent to_indent: fn(String) -> String,
) -> String

Like soft_wrap, but using custom options to measure the strings and indents.

pub fn stack_horizontal(
  blocks: List(String),
  place vertical: Placement,
  gap gap: Int,
  with space: String,
) -> String

Stack multiple blocks of strings horizontally next to each other into multiple columns.

The resulting string will as high as the highest string in the list. All other blocks can be position-ed vertically relative to this size.

Tip; Use limit and inline_styles to lay out the text in your columns!

Example

[
  "--color",
  "Enable color support.\nThis option is enabled by default in a terminal."
]
|> stack_horizontal(place: Top, gap: 4, with: " ")
// --> "--color    Enable color support.                           \n"
//  <> "           This option is enabled by default in a terminal."
pub fn stack_horizontal_with(
  blocks: List(String),
  place vertical: Placement,
  gap gap: Int,
  using options: Options,
  with space: String,
) -> String

Like stack_horizontal, but using custom options for measure.

pub fn stack_vertical(
  blocks: List(String),
  align horizontal: Alignment,
  gap gap: Int,
  with space: String,
) -> String

Stack multiple blocks of strings on top of each other into multiple rows.

The resulting string will as wide as the widest string in the list. All other blocks can be position-ed horizontally relative to this size.

Example

[
  "Hello!",
  "I hope you are feeling fantastic today!"
]
|> stack_vertical(Center, gap: 1, with: " ")
// --> "                Hello!                 \n"
//  <> "                                       \n"
//  <> "I hope you are feeling fantastic today!"
pub fn stack_vertical_with(
  blocks: List(String),
  align horizontal: Alignment,
  gap gap: Int,
  using options: Options,
  with space: String,
) -> String

Like stack_vertical, but using custom options for measure.

pub fn strip_ansi(str: String) -> String

Strip all ansi sequences from a string, using the same matching algorithm that this library uses internally.

This makes it safe to enable count_ansi_codes, or print a string without having to worry that it might mess with the terminal and/or colors.

Unexpected characters inside of escape sequences are ignored (swallowed).

Examples

strip_ansi("\u{1b}[0;33;49;3;9;4mhi~\u{1b}[0m")
// --> "hi~"

strip_ansi("check out \u{1b}]8;;https://gleam.run\u{7}Gleam!\u{1b}]8;;\u{7}")
// --> "check out Gleam!"
pub fn tabs_to_spaces(str: String) -> String

Replace all tab characters found in the string with the amount of spaces that this tab would have had otherwise.

This makes it safe to prepend to a line, without changing the spacing produced by tabs anymore.

Examples

tabs_to_spaces("Hello\tWorld")
// --> "Hello   World" // 3 spaces
pub fn tabs_to_spaces_with(
  str: String,
  using options: Options,
) -> String

Like tabs_to_spaces, but customise the opttions as well.

NB: You can use at_tab_offset and with_tab_width to change the measure of tab characters inside the string!

let options = new() |> with_tab_width(4)
tabs_to_spaces_with("hi\tcutie~", options)
// --> "hi  cutie~"
pub fn with_tab_width(
  options: Options,
  tab_width: Int,
) -> Options

Change the number of columns between tab stops. (Default: 8)

Whenever a tab character \t is encountered, the current column number will be rounded up to the next multiple of this number.

Search Document