string_width

A low-ish level library for building terminal UIs.

Package Version Hex Docs

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 or stack_horizontal, 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 and 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 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

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:

build 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.
dev 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.
help Show this help text.

Measuring strings

  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

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.

Search Document