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
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 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.