Documentation
¶
Overview ¶
Package ansi provides the raw terminal escape sequences and a small styling builder used to render colored, styled text.
Width and Truncate count terminal columns using tables generated from Unicode 17.0.0 (see internal/tools/genwidth and `go generate`): East Asian Wide and Fullwidth runes and emoji-presentation runes take two columns; nonspacing marks, enclosing marks and format characters take none; everything else takes one. Text is measured by extended grapheme cluster (Unicode Standard Annex #29, tables from internal/tools/gengrapheme): a ZWJ sequence, a flag, an emoji with a skin-tone modifier or a base letter with combining marks is one unit, as wide as its widest rune, and Truncate never cuts inside one. For a terminal that draws the parts separately, call SetClusterWidth(false) or set TUI_NO_CLUSTERS=1 to count each rune on its own.
Index ¶
- Constants
- func Clean(raw bool, s string) string
- func CleanAll(raw bool, in []string) []string
- func CursorBack(n int) string
- func CursorDown(n int) string
- func CursorForward(n int) string
- func CursorPosition(row, col int) string
- func CursorUp(n int) string
- func DowngradeString(s string, p Profile) string
- func ExpandTabs(s string) string
- func Hyperlink(text, url string) string
- func KittyKeyboardEnableFlags(flags int) string
- func OSC52Copy(text string) string
- func Passthrough(seq string, getenv func(string) string) string
- func PlainASCIIWidth(s string) (int, bool)
- func SafeLinkTarget(url string) bool
- func Sanitize(s string) string
- func SanitizeKeepSGR(s string) string
- func SetClusterWidth(on bool)deprecated
- func StripANSI(s string) string
- func TrimLeftWidth(s string, width int) string
- func Truncate(s string, width int) string
- func Width(s string) int
- func Wrap(s string, width int) string
- func WrapStyled(s string, width int) string
- type BasicColor
- type Color
- type Color256
- type Measurer
- type Profile
- type RGB
- type Style
- func (s Style) Background(c Color) Style
- func (s Style) Blink() Style
- func (s Style) Bold() Style
- func (s Style) Conceal() Style
- func (s Style) Faint() Style
- func (s Style) Foreground(c Color) Style
- func (s Style) Italic() Style
- func (s Style) Link(url string) Style
- func (s Style) Overline() Style
- func (s Style) Render(text string) string
- func (s Style) Reverse() Style
- func (s Style) Span(parts ...string) string
- func (s Style) Strikethrough() Style
- func (s Style) Underline() Style
- func (s Style) UnderlineColor(c Color) Style
- func (s Style) UnderlineStyle(style UnderlineStyle) Style
- type UnderlineStyle
Examples ¶
Constants ¶
const ( // CSI is the control sequence introducer, ESC [. CSI = "\x1b[" // Reset ends all SGR styling (ESC[0m). Reset = "\x1b[0m" // AltScreenEnable switches to the alternate screen buffer (mode 1049). AltScreenEnable = CSI + "?1049h" // AltScreenDisable returns to the normal screen buffer. AltScreenDisable = CSI + "?1049l" // CursorHide hides the text cursor. CursorHide = CSI + "?25l" // CursorShow shows the text cursor. CursorShow = CSI + "?25h" // ClearScreen erases the whole screen. ClearScreen = CSI + "2J" // ClearLine erases the cursor's whole line. ClearLine = CSI + "2K" // CursorHome moves the cursor to row 1, column 1. CursorHome = CSI + "H" // EraseDown clears from the cursor's current position to the end of // the screen. Used when committing a Println: after moving the // cursor up to the top of the live region via relative addressing, // there is no absolute row to address the region's remaining rows // by, so the only way to guarantee no stale live-region content is // left behind (if the committed text is shorter than the region it // replaces) is to erase everything below the cursor before writing. EraseDown = CSI + "0J" // BracketedPasteEnable makes the terminal wrap pasted text in // ESC[200~ ... ESC[201~ so it arrives as one PasteEvent. BracketedPasteEnable = CSI + "?2004h" // BracketedPasteDisable reverses BracketedPasteEnable. BracketedPasteDisable = CSI + "?2004l" // MouseSGREnable/Disable select SGR mouse-coordinate encoding, needed // so coordinates beyond 223 work and so press/release are unambiguous. // It must be combined with one of the tracking modes below. MouseSGREnable = CSI + "?1006h" // MouseSGRDisable reverses MouseSGREnable. MouseSGRDisable = CSI + "?1006l" // MouseClickEnable/Disable report only button press/release, no drag. MouseClickEnable = CSI + "?1000h" // MouseClickDisable reverses MouseClickEnable. MouseClickDisable = CSI + "?1000l" // MouseCellMotionEnable/Disable additionally report motion while a // button is held (drag), but not free-standing mouse movement. MouseCellMotionEnable = CSI + "?1002h" // MouseCellMotionDisable reverses MouseCellMotionEnable. MouseCellMotionDisable = CSI + "?1002l" // MouseAllMotionEnable/Disable report every mouse movement, held button // or not — the most complete, and the most event traffic. MouseAllMotionEnable = CSI + "?1003h" // MouseAllMotionDisable reverses MouseAllMotionEnable. MouseAllMotionDisable = CSI + "?1003l" // KittyKeyboardEnable pushes progressive-enhancement flag 1 // ("disambiguate escape codes") onto the terminal's kitty-keyboard- // protocol stack: enough to make Shift+Enter, Ctrl+Shift+<letter>, and // similar combinations distinguishable from their unmodified sibling, // while leaving arrows/Home/End/function keys on their existing // legacy encoding. A terminal that doesn't support the protocol simply // ignores it. KittyKeyboardDisable pops one level off the stack, // restoring whatever was active before (nothing, for a terminal that // was never in kitty mode). KittyKeyboardEnable = CSI + ">1u" // KittyKeyboardDisable reverses KittyKeyboardEnable (see above). KittyKeyboardDisable = CSI + "<u" // FocusReportingEnable/Disable (DECSET 1004) make the terminal send // ESC[I when its window gains focus and ESC[O when it loses it, which // input.Reader decodes as an input.FocusEvent. A terminal that doesn't // support the mode ignores both sequences. FocusReportingEnable = CSI + "?1004h" // FocusReportingDisable reverses FocusReportingEnable. FocusReportingDisable = CSI + "?1004l" // SyncOutputEnable/Disable (mode 2026) bracket a frame write so the // terminal buffers it and paints the whole thing atomically instead of // character-by-character, preventing a torn/partial frame from being // visible mid-repaint. A terminal that doesn't recognize mode 2026 // simply ignores both sequences, so this degrades to the previous // unwrapped-write behavior with no feature detection needed. SyncOutputEnable = CSI + "?2026h" // SyncOutputDisable ends a synchronized frame write (see // SyncOutputEnable). SyncOutputDisable = CSI + "?2026l" )
const QueryBackgroundColor = "\x1b]11;?\x1b\\"
QueryBackgroundColor asks the terminal for its background color (OSC 11). A supporting terminal replies with ESC ] 11 ; rgb:RRRR/GGGG/BBBB, which input.Reader decodes as an input.BackgroundColorEvent. Terminals that don't support it stay silent, so callers must time the wait out.
const QueryPalette = "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x1b\\"
QueryPalette asks the terminal for ANSI colours 0-15 (OSC 4). A supporting terminal answers each slot with ESC ] 4 ; N ; rgb:RRRR/GGGG/BBBB, which input.Reader decodes as an input.PaletteColorEvent. Terminals that don't support it stay silent, so callers fall back to the xterm palette.
Variables ¶
This section is empty.
Functions ¶
func Clean ¶
Clean makes a caller-supplied string safe to draw: unless raw, tabs become spaces and every escape sequence and control character except '\n' is removed (Sanitize), so a label, file name or message cannot move the cursor, clear the screen, set the clipboard or restyle the widgets around it. With raw set, s is returned unchanged.
func CursorBack ¶
CursorBack returns the escape sequence to move the cursor left n columns.
func CursorDown ¶
CursorDown returns the escape sequence to move the cursor down n rows.
func CursorForward ¶
CursorForward returns the escape sequence to move the cursor right n columns.
func CursorPosition ¶
CursorPosition returns the escape sequence to move the cursor to the given 1-indexed row/column.
func DowngradeString ¶
DowngradeString rewrites the colour codes in s's SGR sequences to what p can display: truecolor and 256-colour codes become the nearest 256- or 16-colour ones, and under NoColor every foreground, background and underline colour is removed. Attributes (bold, dim, italic, underline shapes, reverse, ...) are left alone, as NO_COLOR asks for no colour, not no styling. Text, non-SGR sequences (cursor movement, OSC hyperlinks) and TrueColor input pass through untouched, so the visible text never changes. It is what Program applies to each frame under WithColorProfile.
func ExpandTabs ¶
ExpandTabs replaces each tab with spaces up to the next multiple of 8 columns. It is ANSI-aware: escape sequences take no columns, and the column counter restarts after '\n' and '\r'. Text without tabs is returned as is.
func Hyperlink ¶
Hyperlink returns text wrapped in an OSC 8 clickable-hyperlink escape sequence pointing at url, for terminals that support it (many modern terminal emulators do; terminals that don't simply show text unadorned, ignoring the surrounding escapes). If url fails SafeLinkTarget, text is returned unchanged with no OSC 8 sequence.
func KittyKeyboardEnableFlags ¶
KittyKeyboardEnableFlags pushes the given kitty keyboard protocol flag bitmask (1 disambiguate, 2 report event types, 4 alternate keys, 8 all keys as escape codes). KittyKeyboardEnableFlags(1) equals KittyKeyboardEnable; KittyKeyboardDisable pops it.
func OSC52Copy ¶
OSC52Copy returns the OSC 52 escape sequence that sets the system clipboard to text, for terminals that support it (support isn't universal; unsupported terminals typically just ignore it). The payload is base64-encoded per the OSC 52 spec.
Under tmux a bare OSC 52 is governed by the "set-clipboard" option (on or external; off discards it), not by "allow-passthrough", which only applies to DCS tmux;-wrapped sequences. A dropped copy looks like a success and cannot be detected from inside the program. See package clipboard.
func Passthrough ¶
Passthrough wraps seq, a DCS, APC or other string sequence meant for the outer terminal, so a terminal multiplexer forwards it instead of dropping it. It reads the environment through getenv (nil means os.Getenv):
- $TMUX set: seq becomes a "DCS tmux;" string, every ESC in seq doubled. tmux also needs "set -g allow-passthrough on" to forward it.
- else $STY set: seq becomes DCS strings, split every 768 bytes, which is how GNU screen passes data through.
- otherwise seq is returned unchanged.
Wrap the whole sequence once; wrapping twice nests the wrapper.
func PlainASCIIWidth ¶
PlainASCIIWidth reports whether every byte of s is printable ASCII (0x20 through 0x7e) and, if so, returns its display width, which is len(s). For such text Sanitize, SanitizeKeepSGR and ExpandTabs are all the identity, so a caller that would sanitize and then measure can do both in this one pass and skip them. It returns 0, false for anything else, including text with a tab, an escape, a DEL or a non-ASCII byte. The empty string is plain, width 0.
func SafeLinkTarget ¶
SafeLinkTarget reports whether url may be embedded in an OSC 8 hyperlink. It must be non-empty, contain no control characters (C0, DEL, C1: ESC, BEL and the ST terminator can all end the OSC early and inject sequences), no spaces or surrounding whitespace, and start with a scheme of http, https, mailto or file (matched case-insensitively). Scheme-less targets, including relative paths and "//host" forms, are rejected: a terminal has no base to resolve them against, so they are ambiguous at best.
func Sanitize ¶
Sanitize neutralises untrusted text for display: it removes every escape sequence (CSI, OSC, DCS, SOS, PM, APC), a lone ESC, and all C0 and C1 control characters (and DEL) except '\n'. Tabs are dropped too; call ExpandTabs first to keep them as spaces. Printable text, including invalid UTF-8, is returned unchanged.
func SanitizeKeepSGR ¶
SanitizeKeepSGR is Sanitize but keeps SGR (style) sequences, ESC [ params m where params contain only digits, ';' and ':'. Every other escape sequence and control character is removed. An unterminated SGR-looking sequence is dropped.
func SetClusterWidth
deprecated
func SetClusterWidth(on bool)
SetClusterWidth turns grapheme-cluster handling on or off for Width, Truncate, TrimLeftWidth and everything built on them. It is on by default: a ZWJ emoji sequence, a flag, an emoji with a skin-tone modifier or a base letter with combining marks is measured, truncated and trimmed as one unit, the way current terminals draw it. Turn it off for a terminal that does not join clusters, to count each rune on its own as before. The environment variable TUI_NO_CLUSTERS=1 does the same, read when the first width is measured. It is safe to call from any goroutine.
Deprecated: for anything tied to one terminal use a Measurer. This function stays as the process-wide default that the zero Measurer follows, for an application to set once at start-up; a Program no longer calls it (the capability probe's answer is Program.Measurer and ResizeMsg.Measurer), so libraries must not either.
A cluster is measured as its widest rune (at most 2 columns), except that an emoji followed by U+FE0F (variation selector 16) takes 2. Segmentation follows Unicode Standard Annex #29, except that CR LF is two clusters, so text measures as it always did.
func StripANSI ¶
StripANSI removes CSI escape sequences (as emitted by Style, e.g. "\x1b[1;31m...\x1b[0m") and string sequences (OSC, DCS, APC, SOS, PM, terminated by BEL or ST) from s, leaving the visible text behind. An unterminated string sequence runs to the end of s.
func TrimLeftWidth ¶
TrimLeftWidth returns s with its first width visible columns removed — the inverse of Truncate. If the cut lands inside a wide rune, that rune is dropped and a space stands in for its remaining column so the remainder keeps its column alignment. Zero-width runes at the cut boundary stay with the remainder, for the other half of a string once it's been split at a column (e.g. layout.Overlay, splicing content into the middle of an existing line).
Unlike Truncate, this can't just carry escape sequences through unmodified: if the cut lands in the middle of a styled span, the discarded prefix carried the SGR sequence that made the rest of the span styled, so it has to be re-emitted at the start of the remainder. This tracks only the single most recently seen non-Reset sequence as "the active style" and re-applies exactly that — correct for spans built the way Style.Render does (one opening sequence, content, one closing Reset, never nested or overlapping), which is the only shape this module's own code ever produces, but not a general ANSI state machine.
func Truncate ¶
Truncate returns s cut to at most width visible columns, the same counting Width uses (ignoring escape sequences, grapheme clusters weighted as Width weighs them). A wide rune or a cluster that would straddle the cut is dropped whole, so a ZWJ sequence, flag or base with its marks is never split. Escape sequences are preserved up through the cut point, so styling applied before it still renders; if that leaves styling "open" (no Reset was reached before the cut), a Reset is appended so it can't bleed into whatever's rendered after this string. OSC, DCS, APC, SOS and PM sequences are skipped like CSI; an OSC 8 hyperlink left open at the cut is closed with an empty OSC 8, so the output is balanced.
Example ¶
package main
import (
"fmt"
"github.com/ows4444/tui/ansi"
)
func main() {
fmt.Println(ansi.Truncate("hello world", 5))
}
Output: hello
func Width ¶
Width returns the display width of s, ignoring any embedded ANSI escape sequences. Each extended grapheme cluster counts for the columns it occupies: 2 for East Asian Wide/Fullwidth and emoji, 0 for combining and zero-width runes, 1 otherwise, and a ZWJ sequence, flag, emoji with a skin-tone modifier or base with combining marks is one cluster, the widest of its runes (see SetClusterWidth to count each rune on its own instead, and runeWidth for the per-rune widths).
It measures in place, skipping CSI sequences as it goes, and allocates nothing; it agrees with measuring StripANSI(s) on every input. The one case that cannot be measured in place is a multi-byte rune whose bytes an escape sequence splits: StripANSI would join them, so Width falls back to it.
Example ¶
Width counts terminal columns, not bytes or runes: escape sequences take none and a wide character takes two.
package main
import (
"fmt"
"github.com/ows4444/tui/ansi"
)
func main() {
fmt.Println(ansi.Width("abc"), ansi.Width(ansi.NewStyle().Bold().Render("abc")), ansi.Width("日本"))
}
Output: 3 3 4
func Wrap ¶
Wrap word-wraps s to width visible columns, preserving existing newlines as paragraph breaks — each paragraph is wrapped independently, rather than treating the whole string as one run of words that could merge separate paragraphs together. A single word longer than width is never split (there's no hyphenation), so a line can still exceed width in that case.
Example ¶
package main
import (
"fmt"
"github.com/ows4444/tui/ansi"
)
func main() {
fmt.Println(ansi.Wrap("the quick brown fox", 9))
}
Output: the quick brown fox
func WrapStyled ¶
WrapStyled word-wraps s to width visible columns like Wrap, but tracks the SGR and OSC 8 hyperlink state as it goes, so every output line is self-contained: a line starts by re-opening the style active at that point and, if a style or link is still open at its end, closes it. A line drawn on its own therefore keeps its style, and none leaks into neighbouring cells. Existing newlines are paragraph breaks; a word longer than width is never split. It runs in time linear in len(s).
Types ¶
type BasicColor ¶
type BasicColor uint8
BasicColor is one of the 16 standard ANSI colors.
const ( Black BasicColor = iota Red Green Yellow Blue Magenta Cyan White BrightBlack BrightRed BrightGreen BrightYellow BrightBlue BrightMagenta BrightCyan BrightWhite )
The 16 standard ANSI colours in SGR order: the eight normal colours (Black to White) followed by their bright variants.
type Color ¶
type Color interface {
// contains filtered or unexported methods
}
Color renders itself as the SGR parameter list for foreground/background.
type Measurer ¶
type Measurer struct {
// contains filtered or unexported fields
}
Measurer measures text the way one particular terminal draws it. The package-level Width, Truncate and TrimLeftWidth use a single process-wide setting (SetClusterWidth); a Measurer carries its own, so two terminals in one process (an SSH server, parallel tests) can disagree about grapheme clusters without one changing the other's output.
The zero Measurer follows the process setting, so a Measurer that was never configured measures exactly like the package-level functions. ClusterMeasurer pins it either way. A Measurer is a small comparable value; copy it freely.
func ClusterMeasurer ¶
ClusterMeasurer returns a Measurer that treats an extended grapheme cluster as one unit when clusters is true and counts each rune on its own when it is false, whatever SetClusterWidth says. See SetClusterWidth for what a cluster measures.
func (Measurer) TrimLeftWidth ¶
TrimLeftWidth is TrimLeftWidth measured by m.
type Profile ¶
type Profile uint8
Profile is the color depth a terminal can display.
func DetectColorProfile ¶
func DetectColorProfile() Profile
DetectColorProfile reports the color depth of standard output. It is DetectColorProfileFor(os.Stdout, os.Getenv): when stdout is not a terminal the result is NoColor unless CLICOLOR_FORCE is set.
func DetectColorProfileEnv ¶
DetectColorProfileEnv is the environment-only detection: it behaves as DetectColorProfileFor with a terminal output, so it does no TTY check.
func DetectColorProfileFor ¶
DetectColorProfileFor reports the color depth for out, reading the environment through getenv. Precedence: NO_COLOR (non-empty) gives NoColor; CLICOLOR_FORCE (non-empty, not "0") enables color even when out is not a terminal; otherwise a non-terminal out or CLICOLOR=0 gives NoColor. Then TERM=dumb gives NoColor, and COLORTERM=truecolor|24bit, WT_SESSION, TERM_PROGRAM and the TERM name pick the depth. An empty TERM is ANSI16 on Windows and NoColor elsewhere.
type Style ¶
type Style struct {
// contains filtered or unexported fields
}
Style is an immutable, chainable SGR (Select Graphic Rendition) builder. Every method returns a new Style, so a base style can be reused safely:
base := ansi.NewStyle().Bold() ok := base.Foreground(ansi.Green) err := base.Foreground(ansi.Red)
func (Style) Background ¶
Background sets the background color.
func (Style) Link ¶
Link makes Render and Span wrap the styled text in an OSC 8 hyperlink to url, one link per non-empty line. A url that fails SafeLinkTarget is dropped, so the text renders without OSC 8, exactly as Hyperlink does.
func (Style) Render ¶
Render wraps text in this style's escape sequence, resetting afterward. Multi-line text is styled per line — each non-empty line gets its own opening sequence and Reset, and empty lines stay bare — so any single line remains styled when repainted on its own (as Program.render's line diff does).
Render's Reset is a full SGR reset (Style has no save/restore stack), so nesting one Style.Render call around text that already contains another Style's Render output does not compose: the inner Reset fires before the outer text that follows it, clobbering the outer style for everything after the nested content, even on a row that's supposed to stay highlighted throughout (e.g. a cursor row embedding a pre-styled swatch). Compute one combined Style for a run that needs to look like nested styling, rather than composing two independently-Render-ed strings.
func (Style) Span ¶
Span renders the concatenation of parts in this style, re-emitting the style after every reset inside them. Unlike Render, nesting works: in outer.Span("a ", inner.Render("b"), " c") the text " c" is still in the outer style even though inner.Render ended with a full reset. Multi-line text is styled per line, as Render does. A style with no attributes returns the parts joined unchanged.
func (Style) Strikethrough ¶
Strikethrough enables struck-through text.
func (Style) UnderlineColor ¶
UnderlineColor sets the underline's color independently of the text's foreground color (SGR 58), so an underline can, for example, mark an error in red under text that otherwise renders in the default color. It has no visible effect unless the style is also underlined (via Underline or UnderlineStyle).
func (Style) UnderlineStyle ¶
func (s Style) UnderlineStyle(style UnderlineStyle) Style
UnderlineStyle enables underlined text drawn in the given shape (curly, dotted, dashed) instead of a plain straight line. It implies Underline, so calling it alone is enough — no need to also call Underline().
type UnderlineStyle ¶
type UnderlineStyle uint8
UnderlineStyle selects the shape of an underline drawn via Style.UnderlineStyle, using the extended "4:n" SGR sub-parameter that Kitty, WezTerm, iTerm2 and recent VTE/Ghostty support. A terminal that doesn't understand the colon sub-parameter form typically falls back to an ordinary straight underline instead of ignoring it outright.
const ( // UnderlineCurly draws a wavy underline (SGR 4:3), the shape commonly // used for spell-check/lint-style annotations. UnderlineCurly UnderlineStyle = 3 // UnderlineDotted draws a dotted underline (SGR 4:4). UnderlineDotted UnderlineStyle = 4 // UnderlineDashed draws a dashed underline (SGR 4:5). UnderlineDashed UnderlineStyle = 5 )