termsafe

package
v0.14.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package termsafe filters text someone else wrote before it reaches a terminal, and checks text an application is about to publish against the same rules.

A terminal obeys the escape sequences and control characters it is sent. Text read from a record, a log line or a store was written by whoever published it, so printing it raw lets that publisher drive the reader's terminal: set the window title, move the cursor, clear the screen, or make one string look like another with bidirectional overrides. Sanitize passes printable text, spaces and newlines, turns a tab into a space, drops every control character, every escape sequence and the zero-width and bidirectional characters, and bounds the result at MaxLines lines of MaxCols columns, so one value cannot scroll a terminal indefinitely. Validate and ValidateBounded are the publishing side of the same rules: they name the first thing in a value that Sanitize would have to strip.

Unlike the rest of this module, this package is about terminals. It still reads no environment of its own accord: an application that wants to know whether the locale draws UTF-8 passes its own lookup to UTF8Locale.

Index

Examples

Constants

View Source
const (
	MaxLines = 200
	MaxCols  = 512
)

MaxLines and MaxCols bound what Sanitize returns: at most MaxLines lines, each of at most MaxCols printed runes. ValidateBounded holds a value to the same two bounds.

Variables

View Source
var ErrUnsafe = errors.New("termsafe: text a terminal would have to be protected from")

ErrUnsafe matches, through errors.Is, every error Validate and ValidateBounded return.

Functions

func Abbrev

func Abbrev(s string) string

Abbrev is the first twelve characters of s, filtered by Text: the form a message names a key by, from its hex. A key read from a file someone else wrote may be shorter than that, or not hex at all, so it is cut by rune, which never panics, and filtered like any other text from outside.

func Sanitize

func Sanitize(s string, o Options) string

Sanitize returns s with only printable runes, spaces, tabs (as spaces) and newlines, bounded at MaxLines lines of MaxCols columns. A value cut at the line bound ends with "[truncated]". It never returns a byte sequence a terminal interprets, except an SGR sequence when o.ANSI is set, which is re-validated here rather than copied and is always followed by a reset: at the end, or before "[truncated]" when the value is cut.

Invalid UTF-8 becomes U+FFFD. Without o.ANSI, a half block drawn on a set background becomes a full block: half-block art inks a cell's second half with the background colour, and dropping the colour would otherwise break the picture into stripes, where a full block keeps its shape.

Example

A value from a record somebody else published is filtered before it is printed. The window-title sequence and the screen clear are dropped, the bidirectional override that would reverse the rest of the line is dropped, and the colour survives only when the reader asked for it.

package main

import (
	"fmt"

	"github.com/lightwebinc/bcommon/termsafe"
)

func main() {
	hostile := "status \x1b]0;you have been pwned\x07\x1b[2J\x1b[32mgreen\x1b[0m ‮evil"

	fmt.Printf("%q\n", termsafe.Text(hostile))
	fmt.Printf("%q\n", termsafe.Sanitize(hostile, termsafe.Options{ANSI: true}))
	fmt.Printf("%q\n", termsafe.Sanitize("café ☕", termsafe.Options{ASCII: true}))
}
Output:
"status green evil"
"status \x1b[32mgreen\x1b[0m evil\x1b[0m"
"caf? ?"

func Text

func Text(s string) string

Text is Sanitize with no options: what a pipe, a log or a label gets.

func UTF8Locale

func UTF8Locale(getenv func(string) string) bool

UTF8Locale reports whether the locale says the terminal draws UTF-8, from the first of LC_ALL, LC_CTYPE and LANG that getenv answers with a value. A locale that says nothing is taken as UTF-8. The application passes its own lookup, os.Getenv for the process's environment, so this package reads no environment of its own.

Example

Whether to degrade to ASCII is the locale's answer, read through the lookup the application passes: os.Getenv in a command, a map here.

package main

import (
	"fmt"
	"strings"

	"github.com/lightwebinc/bcommon/termsafe"
)

func main() {
	env := map[string]string{"LC_ALL": "C", "LANG": "en_US.UTF-8"}
	getenv := func(k string) string { return env[k] }

	fmt.Println(termsafe.UTF8Locale(getenv))
	fmt.Println(termsafe.Abbrev("02" + strings.Repeat("ab", 32)))
}
Output:
false
02ababababab

func Validate

func Validate(field, s string) error

Validate reports the first thing in s that Sanitize would have to strip: invalid UTF-8, a control character, an escape sequence other than a colour (SGR) sequence, a zero-width or bidirectional character, or another non-printable character. Printable text, spaces, tabs and newlines pass, at any length. field names s in the error, which begins with it: "plan line 3 has a control character (U+0007)".

func ValidateBounded

func ValidateBounded(field, s string) error

ValidateBounded is Validate that also holds s to MaxLines lines of MaxCols columns. The two bounds are display bounds, not content rules: they suit a value meant to be read on a screen, and Sanitize applies them to whatever it prints anyway, so content that is meant to be longer than a screen belongs with Validate.

Example

The publishing side of the same rules: a value that a reader would have to strip is refused before it is published, naming the first offence. An application puts its own words in front and keeps both matches.

package main

import (
	"errors"
	"fmt"

	"github.com/lightwebinc/bcommon/termsafe"
)

func main() {
	errBody := errors.New("body text")
	for _, v := range []string{"back monday\n\x1b[1mbold\x1b[0m is fine", "ring\x07 the bell", "\x1b]0;title\x07"} {
		if err := termsafe.ValidateBounded("plan", v); err != nil {
			err = fmt.Errorf("%w: %w", errBody, err)
			fmt.Println(err, errors.Is(err, errBody), errors.Is(err, termsafe.ErrUnsafe))
			continue
		}
		fmt.Println("ok")
	}
}
Output:
ok
body text: plan line 1 has a control character (U+0007) true true
body text: plan line 1 has an escape sequence that is not a colour (SGR) true true

Types

type Options

type Options struct {
	// ANSI keeps colour and weight (SGR) sequences, and nothing else. Set it
	// only when the reader asked for colour: the sequences are re-validated
	// rather than copied, and a reset is always written at the end.
	ANSI bool
	// ASCII degrades every rune above 0x7E to "?", for a terminal that
	// cannot draw it.
	ASCII bool
}

Options is how a value may reach the terminal.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL