Str.Visual
Str / Visual
Import
import { Str } from '@wollybeard/kit'
// Access via namespace
Str.Visual.someFunction()
import * as Str from '@wollybeard/kit/str'
// Access via namespace
Str.Visual.someFunction()
Namespaces
Table
- Visual-aware table operations for multi-column text layout.
Text Formatting
[F]
width
(text: string): number
Parameters:
text
- The text to measure
Returns: The visual width of the text
Get the visual width of a string, ignoring ANSI escape codes and counting grapheme clusters.
This is the "true" visual width as it would appear in a terminal:
- ANSI escape codes (colors, styles) are stripped before counting
- Grapheme clusters (emojis, combining characters) count as single units
Examples:
// ANSI codes are stripped
Str.Visual.width('\x1b[31mred\x1b[0m') // 3
// Grapheme clusters count as 1
Str.Visual.width('👨👩👧👦') // 1 (family emoji)
Str.Visual.width('é') // 1 (e + combining accent)
Str.Visual.width('🇺🇸') // 1 (flag emoji)
// Empty string
Str.Visual.width('') // 0
Str.Visual.width('\x1b[31m\x1b[0m') // 0 (only ANSI codes)
[F]
pad
(text: string, size: number, side?: "left" | "right" = `left`, char?: string = defaultPadCharacter): string
Parameters:
text
- The text to padsize
- Target visual size (including text)side
- Which side to add padding ('left' or 'right')char
- Character to use for padding (default: space)
Returns: The padded text (or original if already wider than size)
Add padding to text, calculated based on visual length.
The padding size is adjusted to account for ANSI escape codes, so the final output has the desired visual width.
Examples:
// Regular text
Str.Visual.pad('hi', 5, 'right') // 'hi ' (visual width 5)
// With ANSI codes - padding accounts for escape codes
const colored = '\x1b[31mOK\x1b[0m'
Str.Visual.pad(colored, 5, 'right') // Adds 3 spaces (visual: "OK ")
// Text already wider than target size
Str.Visual.pad('hello', 3, 'left') // 'hello' (unchanged)
[C]
padOn
;((text: string) =>
(size: number) =>
(side?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of pad with text first.
[C]
padWith
;((size: number) =>
(text: string) =>
(side?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of pad with size first.
Examples:
const pad10 = Str.Visual.padWith(10)
pad10('\x1b[32mSuccess\x1b[0m', 'right') // Visual width 10
[F]
span
(text: string, width: number, align?: "left" | "right" = `left`, char?: string = defaultPadCharacter): string
Parameters:
text
- The text to alignwidth
- Target visual widthalign
- Content alignment ('left' or 'right')char
- Character to use for padding (default: space)
Returns: The aligned text
Align text within a specified visual width by adding padding.
This ensures text spans exactly the target width, aligning content to the left or right. If the text is already wider than the target width, no padding is added.
Examples:
// Left-align (pad right)
Str.Visual.span('hi', 5, 'left') // 'hi '
// Right-align (pad left)
Str.Visual.span('hi', 5, 'right') // ' hi'
// With ANSI codes
const colored = '\x1b[34mID\x1b[0m'
Str.Visual.span(colored, 6, 'left') // Visual: "ID "
[C]
spanOn
;((text: string) =>
(width: number) =>
(align?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of span with text first.
[C]
spanWith
;((width: number) =>
(text: string) =>
(align?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of span with width first.
Examples:
const span8 = Str.Visual.spanWith(8)
span8('Name', 'left') // 'Name '
span8('Age', 'right') // ' Age'
[F]
fit
(text: string, width: number, align?: "left" | "right" = `left`, char?: string = defaultPadCharacter): string
Parameters:
text
- The text to constrainwidth
- Exact target visual widthalign
- Content alignment ('left' or 'right')char
- Character to use for padding (default: space)
Returns: Text constrained to exact width
Constrain text to exact visual width by cropping and/or padding.
Unlike span which only pads (leaving text unchanged if too long), this function guarantees the exact width by:
- Cropping text if it exceeds the target width
- Padding text if it's shorter than the target width
This is useful for fixed-width layouts where column widths must be exact, such as table columns, status bars, and terminal UIs.
Examples:
// Text too long - gets cropped
Str.Visual.fit('hello world', 5, 'left') // 'hello'
// Text too short - gets padded
Str.Visual.fit('hi', 5, 'left') // 'hi '
Str.Visual.fit('hi', 5, 'right') // ' hi'
// Perfect fit - unchanged
Str.Visual.fit('exact', 5, 'left') // 'exact'
// With ANSI codes
const colored = '\x1b[31mvery long colored text\x1b[0m'
Str.Visual.fit(colored, 8, 'left') // '\x1b[31mvery lon\x1b[0m' (visual: "very lon")
// Use case: Fixed-width table columns
const columns = ['Name', 'Email', 'Status'].map(
(header, i) => Str.Visual.fit(header, [10, 20, 8][i], 'left'),
)
// ['Name ', 'Email ', 'Status ']
[C]
fitOn
;((text: string) =>
(width: number) =>
(align?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of fit with text first.
[C]
fitWith
;((width: number) =>
(text: string) =>
(align?: 'left' | 'right' | undefined) =>
(char?: string | undefined) => string)
Curried version of fit with width first.
Examples:
// Create fixed-width formatters
const nameColumn = Str.Visual.fitWith(20)
const statusColumn = Str.Visual.fitWith(10)
nameColumn('John Doe', 'left') // 'John Doe '
statusColumn('Active', 'left') // 'Active '
statusColumn('Very Long Status', 'left') // 'Very Long '
[F]
take
(text: string, size: number): string
Parameters:
text
- The text to extract fromsize
- Visual length to take
Returns: The extracted substring
Take a substring by visual length.
Extracts characters from the start of the string up to the specified visual width. Accounts for ANSI codes and grapheme clusters, so the result has the desired visual length.
Examples:
// Regular text
Str.Visual.take('hello', 3) // 'hel'
// With ANSI codes
const colored = '\x1b[31mhello\x1b[0m world'
Str.Visual.take(colored, 5) // '\x1b[31mhello\x1b[0m' (visual: "hello")
// With emoji
Str.Visual.take('👋 hello', 2) // '👋 ' (emoji + space)
[C]
takeOn
;((text: string) => (size: number) => string)
Curried version of take with text first.
[C]
takeWith
;((size: number) => (text: string) => string)
Curried version of take with size first.
Examples:
const take10 = Str.Visual.takeWith(10)
take10('a long string here') // First 10 visual chars
[F]
takeWords
(text: string, size: number): { taken: string; remaining: string; }
Parameters:
text
- The text to splitsize
- Maximum visual length
Returns: Object with taken
words and remaining
text
Split text into words by visual length, respecting word boundaries.
Extracts words from the start of the string until reaching the visual width limit. Avoids breaking words mid-way when possible (though single words longer than size will be taken anyway).
Examples:
// Splits at word boundaries
Str.Visual.takeWords('hello world here', 12)
// { taken: 'hello world', remaining: 'here' }
// Single word too long - takes it anyway
Str.Visual.takeWords('verylongword more', 8)
// { taken: 'verylongword', remaining: 'more' }
// With ANSI codes
const colored = '\x1b[32mone\x1b[0m two three'
Str.Visual.takeWords(colored, 7)
// { taken: '\x1b[32mone\x1b[0m two', remaining: 'three' }
[C]
takeWordsOn
;((text: string) => (size: number) => {
taken: string
remaining: string
})
Curried version of takeWords with text first.
[C]
takeWordsWith
;((size: number) => (text: string) => {
taken: string
remaining: string
})
Curried version of takeWords with size first.
Examples:
const take20 = Str.Visual.takeWordsWith(20)
take20('Lorem ipsum dolor sit amet')
// { taken: 'Lorem ipsum dolor', remaining: 'sit amet' }
[F]
wrap
(text: string, width: number): string[]
Parameters:
text
- Text to wrap (may contain existing newlines)width
- Maximum visual width per line
Returns: Array of wrapped lines
Wrap text to fit within visual width, respecting word boundaries.
Breaks text into lines that fit the specified visual width. Respects existing newlines in the input and breaks long lines at word boundaries when possible.
Examples:
// Basic wrapping
Str.Visual.wrap('hello world here', 10)
// ['hello', 'world here']
// Respects existing newlines
Str.Visual.wrap('line one\nline two is long', 10)
// ['line one', 'line two', 'is long']
// With ANSI codes - visual width accounts for escape codes
const colored = '\x1b[31mthis is red text\x1b[0m and normal'
Str.Visual.wrap(colored, 12)
// ['\x1b[31mthis is red\x1b[0m', 'text and', 'normal']
[C]
wrapOn
(text: string) => (width: number) => string[]
Curried version of wrap with text first.
[C]
wrapWith
(width: number) => (text: string) => string[]
Curried version of wrap with width first.
Examples:
const wrap80 = Str.Visual.wrapWith(80)
wrap80('long text here...') // Wraps to 80 columns
[F]
size
(text: string): { maxWidth: number; height: number; }
Parameters:
text
- The text to measure
Returns: Object with maxWidth
and height
properties
Get the visual size (dimensions) of text.
Returns the maximum visual width (longest line) and height (line count). Accounts for ANSI codes and grapheme clusters.
Examples:
Str.Visual.size('hello\nworld')
// { maxWidth: 5, height: 2 }
// With ANSI codes
const colored = '\x1b[31mred\x1b[0m\n\x1b[32mgreen!\x1b[0m'
Str.Visual.size(colored)
// { maxWidth: 6, height: 2 } (visual: "red" and "green!")
// Empty string
Str.Visual.size('')
// { maxWidth: 0, height: 0 }
[F]
maxWidth
(text: string): number
Parameters:
text
- The text to measure
Returns: The maximum visual width across all lines
Get the maximum visual width of text (longest line).
Convenience function that returns just the width from size. Useful when you only need width and not height.
Examples:
Str.Visual.maxWidth('short\nlonger line\nhi') // 11
// With ANSI codes
Str.Visual.maxWidth('\x1b[31mred\x1b[0m\n\x1b[32mgreen\x1b[0m') // 5