chore: bump deps
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-08-12 07:04:57 +02:00
committed by decentral1se
parent 157d131b37
commit 56a68dfa91
981 changed files with 36486 additions and 39650 deletions

View File

@ -1,40 +0,0 @@
run:
tests: false
issues-exit-code: 0
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- exhaustive
- goconst
- godot
- godox
- mnd
- gomoddirectives
- goprintffuncname
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- wrapcheck
# disable default linters, they are already enabled in .golangci.yml
disable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused

View File

@ -1,24 +1,22 @@
version: "2"
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- gofumpt
- goimports
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
@ -26,3 +24,17 @@ linters:
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax

View File

@ -1,11 +1,15 @@
# Bubble Tea
<p>
<a href="https://stuff.charm.sh/bubbletea/bubbletea-4k.png"><img src="https://github.com/charmbracelet/bubbletea/assets/25087/108d4fdb-d554-4910-abed-2a5f5586a60e" width="313" alt="Bubble Tea Title Treatment"></a><br>
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://stuff.charm.sh/bubbletea/bubble-tea-v2-light.png" width="308">
<source media="(prefers-color-scheme: dark)" srcset="https://stuff.charm.sh/bubbletea/bubble-tea-v2-dark.png" width="312">
<img src="https://stuff.charm.sh/bubbletea/bubble-tea-v2-light.png" width="308" />
</picture>
<br>
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
<a href="https://www.phorm.ai/query?projectId=a0e324b6-b706-4546-b951-6671ea60c13f"><img src="https://stuff.charm.sh/misc/phorm-badge.svg" alt="phorm.ai"></a>
</p>
The fun, functional and stateful way to build terminal apps. A Go framework

View File

@ -0,0 +1,14 @@
# https://taskfile.dev
version: '3'
tasks:
lint:
desc: Run lint
cmds:
- golangci-lint run
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}

View File

@ -114,6 +114,7 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
// Execute system command.
if err := c.Run(); err != nil {
p.renderer.resetLinesRendered()
_ = p.RestoreTerminal() // also try to restore the terminal.
if fn != nil {
go p.Send(fn(err))
@ -121,6 +122,9 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
return
}
// Maintain the existing output from the command
p.renderer.resetLinesRendered()
// Have the program re-capture input.
err := p.RestoreTerminal()
if fn != nil {

View File

@ -4,11 +4,16 @@
package tea
import (
"fmt"
"io"
"github.com/muesli/cancelreader"
)
func newInputReader(r io.Reader, _ bool) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r)
cr, err := cancelreader.NewReader(r)
if err != nil {
return nil, fmt.Errorf("bubbletea: error creating cancel reader: %w", err)
}
return cr, nil
}

View File

@ -67,6 +67,8 @@ func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, e
func (r *conInputReader) Cancel() bool {
r.setCanceled()
// Warning: These cancel methods do not reliably work on console input
// and should not be counted on.
return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
}

View File

@ -622,7 +622,7 @@ func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
case '<':
if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil {
// SGR mouse events length is the length of the match plus the length of the escape sequence
mouseEventSGRLen := matchIndices[1] + 3 //nolint:gomnd
mouseEventSGRLen := matchIndices[1] + 3 //nolint:mnd
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
}
}

View File

@ -119,13 +119,12 @@ func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) {
}
// detectReportFocus detects a focus report sequence.
// nolint: gomnd
func detectReportFocus(input []byte) (hasRF bool, width int, msg Msg) {
switch {
case bytes.Equal(input, []byte("\x1b[I")):
return true, 3, FocusMsg{}
return true, 3, FocusMsg{} //nolint:mnd
case bytes.Equal(input, []byte("\x1b[O")):
return true, 3, BlurMsg{}
return true, 3, BlurMsg{} //nolint:mnd
}
return false, 0, nil
}

View File

@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"time"
"github.com/erikgeiser/coninput"
localereader "github.com/mattn/go-localereader"
@ -25,14 +26,10 @@ func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader)
var ps coninput.ButtonState // keep track of previous mouse state
var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event
for {
events, err := coninput.ReadNConsoleInputs(con.conin, 16)
events, err := peekAndReadConsInput(con)
if err != nil {
if con.isCanceled() {
return cancelreader.ErrCanceled
}
return fmt.Errorf("read coninput events: %w", err)
return err
}
for _, event := range events {
var msgs []Msg
switch e := event.Unwrap().(type) {
@ -87,13 +84,57 @@ func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader)
if err != nil {
return fmt.Errorf("coninput context error: %w", err)
}
return err
return nil
}
}
}
}
}
// Peek for new input in a tight loop and then read the input.
// windows.CancelIo* does not work reliably so peek first and only use the data if
// the console input is not cancelled.
func peekAndReadConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
events, err := peekConsInput(con)
if err != nil {
return events, err
}
events, err = coninput.ReadNConsoleInputs(con.conin, intToUint32OrDie(len(events)))
if con.isCanceled() {
return events, cancelreader.ErrCanceled
}
if err != nil {
return events, fmt.Errorf("read coninput events: %w", err)
}
return events, nil
}
// Convert i to unit32 or panic if it cannot be converted. Check satisifes lint G115.
func intToUint32OrDie(i int) uint32 {
if i < 0 {
panic("cannot convert numEvents " + fmt.Sprint(i) + " to uint32")
}
return uint32(i)
}
// Keeps peeking until there is data or the input is cancelled.
func peekConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
for {
events, err := coninput.PeekNConsoleInputs(con.conin, 16)
if con.isCanceled() {
return events, cancelreader.ErrCanceled
}
if err != nil {
return events, fmt.Errorf("peek coninput events: %w", err)
}
if len(events) > 0 {
return events, nil
}
// Sleep for a bit to avoid busy waiting.
time.Sleep(16 * time.Millisecond)
}
}
func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) {
btn := p ^ s
action = MouseActionPress
@ -114,7 +155,7 @@ func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action Mou
case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
button = MouseButtonForward
}
return
return button, action
}
switch {
@ -147,7 +188,7 @@ func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg {
if ev.Action == MouseActionRelease {
ev.Type = MouseRelease
}
switch ev.Button {
switch ev.Button { //nolint:exhaustive
case MouseButtonLeft:
ev.Type = MouseLeft
case MouseButtonMiddle:
@ -190,7 +231,7 @@ func keyType(e coninput.KeyEventRecord) KeyType {
shiftPressed := e.ControlKeyState.Contains(coninput.SHIFT_PRESSED)
ctrlPressed := e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED)
switch code {
switch code { //nolint:exhaustive
case coninput.VK_RETURN:
return KeyEnter
case coninput.VK_BACK:
@ -276,6 +317,46 @@ func keyType(e coninput.KeyEventRecord) KeyType {
return KeyPgDown
case coninput.VK_DELETE:
return KeyDelete
case coninput.VK_F1:
return KeyF1
case coninput.VK_F2:
return KeyF2
case coninput.VK_F3:
return KeyF3
case coninput.VK_F4:
return KeyF4
case coninput.VK_F5:
return KeyF5
case coninput.VK_F6:
return KeyF6
case coninput.VK_F7:
return KeyF7
case coninput.VK_F8:
return KeyF8
case coninput.VK_F9:
return KeyF9
case coninput.VK_F10:
return KeyF10
case coninput.VK_F11:
return KeyF11
case coninput.VK_F12:
return KeyF12
case coninput.VK_F13:
return KeyF13
case coninput.VK_F14:
return KeyF14
case coninput.VK_F15:
return KeyF15
case coninput.VK_F16:
return KeyF16
case coninput.VK_F17:
return KeyF17
case coninput.VK_F18:
return KeyF18
case coninput.VK_F19:
return KeyF19
case coninput.VK_F20:
return KeyF20
default:
switch {
case e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED) && e.ControlKeyState.Contains(coninput.RIGHT_ALT_PRESSED):
@ -348,7 +429,7 @@ func keyType(e coninput.KeyEventRecord) KeyType {
return KeyCtrlUnderscore
}
switch code {
switch code { //nolint:exhaustive
case coninput.VK_OEM_4:
return KeyCtrlOpenBracket
case coninput.VK_OEM_6:

View File

@ -33,7 +33,7 @@ type LogOptionsSetter interface {
// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:mnd
if err != nil {
return nil, fmt.Errorf("error opening file for logging: %w", err)
}

View File

@ -172,7 +172,7 @@ const (
func parseSGRMouseEvent(buf []byte) MouseEvent {
str := string(buf[3:])
matches := mouseSGRRegex.FindStringSubmatch(str)
if len(matches) != 5 { //nolint:gomnd
if len(matches) != 5 { //nolint:mnd
// Unreachable, we already checked the regex in `detectOneMsg`.
panic("invalid mouse event")
}

View File

@ -26,3 +26,4 @@ func (n nilRenderer) setWindowTitle(_ string) {}
func (n nilRenderer) reportFocus() bool { return false }
func (n nilRenderer) enableReportFocus() {}
func (n nilRenderer) disableReportFocus() {}
func (n nilRenderer) resetLinesRendered() {}

View File

@ -19,7 +19,7 @@ type ProgramOption func(*Program)
// cancelled it will exit with an error ErrProgramKilled.
func WithContext(ctx context.Context) ProgramOption {
return func(p *Program) {
p.ctx = ctx
p.externalCtx = ctx
}
}

View File

@ -79,6 +79,9 @@ type renderer interface {
// disableReportFocus stops reporting focus events to the program.
disableReportFocus()
// resetLinesRendered ensures exec output remains on screen on exit
resetLinesRendered()
}
// repaintMsg forces a full repaint.

View File

@ -545,6 +545,10 @@ func (r *standardRenderer) clearIgnoredLines() {
r.ignoreLines = nil
}
func (r *standardRenderer) resetLinesRendered() {
r.linesRendered = 0
}
// insertTop effectively scrolls up. It inserts lines at the top of a given
// area designated to be a scrollable region, pushing everything else down.
// This is roughly how ncurses does it.

View File

@ -27,6 +27,9 @@ import (
"golang.org/x/sync/errgroup"
)
// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
var ErrProgramPanic = errors.New("program experienced a panic")
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
var ErrProgramKilled = errors.New("program was killed")
@ -147,6 +150,12 @@ type Program struct {
inputType inputType
// externalCtx is a context that was passed in via WithContext, otherwise defaulting
// to ctx.Background() (in case it was not), the internal context is derived from it.
externalCtx context.Context
// ctx is the programs's internal context for signalling internal teardown.
// It is built and derived from the externalCtx in NewProgram().
ctx context.Context
cancel context.CancelFunc
@ -243,11 +252,11 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
// A context can be provided with a ProgramOption, but if none was provided
// we'll use the default background context.
if p.ctx == nil {
p.ctx = context.Background()
if p.externalCtx == nil {
p.externalCtx = context.Background()
}
// Initialize context and teardown channel.
p.ctx, p.cancel = context.WithCancel(p.ctx)
p.ctx, p.cancel = context.WithCancel(p.externalCtx)
// if no output was set, set it to stdout
if p.output == nil {
@ -346,7 +355,11 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
go func() {
// Recover from panics.
if !p.startupOptions.has(withoutCatchPanics) {
defer p.recoverFromPanic()
defer func() {
if r := recover(); r != nil {
p.recoverFromGoPanic(r)
}
}()
}
msg := cmd() // this can be long.
@ -422,7 +435,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
// work.
if runtime.GOOS == "windows" && !p.mouseMode {
p.mouseMode = true
p.initCancelReader(true) //nolint:errcheck
p.initCancelReader(true) //nolint:errcheck,gosec
}
case disableMouseMsg:
@ -433,7 +446,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
// mouse events.
if runtime.GOOS == "windows" && p.mouseMode {
p.mouseMode = false
p.initCancelReader(true) //nolint:errcheck
p.initCancelReader(true) //nolint:errcheck,gosec
}
case showCursorMsg:
@ -460,7 +473,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
case BatchMsg:
for _, cmd := range msg {
cmds <- cmd
select {
case <-p.ctx.Done():
return model, nil
case cmds <- cmd:
}
}
continue
@ -483,7 +500,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
})
}
//nolint:errcheck
//nolint:errcheck,gosec
g.Wait() // wait for all commands from batch msg to finish
continue
}
@ -506,7 +523,13 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
var cmd Cmd
model, cmd = model.Update(msg) // run update
cmds <- cmd // process command (if any)
select {
case <-p.ctx.Done():
return model, nil
case cmds <- cmd: // process command (if any)
}
p.renderer.write(model.View()) // send view to renderer
}
}
@ -515,11 +538,15 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
// Run initializes the program and runs its event loops, blocking until it gets
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
// Returns the final model.
func (p *Program) Run() (Model, error) {
func (p *Program) Run() (returnModel Model, returnErr error) {
p.handlers = channelHandlers{}
cmds := make(chan Cmd)
p.errs = make(chan error)
p.finished = make(chan struct{}, 1)
p.errs = make(chan error, 1)
p.finished = make(chan struct{})
defer func() {
close(p.finished)
}()
defer p.cancel()
@ -568,7 +595,12 @@ func (p *Program) Run() (Model, error) {
// Recover from panics.
if !p.startupOptions.has(withoutCatchPanics) {
defer p.recoverFromPanic()
defer func() {
if r := recover(); r != nil {
returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
p.recoverFromPanic(r)
}
}()
}
// If no renderer is set use the standard one.
@ -645,11 +677,27 @@ func (p *Program) Run() (Model, error) {
// Run event loop, handle updates and draw.
model, err := p.eventLoop(model, cmds)
killed := p.ctx.Err() != nil || err != nil
if killed && err == nil {
err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err())
if err == nil && len(p.errs) > 0 {
err = <-p.errs // Drain a leftover error in case eventLoop crashed
}
if err == nil {
killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil
if killed {
if err == nil && p.externalCtx.Err() != nil {
// Return also as context error the cancellation of an external context.
// This is the context the user knows about and should be able to act on.
err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err())
} else if err == nil && p.ctx.Err() != nil {
// Return only that the program was killed (not the internal mechanism).
// The user does not know or need to care about the internal program context.
err = ErrProgramKilled
} else {
// Return that the program was killed and also the error that caused it.
err = fmt.Errorf("%w: %w", ErrProgramKilled, err)
}
} else {
// Graceful shutdown of the program (not killed):
// Ensure we rendered the final state of the model.
p.renderer.write(model.View())
}
@ -704,11 +752,11 @@ func (p *Program) Quit() {
p.Send(Quit())
}
// Kill stops the program immediately and restores the former terminal state.
// Kill signals the program to stop immediately and restore the former terminal state.
// The final render that you would normally see when quitting will be skipped.
// [program.Run] returns a [ErrProgramKilled] error.
func (p *Program) Kill() {
p.shutdown(true)
p.cancel()
}
// Wait waits/blocks until the underlying Program finished shutting down.
@ -717,7 +765,11 @@ func (p *Program) Wait() {
}
// shutdown performs operations to free up resources and restore the terminal
// to its original state.
// to its original state. It is called once at the end of the program's lifetime.
//
// This method should not be called to signal the program to be killed/shutdown.
// Doing so can lead to race conditions with the eventual call at the program's end.
// As alternatives, the [Quit] or [Kill] convenience methods should be used instead.
func (p *Program) shutdown(kill bool) {
p.cancel()
@ -744,19 +796,30 @@ func (p *Program) shutdown(kill bool) {
}
_ = p.restoreTerminalState()
if !kill {
p.finished <- struct{}{}
}
}
// recoverFromPanic recovers from a panic, prints the stack trace, and restores
// the terminal to a usable state.
func (p *Program) recoverFromPanic() {
if r := recover(); r != nil {
p.shutdown(true)
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
func (p *Program) recoverFromPanic(r interface{}) {
select {
case p.errs <- ErrProgramPanic:
default:
}
p.shutdown(true) // Ok to call here, p.Run() cannot do it anymore.
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
}
// recoverFromGoPanic recovers from a goroutine panic, prints a stack trace and
// signals for the program to be killed and terminal restored to a usable state.
func (p *Program) recoverFromGoPanic(r interface{}) {
select {
case p.errs <- ErrProgramPanic:
default:
}
p.cancel()
fmt.Printf("Caught goroutine panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
}
// ReleaseTerminal restores the original terminal state and cancels the input

View File

@ -52,7 +52,7 @@ func (p *Program) restoreTerminalState() error {
p.renderer.exitAltScreen()
// give the terminal a moment to catch up
time.Sleep(time.Millisecond * 10) //nolint:gomnd
time.Sleep(time.Millisecond * 10) //nolint:mnd
}
}
@ -109,7 +109,7 @@ func (p *Program) readLoop() {
func (p *Program) waitForReadLoop() {
select {
case <-p.readLoopDone:
case <-time.After(500 * time.Millisecond): //nolint:gomnd
case <-time.After(500 * time.Millisecond): //nolint:mnd
// The read loop hangs, which means the input
// cancelReader's cancel function has returned true even
// though it was not able to cancel the read.

View File

@ -19,7 +19,7 @@ func (p *Program) initInput() (err error) {
p.ttyInput = f
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
if err != nil {
return err
return fmt.Errorf("error making raw: %w", err)
}
// Enable VT input
@ -38,7 +38,7 @@ func (p *Program) initInput() (err error) {
p.ttyOutput = f
p.previousOutputState, err = term.GetState(f.Fd())
if err != nil {
return err
return fmt.Errorf("error getting state: %w", err)
}
var mode uint32
@ -51,14 +51,14 @@ func (p *Program) initInput() (err error) {
}
}
return
return nil
}
// Open the Windows equivalent of a TTY.
func openInputTTY() (*os.File, error) {
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644)
if err != nil {
return nil, err
return nil, fmt.Errorf("error opening file: %w", err)
}
return f, nil
}

View File

@ -1,40 +0,0 @@
run:
tests: false
issues-exit-code: 0
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- exhaustive
- goconst
- godot
- godox
- mnd
- gomoddirectives
- goprintffuncname
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- wrapcheck
# disable default linters, they are already enabled in .golangci.yml
disable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused

View File

@ -1,24 +1,23 @@
version: "2"
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- gofumpt
- goimports
- exhaustive
- goconst
- godot
- godox
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
@ -26,3 +25,17 @@ linters:
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax

4
vendor/github.com/charmbracelet/colorprofile/doc.go generated vendored Normal file
View File

@ -0,0 +1,4 @@
// Package colorprofile provides a way to downsample ANSI escape sequence
// colors and styles automatically based on output, environment variables, and
// Terminfo databases.
package colorprofile

View File

@ -12,6 +12,8 @@ import (
"github.com/xo/terminfo"
)
const dumbTerm = "dumb"
// Detect returns the color profile based on the terminal output, and
// environment variables. This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE
// environment variables.
@ -29,10 +31,10 @@ import (
// See https://no-color.org/ and https://bixense.com/clicolors/ for more information.
func Detect(output io.Writer, env []string) Profile {
out, ok := output.(term.File)
isatty := ok && term.IsTerminal(out.Fd())
environ := newEnviron(env)
term := environ.get("TERM")
isDumb := term == "dumb"
isatty := isTTYForced(environ) || (ok && term.IsTerminal(out.Fd()))
term, ok := environ.lookup("TERM")
isDumb := !ok || term == dumbTerm
envp := colorProfile(isatty, environ)
if envp == TrueColor || envNoColor(environ) {
// We already know we have TrueColor, or NO_COLOR is set.
@ -69,7 +71,8 @@ func Env(env []string) (p Profile) {
}
func colorProfile(isatty bool, env environ) (p Profile) {
isDumb := env.get("TERM") == "dumb"
term, ok := env.lookup("TERM")
isDumb := (!ok && runtime.GOOS != "windows") || term == dumbTerm
envp := envColorProfile(env)
if !isatty || isDumb {
// Check if the output is a terminal.
@ -83,7 +86,7 @@ func colorProfile(isatty bool, env environ) (p Profile) {
if p > Ascii {
p = Ascii
}
return
return //nolint:nakedret
}
if cliColorForced(env) {
@ -94,7 +97,7 @@ func colorProfile(isatty bool, env environ) (p Profile) {
p = envp
}
return
return //nolint:nakedret
}
if cliColor(env) {
@ -123,6 +126,11 @@ func cliColorForced(env environ) bool {
return cliColorForce
}
func isTTYForced(env environ) bool {
skip, _ := strconv.ParseBool(env.get("TTY_FORCE"))
return skip
}
func colorTerm(env environ) bool {
colorTerm := strings.ToLower(env.get("COLORTERM"))
return colorTerm == "truecolor" || colorTerm == "24bit" ||
@ -132,7 +140,7 @@ func colorTerm(env environ) bool {
// envColorProfile returns infers the color profile from the environment.
func envColorProfile(env environ) (p Profile) {
term, ok := env.lookup("TERM")
if !ok || len(term) == 0 || term == "dumb" {
if !ok || len(term) == 0 || term == dumbTerm {
p = NoTTY
if runtime.GOOS == "windows" {
// Use Windows API to detect color profile. Windows Terminal and
@ -184,7 +192,12 @@ func envColorProfile(env environ) (p Profile) {
p = ANSI256
}
return
// Direct color terminals support true colors.
if strings.HasSuffix(term, "direct") {
return TrueColor
}
return //nolint:nakedret
}
// Terminfo returns the color profile based on the terminal's terminfo
@ -278,10 +291,3 @@ func (e environ) get(key string) string {
v, _ := e.lookup(key)
return v
}
func max[T ~byte | ~int](a, b T) T {
if a > b {
return a
}
return b
}

View File

@ -12,15 +12,15 @@ import (
type Profile byte
const (
// NoTTY, not a terminal profile.
// NoTTY is a profile with no terminal support.
NoTTY Profile = iota
// Ascii, uncolored profile.
// Ascii is a profile with no color support.
Ascii //nolint:revive
// ANSI, 4-bit color profile.
// ANSI is a profile with 16 colors (4-bit).
ANSI
// ANSI256, 8-bit color profile.
// ANSI256 is a profile with 256 colors (8-bit).
ANSI256
// TrueColor, 24-bit color profile.
// TrueColor is a profile with 16 million colors (24-bit).
TrueColor
)

View File

@ -2,6 +2,7 @@ package colorprofile
import (
"bytes"
"fmt"
"image/color"
"io"
"strconv"
@ -37,11 +38,13 @@ type Writer struct {
func (w *Writer) Write(p []byte) (int, error) {
switch w.Profile {
case TrueColor:
return w.Forward.Write(p)
return w.Forward.Write(p) //nolint:wrapcheck
case NoTTY:
return io.WriteString(w.Forward, ansi.Strip(string(p)))
default:
return io.WriteString(w.Forward, ansi.Strip(string(p))) //nolint:wrapcheck
case Ascii, ANSI, ANSI256:
return w.downsample(p)
default:
return 0, fmt.Errorf("invalid profile: %v", w.Profile)
}
}
@ -63,7 +66,7 @@ func (w *Writer) downsample(p []byte) (int, error) {
default:
// If we're not a style SGR sequence, just write the bytes.
if n, err := buf.Write(seq); err != nil {
return n, err
return n, err //nolint:wrapcheck
}
}
@ -71,7 +74,7 @@ func (w *Writer) downsample(p []byte) (int, error) {
state = newState
}
return w.Forward.Write(buf.Bytes())
return w.Forward.Write(buf.Bytes()) //nolint:wrapcheck
}
// WriteString writes the given text to the underlying writer.

View File

@ -1,17 +1,6 @@
version: "2"
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
@ -19,8 +8,6 @@ linters:
- goconst
- godot
- godox
- gofumpt
- goimports
- gomoddirectives
- goprintffuncname
- gosec
@ -39,3 +26,16 @@ linters:
- unparam
- whitespace
- wrapcheck
exclusions:
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax

View File

@ -88,7 +88,13 @@ func (l *Logger) writeSlogValue(jw *jsonWriter, v slogValue) {
}
jw.end()
default:
jw.objectValue(v.Any())
a := v.Any()
_, jm := a.(json.Marshaler)
if err, ok := a.(error); ok && !jm {
jw.objectValue(err.Error())
} else {
jw.objectValue(a)
}
}
}

View File

@ -17,6 +17,8 @@ type (
slogLogValuer = slog.LogValuer
)
var slogAnyValue = slog.AnyValue
const slogKindGroup = slog.KindGroup
// Enabled reports whether the logger is enabled for the given level.

View File

@ -18,6 +18,8 @@ type (
slogLogValuer = slog.LogValuer
)
var slogAnyValue = slog.AnyValue
const slogKindGroup = slog.KindGroup
// Enabled reports whether the logger is enabled for the given level.

View File

@ -7,5 +7,5 @@ import "io"
//
// This is a syntactic sugar over [io.WriteString].
func Execute(w io.Writer, s string) (int, error) {
return io.WriteString(w, s)
return io.WriteString(w, s) //nolint:wrapcheck
}

View File

@ -3,63 +3,93 @@ package ansi
import (
"fmt"
"image/color"
"github.com/lucasb-eyer/go-colorful"
)
// Colorizer is a [color.Color] interface that can be formatted as a string.
type Colorizer interface {
color.Color
fmt.Stringer
// HexColor is a [color.Color] that can be formatted as a hex string.
type HexColor string
// RGBA returns the RGBA values of the color.
func (h HexColor) RGBA() (r, g, b, a uint32) {
hex := h.color()
if hex == nil {
return 0, 0, 0, 0
}
return hex.RGBA()
}
// HexColorizer is a [color.Color] that can be formatted as a hex string.
type HexColorizer struct{ color.Color }
var _ Colorizer = HexColorizer{}
// Hex returns the hex representation of the color. If the color is invalid, it
// returns an empty string.
func (h HexColor) Hex() string {
hex := h.color()
if hex == nil {
return ""
}
return hex.Hex()
}
// String returns the color as a hex string. If the color is nil, an empty
// string is returned.
func (h HexColorizer) String() string {
if h.Color == nil {
return ""
}
r, g, b, _ := h.RGBA()
// Get the lower 8 bits
r &= 0xff
g &= 0xff
b &= 0xff
return fmt.Sprintf("#%02x%02x%02x", uint8(r), uint8(g), uint8(b)) //nolint:gosec
func (h HexColor) String() string {
return h.Hex()
}
// XRGBColorizer is a [color.Color] that can be formatted as an XParseColor
// color returns the underlying color of the HexColor.
func (h HexColor) color() *colorful.Color {
hex, err := colorful.Hex(string(h))
if err != nil {
return nil
}
return &hex
}
// XRGBColor is a [color.Color] that can be formatted as an XParseColor
// rgb: string.
//
// See: https://linux.die.net/man/3/xparsecolor
type XRGBColorizer struct{ color.Color }
type XRGBColor struct {
color.Color
}
var _ Colorizer = XRGBColorizer{}
// RGBA returns the RGBA values of the color.
func (x XRGBColor) RGBA() (r, g, b, a uint32) {
if x.Color == nil {
return 0, 0, 0, 0
}
return x.Color.RGBA()
}
// String returns the color as an XParseColor rgb: string. If the color is nil,
// an empty string is returned.
func (x XRGBColorizer) String() string {
func (x XRGBColor) String() string {
if x.Color == nil {
return ""
}
r, g, b, _ := x.RGBA()
r, g, b, _ := x.Color.RGBA()
// Get the lower 8 bits
return fmt.Sprintf("rgb:%04x/%04x/%04x", r, g, b)
}
// XRGBAColorizer is a [color.Color] that can be formatted as an XParseColor
// XRGBAColor is a [color.Color] that can be formatted as an XParseColor
// rgba: string.
//
// See: https://linux.die.net/man/3/xparsecolor
type XRGBAColorizer struct{ color.Color }
type XRGBAColor struct {
color.Color
}
var _ Colorizer = XRGBAColorizer{}
// RGBA returns the RGBA values of the color.
func (x XRGBAColor) RGBA() (r, g, b, a uint32) {
if x.Color == nil {
return 0, 0, 0, 0
}
return x.Color.RGBA()
}
// String returns the color as an XParseColor rgba: string. If the color is nil,
// an empty string is returned.
func (x XRGBAColorizer) String() string {
func (x XRGBAColor) String() string {
if x.Color == nil {
return ""
}
@ -74,19 +104,12 @@ func (x XRGBAColorizer) String() string {
// OSC 10 ; color ST
// OSC 10 ; color BEL
//
// Where color is the encoded color number.
// Where color is the encoded color number. Most terminals support hex,
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
// or [XRGBAColor] to format the color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetForegroundColor(c color.Color) string {
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
func SetForegroundColor(s string) string {
return "\x1b]10;" + s + "\x07"
}
@ -108,19 +131,12 @@ const ResetForegroundColor = "\x1b]110\x07"
// OSC 11 ; color ST
// OSC 11 ; color BEL
//
// Where color is the encoded color number.
// Where color is the encoded color number. Most terminals support hex,
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
// or [XRGBAColor] to format the color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetBackgroundColor(c color.Color) string {
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
func SetBackgroundColor(s string) string {
return "\x1b]11;" + s + "\x07"
}
@ -141,19 +157,12 @@ const ResetBackgroundColor = "\x1b]111\x07"
// OSC 12 ; color ST
// OSC 12 ; color BEL
//
// Where color is the encoded color number.
// Where color is the encoded color number. Most terminals support hex,
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
// or [XRGBAColor] to format the color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetCursorColor(c color.Color) string {
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
func SetCursorColor(s string) string {
return "\x1b]12;" + s + "\x07"
}

View File

@ -39,17 +39,17 @@ func SCS(gset byte, charset byte) string {
return SelectCharacterSet(gset, charset)
}
// Locking Shift 1 Right (LS1R) shifts G1 into GR character set.
// LS1R (Locking Shift 1 Right) shifts G1 into GR character set.
const LS1R = "\x1b~"
// Locking Shift 2 (LS2) shifts G2 into GL character set.
// LS2 (Locking Shift 2) shifts G2 into GL character set.
const LS2 = "\x1bn"
// Locking Shift 2 Right (LS2R) shifts G2 into GR character set.
// LS2R (Locking Shift 2 Right) shifts G2 into GR character set.
const LS2R = "\x1b}"
// Locking Shift 3 (LS3) shifts G3 into GL character set.
// LS3 (Locking Shift 3) shifts G3 into GL character set.
const LS3 = "\x1bo"
// Locking Shift 3 Right (LS3R) shifts G3 into GR character set.
// LS3R (Locking Shift 3 Right) shifts G3 into GR character set.
const LS3R = "\x1b|"

View File

@ -2,34 +2,9 @@ package ansi
import (
"image/color"
)
// Technically speaking, the 16 basic ANSI colors are arbitrary and can be
// customized at the terminal level. Given that, we're returning what we feel
// are good defaults.
//
// This could also be a slice, but we use a map to make the mappings very
// explicit.
//
// See: https://www.ditig.com/publications/256-colors-cheat-sheet
var lowANSI = map[uint32]uint32{
0: 0x000000, // black
1: 0x800000, // red
2: 0x008000, // green
3: 0x808000, // yellow
4: 0x000080, // blue
5: 0x800080, // magenta
6: 0x008080, // cyan
7: 0xc0c0c0, // white
8: 0x808080, // bright black
9: 0xff0000, // bright red
10: 0x00ff00, // bright green
11: 0xffff00, // bright yellow
12: 0x0000ff, // bright blue
13: 0xff00ff, // bright magenta
14: 0x00ffff, // bright cyan
15: 0xffffff, // bright white
}
"github.com/lucasb-eyer/go-colorful"
)
// Color is a color that can be used in a terminal. ANSI (including
// ANSI256) and 24-bit "true colors" fall under this category.
@ -100,28 +75,33 @@ func (c BasicColor) RGBA() (uint32, uint32, uint32, uint32) {
return 0, 0, 0, 0xffff
}
r, g, b := ansiToRGB(ansi)
return toRGBA(r, g, b)
return ansiToRGB(byte(ansi)).RGBA()
}
// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255.
type ExtendedColor uint8
// IndexedColor is an ANSI 256 (8-bit) color with a value from 0 to 255.
type IndexedColor uint8
var _ Color = ExtendedColor(0)
var _ Color = IndexedColor(0)
// RGBA returns the red, green, blue and alpha components of the color. It
// satisfies the color.Color interface.
func (c ExtendedColor) RGBA() (uint32, uint32, uint32, uint32) {
r, g, b := ansiToRGB(uint32(c))
return toRGBA(r, g, b)
func (c IndexedColor) RGBA() (uint32, uint32, uint32, uint32) {
return ansiToRGB(byte(c)).RGBA()
}
// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255.
//
// Deprecated: use [IndexedColor] instead.
type ExtendedColor = IndexedColor
// TrueColor is a 24-bit color that can be used in the terminal.
// This can be used to represent RGB colors.
//
// For example, the color red can be represented as:
//
// TrueColor(0xff0000)
//
// Deprecated: use [RGBColor] instead.
type TrueColor uint32
var _ Color = TrueColor(0)
@ -133,44 +113,25 @@ func (c TrueColor) RGBA() (uint32, uint32, uint32, uint32) {
return toRGBA(r, g, b)
}
// RGBColor is a 24-bit color that can be used in the terminal.
// This can be used to represent RGB colors.
type RGBColor struct {
R uint8
G uint8
B uint8
}
// RGBA returns the red, green, blue and alpha components of the color. It
// satisfies the color.Color interface.
func (c RGBColor) RGBA() (uint32, uint32, uint32, uint32) {
return toRGBA(uint32(c.R), uint32(c.G), uint32(c.B))
}
// ansiToRGB converts an ANSI color to a 24-bit RGB color.
//
// r, g, b := ansiToRGB(57)
func ansiToRGB(ansi uint32) (uint32, uint32, uint32) {
// For out-of-range values return black.
if ansi > 255 {
return 0, 0, 0
}
// Low ANSI.
if ansi < 16 {
h, ok := lowANSI[ansi]
if !ok {
return 0, 0, 0
}
r, g, b := hexToRGB(h)
return r, g, b
}
// Grays.
if ansi > 231 {
s := (ansi-232)*10 + 8
return s, s, s
}
// ANSI256.
n := ansi - 16
b := n % 6
g := (n - b) / 6 % 6
r := (n - b - g*6) / 36 % 6
for _, v := range []*uint32{&r, &g, &b} {
if *v > 0 {
c := *v*40 + 55
*v = c
}
}
return r, g, b
func ansiToRGB(ansi byte) color.Color {
return ansiHex[ansi]
}
// hexToRGB converts a number in hexadecimal format to red, green, and blue
@ -194,3 +155,630 @@ func toRGBA(r, g, b uint32) (uint32, uint32, uint32, uint32) {
b |= b << 8
return r, g, b, 0xffff
}
//nolint:unused
func distSq(r1, g1, b1, r2, g2, b2 int) int {
return ((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2))
}
func to6Cube[T int | float64](v T) int {
if v < 48 {
return 0
}
if v < 115 {
return 1
}
return int((v - 35) / 40)
}
// Convert256 converts a [color.Color], usually a 24-bit color, to xterm(1) 256
// color palette.
//
// xterm provides a 6x6x6 color cube (16 - 231) and 24 greys (232 - 255). We
// map our RGB color to the closest in the cube, also work out the closest
// grey, and use the nearest of the two based on the lightness of the color.
//
// Note that the xterm has much lower resolution for darker colors (they are
// not evenly spread out), so our 6 levels are not evenly spread: 0x0, 0x5f
// (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are more
// evenly spread (8, 18, 28 ... 238).
func Convert256(c color.Color) IndexedColor {
// If the color is already an IndexedColor, return it.
if i, ok := c.(IndexedColor); ok {
return i
}
// Note: this is mostly ported from tmux/colour.c.
col, ok := colorful.MakeColor(c)
if !ok {
return IndexedColor(0)
}
r := col.R * 255
g := col.G * 255
b := col.B * 255
q2c := [6]int{0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
// Map RGB to 6x6x6 cube.
qr := to6Cube(r)
cr := q2c[qr]
qg := to6Cube(g)
cg := q2c[qg]
qb := to6Cube(b)
cb := q2c[qb]
// If we have hit the color exactly, return early.
ci := (36 * qr) + (6 * qg) + qb
if cr == int(r) && cg == int(g) && cb == int(b) {
return IndexedColor(16 + ci) //nolint:gosec
}
// Work out the closest grey (average of RGB).
greyAvg := int(r+g+b) / 3
var greyIdx int
if greyAvg > 238 {
greyIdx = 23
} else {
greyIdx = (greyAvg - 3) / 10
}
grey := 8 + (10 * greyIdx)
// Return the one which is nearer to the original input rgb value
// XXX: This is where it differs from tmux's implementation, we prefer the
// closer color to the original in terms of light distances rather than the
// cube distance.
c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
g2 := colorful.Color{R: float64(grey) / 255.0, G: float64(grey) / 255.0, B: float64(grey) / 255.0}
colorDist := col.DistanceHSLuv(c2)
grayDist := col.DistanceHSLuv(g2)
if colorDist <= grayDist {
return IndexedColor(16 + ci) //nolint:gosec
}
return IndexedColor(232 + greyIdx) //nolint:gosec
// // Is grey or 6x6x6 color closest?
// d := distSq(cr, cg, cb, int(r), int(g), int(b))
// if distSq(grey, grey, grey, int(r), int(g), int(b)) < d {
// return IndexedColor(232 + greyIdx) //nolint:gosec
// }
// return IndexedColor(16 + ci) //nolint:gosec
}
// Convert16 converts a [color.Color] to a 16-color ANSI color. It will first
// try to find a match in the 256 xterm(1) color palette, and then map that to
// the 16-color ANSI palette.
func Convert16(c color.Color) BasicColor {
switch c := c.(type) {
case BasicColor:
// If the color is already a BasicColor, return it.
return c
case IndexedColor:
// If the color is already an IndexedColor, return the corresponding
// BasicColor.
return ansi256To16[c]
default:
c256 := Convert256(c)
return ansi256To16[c256]
}
}
// RGB values of ANSI colors (0-255).
var ansiHex = [...]color.RGBA{
0: {R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // "#000000"
1: {R: 0x80, G: 0x00, B: 0x00, A: 0xff}, // "#800000"
2: {R: 0x00, G: 0x80, B: 0x00, A: 0xff}, // "#008000"
3: {R: 0x80, G: 0x80, B: 0x00, A: 0xff}, // "#808000"
4: {R: 0x00, G: 0x00, B: 0x80, A: 0xff}, // "#000080"
5: {R: 0x80, G: 0x00, B: 0x80, A: 0xff}, // "#800080"
6: {R: 0x00, G: 0x80, B: 0x80, A: 0xff}, // "#008080"
7: {R: 0xc0, G: 0xc0, B: 0xc0, A: 0xff}, // "#c0c0c0"
8: {R: 0x80, G: 0x80, B: 0x80, A: 0xff}, // "#808080"
9: {R: 0xff, G: 0x00, B: 0x00, A: 0xff}, // "#ff0000"
10: {R: 0x00, G: 0xff, B: 0x00, A: 0xff}, // "#00ff00"
11: {R: 0xff, G: 0xff, B: 0x00, A: 0xff}, // "#ffff00"
12: {R: 0x00, G: 0x00, B: 0xff, A: 0xff}, // "#0000ff"
13: {R: 0xff, G: 0x00, B: 0xff, A: 0xff}, // "#ff00ff"
14: {R: 0x00, G: 0xff, B: 0xff, A: 0xff}, // "#00ffff"
15: {R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // "#ffffff"
16: {R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // "#000000"
17: {R: 0x00, G: 0x00, B: 0x5f, A: 0xff}, // "#00005f"
18: {R: 0x00, G: 0x00, B: 0x87, A: 0xff}, // "#000087"
19: {R: 0x00, G: 0x00, B: 0xaf, A: 0xff}, // "#0000af"
20: {R: 0x00, G: 0x00, B: 0xd7, A: 0xff}, // "#0000d7"
21: {R: 0x00, G: 0x00, B: 0xff, A: 0xff}, // "#0000ff"
22: {R: 0x00, G: 0x5f, B: 0x00, A: 0xff}, // "#005f00"
23: {R: 0x00, G: 0x5f, B: 0x5f, A: 0xff}, // "#005f5f"
24: {R: 0x00, G: 0x5f, B: 0x87, A: 0xff}, // "#005f87"
25: {R: 0x00, G: 0x5f, B: 0xaf, A: 0xff}, // "#005faf"
26: {R: 0x00, G: 0x5f, B: 0xd7, A: 0xff}, // "#005fd7"
27: {R: 0x00, G: 0x5f, B: 0xff, A: 0xff}, // "#005fff"
28: {R: 0x00, G: 0x87, B: 0x00, A: 0xff}, // "#008700"
29: {R: 0x00, G: 0x87, B: 0x5f, A: 0xff}, // "#00875f"
30: {R: 0x00, G: 0x87, B: 0x87, A: 0xff}, // "#008787"
31: {R: 0x00, G: 0x87, B: 0xaf, A: 0xff}, // "#0087af"
32: {R: 0x00, G: 0x87, B: 0xd7, A: 0xff}, // "#0087d7"
33: {R: 0x00, G: 0x87, B: 0xff, A: 0xff}, // "#0087ff"
34: {R: 0x00, G: 0xaf, B: 0x00, A: 0xff}, // "#00af00"
35: {R: 0x00, G: 0xaf, B: 0x5f, A: 0xff}, // "#00af5f"
36: {R: 0x00, G: 0xaf, B: 0x87, A: 0xff}, // "#00af87"
37: {R: 0x00, G: 0xaf, B: 0xaf, A: 0xff}, // "#00afaf"
38: {R: 0x00, G: 0xaf, B: 0xd7, A: 0xff}, // "#00afd7"
39: {R: 0x00, G: 0xaf, B: 0xff, A: 0xff}, // "#00afff"
40: {R: 0x00, G: 0xd7, B: 0x00, A: 0xff}, // "#00d700"
41: {R: 0x00, G: 0xd7, B: 0x5f, A: 0xff}, // "#00d75f"
42: {R: 0x00, G: 0xd7, B: 0x87, A: 0xff}, // "#00d787"
43: {R: 0x00, G: 0xd7, B: 0xaf, A: 0xff}, // "#00d7af"
44: {R: 0x00, G: 0xd7, B: 0xd7, A: 0xff}, // "#00d7d7"
45: {R: 0x00, G: 0xd7, B: 0xff, A: 0xff}, // "#00d7ff"
46: {R: 0x00, G: 0xff, B: 0x00, A: 0xff}, // "#00ff00"
47: {R: 0x00, G: 0xff, B: 0x5f, A: 0xff}, // "#00ff5f"
48: {R: 0x00, G: 0xff, B: 0x87, A: 0xff}, // "#00ff87"
49: {R: 0x00, G: 0xff, B: 0xaf, A: 0xff}, // "#00ffaf"
50: {R: 0x00, G: 0xff, B: 0xd7, A: 0xff}, // "#00ffd7"
51: {R: 0x00, G: 0xff, B: 0xff, A: 0xff}, // "#00ffff"
52: {R: 0x5f, G: 0x00, B: 0x00, A: 0xff}, // "#5f0000"
53: {R: 0x5f, G: 0x00, B: 0x5f, A: 0xff}, // "#5f005f"
54: {R: 0x5f, G: 0x00, B: 0x87, A: 0xff}, // "#5f0087"
55: {R: 0x5f, G: 0x00, B: 0xaf, A: 0xff}, // "#5f00af"
56: {R: 0x5f, G: 0x00, B: 0xd7, A: 0xff}, // "#5f00d7"
57: {R: 0x5f, G: 0x00, B: 0xff, A: 0xff}, // "#5f00ff"
58: {R: 0x5f, G: 0x5f, B: 0x00, A: 0xff}, // "#5f5f00"
59: {R: 0x5f, G: 0x5f, B: 0x5f, A: 0xff}, // "#5f5f5f"
60: {R: 0x5f, G: 0x5f, B: 0x87, A: 0xff}, // "#5f5f87"
61: {R: 0x5f, G: 0x5f, B: 0xaf, A: 0xff}, // "#5f5faf"
62: {R: 0x5f, G: 0x5f, B: 0xd7, A: 0xff}, // "#5f5fd7"
63: {R: 0x5f, G: 0x5f, B: 0xff, A: 0xff}, // "#5f5fff"
64: {R: 0x5f, G: 0x87, B: 0x00, A: 0xff}, // "#5f8700"
65: {R: 0x5f, G: 0x87, B: 0x5f, A: 0xff}, // "#5f875f"
66: {R: 0x5f, G: 0x87, B: 0x87, A: 0xff}, // "#5f8787"
67: {R: 0x5f, G: 0x87, B: 0xaf, A: 0xff}, // "#5f87af"
68: {R: 0x5f, G: 0x87, B: 0xd7, A: 0xff}, // "#5f87d7"
69: {R: 0x5f, G: 0x87, B: 0xff, A: 0xff}, // "#5f87ff"
70: {R: 0x5f, G: 0xaf, B: 0x00, A: 0xff}, // "#5faf00"
71: {R: 0x5f, G: 0xaf, B: 0x5f, A: 0xff}, // "#5faf5f"
72: {R: 0x5f, G: 0xaf, B: 0x87, A: 0xff}, // "#5faf87"
73: {R: 0x5f, G: 0xaf, B: 0xaf, A: 0xff}, // "#5fafaf"
74: {R: 0x5f, G: 0xaf, B: 0xd7, A: 0xff}, // "#5fafd7"
75: {R: 0x5f, G: 0xaf, B: 0xff, A: 0xff}, // "#5fafff"
76: {R: 0x5f, G: 0xd7, B: 0x00, A: 0xff}, // "#5fd700"
77: {R: 0x5f, G: 0xd7, B: 0x5f, A: 0xff}, // "#5fd75f"
78: {R: 0x5f, G: 0xd7, B: 0x87, A: 0xff}, // "#5fd787"
79: {R: 0x5f, G: 0xd7, B: 0xaf, A: 0xff}, // "#5fd7af"
80: {R: 0x5f, G: 0xd7, B: 0xd7, A: 0xff}, // "#5fd7d7"
81: {R: 0x5f, G: 0xd7, B: 0xff, A: 0xff}, // "#5fd7ff"
82: {R: 0x5f, G: 0xff, B: 0x00, A: 0xff}, // "#5fff00"
83: {R: 0x5f, G: 0xff, B: 0x5f, A: 0xff}, // "#5fff5f"
84: {R: 0x5f, G: 0xff, B: 0x87, A: 0xff}, // "#5fff87"
85: {R: 0x5f, G: 0xff, B: 0xaf, A: 0xff}, // "#5fffaf"
86: {R: 0x5f, G: 0xff, B: 0xd7, A: 0xff}, // "#5fffd7"
87: {R: 0x5f, G: 0xff, B: 0xff, A: 0xff}, // "#5fffff"
88: {R: 0x87, G: 0x00, B: 0x00, A: 0xff}, // "#870000"
89: {R: 0x87, G: 0x00, B: 0x5f, A: 0xff}, // "#87005f"
90: {R: 0x87, G: 0x00, B: 0x87, A: 0xff}, // "#870087"
91: {R: 0x87, G: 0x00, B: 0xaf, A: 0xff}, // "#8700af"
92: {R: 0x87, G: 0x00, B: 0xd7, A: 0xff}, // "#8700d7"
93: {R: 0x87, G: 0x00, B: 0xff, A: 0xff}, // "#8700ff"
94: {R: 0x87, G: 0x5f, B: 0x00, A: 0xff}, // "#875f00"
95: {R: 0x87, G: 0x5f, B: 0x5f, A: 0xff}, // "#875f5f"
96: {R: 0x87, G: 0x5f, B: 0x87, A: 0xff}, // "#875f87"
97: {R: 0x87, G: 0x5f, B: 0xaf, A: 0xff}, // "#875faf"
98: {R: 0x87, G: 0x5f, B: 0xd7, A: 0xff}, // "#875fd7"
99: {R: 0x87, G: 0x5f, B: 0xff, A: 0xff}, // "#875fff"
100: {R: 0x87, G: 0x87, B: 0x00, A: 0xff}, // "#878700"
101: {R: 0x87, G: 0x87, B: 0x5f, A: 0xff}, // "#87875f"
102: {R: 0x87, G: 0x87, B: 0x87, A: 0xff}, // "#878787"
103: {R: 0x87, G: 0x87, B: 0xaf, A: 0xff}, // "#8787af"
104: {R: 0x87, G: 0x87, B: 0xd7, A: 0xff}, // "#8787d7"
105: {R: 0x87, G: 0x87, B: 0xff, A: 0xff}, // "#8787ff"
106: {R: 0x87, G: 0xaf, B: 0x00, A: 0xff}, // "#87af00"
107: {R: 0x87, G: 0xaf, B: 0x5f, A: 0xff}, // "#87af5f"
108: {R: 0x87, G: 0xaf, B: 0x87, A: 0xff}, // "#87af87"
109: {R: 0x87, G: 0xaf, B: 0xaf, A: 0xff}, // "#87afaf"
110: {R: 0x87, G: 0xaf, B: 0xd7, A: 0xff}, // "#87afd7"
111: {R: 0x87, G: 0xaf, B: 0xff, A: 0xff}, // "#87afff"
112: {R: 0x87, G: 0xd7, B: 0x00, A: 0xff}, // "#87d700"
113: {R: 0x87, G: 0xd7, B: 0x5f, A: 0xff}, // "#87d75f"
114: {R: 0x87, G: 0xd7, B: 0x87, A: 0xff}, // "#87d787"
115: {R: 0x87, G: 0xd7, B: 0xaf, A: 0xff}, // "#87d7af"
116: {R: 0x87, G: 0xd7, B: 0xd7, A: 0xff}, // "#87d7d7"
117: {R: 0x87, G: 0xd7, B: 0xff, A: 0xff}, // "#87d7ff"
118: {R: 0x87, G: 0xff, B: 0x00, A: 0xff}, // "#87ff00"
119: {R: 0x87, G: 0xff, B: 0x5f, A: 0xff}, // "#87ff5f"
120: {R: 0x87, G: 0xff, B: 0x87, A: 0xff}, // "#87ff87"
121: {R: 0x87, G: 0xff, B: 0xaf, A: 0xff}, // "#87ffaf"
122: {R: 0x87, G: 0xff, B: 0xd7, A: 0xff}, // "#87ffd7"
123: {R: 0x87, G: 0xff, B: 0xff, A: 0xff}, // "#87ffff"
124: {R: 0xaf, G: 0x00, B: 0x00, A: 0xff}, // "#af0000"
125: {R: 0xaf, G: 0x00, B: 0x5f, A: 0xff}, // "#af005f"
126: {R: 0xaf, G: 0x00, B: 0x87, A: 0xff}, // "#af0087"
127: {R: 0xaf, G: 0x00, B: 0xaf, A: 0xff}, // "#af00af"
128: {R: 0xaf, G: 0x00, B: 0xd7, A: 0xff}, // "#af00d7"
129: {R: 0xaf, G: 0x00, B: 0xff, A: 0xff}, // "#af00ff"
130: {R: 0xaf, G: 0x5f, B: 0x00, A: 0xff}, // "#af5f00"
131: {R: 0xaf, G: 0x5f, B: 0x5f, A: 0xff}, // "#af5f5f"
132: {R: 0xaf, G: 0x5f, B: 0x87, A: 0xff}, // "#af5f87"
133: {R: 0xaf, G: 0x5f, B: 0xaf, A: 0xff}, // "#af5faf"
134: {R: 0xaf, G: 0x5f, B: 0xd7, A: 0xff}, // "#af5fd7"
135: {R: 0xaf, G: 0x5f, B: 0xff, A: 0xff}, // "#af5fff"
136: {R: 0xaf, G: 0x87, B: 0x00, A: 0xff}, // "#af8700"
137: {R: 0xaf, G: 0x87, B: 0x5f, A: 0xff}, // "#af875f"
138: {R: 0xaf, G: 0x87, B: 0x87, A: 0xff}, // "#af8787"
139: {R: 0xaf, G: 0x87, B: 0xaf, A: 0xff}, // "#af87af"
140: {R: 0xaf, G: 0x87, B: 0xd7, A: 0xff}, // "#af87d7"
141: {R: 0xaf, G: 0x87, B: 0xff, A: 0xff}, // "#af87ff"
142: {R: 0xaf, G: 0xaf, B: 0x00, A: 0xff}, // "#afaf00"
143: {R: 0xaf, G: 0xaf, B: 0x5f, A: 0xff}, // "#afaf5f"
144: {R: 0xaf, G: 0xaf, B: 0x87, A: 0xff}, // "#afaf87"
145: {R: 0xaf, G: 0xaf, B: 0xaf, A: 0xff}, // "#afafaf"
146: {R: 0xaf, G: 0xaf, B: 0xd7, A: 0xff}, // "#afafd7"
147: {R: 0xaf, G: 0xaf, B: 0xff, A: 0xff}, // "#afafff"
148: {R: 0xaf, G: 0xd7, B: 0x00, A: 0xff}, // "#afd700"
149: {R: 0xaf, G: 0xd7, B: 0x5f, A: 0xff}, // "#afd75f"
150: {R: 0xaf, G: 0xd7, B: 0x87, A: 0xff}, // "#afd787"
151: {R: 0xaf, G: 0xd7, B: 0xaf, A: 0xff}, // "#afd7af"
152: {R: 0xaf, G: 0xd7, B: 0xd7, A: 0xff}, // "#afd7d7"
153: {R: 0xaf, G: 0xd7, B: 0xff, A: 0xff}, // "#afd7ff"
154: {R: 0xaf, G: 0xff, B: 0x00, A: 0xff}, // "#afff00"
155: {R: 0xaf, G: 0xff, B: 0x5f, A: 0xff}, // "#afff5f"
156: {R: 0xaf, G: 0xff, B: 0x87, A: 0xff}, // "#afff87"
157: {R: 0xaf, G: 0xff, B: 0xaf, A: 0xff}, // "#afffaf"
158: {R: 0xaf, G: 0xff, B: 0xd7, A: 0xff}, // "#afffd7"
159: {R: 0xaf, G: 0xff, B: 0xff, A: 0xff}, // "#afffff"
160: {R: 0xd7, G: 0x00, B: 0x00, A: 0xff}, // "#d70000"
161: {R: 0xd7, G: 0x00, B: 0x5f, A: 0xff}, // "#d7005f"
162: {R: 0xd7, G: 0x00, B: 0x87, A: 0xff}, // "#d70087"
163: {R: 0xd7, G: 0x00, B: 0xaf, A: 0xff}, // "#d700af"
164: {R: 0xd7, G: 0x00, B: 0xd7, A: 0xff}, // "#d700d7"
165: {R: 0xd7, G: 0x00, B: 0xff, A: 0xff}, // "#d700ff"
166: {R: 0xd7, G: 0x5f, B: 0x00, A: 0xff}, // "#d75f00"
167: {R: 0xd7, G: 0x5f, B: 0x5f, A: 0xff}, // "#d75f5f"
168: {R: 0xd7, G: 0x5f, B: 0x87, A: 0xff}, // "#d75f87"
169: {R: 0xd7, G: 0x5f, B: 0xaf, A: 0xff}, // "#d75faf"
170: {R: 0xd7, G: 0x5f, B: 0xd7, A: 0xff}, // "#d75fd7"
171: {R: 0xd7, G: 0x5f, B: 0xff, A: 0xff}, // "#d75fff"
172: {R: 0xd7, G: 0x87, B: 0x00, A: 0xff}, // "#d78700"
173: {R: 0xd7, G: 0x87, B: 0x5f, A: 0xff}, // "#d7875f"
174: {R: 0xd7, G: 0x87, B: 0x87, A: 0xff}, // "#d78787"
175: {R: 0xd7, G: 0x87, B: 0xaf, A: 0xff}, // "#d787af"
176: {R: 0xd7, G: 0x87, B: 0xd7, A: 0xff}, // "#d787d7"
177: {R: 0xd7, G: 0x87, B: 0xff, A: 0xff}, // "#d787ff"
178: {R: 0xd7, G: 0xaf, B: 0x00, A: 0xff}, // "#d7af00"
179: {R: 0xd7, G: 0xaf, B: 0x5f, A: 0xff}, // "#d7af5f"
180: {R: 0xd7, G: 0xaf, B: 0x87, A: 0xff}, // "#d7af87"
181: {R: 0xd7, G: 0xaf, B: 0xaf, A: 0xff}, // "#d7afaf"
182: {R: 0xd7, G: 0xaf, B: 0xd7, A: 0xff}, // "#d7afd7"
183: {R: 0xd7, G: 0xaf, B: 0xff, A: 0xff}, // "#d7afff"
184: {R: 0xd7, G: 0xd7, B: 0x00, A: 0xff}, // "#d7d700"
185: {R: 0xd7, G: 0xd7, B: 0x5f, A: 0xff}, // "#d7d75f"
186: {R: 0xd7, G: 0xd7, B: 0x87, A: 0xff}, // "#d7d787"
187: {R: 0xd7, G: 0xd7, B: 0xaf, A: 0xff}, // "#d7d7af"
188: {R: 0xd7, G: 0xd7, B: 0xd7, A: 0xff}, // "#d7d7d7"
189: {R: 0xd7, G: 0xd7, B: 0xff, A: 0xff}, // "#d7d7ff"
190: {R: 0xd7, G: 0xff, B: 0x00, A: 0xff}, // "#d7ff00"
191: {R: 0xd7, G: 0xff, B: 0x5f, A: 0xff}, // "#d7ff5f"
192: {R: 0xd7, G: 0xff, B: 0x87, A: 0xff}, // "#d7ff87"
193: {R: 0xd7, G: 0xff, B: 0xaf, A: 0xff}, // "#d7ffaf"
194: {R: 0xd7, G: 0xff, B: 0xd7, A: 0xff}, // "#d7ffd7"
195: {R: 0xd7, G: 0xff, B: 0xff, A: 0xff}, // "#d7ffff"
196: {R: 0xff, G: 0x00, B: 0x00, A: 0xff}, // "#ff0000"
197: {R: 0xff, G: 0x00, B: 0x5f, A: 0xff}, // "#ff005f"
198: {R: 0xff, G: 0x00, B: 0x87, A: 0xff}, // "#ff0087"
199: {R: 0xff, G: 0x00, B: 0xaf, A: 0xff}, // "#ff00af"
200: {R: 0xff, G: 0x00, B: 0xd7, A: 0xff}, // "#ff00d7"
201: {R: 0xff, G: 0x00, B: 0xff, A: 0xff}, // "#ff00ff"
202: {R: 0xff, G: 0x5f, B: 0x00, A: 0xff}, // "#ff5f00"
203: {R: 0xff, G: 0x5f, B: 0x5f, A: 0xff}, // "#ff5f5f"
204: {R: 0xff, G: 0x5f, B: 0x87, A: 0xff}, // "#ff5f87"
205: {R: 0xff, G: 0x5f, B: 0xaf, A: 0xff}, // "#ff5faf"
206: {R: 0xff, G: 0x5f, B: 0xd7, A: 0xff}, // "#ff5fd7"
207: {R: 0xff, G: 0x5f, B: 0xff, A: 0xff}, // "#ff5fff"
208: {R: 0xff, G: 0x87, B: 0x00, A: 0xff}, // "#ff8700"
209: {R: 0xff, G: 0x87, B: 0x5f, A: 0xff}, // "#ff875f"
210: {R: 0xff, G: 0x87, B: 0x87, A: 0xff}, // "#ff8787"
211: {R: 0xff, G: 0x87, B: 0xaf, A: 0xff}, // "#ff87af"
212: {R: 0xff, G: 0x87, B: 0xd7, A: 0xff}, // "#ff87d7"
213: {R: 0xff, G: 0x87, B: 0xff, A: 0xff}, // "#ff87ff"
214: {R: 0xff, G: 0xaf, B: 0x00, A: 0xff}, // "#ffaf00"
215: {R: 0xff, G: 0xaf, B: 0x5f, A: 0xff}, // "#ffaf5f"
216: {R: 0xff, G: 0xaf, B: 0x87, A: 0xff}, // "#ffaf87"
217: {R: 0xff, G: 0xaf, B: 0xaf, A: 0xff}, // "#ffafaf"
218: {R: 0xff, G: 0xaf, B: 0xd7, A: 0xff}, // "#ffafd7"
219: {R: 0xff, G: 0xaf, B: 0xff, A: 0xff}, // "#ffafff"
220: {R: 0xff, G: 0xd7, B: 0x00, A: 0xff}, // "#ffd700"
221: {R: 0xff, G: 0xd7, B: 0x5f, A: 0xff}, // "#ffd75f"
222: {R: 0xff, G: 0xd7, B: 0x87, A: 0xff}, // "#ffd787"
223: {R: 0xff, G: 0xd7, B: 0xaf, A: 0xff}, // "#ffd7af"
224: {R: 0xff, G: 0xd7, B: 0xd7, A: 0xff}, // "#ffd7d7"
225: {R: 0xff, G: 0xd7, B: 0xff, A: 0xff}, // "#ffd7ff"
226: {R: 0xff, G: 0xff, B: 0x00, A: 0xff}, // "#ffff00"
227: {R: 0xff, G: 0xff, B: 0x5f, A: 0xff}, // "#ffff5f"
228: {R: 0xff, G: 0xff, B: 0x87, A: 0xff}, // "#ffff87"
229: {R: 0xff, G: 0xff, B: 0xaf, A: 0xff}, // "#ffffaf"
230: {R: 0xff, G: 0xff, B: 0xd7, A: 0xff}, // "#ffffd7"
231: {R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // "#ffffff"
232: {R: 0x08, G: 0x08, B: 0x08, A: 0xff}, // "#080808"
233: {R: 0x12, G: 0x12, B: 0x12, A: 0xff}, // "#121212"
234: {R: 0x1c, G: 0x1c, B: 0x1c, A: 0xff}, // "#1c1c1c"
235: {R: 0x26, G: 0x26, B: 0x26, A: 0xff}, // "#262626"
236: {R: 0x30, G: 0x30, B: 0x30, A: 0xff}, // "#303030"
237: {R: 0x3a, G: 0x3a, B: 0x3a, A: 0xff}, // "#3a3a3a"
238: {R: 0x44, G: 0x44, B: 0x44, A: 0xff}, // "#444444"
239: {R: 0x4e, G: 0x4e, B: 0x4e, A: 0xff}, // "#4e4e4e"
240: {R: 0x58, G: 0x58, B: 0x58, A: 0xff}, // "#585858"
241: {R: 0x62, G: 0x62, B: 0x62, A: 0xff}, // "#626262"
242: {R: 0x6c, G: 0x6c, B: 0x6c, A: 0xff}, // "#6c6c6c"
243: {R: 0x76, G: 0x76, B: 0x76, A: 0xff}, // "#767676"
244: {R: 0x80, G: 0x80, B: 0x80, A: 0xff}, // "#808080"
245: {R: 0x8a, G: 0x8a, B: 0x8a, A: 0xff}, // "#8a8a8a"
246: {R: 0x94, G: 0x94, B: 0x94, A: 0xff}, // "#949494"
247: {R: 0x9e, G: 0x9e, B: 0x9e, A: 0xff}, // "#9e9e9e"
248: {R: 0xa8, G: 0xa8, B: 0xa8, A: 0xff}, // "#a8a8a8"
249: {R: 0xb2, G: 0xb2, B: 0xb2, A: 0xff}, // "#b2b2b2"
250: {R: 0xbc, G: 0xbc, B: 0xbc, A: 0xff}, // "#bcbcbc"
251: {R: 0xc6, G: 0xc6, B: 0xc6, A: 0xff}, // "#c6c6c6"
252: {R: 0xd0, G: 0xd0, B: 0xd0, A: 0xff}, // "#d0d0d0"
253: {R: 0xda, G: 0xda, B: 0xda, A: 0xff}, // "#dadada"
254: {R: 0xe4, G: 0xe4, B: 0xe4, A: 0xff}, // "#e4e4e4"
255: {R: 0xee, G: 0xee, B: 0xee, A: 0xff}, // "#eeeeee"
}
var ansi256To16 = [...]BasicColor{
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
10: 10,
11: 11,
12: 12,
13: 13,
14: 14,
15: 15,
16: 0,
17: 4,
18: 4,
19: 4,
20: 12,
21: 12,
22: 2,
23: 6,
24: 4,
25: 4,
26: 12,
27: 12,
28: 2,
29: 2,
30: 6,
31: 4,
32: 12,
33: 12,
34: 2,
35: 2,
36: 2,
37: 6,
38: 12,
39: 12,
40: 10,
41: 10,
42: 10,
43: 10,
44: 14,
45: 12,
46: 10,
47: 10,
48: 10,
49: 10,
50: 10,
51: 14,
52: 1,
53: 5,
54: 4,
55: 4,
56: 12,
57: 12,
58: 3,
59: 8,
60: 4,
61: 4,
62: 12,
63: 12,
64: 2,
65: 2,
66: 6,
67: 4,
68: 12,
69: 12,
70: 2,
71: 2,
72: 2,
73: 6,
74: 12,
75: 12,
76: 10,
77: 10,
78: 10,
79: 10,
80: 14,
81: 12,
82: 10,
83: 10,
84: 10,
85: 10,
86: 10,
87: 14,
88: 1,
89: 1,
90: 5,
91: 4,
92: 12,
93: 12,
94: 1,
95: 1,
96: 5,
97: 4,
98: 12,
99: 12,
100: 3,
101: 3,
102: 8,
103: 4,
104: 12,
105: 12,
106: 2,
107: 2,
108: 2,
109: 6,
110: 12,
111: 12,
112: 10,
113: 10,
114: 10,
115: 10,
116: 14,
117: 12,
118: 10,
119: 10,
120: 10,
121: 10,
122: 10,
123: 14,
124: 1,
125: 1,
126: 1,
127: 5,
128: 12,
129: 12,
130: 1,
131: 1,
132: 1,
133: 5,
134: 12,
135: 12,
136: 1,
137: 1,
138: 1,
139: 5,
140: 12,
141: 12,
142: 3,
143: 3,
144: 3,
145: 7,
146: 12,
147: 12,
148: 10,
149: 10,
150: 10,
151: 10,
152: 14,
153: 12,
154: 10,
155: 10,
156: 10,
157: 10,
158: 10,
159: 14,
160: 9,
161: 9,
162: 9,
163: 9,
164: 13,
165: 12,
166: 9,
167: 9,
168: 9,
169: 9,
170: 13,
171: 12,
172: 9,
173: 9,
174: 9,
175: 9,
176: 13,
177: 12,
178: 9,
179: 9,
180: 9,
181: 9,
182: 13,
183: 12,
184: 11,
185: 11,
186: 11,
187: 11,
188: 7,
189: 12,
190: 10,
191: 10,
192: 10,
193: 10,
194: 10,
195: 14,
196: 9,
197: 9,
198: 9,
199: 9,
200: 9,
201: 13,
202: 9,
203: 9,
204: 9,
205: 9,
206: 9,
207: 13,
208: 9,
209: 9,
210: 9,
211: 9,
212: 9,
213: 13,
214: 9,
215: 9,
216: 9,
217: 9,
218: 9,
219: 13,
220: 9,
221: 9,
222: 9,
223: 9,
224: 9,
225: 13,
226: 11,
227: 11,
228: 11,
229: 11,
230: 11,
231: 15,
232: 0,
233: 0,
234: 0,
235: 0,
236: 0,
237: 0,
238: 8,
239: 8,
240: 8,
241: 8,
242: 8,
243: 8,
244: 7,
245: 7,
246: 7,
247: 7,
248: 7,
249: 7,
250: 15,
251: 15,
252: 15,
253: 15,
254: 15,
255: 15,
}

View File

@ -38,6 +38,25 @@ const RequestXTVersion = RequestNameVersion
// If no attributes are given, or if the attribute is 0, this function returns
// the request sequence. Otherwise, it returns the response sequence.
//
// Common attributes include:
// - 1 132 columns
// - 2 Printer port
// - 4 Sixel
// - 6 Selective erase
// - 7 Soft character set (DRCS)
// - 8 User-defined keys (UDKs)
// - 9 National replacement character sets (NRCS) (International terminal only)
// - 12 Yugoslavian (SCS)
// - 15 Technical character set
// - 18 Windowing capability
// - 21 Horizontal scrolling
// - 23 Greek
// - 24 Turkish
// - 42 ISO Latin-2 character set
// - 44 PCTerm
// - 45 Soft key map
// - 46 ASCII emulation
//
// See https://vt100.net/docs/vt510-rm/DA1.html
func PrimaryDeviceAttributes(attrs ...int) string {
if len(attrs) == 0 {

View File

@ -1,6 +1,8 @@
package ansi
import "strconv"
import (
"strconv"
)
// SaveCursor (DECSC) is an escape sequence that saves the current cursor
// position.
@ -260,7 +262,7 @@ func CHA(col int) string {
// See: https://vt100.net/docs/vt510-rm/CUP.html
func CursorPosition(col, row int) string {
if row <= 0 && col <= 0 {
return HomeCursorPosition
return CursorHomePosition
}
var r, c string
@ -356,8 +358,8 @@ func CHT(n int) string {
return CursorHorizontalForwardTab(n)
}
// EraseCharacter (ECH) returns a sequence for erasing n characters and moving
// the cursor to the right. This doesn't affect other cell attributes.
// EraseCharacter (ECH) returns a sequence for erasing n characters from the
// screen. This doesn't affect other cell attributes.
//
// Default is 1.
//
@ -589,7 +591,7 @@ const ReverseIndex = "\x1bM"
//
// Default is 1.
//
// CSI n `
// CSI n \`
//
// See: https://vt100.net/docs/vt510-rm/HPA.html
func HorizontalPositionAbsolute(col int) string {

67
vendor/github.com/charmbracelet/x/ansi/finalterm.go generated vendored Normal file
View File

@ -0,0 +1,67 @@
package ansi
import "strings"
// FinalTerm returns an escape sequence that is used for shell integrations.
// Originally, FinalTerm designed the protocol hence the name.
//
// OSC 133 ; Ps ; Pm ST
// OSC 133 ; Ps ; Pm BEL
//
// See: https://iterm2.com/documentation-shell-integration.html
func FinalTerm(pm ...string) string {
return "\x1b]133;" + strings.Join(pm, ";") + "\x07"
}
// FinalTermPrompt returns an escape sequence that is used for shell
// integrations prompt marks. This is sent just before the start of the shell
// prompt.
//
// This is an alias for FinalTerm("A").
func FinalTermPrompt(pm ...string) string {
if len(pm) == 0 {
return FinalTerm("A")
}
return FinalTerm(append([]string{"A"}, pm...)...)
}
// FinalTermCmdStart returns an escape sequence that is used for shell
// integrations command start marks. This is sent just after the end of the
// shell prompt, before the user enters a command.
//
// This is an alias for FinalTerm("B").
func FinalTermCmdStart(pm ...string) string {
if len(pm) == 0 {
return FinalTerm("B")
}
return FinalTerm(append([]string{"B"}, pm...)...)
}
// FinalTermCmdExecuted returns an escape sequence that is used for shell
// integrations command executed marks. This is sent just before the start of
// the command output.
//
// This is an alias for FinalTerm("C").
func FinalTermCmdExecuted(pm ...string) string {
if len(pm) == 0 {
return FinalTerm("C")
}
return FinalTerm(append([]string{"C"}, pm...)...)
}
// FinalTermCmdFinished returns an escape sequence that is used for shell
// integrations command finished marks.
//
// If the command was sent after
// [FinalTermCmdStart], it indicates that the command was aborted. If the
// command was sent after [FinalTermCmdExecuted], it indicates the end of the
// command output. If neither was sent, [FinalTermCmdFinished] should be
// ignored.
//
// This is an alias for FinalTerm("D").
func FinalTermCmdFinished(pm ...string) string {
if len(pm) == 0 {
return FinalTerm("D")
}
return FinalTerm(append([]string{"D"}, pm...)...)
}

View File

@ -2,17 +2,47 @@ package ansi
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"io"
"os"
"strconv"
"strings"
"github.com/charmbracelet/x/ansi/kitty"
)
// SixelGraphics returns a sequence that encodes the given sixel image payload to
// a DCS sixel sequence.
//
// DCS p1; p2; p3; q [sixel payload] ST
//
// p1 = pixel aspect ratio, deprecated and replaced by pixel metrics in the payload
//
// p2 = This is supposed to be 0 for transparency, but terminals don't seem to
// to use it properly. Value 0 leaves an unsightly black bar on all terminals
// I've tried and looks correct with value 1.
//
// p3 = Horizontal grid size parameter. Everyone ignores this and uses a fixed grid
// size, as far as I can tell.
//
// See https://shuford.invisible-island.net/all_about_sixels.txt
func SixelGraphics(p1, p2, p3 int, payload []byte) string {
var buf bytes.Buffer
buf.WriteString("\x1bP")
if p1 >= 0 {
buf.WriteString(strconv.Itoa(p1))
}
buf.WriteByte(';')
if p2 >= 0 {
buf.WriteString(strconv.Itoa(p2))
}
if p3 > 0 {
buf.WriteByte(';')
buf.WriteString(strconv.Itoa(p3))
}
buf.WriteByte('q')
buf.Write(payload)
buf.WriteString("\x1b\\")
return buf.String()
}
// KittyGraphics returns a sequence that encodes the given image in the Kitty
// graphics protocol.
//
@ -30,170 +60,3 @@ func KittyGraphics(payload []byte, opts ...string) string {
buf.WriteString("\x1b\\")
return buf.String()
}
var (
// KittyGraphicsTempDir is the directory where temporary files are stored.
// This is used in [WriteKittyGraphics] along with [os.CreateTemp].
KittyGraphicsTempDir = ""
// KittyGraphicsTempPattern is the pattern used to create temporary files.
// This is used in [WriteKittyGraphics] along with [os.CreateTemp].
// The Kitty Graphics protocol requires the file path to contain the
// substring "tty-graphics-protocol".
KittyGraphicsTempPattern = "tty-graphics-protocol-*"
)
// WriteKittyGraphics writes an image using the Kitty Graphics protocol with
// the given options to w. It chunks the written data if o.Chunk is true.
//
// You can omit m and use nil when rendering an image from a file. In this
// case, you must provide a file path in o.File and use o.Transmission =
// [kitty.File]. You can also use o.Transmission = [kitty.TempFile] to write
// the image to a temporary file. In that case, the file path is ignored, and
// the image is written to a temporary file that is automatically deleted by
// the terminal.
//
// See https://sw.kovidgoyal.net/kitty/graphics-protocol/
func WriteKittyGraphics(w io.Writer, m image.Image, o *kitty.Options) error {
if o == nil {
o = &kitty.Options{}
}
if o.Transmission == 0 && len(o.File) != 0 {
o.Transmission = kitty.File
}
var data bytes.Buffer // the data to be encoded into base64
e := &kitty.Encoder{
Compress: o.Compression == kitty.Zlib,
Format: o.Format,
}
switch o.Transmission {
case kitty.Direct:
if err := e.Encode(&data, m); err != nil {
return fmt.Errorf("failed to encode direct image: %w", err)
}
case kitty.SharedMemory:
// TODO: Implement shared memory
return fmt.Errorf("shared memory transmission is not yet implemented")
case kitty.File:
if len(o.File) == 0 {
return kitty.ErrMissingFile
}
f, err := os.Open(o.File)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close() //nolint:errcheck
stat, err := f.Stat()
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
mode := stat.Mode()
if !mode.IsRegular() {
return fmt.Errorf("file is not a regular file")
}
// Write the file path to the buffer
if _, err := data.WriteString(f.Name()); err != nil {
return fmt.Errorf("failed to write file path to buffer: %w", err)
}
case kitty.TempFile:
f, err := os.CreateTemp(KittyGraphicsTempDir, KittyGraphicsTempPattern)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close() //nolint:errcheck
if err := e.Encode(f, m); err != nil {
return fmt.Errorf("failed to encode image to file: %w", err)
}
// Write the file path to the buffer
if _, err := data.WriteString(f.Name()); err != nil {
return fmt.Errorf("failed to write file path to buffer: %w", err)
}
}
// Encode image to base64
var payload bytes.Buffer // the base64 encoded image to be written to w
b64 := base64.NewEncoder(base64.StdEncoding, &payload)
if _, err := data.WriteTo(b64); err != nil {
return fmt.Errorf("failed to write base64 encoded image to payload: %w", err)
}
if err := b64.Close(); err != nil {
return err
}
// If not chunking, write all at once
if !o.Chunk {
_, err := io.WriteString(w, KittyGraphics(payload.Bytes(), o.Options()...))
return err
}
// Write in chunks
var (
err error
n int
)
chunk := make([]byte, kitty.MaxChunkSize)
isFirstChunk := true
for {
// Stop if we read less than the chunk size [kitty.MaxChunkSize].
n, err = io.ReadFull(&payload, chunk)
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("failed to read chunk: %w", err)
}
opts := buildChunkOptions(o, isFirstChunk, false)
if _, err := io.WriteString(w, KittyGraphics(chunk[:n], opts...)); err != nil {
return err
}
isFirstChunk = false
}
// Write the last chunk
opts := buildChunkOptions(o, isFirstChunk, true)
_, err = io.WriteString(w, KittyGraphics(chunk[:n], opts...))
return err
}
// buildChunkOptions creates the options slice for a chunk
func buildChunkOptions(o *kitty.Options, isFirstChunk, isLastChunk bool) []string {
var opts []string
if isFirstChunk {
opts = o.Options()
} else {
// These options are allowed in subsequent chunks
if o.Quite > 0 {
opts = append(opts, fmt.Sprintf("q=%d", o.Quite))
}
if o.Action == kitty.Frame {
opts = append(opts, "a=f")
}
}
if !isFirstChunk || !isLastChunk {
// We don't need to encode the (m=) option when we only have one chunk.
if isLastChunk {
opts = append(opts, "m=0")
} else {
opts = append(opts, "m=1")
}
}
return opts
}

View File

@ -1,85 +0,0 @@
package kitty
import (
"compress/zlib"
"fmt"
"image"
"image/color"
"image/png"
"io"
)
// Decoder is a decoder for the Kitty graphics protocol. It supports decoding
// images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats. It can also
// decompress data using zlib.
// The default format is 32-bit [RGBA].
type Decoder struct {
// Uses zlib decompression.
Decompress bool
// Can be one of [RGB], [RGBA], or [PNG].
Format int
// Width of the image in pixels. This can be omitted if the image is [PNG]
// formatted.
Width int
// Height of the image in pixels. This can be omitted if the image is [PNG]
// formatted.
Height int
}
// Decode decodes the image data from r in the specified format.
func (d *Decoder) Decode(r io.Reader) (image.Image, error) {
if d.Decompress {
zr, err := zlib.NewReader(r)
if err != nil {
return nil, fmt.Errorf("failed to create zlib reader: %w", err)
}
defer zr.Close() //nolint:errcheck
r = zr
}
if d.Format == 0 {
d.Format = RGBA
}
switch d.Format {
case RGBA, RGB:
return d.decodeRGBA(r, d.Format == RGBA)
case PNG:
return png.Decode(r)
default:
return nil, fmt.Errorf("unsupported format: %d", d.Format)
}
}
// decodeRGBA decodes the image data in 32-bit RGBA or 24-bit RGB formats.
func (d *Decoder) decodeRGBA(r io.Reader, alpha bool) (image.Image, error) {
m := image.NewRGBA(image.Rect(0, 0, d.Width, d.Height))
var buf []byte
if alpha {
buf = make([]byte, 4)
} else {
buf = make([]byte, 3)
}
for y := 0; y < d.Height; y++ {
for x := 0; x < d.Width; x++ {
if _, err := io.ReadFull(r, buf[:]); err != nil {
return nil, fmt.Errorf("failed to read pixel data: %w", err)
}
if alpha {
m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], buf[3]})
} else {
m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], 0xff})
}
}
}
return m, nil
}

View File

@ -1,64 +0,0 @@
package kitty
import (
"compress/zlib"
"fmt"
"image"
"image/png"
"io"
)
// Encoder is an encoder for the Kitty graphics protocol. It supports encoding
// images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats, and
// compressing the data using zlib.
// The default format is 32-bit [RGBA].
type Encoder struct {
// Uses zlib compression.
Compress bool
// Can be one of [RGBA], [RGB], or [PNG].
Format int
}
// Encode encodes the image data in the specified format and writes it to w.
func (e *Encoder) Encode(w io.Writer, m image.Image) error {
if m == nil {
return nil
}
if e.Compress {
zw := zlib.NewWriter(w)
defer zw.Close() //nolint:errcheck
w = zw
}
if e.Format == 0 {
e.Format = RGBA
}
switch e.Format {
case RGBA, RGB:
bounds := m.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := m.At(x, y).RGBA()
switch e.Format {
case RGBA:
w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)}) //nolint:errcheck
case RGB:
w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8)}) //nolint:errcheck
}
}
}
case PNG:
if err := png.Encode(w, m); err != nil {
return fmt.Errorf("failed to encode PNG: %w", err)
}
default:
return fmt.Errorf("unsupported format: %d", e.Format)
}
return nil
}

View File

@ -1,414 +0,0 @@
package kitty
import "errors"
// ErrMissingFile is returned when the file path is missing.
var ErrMissingFile = errors.New("missing file path")
// MaxChunkSize is the maximum chunk size for the image data.
const MaxChunkSize = 1024 * 4
// Placeholder is a special Unicode character that can be used as a placeholder
// for an image.
const Placeholder = '\U0010EEEE'
// Graphics image format.
const (
// 32-bit RGBA format.
RGBA = 32
// 24-bit RGB format.
RGB = 24
// PNG format.
PNG = 100
)
// Compression types.
const (
Zlib = 'z'
)
// Transmission types.
const (
// The data transmitted directly in the escape sequence.
Direct = 'd'
// The data transmitted in a regular file.
File = 'f'
// A temporary file is used and deleted after transmission.
TempFile = 't'
// A shared memory object.
// For POSIX see https://pubs.opengroup.org/onlinepubs/9699919799/functions/shm_open.html
// For Windows see https://docs.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory
SharedMemory = 's'
)
// Action types.
const (
// Transmit image data.
Transmit = 't'
// TransmitAndPut transmit image data and display (put) it.
TransmitAndPut = 'T'
// Query terminal for image info.
Query = 'q'
// Put (display) previously transmitted image.
Put = 'p'
// Delete image.
Delete = 'd'
// Frame transmits data for animation frames.
Frame = 'f'
// Animate controls animation.
Animate = 'a'
// Compose composes animation frames.
Compose = 'c'
)
// Delete types.
const (
// Delete all placements visible on screen
DeleteAll = 'a'
// Delete all images with the specified id, specified using the i key. If
// you specify a p key for the placement id as well, then only the
// placement with the specified image id and placement id will be deleted.
DeleteID = 'i'
// Delete newest image with the specified number, specified using the I
// key. If you specify a p key for the placement id as well, then only the
// placement with the specified number and placement id will be deleted.
DeleteNumber = 'n'
// Delete all placements that intersect with the current cursor position.
DeleteCursor = 'c'
// Delete animation frames.
DeleteFrames = 'f'
// Delete all placements that intersect a specific cell, the cell is
// specified using the x and y keys
DeleteCell = 'p'
// Delete all placements that intersect a specific cell having a specific
// z-index. The cell and z-index is specified using the x, y and z keys.
DeleteCellZ = 'q'
// Delete all images whose id is greater than or equal to the value of the x
// key and less than or equal to the value of the y.
DeleteRange = 'r'
// Delete all placements that intersect the specified column, specified using
// the x key.
DeleteColumn = 'x'
// Delete all placements that intersect the specified row, specified using
// the y key.
DeleteRow = 'y'
// Delete all placements that have the specified z-index, specified using the
// z key.
DeleteZ = 'z'
)
// Diacritic returns the diacritic rune at the specified index. If the index is
// out of bounds, the first diacritic rune is returned.
func Diacritic(i int) rune {
if i < 0 || i >= len(diacritics) {
return diacritics[0]
}
return diacritics[i]
}
// From https://sw.kovidgoyal.net/kitty/_downloads/f0a0de9ec8d9ff4456206db8e0814937/rowcolumn-diacritics.txt
// See https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders for further explanation.
var diacritics = []rune{
'\u0305',
'\u030D',
'\u030E',
'\u0310',
'\u0312',
'\u033D',
'\u033E',
'\u033F',
'\u0346',
'\u034A',
'\u034B',
'\u034C',
'\u0350',
'\u0351',
'\u0352',
'\u0357',
'\u035B',
'\u0363',
'\u0364',
'\u0365',
'\u0366',
'\u0367',
'\u0368',
'\u0369',
'\u036A',
'\u036B',
'\u036C',
'\u036D',
'\u036E',
'\u036F',
'\u0483',
'\u0484',
'\u0485',
'\u0486',
'\u0487',
'\u0592',
'\u0593',
'\u0594',
'\u0595',
'\u0597',
'\u0598',
'\u0599',
'\u059C',
'\u059D',
'\u059E',
'\u059F',
'\u05A0',
'\u05A1',
'\u05A8',
'\u05A9',
'\u05AB',
'\u05AC',
'\u05AF',
'\u05C4',
'\u0610',
'\u0611',
'\u0612',
'\u0613',
'\u0614',
'\u0615',
'\u0616',
'\u0617',
'\u0657',
'\u0658',
'\u0659',
'\u065A',
'\u065B',
'\u065D',
'\u065E',
'\u06D6',
'\u06D7',
'\u06D8',
'\u06D9',
'\u06DA',
'\u06DB',
'\u06DC',
'\u06DF',
'\u06E0',
'\u06E1',
'\u06E2',
'\u06E4',
'\u06E7',
'\u06E8',
'\u06EB',
'\u06EC',
'\u0730',
'\u0732',
'\u0733',
'\u0735',
'\u0736',
'\u073A',
'\u073D',
'\u073F',
'\u0740',
'\u0741',
'\u0743',
'\u0745',
'\u0747',
'\u0749',
'\u074A',
'\u07EB',
'\u07EC',
'\u07ED',
'\u07EE',
'\u07EF',
'\u07F0',
'\u07F1',
'\u07F3',
'\u0816',
'\u0817',
'\u0818',
'\u0819',
'\u081B',
'\u081C',
'\u081D',
'\u081E',
'\u081F',
'\u0820',
'\u0821',
'\u0822',
'\u0823',
'\u0825',
'\u0826',
'\u0827',
'\u0829',
'\u082A',
'\u082B',
'\u082C',
'\u082D',
'\u0951',
'\u0953',
'\u0954',
'\u0F82',
'\u0F83',
'\u0F86',
'\u0F87',
'\u135D',
'\u135E',
'\u135F',
'\u17DD',
'\u193A',
'\u1A17',
'\u1A75',
'\u1A76',
'\u1A77',
'\u1A78',
'\u1A79',
'\u1A7A',
'\u1A7B',
'\u1A7C',
'\u1B6B',
'\u1B6D',
'\u1B6E',
'\u1B6F',
'\u1B70',
'\u1B71',
'\u1B72',
'\u1B73',
'\u1CD0',
'\u1CD1',
'\u1CD2',
'\u1CDA',
'\u1CDB',
'\u1CE0',
'\u1DC0',
'\u1DC1',
'\u1DC3',
'\u1DC4',
'\u1DC5',
'\u1DC6',
'\u1DC7',
'\u1DC8',
'\u1DC9',
'\u1DCB',
'\u1DCC',
'\u1DD1',
'\u1DD2',
'\u1DD3',
'\u1DD4',
'\u1DD5',
'\u1DD6',
'\u1DD7',
'\u1DD8',
'\u1DD9',
'\u1DDA',
'\u1DDB',
'\u1DDC',
'\u1DDD',
'\u1DDE',
'\u1DDF',
'\u1DE0',
'\u1DE1',
'\u1DE2',
'\u1DE3',
'\u1DE4',
'\u1DE5',
'\u1DE6',
'\u1DFE',
'\u20D0',
'\u20D1',
'\u20D4',
'\u20D5',
'\u20D6',
'\u20D7',
'\u20DB',
'\u20DC',
'\u20E1',
'\u20E7',
'\u20E9',
'\u20F0',
'\u2CEF',
'\u2CF0',
'\u2CF1',
'\u2DE0',
'\u2DE1',
'\u2DE2',
'\u2DE3',
'\u2DE4',
'\u2DE5',
'\u2DE6',
'\u2DE7',
'\u2DE8',
'\u2DE9',
'\u2DEA',
'\u2DEB',
'\u2DEC',
'\u2DED',
'\u2DEE',
'\u2DEF',
'\u2DF0',
'\u2DF1',
'\u2DF2',
'\u2DF3',
'\u2DF4',
'\u2DF5',
'\u2DF6',
'\u2DF7',
'\u2DF8',
'\u2DF9',
'\u2DFA',
'\u2DFB',
'\u2DFC',
'\u2DFD',
'\u2DFE',
'\u2DFF',
'\uA66F',
'\uA67C',
'\uA67D',
'\uA6F0',
'\uA6F1',
'\uA8E0',
'\uA8E1',
'\uA8E2',
'\uA8E3',
'\uA8E4',
'\uA8E5',
'\uA8E6',
'\uA8E7',
'\uA8E8',
'\uA8E9',
'\uA8EA',
'\uA8EB',
'\uA8EC',
'\uA8ED',
'\uA8EE',
'\uA8EF',
'\uA8F0',
'\uA8F1',
'\uAAB0',
'\uAAB2',
'\uAAB3',
'\uAAB7',
'\uAAB8',
'\uAABE',
'\uAABF',
'\uAAC1',
'\uFE20',
'\uFE21',
'\uFE22',
'\uFE23',
'\uFE24',
'\uFE25',
'\uFE26',
'\U00010A0F',
'\U00010A38',
'\U0001D185',
'\U0001D186',
'\U0001D187',
'\U0001D188',
'\U0001D189',
'\U0001D1AA',
'\U0001D1AB',
'\U0001D1AC',
'\U0001D1AD',
'\U0001D242',
'\U0001D243',
'\U0001D244',
}

View File

@ -1,367 +0,0 @@
package kitty
import (
"encoding"
"fmt"
"strconv"
"strings"
)
var (
_ encoding.TextMarshaler = Options{}
_ encoding.TextUnmarshaler = &Options{}
)
// Options represents a Kitty Graphics Protocol options.
type Options struct {
// Common options.
// Action (a=t) is the action to be performed on the image. Can be one of
// [Transmit], [TransmitDisplay], [Query], [Put], [Delete], [Frame],
// [Animate], [Compose].
Action byte
// Quite mode (q=0) is the quiet mode. Can be either zero, one, or two
// where zero is the default, 1 suppresses OK responses, and 2 suppresses
// both OK and error responses.
Quite byte
// Transmission options.
// ID (i=) is the image ID. The ID is a unique identifier for the image.
// Must be a positive integer up to [math.MaxUint32].
ID int
// PlacementID (p=) is the placement ID. The placement ID is a unique
// identifier for the placement of the image. Must be a positive integer up
// to [math.MaxUint32].
PlacementID int
// Number (I=0) is the number of images to be transmitted.
Number int
// Format (f=32) is the image format. One of [RGBA], [RGB], [PNG].
Format int
// ImageWidth (s=0) is the transmitted image width.
ImageWidth int
// ImageHeight (v=0) is the transmitted image height.
ImageHeight int
// Compression (o=) is the image compression type. Can be [Zlib] or zero.
Compression byte
// Transmission (t=d) is the image transmission type. Can be [Direct], [File],
// [TempFile], or[SharedMemory].
Transmission byte
// File is the file path to be used when the transmission type is [File].
// If [Options.Transmission] is omitted i.e. zero and this is non-empty,
// the transmission type is set to [File].
File string
// Size (S=0) is the size to be read from the transmission medium.
Size int
// Offset (O=0) is the offset byte to start reading from the transmission
// medium.
Offset int
// Chunk (m=) whether the image is transmitted in chunks. Can be either
// zero or one. When true, the image is transmitted in chunks. Each chunk
// must be a multiple of 4, and up to [MaxChunkSize] bytes. Each chunk must
// have the m=1 option except for the last chunk which must have m=0.
Chunk bool
// Display options.
// X (x=0) is the pixel X coordinate of the image to start displaying.
X int
// Y (y=0) is the pixel Y coordinate of the image to start displaying.
Y int
// Z (z=0) is the Z coordinate of the image to display.
Z int
// Width (w=0) is the width of the image to display.
Width int
// Height (h=0) is the height of the image to display.
Height int
// OffsetX (X=0) is the OffsetX coordinate of the cursor cell to start
// displaying the image. OffsetX=0 is the leftmost cell. This must be
// smaller than the terminal cell width.
OffsetX int
// OffsetY (Y=0) is the OffsetY coordinate of the cursor cell to start
// displaying the image. OffsetY=0 is the topmost cell. This must be
// smaller than the terminal cell height.
OffsetY int
// Columns (c=0) is the number of columns to display the image. The image
// will be scaled to fit the number of columns.
Columns int
// Rows (r=0) is the number of rows to display the image. The image will be
// scaled to fit the number of rows.
Rows int
// VirtualPlacement (U=0) whether to use virtual placement. This is used
// with Unicode [Placeholder] to display images.
VirtualPlacement bool
// DoNotMoveCursor (C=0) whether to move the cursor after displaying the
// image.
DoNotMoveCursor bool
// ParentID (P=0) is the parent image ID. The parent ID is the ID of the
// image that is the parent of the current image. This is used with Unicode
// [Placeholder] to display images relative to the parent image.
ParentID int
// ParentPlacementID (Q=0) is the parent placement ID. The parent placement
// ID is the ID of the placement of the parent image. This is used with
// Unicode [Placeholder] to display images relative to the parent image.
ParentPlacementID int
// Delete options.
// Delete (d=a) is the delete action. Can be one of [DeleteAll],
// [DeleteID], [DeleteNumber], [DeleteCursor], [DeleteFrames],
// [DeleteCell], [DeleteCellZ], [DeleteRange], [DeleteColumn], [DeleteRow],
// [DeleteZ].
Delete byte
// DeleteResources indicates whether to delete the resources associated
// with the image.
DeleteResources bool
}
// Options returns the options as a slice of a key-value pairs.
func (o *Options) Options() (opts []string) {
opts = []string{}
if o.Format == 0 {
o.Format = RGBA
}
if o.Action == 0 {
o.Action = Transmit
}
if o.Delete == 0 {
o.Delete = DeleteAll
}
if o.Transmission == 0 {
if len(o.File) > 0 {
o.Transmission = File
} else {
o.Transmission = Direct
}
}
if o.Format != RGBA {
opts = append(opts, fmt.Sprintf("f=%d", o.Format))
}
if o.Quite > 0 {
opts = append(opts, fmt.Sprintf("q=%d", o.Quite))
}
if o.ID > 0 {
opts = append(opts, fmt.Sprintf("i=%d", o.ID))
}
if o.PlacementID > 0 {
opts = append(opts, fmt.Sprintf("p=%d", o.PlacementID))
}
if o.Number > 0 {
opts = append(opts, fmt.Sprintf("I=%d", o.Number))
}
if o.ImageWidth > 0 {
opts = append(opts, fmt.Sprintf("s=%d", o.ImageWidth))
}
if o.ImageHeight > 0 {
opts = append(opts, fmt.Sprintf("v=%d", o.ImageHeight))
}
if o.Transmission != Direct {
opts = append(opts, fmt.Sprintf("t=%c", o.Transmission))
}
if o.Size > 0 {
opts = append(opts, fmt.Sprintf("S=%d", o.Size))
}
if o.Offset > 0 {
opts = append(opts, fmt.Sprintf("O=%d", o.Offset))
}
if o.Compression == Zlib {
opts = append(opts, fmt.Sprintf("o=%c", o.Compression))
}
if o.VirtualPlacement {
opts = append(opts, "U=1")
}
if o.DoNotMoveCursor {
opts = append(opts, "C=1")
}
if o.ParentID > 0 {
opts = append(opts, fmt.Sprintf("P=%d", o.ParentID))
}
if o.ParentPlacementID > 0 {
opts = append(opts, fmt.Sprintf("Q=%d", o.ParentPlacementID))
}
if o.X > 0 {
opts = append(opts, fmt.Sprintf("x=%d", o.X))
}
if o.Y > 0 {
opts = append(opts, fmt.Sprintf("y=%d", o.Y))
}
if o.Z > 0 {
opts = append(opts, fmt.Sprintf("z=%d", o.Z))
}
if o.Width > 0 {
opts = append(opts, fmt.Sprintf("w=%d", o.Width))
}
if o.Height > 0 {
opts = append(opts, fmt.Sprintf("h=%d", o.Height))
}
if o.OffsetX > 0 {
opts = append(opts, fmt.Sprintf("X=%d", o.OffsetX))
}
if o.OffsetY > 0 {
opts = append(opts, fmt.Sprintf("Y=%d", o.OffsetY))
}
if o.Columns > 0 {
opts = append(opts, fmt.Sprintf("c=%d", o.Columns))
}
if o.Rows > 0 {
opts = append(opts, fmt.Sprintf("r=%d", o.Rows))
}
if o.Delete != DeleteAll || o.DeleteResources {
da := o.Delete
if o.DeleteResources {
da = da - ' ' // to uppercase
}
opts = append(opts, fmt.Sprintf("d=%c", da))
}
if o.Action != Transmit {
opts = append(opts, fmt.Sprintf("a=%c", o.Action))
}
return
}
// String returns the string representation of the options.
func (o Options) String() string {
return strings.Join(o.Options(), ",")
}
// MarshalText returns the string representation of the options.
func (o Options) MarshalText() ([]byte, error) {
return []byte(o.String()), nil
}
// UnmarshalText parses the options from the given string.
func (o *Options) UnmarshalText(text []byte) error {
opts := strings.Split(string(text), ",")
for _, opt := range opts {
ps := strings.SplitN(opt, "=", 2)
if len(ps) != 2 || len(ps[1]) == 0 {
continue
}
switch ps[0] {
case "a":
o.Action = ps[1][0]
case "o":
o.Compression = ps[1][0]
case "t":
o.Transmission = ps[1][0]
case "d":
d := ps[1][0]
if d >= 'A' && d <= 'Z' {
o.DeleteResources = true
d = d + ' ' // to lowercase
}
o.Delete = d
case "i", "q", "p", "I", "f", "s", "v", "S", "O", "m", "x", "y", "z", "w", "h", "X", "Y", "c", "r", "U", "P", "Q":
v, err := strconv.Atoi(ps[1])
if err != nil {
continue
}
switch ps[0] {
case "i":
o.ID = v
case "q":
o.Quite = byte(v)
case "p":
o.PlacementID = v
case "I":
o.Number = v
case "f":
o.Format = v
case "s":
o.ImageWidth = v
case "v":
o.ImageHeight = v
case "S":
o.Size = v
case "O":
o.Offset = v
case "m":
o.Chunk = v == 0 || v == 1
case "x":
o.X = v
case "y":
o.Y = v
case "z":
o.Z = v
case "w":
o.Width = v
case "h":
o.Height = v
case "X":
o.OffsetX = v
case "Y":
o.OffsetY = v
case "c":
o.Columns = v
case "r":
o.Rows = v
case "U":
o.VirtualPlacement = v == 1
case "P":
o.ParentID = v
case "Q":
o.ParentPlacementID = v
}
}
}
return nil
}

View File

@ -48,7 +48,7 @@ type Mode interface {
Mode() int
}
// SetMode (SM) returns a sequence to set a mode.
// SetMode (SM) or (DECSET) returns a sequence to set a mode.
// The mode arguments are a list of modes to set.
//
// If one of the modes is a [DECMode], the function will returns two escape
@ -72,7 +72,12 @@ func SM(modes ...Mode) string {
return SetMode(modes...)
}
// ResetMode (RM) returns a sequence to reset a mode.
// DECSET is an alias for [SetMode].
func DECSET(modes ...Mode) string {
return SetMode(modes...)
}
// ResetMode (RM) or (DECRST) returns a sequence to reset a mode.
// The mode arguments are a list of modes to reset.
//
// If one of the modes is a [DECMode], the function will returns two escape
@ -96,9 +101,14 @@ func RM(modes ...Mode) string {
return ResetMode(modes...)
}
// DECRST is an alias for [ResetMode].
func DECRST(modes ...Mode) string {
return ResetMode(modes...)
}
func setMode(reset bool, modes ...Mode) (s string) {
if len(modes) == 0 {
return
return //nolint:nakedret
}
cmd := "h"
@ -132,7 +142,7 @@ func setMode(reset bool, modes ...Mode) (s string) {
if len(dec) > 0 {
s += seq + "?" + strings.Join(dec, ";") + cmd
}
return
return //nolint:nakedret
}
// RequestMode (DECRQM) returns a sequence to request a mode from the terminal.
@ -243,6 +253,21 @@ const (
RequestInsertReplaceMode = "\x1b[4$p"
)
// BiDirectional Support Mode (BDSM) is a mode that determines whether the
// terminal supports bidirectional text. When enabled, the terminal supports
// bidirectional text and is set to implicit bidirectional mode. When disabled,
// the terminal does not support bidirectional text.
//
// See ECMA-48 7.2.1.
const (
BiDirectionalSupportMode = ANSIMode(8)
BDSM = BiDirectionalSupportMode
SetBiDirectionalSupportMode = "\x1b[8h"
ResetBiDirectionalSupportMode = "\x1b[8l"
RequestBiDirectionalSupportMode = "\x1b[8$p"
)
// Send Receive Mode (SRM) or Local Echo Mode is a mode that determines whether
// the terminal echoes characters back to the host. When enabled, the terminal
// sends characters to the host as they are typed.
@ -297,7 +322,7 @@ const (
// Deprecated: use [SetCursorKeysMode] and [ResetCursorKeysMode] instead.
const (
EnableCursorKeys = "\x1b[?1h"
EnableCursorKeys = "\x1b[?1h" //nolint:revive // grouped constants
DisableCursorKeys = "\x1b[?1l"
)
@ -548,8 +573,9 @@ const (
// Deprecated: use [SetFocusEventMode], [ResetFocusEventMode], and
// [RequestFocusEventMode] instead.
// Focus reporting mode constants.
const (
ReportFocusMode = DECMode(1004)
ReportFocusMode = DECMode(1004) //nolint:revive // grouped constants
EnableReportFocus = "\x1b[?1004h"
DisableReportFocus = "\x1b[?1004l"
@ -577,7 +603,7 @@ const (
// Deprecated: use [SgrExtMouseMode] [SetSgrExtMouseMode],
// [ResetSgrExtMouseMode], and [RequestSgrExtMouseMode] instead.
const (
MouseSgrExtMode = DECMode(1006)
MouseSgrExtMode = DECMode(1006) //nolint:revive // grouped constants
EnableMouseSgrExt = "\x1b[?1006h"
DisableMouseSgrExt = "\x1b[?1006l"
RequestMouseSgrExt = "\x1b[?1006$p"
@ -693,7 +719,7 @@ const (
// Deprecated: use [SetBracketedPasteMode], [ResetBracketedPasteMode], and
// [RequestBracketedPasteMode] instead.
const (
EnableBracketedPaste = "\x1b[?2004h"
EnableBracketedPaste = "\x1b[?2004h" //nolint:revive // grouped constants
DisableBracketedPaste = "\x1b[?2004l"
RequestBracketedPaste = "\x1b[?2004$p"
)
@ -710,6 +736,8 @@ const (
RequestSynchronizedOutputMode = "\x1b[?2026$p"
)
// Synchronized Output Mode. See [SynchronizedOutputMode].
//
// Deprecated: use [SynchronizedOutputMode], [SetSynchronizedOutputMode], and
// [ResetSynchronizedOutputMode], and [RequestSynchronizedOutputMode] instead.
const (
@ -720,12 +748,28 @@ const (
RequestSyncdOutput = "\x1b[?2026$p"
)
// Unicode Core Mode is a mode that determines whether the terminal should use
// Unicode grapheme clustering to calculate the width of glyphs for each
// terminal cell.
//
// See: https://github.com/contour-terminal/terminal-unicode-core
const (
UnicodeCoreMode = DECMode(2027)
SetUnicodeCoreMode = "\x1b[?2027h"
ResetUnicodeCoreMode = "\x1b[?2027l"
RequestUnicodeCoreMode = "\x1b[?2027$p"
)
// Grapheme Clustering Mode is a mode that determines whether the terminal
// should look for grapheme clusters instead of single runes in the rendered
// text. This makes the terminal properly render combining characters such as
// emojis.
//
// See: https://github.com/contour-terminal/terminal-unicode-core
//
// Deprecated: use [GraphemeClusteringMode], [SetUnicodeCoreMode],
// [ResetUnicodeCoreMode], and [RequestUnicodeCoreMode] instead.
const (
GraphemeClusteringMode = DECMode(2027)
@ -734,14 +778,54 @@ const (
RequestGraphemeClusteringMode = "\x1b[?2027$p"
)
// Deprecated: use [SetGraphemeClusteringMode], [ResetGraphemeClusteringMode], and
// [RequestGraphemeClusteringMode] instead.
// Grapheme Clustering Mode. See [GraphemeClusteringMode].
//
// Deprecated: use [SetUnicodeCoreMode], [ResetUnicodeCoreMode], and
// [RequestUnicodeCoreMode] instead.
const (
EnableGraphemeClustering = "\x1b[?2027h"
DisableGraphemeClustering = "\x1b[?2027l"
RequestGraphemeClustering = "\x1b[?2027$p"
)
// LightDarkMode is a mode that enables reporting the operating system's color
// scheme (light or dark) preference. It reports the color scheme as a [DSR]
// and [LightDarkReport] escape sequences encoded as follows:
//
// CSI ? 997 ; 1 n for dark mode
// CSI ? 997 ; 2 n for light mode
//
// The color preference can also be requested via the following [DSR] and
// [RequestLightDarkReport] escape sequences:
//
// CSI ? 996 n
//
// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
const (
LightDarkMode = DECMode(2031)
SetLightDarkMode = "\x1b[?2031h"
ResetLightDarkMode = "\x1b[?2031l"
RequestLightDarkMode = "\x1b[?2031$p"
)
// InBandResizeMode is a mode that reports terminal resize events as escape
// sequences. This is useful for systems that do not support [SIGWINCH] like
// Windows.
//
// The terminal then sends the following encoding:
//
// CSI 48 ; cellsHeight ; cellsWidth ; pixelHeight ; pixelWidth t
//
// See: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
const (
InBandResizeMode = DECMode(2048)
SetInBandResizeMode = "\x1b[?2048h"
ResetInBandResizeMode = "\x1b[?2048l"
RequestInBandResizeMode = "\x1b[?2048$p"
)
// Win32Input is a mode that determines whether input is processed by the
// Win32 console and Conpty.
//
@ -757,7 +841,7 @@ const (
// Deprecated: use [SetWin32InputMode], [ResetWin32InputMode], and
// [RequestWin32InputMode] instead.
const (
EnableWin32Input = "\x1b[?9001h"
EnableWin32Input = "\x1b[?9001h" //nolint:revive // grouped constants
DisableWin32Input = "\x1b[?9001l"
RequestWin32Input = "\x1b[?9001$p"
)

View File

@ -4,12 +4,6 @@ package ansi
// all modes are [ModeNotRecognized].
type Modes map[Mode]ModeSetting
// NewModes creates a new Modes map. By default, all modes are
// [ModeNotRecognized].
func NewModes() Modes {
return make(Modes)
}
// Get returns the setting of a terminal mode. If the mode is not set, it
// returns [ModeNotRecognized].
func (m Modes) Get(mode Mode) ModeSetting {

View File

@ -134,7 +134,7 @@ func EncodeMouseButton(b MouseButton, motion, shift, alt, ctrl bool) (m byte) {
m |= bitMotion
}
return
return //nolint:nakedret
}
// x10Offset is the offset for X10 mouse events.

View File

@ -150,7 +150,7 @@ func (p *Parser) StateName() string {
// Parse parses the given dispatcher and byte buffer.
// Deprecated: Loop over the buffer and call [Parser.Advance] instead.
func (p *Parser) Parse(b []byte) {
for i := 0; i < len(b); i++ {
for i := range b {
p.Advance(b[i])
}
}
@ -245,7 +245,7 @@ func (p *Parser) parseStringCmd() {
if p.dataLen >= 0 {
datalen = p.dataLen
}
for i := 0; i < datalen; i++ {
for i := range datalen {
d := p.data[i]
if d < '0' || d > '9' {
break

View File

@ -1,3 +1,4 @@
// Package parser provides ANSI escape sequence parsing functionality.
package parser
// Action is a DEC ANSI parser action.
@ -19,7 +20,7 @@ const (
IgnoreAction = NoneAction
)
// nolint: unused
// ActionNames provides string names for parser actions.
var ActionNames = []string{
"NoneAction",
"ClearAction",
@ -58,7 +59,7 @@ const (
Utf8State
)
// nolint: unused
// StateNames provides string names for parser states.
var StateNames = []string{
"GroundState",
"CsiEntryState",

View File

@ -78,7 +78,7 @@ func Subparams(params []int, i int) []int {
// Count the number of parameters before the given parameter index.
var count int
var j int
for j = 0; j < len(params); j++ {
for j = range params {
if count == i {
break
}
@ -116,7 +116,7 @@ func Subparams(params []int, i int) []int {
// sub-parameters.
func Len(params []int) int {
var n int
for i := 0; i < len(params); i++ {
for i := range params {
if !HasMore(params, i) {
n++
}
@ -128,7 +128,7 @@ func Len(params []int) int {
// function for each parameter.
// The function should return false to stop the iteration.
func Range(params []int, fn func(i int, param int, hasMore bool) bool) {
for i := 0; i < len(params); i++ {
for i := range params {
if !fn(i, Param(params, i), HasMore(params, i)) {
break
}

View File

@ -30,7 +30,7 @@ func NewTransitionTable(size int) TransitionTable {
// SetDefault sets default transition.
func (t TransitionTable) SetDefault(action Action, state State) {
for i := 0; i < len(t); i++ {
for i := range t {
t[i] = action<<TransitionActionShift | state
}
}
@ -63,7 +63,7 @@ func (t TransitionTable) Transition(state State, code byte) (State, Action) {
return value & TransitionStateMask, value >> TransitionActionShift
}
// byte range macro
// byte range macro.
func r(start, end byte) []byte {
var a []byte
for i := int(start); i <= int(end); i++ {

View File

@ -359,7 +359,7 @@ func parseOscCmd(p *Parser) {
if p == nil || p.cmd != parser.MissingCommand {
return
}
for j := 0; j < p.dataLen; j++ {
for j := range p.dataLen {
d := p.data[j]
if d < '0' || d > '9' {
break

View File

@ -21,10 +21,7 @@ func ScreenPassthrough(seq string, limit int) string {
b.WriteString("\x1bP")
if limit > 0 {
for i := 0; i < len(seq); i += limit {
end := i + limit
if end > len(seq) {
end = len(seq)
}
end := min(i+limit, len(seq))
b.WriteString(seq[i:end])
if end < len(seq) {
b.WriteString("\x1b\\\x1bP")
@ -52,7 +49,7 @@ func ScreenPassthrough(seq string, limit int) string {
func TmuxPassthrough(seq string) string {
var b bytes.Buffer
b.WriteString("\x1bPtmux;")
for i := 0; i < len(seq); i++ {
for i := range len(seq) {
if seq[i] == ESC {
b.WriteByte(ESC)
}

View File

@ -351,7 +351,7 @@ func DECRQPSR(n int) string {
//
// See: https://vt100.net/docs/vt510-rm/DECTABSR.html
func TabStopReport(stops ...int) string {
var s []string
var s []string //nolint:prealloc
for _, v := range stops {
s = append(s, strconv.Itoa(v))
}
@ -376,7 +376,7 @@ func DECTABSR(stops ...int) string {
//
// See: https://vt100.net/docs/vt510-rm/DECCIR.html
func CursorInformationReport(values ...int) string {
var s []string
var s []string //nolint:prealloc
for _, v := range values {
s = append(s, strconv.Itoa(v))
}
@ -395,7 +395,7 @@ func DECCIR(values ...int) string {
//
// CSI Pn b
//
// See: ECMA-48 § 8.3.103
// See: ECMA-48 § 8.3.103.
func RepeatPreviousCharacter(n int) string {
var s string
if n > 1 {

View File

@ -1,8 +1,6 @@
package ansi
import "strconv"
// Select Graphic Rendition (SGR) is a command that sets display attributes.
// SelectGraphicRendition (SGR) is a command that sets display attributes.
//
// Default is 0.
//
@ -14,20 +12,7 @@ func SelectGraphicRendition(ps ...Attr) string {
return ResetStyle
}
var s Style
for _, p := range ps {
attr, ok := attrStrings[p]
if ok {
s = append(s, attr)
} else {
if p < 0 {
p = 0
}
s = append(s, strconv.Itoa(p))
}
}
return s.String()
return NewStyle(ps...).String()
}
// SGR is an alias for [SelectGraphicRendition].
@ -36,60 +21,59 @@ func SGR(ps ...Attr) string {
}
var attrStrings = map[int]string{
ResetAttr: "0",
BoldAttr: "1",
FaintAttr: "2",
ItalicAttr: "3",
UnderlineAttr: "4",
SlowBlinkAttr: "5",
RapidBlinkAttr: "6",
ReverseAttr: "7",
ConcealAttr: "8",
StrikethroughAttr: "9",
NoBoldAttr: "21",
NormalIntensityAttr: "22",
NoItalicAttr: "23",
NoUnderlineAttr: "24",
NoBlinkAttr: "25",
NoReverseAttr: "27",
NoConcealAttr: "28",
NoStrikethroughAttr: "29",
BlackForegroundColorAttr: "30",
RedForegroundColorAttr: "31",
GreenForegroundColorAttr: "32",
YellowForegroundColorAttr: "33",
BlueForegroundColorAttr: "34",
MagentaForegroundColorAttr: "35",
CyanForegroundColorAttr: "36",
WhiteForegroundColorAttr: "37",
ExtendedForegroundColorAttr: "38",
DefaultForegroundColorAttr: "39",
BlackBackgroundColorAttr: "40",
RedBackgroundColorAttr: "41",
GreenBackgroundColorAttr: "42",
YellowBackgroundColorAttr: "43",
BlueBackgroundColorAttr: "44",
MagentaBackgroundColorAttr: "45",
CyanBackgroundColorAttr: "46",
WhiteBackgroundColorAttr: "47",
ExtendedBackgroundColorAttr: "48",
DefaultBackgroundColorAttr: "49",
ExtendedUnderlineColorAttr: "58",
DefaultUnderlineColorAttr: "59",
BrightBlackForegroundColorAttr: "90",
BrightRedForegroundColorAttr: "91",
BrightGreenForegroundColorAttr: "92",
BrightYellowForegroundColorAttr: "93",
BrightBlueForegroundColorAttr: "94",
BrightMagentaForegroundColorAttr: "95",
BrightCyanForegroundColorAttr: "96",
BrightWhiteForegroundColorAttr: "97",
BrightBlackBackgroundColorAttr: "100",
BrightRedBackgroundColorAttr: "101",
BrightGreenBackgroundColorAttr: "102",
BrightYellowBackgroundColorAttr: "103",
BrightBlueBackgroundColorAttr: "104",
BrightMagentaBackgroundColorAttr: "105",
BrightCyanBackgroundColorAttr: "106",
BrightWhiteBackgroundColorAttr: "107",
ResetAttr: resetAttr,
BoldAttr: boldAttr,
FaintAttr: faintAttr,
ItalicAttr: italicAttr,
UnderlineAttr: underlineAttr,
SlowBlinkAttr: slowBlinkAttr,
RapidBlinkAttr: rapidBlinkAttr,
ReverseAttr: reverseAttr,
ConcealAttr: concealAttr,
StrikethroughAttr: strikethroughAttr,
NormalIntensityAttr: normalIntensityAttr,
NoItalicAttr: noItalicAttr,
NoUnderlineAttr: noUnderlineAttr,
NoBlinkAttr: noBlinkAttr,
NoReverseAttr: noReverseAttr,
NoConcealAttr: noConcealAttr,
NoStrikethroughAttr: noStrikethroughAttr,
BlackForegroundColorAttr: blackForegroundColorAttr,
RedForegroundColorAttr: redForegroundColorAttr,
GreenForegroundColorAttr: greenForegroundColorAttr,
YellowForegroundColorAttr: yellowForegroundColorAttr,
BlueForegroundColorAttr: blueForegroundColorAttr,
MagentaForegroundColorAttr: magentaForegroundColorAttr,
CyanForegroundColorAttr: cyanForegroundColorAttr,
WhiteForegroundColorAttr: whiteForegroundColorAttr,
ExtendedForegroundColorAttr: extendedForegroundColorAttr,
DefaultForegroundColorAttr: defaultForegroundColorAttr,
BlackBackgroundColorAttr: blackBackgroundColorAttr,
RedBackgroundColorAttr: redBackgroundColorAttr,
GreenBackgroundColorAttr: greenBackgroundColorAttr,
YellowBackgroundColorAttr: yellowBackgroundColorAttr,
BlueBackgroundColorAttr: blueBackgroundColorAttr,
MagentaBackgroundColorAttr: magentaBackgroundColorAttr,
CyanBackgroundColorAttr: cyanBackgroundColorAttr,
WhiteBackgroundColorAttr: whiteBackgroundColorAttr,
ExtendedBackgroundColorAttr: extendedBackgroundColorAttr,
DefaultBackgroundColorAttr: defaultBackgroundColorAttr,
ExtendedUnderlineColorAttr: extendedUnderlineColorAttr,
DefaultUnderlineColorAttr: defaultUnderlineColorAttr,
BrightBlackForegroundColorAttr: brightBlackForegroundColorAttr,
BrightRedForegroundColorAttr: brightRedForegroundColorAttr,
BrightGreenForegroundColorAttr: brightGreenForegroundColorAttr,
BrightYellowForegroundColorAttr: brightYellowForegroundColorAttr,
BrightBlueForegroundColorAttr: brightBlueForegroundColorAttr,
BrightMagentaForegroundColorAttr: brightMagentaForegroundColorAttr,
BrightCyanForegroundColorAttr: brightCyanForegroundColorAttr,
BrightWhiteForegroundColorAttr: brightWhiteForegroundColorAttr,
BrightBlackBackgroundColorAttr: brightBlackBackgroundColorAttr,
BrightRedBackgroundColorAttr: brightRedBackgroundColorAttr,
BrightGreenBackgroundColorAttr: brightGreenBackgroundColorAttr,
BrightYellowBackgroundColorAttr: brightYellowBackgroundColorAttr,
BrightBlueBackgroundColorAttr: brightBlueBackgroundColorAttr,
BrightMagentaBackgroundColorAttr: brightMagentaBackgroundColorAttr,
BrightCyanBackgroundColorAttr: brightCyanBackgroundColorAttr,
BrightWhiteBackgroundColorAttr: brightWhiteBackgroundColorAttr,
}

View File

@ -11,10 +11,10 @@ type StatusReport interface {
StatusReport() int
}
// ANSIReport represents an ANSI terminal status report.
// ANSIStatusReport represents an ANSI terminal status report.
type ANSIStatusReport int //nolint:revive
// Report returns the status report identifier.
// StatusReport returns the status report identifier.
func (s ANSIStatusReport) StatusReport() int {
return int(s)
}
@ -22,7 +22,7 @@ func (s ANSIStatusReport) StatusReport() int {
// DECStatusReport represents a DEC terminal status report.
type DECStatusReport int
// Status returns the status report identifier.
// StatusReport returns the status report identifier.
func (s DECStatusReport) StatusReport() int {
return int(s)
}
@ -89,6 +89,16 @@ const RequestCursorPositionReport = "\x1b[6n"
// See: https://vt100.net/docs/vt510-rm/DECXCPR.html
const RequestExtendedCursorPositionReport = "\x1b[?6n"
// RequestLightDarkReport is a control sequence that requests the terminal to
// report its operating system light/dark color preference. Supported terminals
// should respond with a [LightDarkReport] sequence as follows:
//
// CSI ? 997 ; 1 n for dark mode
// CSI ? 997 ; 2 n for light mode
//
// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
const RequestLightDarkReport = "\x1b[?996n"
// CursorPositionReport (CPR) is a control sequence that reports the cursor's
// position.
//
@ -142,3 +152,17 @@ func ExtendedCursorPositionReport(line, column, page int) string {
func DECXCPR(line, column, page int) string {
return ExtendedCursorPositionReport(line, column, page)
}
// LightDarkReport is a control sequence that reports the terminal's operating
// system light/dark color preference.
//
// CSI ? 997 ; 1 n for dark mode
// CSI ? 997 ; 2 n for light mode
//
// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
func LightDarkReport(dark bool) string {
if dark {
return "\x1b[?997;1n"
}
return "\x1b[?997;2n"
}

View File

@ -17,6 +17,26 @@ type Attr = int
// Style represents an ANSI SGR (Select Graphic Rendition) style.
type Style []string
// NewStyle returns a new style with the given attributes.
func NewStyle(attrs ...Attr) Style {
if len(attrs) == 0 {
return Style{}
}
s := make(Style, 0, len(attrs))
for _, a := range attrs {
attr, ok := attrStrings[a]
if ok {
s = append(s, attr)
} else {
if a < 0 {
a = 0
}
s = append(s, strconv.Itoa(a))
}
}
return s
}
// String returns the ANSI SGR (Select Graphic Rendition) style sequence for
// the given style.
func (s Style) String() string {
@ -127,11 +147,6 @@ func (s Style) Strikethrough() Style {
return append(s, strikethroughAttr)
}
// NoBold appends the no bold style attribute to the style.
func (s Style) NoBold() Style {
return append(s, noBoldAttr)
}
// NormalIntensity appends the normal intensity style attribute to the style.
func (s Style) NormalIntensity() Style {
return append(s, normalIntensityAttr)
@ -236,7 +251,6 @@ const (
ReverseAttr Attr = 7
ConcealAttr Attr = 8
StrikethroughAttr Attr = 9
NoBoldAttr Attr = 21 // Some terminals treat this as double underline.
NormalIntensityAttr Attr = 22
NoItalicAttr Attr = 23
NoUnderlineAttr Attr = 24
@ -298,7 +312,6 @@ const (
reverseAttr = "7"
concealAttr = "8"
strikethroughAttr = "9"
noBoldAttr = "21"
normalIntensityAttr = "22"
noItalicAttr = "23"
noUnderlineAttr = "24"
@ -581,7 +594,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
B: uint8(b), //nolint:gosec
A: 0xff,
}
return
return //nolint:nakedret
case 3: // CMY direct color
if len(params) < 5 {
@ -599,7 +612,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
Y: uint8(y), //nolint:gosec
K: 0,
}
return
return //nolint:nakedret
case 4: // CMYK direct color
if len(params) < 6 {
@ -617,7 +630,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
Y: uint8(y), //nolint:gosec
K: uint8(k), //nolint:gosec
}
return
return //nolint:nakedret
case 5: // indexed color
if len(params) < 3 {
@ -652,7 +665,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
B: uint8(b), //nolint:gosec
A: uint8(a), //nolint:gosec
}
return
return //nolint:nakedret
default:
return 0

View File

@ -5,7 +5,7 @@ import (
"strings"
)
// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings.
// XTGETTCAP (RequestTermcap) requests Termcap/Terminfo strings.
//
// DCS + q <Pt> ST
//

View File

@ -30,3 +30,19 @@ func SetIconName(s string) string {
func SetWindowTitle(s string) string {
return "\x1b]2;" + s + "\x07"
}
// DECSWT is a sequence for setting the window title.
//
// This is an alias for [SetWindowTitle]("1;<name>").
// See: EK-VT520-RM 5156 https://vt100.net/dec/ek-vt520-rm.pdf
func DECSWT(name string) string {
return SetWindowTitle("1;" + name)
}
// DECSIN is a sequence for setting the icon name.
//
// This is an alias for [SetWindowTitle]("L;<name>").
// See: EK-VT520-RM 5134 https://vt100.net/dec/ek-vt520-rm.pdf
func DECSIN(name string) string {
return SetWindowTitle("L;" + name)
}

View File

@ -10,8 +10,7 @@ import (
// Cut the string, without adding any prefix or tail strings. This function is
// aware of ANSI escape codes and will not break them, and accounts for
// wide-characters (such as East-Asian characters and emojis). Note that the
// [left] parameter is inclusive, while [right] isn't.
// wide-characters (such as East-Asian characters and emojis).
// This treats the text as a sequence of graphemes.
func Cut(s string, left, right int) string {
return cut(GraphemeWidth, s, left, right)
@ -19,8 +18,10 @@ func Cut(s string, left, right int) string {
// CutWc the string, without adding any prefix or tail strings. This function is
// aware of ANSI escape codes and will not break them, and accounts for
// wide-characters (such as East-Asian characters and emojis). Note that the
// [left] parameter is inclusive, while [right] isn't.
// wide-characters (such as East-Asian characters and emojis).
// Note that the [left] parameter is inclusive, while [right] isn't,
// which is to say it'll return `[left, right)`.
//
// This treats the text as a sequence of wide characters and runes.
func CutWc(s string, left, right int) string {
return cut(WcWidth, s, left, right)
@ -41,7 +42,7 @@ func cut(m Method, s string, left, right int) string {
if left == 0 {
return truncate(s, right, "")
}
return truncateLeft(Truncate(s, right, ""), left, "")
return truncateLeft(truncate(s, right, ""), left, "")
}
// Truncate truncates a string to a given length, adding a tail to the end if
@ -99,6 +100,7 @@ func truncate(m Method, s string, length int, tail string) string {
// increment the index by the length of the cluster
i += len(cluster)
curWidth += width
// Are we ignoring? Skip to the next byte
if ignoring {
@ -107,16 +109,15 @@ func truncate(m Method, s string, length int, tail string) string {
// Is this gonna be too wide?
// If so write the tail and stop collecting.
if curWidth+width > length && !ignoring {
if curWidth > length && !ignoring {
ignoring = true
buf.WriteString(tail)
}
if curWidth+width > length {
if curWidth > length {
continue
}
curWidth += width
buf.Write(cluster)
// Done collecting, now we're back in the ground state.
@ -142,6 +143,14 @@ func truncate(m Method, s string, length int, tail string) string {
// collects printable ASCII
curWidth++
fallthrough
case parser.ExecuteAction:
// execute action will be things like \n, which, if outside the cut,
// should be ignored.
if ignoring {
i++
continue
}
fallthrough
default:
buf.WriteByte(b[i])
i++
@ -214,14 +223,14 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
buf.WriteString(prefix)
}
if ignoring {
continue
}
if curWidth > n {
buf.Write(cluster)
}
if ignoring {
continue
}
pstate = parser.GroundState
continue
}
@ -240,6 +249,14 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
continue
}
fallthrough
case parser.ExecuteAction:
// execute action will be things like \n, which, if outside the cut,
// should be ignored.
if ignoring {
i++
continue
}
fallthrough
default:
buf.WriteByte(b[i])

View File

@ -10,7 +10,7 @@ import (
)
// colorToHexString returns a hex string representation of a color.
func colorToHexString(c color.Color) string {
func colorToHexString(c color.Color) string { //nolint:unused
if c == nil {
return ""
}
@ -28,7 +28,7 @@ func colorToHexString(c color.Color) string {
// rgbToHex converts red, green, and blue values to a hexadecimal value.
//
// hex := rgbToHex(0, 0, 255) // 0x0000FF
func rgbToHex(r, g, b uint32) uint32 {
func rgbToHex(r, g, b uint32) uint32 { //nolint:unused
return r<<16 + g<<8 + b
}
@ -90,17 +90,3 @@ func XParseColor(s string) color.Color {
}
return nil
}
type ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func max[T ordered](a, b T) T { //nolint:predeclared
if a > b {
return a
}
return b
}

View File

@ -19,7 +19,7 @@ func Strip(s string) string {
// This implements a subset of the Parser to only collect runes and
// printable characters.
for i := 0; i < len(s); i++ {
for i := range len(s) {
if pstate == parser.Utf8State {
// During this state, collect rw bytes to form a valid rune in the
// buffer. After getting all the rune bytes into the buffer,

View File

@ -10,7 +10,7 @@ import (
"github.com/rivo/uniseg"
)
// nbsp is a non-breaking space
// nbsp is a non-breaking space.
const nbsp = 0xA0
// Hardwrap wraps a string or a block of text to a given line length, breaking
@ -55,7 +55,7 @@ func hardwrap(m Method, s string, limit int, preserveSpace bool) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
if state == parser.Utf8State { //nolint:nestif
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
@ -190,7 +190,7 @@ func wordwrap(m Method, s string, limit int, breakpoints string) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
if state == parser.Utf8State { //nolint:nestif
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
@ -303,20 +303,22 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
}
var (
cluster []byte
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
curWidth int // written width of the line
wordLen int // word buffer len without ANSI escape codes
pstate = parser.GroundState // initial state
b = []byte(s)
cluster []byte
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
spaceWidth int // width of the space buffer
curWidth int // written width of the line
wordLen int // word buffer len without ANSI escape codes
pstate = parser.GroundState // initial state
b = []byte(s)
)
addSpace := func() {
curWidth += space.Len()
curWidth += spaceWidth
buf.Write(space.Bytes())
space.Reset()
spaceWidth = 0
}
addWord := func() {
@ -335,12 +337,13 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
buf.WriteByte('\n')
curWidth = 0
space.Reset()
spaceWidth = 0
}
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
if state == parser.Utf8State { //nolint:nestif
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
@ -353,6 +356,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space
addWord()
space.WriteRune(r)
spaceWidth += width
case bytes.ContainsAny(cluster, breakpoints):
addSpace()
if curWidth+wordLen+width > limit {
@ -372,7 +376,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
word.Write(cluster)
wordLen += width
if curWidth+wordLen+space.Len() > limit {
if curWidth+wordLen+spaceWidth > limit {
addNewline()
}
}
@ -386,13 +390,14 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
switch r := rune(b[i]); {
case r == '\n':
if wordLen == 0 {
if curWidth+space.Len() > limit {
if curWidth+spaceWidth > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
spaceWidth = 0
}
addWord()
@ -400,6 +405,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
case unicode.IsSpace(r):
addWord()
space.WriteRune(r)
spaceWidth++
case r == '-':
fallthrough
case runeContainsAny(r, breakpoints):
@ -426,7 +432,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
addWord()
}
if curWidth+wordLen+space.Len() > limit {
if curWidth+wordLen+spaceWidth > limit {
addNewline()
}
}
@ -443,13 +449,14 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
}
if wordLen == 0 {
if curWidth+space.Len() > limit {
if curWidth+spaceWidth > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
spaceWidth = 0
}
addWord()